agent-evidence-admission
September 18, 2026 ยท View on GitHub
Four policy engines that admit or refuse a Kubernetes workload on adversarial-execution-evidence, each one declaring exactly which of the specification's obligations it enforces, which it only approximates, and which it cannot reach at all.
The predicate is https://in-toto.io/attestation/adversarial-execution-evidence/v0.7, under review
at in-toto/attestation#570. The corpus these
rails are measured against is
probityai/agent-evidence-vectors, at the
tag v0.10.1.
Verify the claim in four commands
Nothing below asks you to trust a table in this repository. From a fresh clone, with
opa, kyverno, and cue on the path:
git clone --depth 1 --branch v0.10.1 \
https://github.com/probityai/agent-evidence-vectors.git /tmp/vectors
python3 rego/gen_corpus_vectors.py --check --corpus /tmp/vectors
python3 scripts/gen_profile_map.py --jobs 4 && git diff --exit-code -- profiles/
python3 scripts/profile-map-gate.py
The second command says the vectors these rails run on really are that corpus at that tag. The third runs all four rails over all of them and refuses if the committed maps are not what the run produces. The fourth refuses any row that declares an obligation enforced while the run shows that rail answering differently from the oracle on a vector citing it.
The third command takes roughly forty minutes. That is the honest cost of the claim, and it is the reason the maps are committed at all: a reader who does not want to spend it can read the measurement, and a reader who does not want to trust it can. Continuous integration spends it on every push.
What is here
| path | what it is |
|---|---|
rego/execution_evidence.rego | The oracle. 2,571 lines of Rego v1 that decode the statement, re-derive the coverage partition, and the result, and refuse on 11 numbered rules. Run opa test --timeout 120s rego/ for 193 tests. |
kyverno/clusterpolicy-*.yaml | Three Kyverno ClusterPolicy documents on the JMESPath rail: enforce, audit, and enforce with a freshness window. They carry the same conditions and a gate compares the condition sets directly, because a corpus catches a condition that changes an answer and cannot catch one that vanishes where no vector exercises it. |
kyverno/imagevalidatingpolicy-*.yaml | The same policy on Kyverno's CEL rail. |
policy-controller/clusterimagepolicy-*.yaml | Three sigstore policy-controller documents: a Rego one, a CUE one, and a deep one that embeds the whole oracle module, generated by gen_soundness_cip.py so the embedded copy cannot drift from the module. |
conformance/run_policy_conformance.py | Runs every rail over the corpus projection in rego/corpus_vectors.json and refuses a rail that answers differently from the oracle without a declared reason. |
PROFILE-REGISTRY.md, profiles/*/PROFILE-MAP.json | 93 obligations, one row per rail per obligation, measured. |
docs/ADVERSARIAL-RATCHET.md | The open adversarial-mutation findings against these rails, as vector ids, families, axes, and soundness classes. |
docs/CONSUMER-POLICY.md | The eleven consumer pins, what each one decides, and what an absent value means. Nine of the eleven rules these rails enforce are facts about your deployment rather than about the statement. |
PROVENANCE.md | Per lifted file: where it came from, at which commit, and the SHA-256 of the source bytes. |
.github/workflows/ci.yml | The pins: the corpus tag and its commit, and each engine version with the digest of its release asset. |
Why the rails are published
A policy that refuses a workload is a claim about what it checked, and the honest form of that claim is a per-obligation map somebody else can re-run. Publishing the rails costs nothing that matters: no production key ships here, no producer-side machinery ships here, and every obligation these rails evaluate is already written down in a public specification. What it buys is that an implementer who disagrees with a disposition can point at the row and the vector.
Scope is deliberate and narrow. The bundle carries the adversarial-execution-evidence rails and nothing else.
What was removed
Nine branches for predicate types published under a separate namespace were dropped when this copy
was derived. Each one is named here, so that dropping it is a fact rather than an inference from a diff:
security-verdict, substrate-integrity, sut-provenance, signed-evidence, ml-bom,
static-scan-verdict, and evidence-scope-disclosure at v1, and mesh-quiescence and
a2a-mesh-scorecard at v2. A statement carrying any of them is an unknown type here and is refused
fail-closed, which is what these rails do with every type they do not recognize.
known_predicate_types therefore has exactly one member and
verdict_types_requiring_catch_policy is empty. Three tests hold that shape in place.
The key is a placeholder
keys/observation-key.pub is a throwaway ed25519 public key generated for this bundle, and its
private half was destroyed. Every rail carries the same placeholder, so the cross-rail key-parity
check in the conformance harness is a real check. Replace it with the observation key you actually
trust before deploying anything here, in all four places at once: the harness compares them and
fails when they disagree.
The trust ceiling, stated before you rely on any of it
These rails evaluate an already-verified statement. They do not verify one.
The envelope signature is checked by the incumbent, which is Kyverno's or policy-controller's own key authority, before the policy body ever runs. Everything below runs on a statement that authority has already accepted.
Inside that statement, a rail that evaluates coverage without verifying the observation records'
own signatures cannot distinguish a substrate observation from an assembly-plane forgery. Every
record field the oracle reads is decoded out of a base64 payload whose signature it never checks.
The module requires that each record carries a non-empty signatures array, and that is a
structural presence check and not verification. The header of rego/execution_evidence.rego says so
in its own words, at greater length, and it is worth reading before deployment.
Bring your own corpus, and the ceiling moves with it. Nine of the eleven rules these rails
enforce are consumer pins: the corpus digest and the substrate digest you expect, the corpus classes
you demand a run to have assessed, the freshness bounds you set, and the result tokens you admit.
None of those has a safe default that means anything, so an unpinned consumer is DENIED rather than
admitted, and two knobs exist to say out loud that you are choosing to run unpinned
(allow_unpinned_anchors, allow_unpinned_scope). A deployment that sets both and pins nothing has
turned this into a shape check.
The scope pin is the one worth reading twice. A producer that withdraws a coverage class emits a
statement byte-identical to one an honest producer with no coverage emits. Your pinned
demanded_classes is the only place the deciding fact exists, and no rail can supply it for you.
The profile registry
93 obligations, four rails, and every disposition measured rather than typed.
An obligation is a condition the conformance corpus cites by id, registered in that corpus's own
condition table and vendored here as registry/conditions.json with the corpus digest it was read
at. scripts/gen_profile_map.py runs every rail over every vector and writes one row per rail per
obligation:
- enforced the rail matched the oracle on every vector citing the obligation, and
profiles/rule-index.jsonnames a rule in that rail's own artifact. - approximated the rail matched the oracle on every such vector and no rule has been named. That is the default, because agreement on the vectors that exist is weaker than a named rule.
- unreachable the rail is not shown to enforce it, with an obstruction from a closed vocabulary saying which of two reasons applies: the rail diverges from the oracle on a vector citing the obligation, or no vector citing it appears in this projection at all.
scripts/profile-map-gate.py refuses a map that claims more than the measurement supports. Four
checks, and the fourth is the one it exists for: an obligation declared enforced whose vector the
rail answers differently from the oracle is a hard error. The conformance harness already refuses
an undeclared divergence at the level of one vector; this lifts the same discipline to the level of
an obligation.
python3 scripts/gen_condition_registry.py --corpus <vectors checkout>
python3 scripts/gen_profile_map.py --jobs 2
python3 scripts/gen_profile_registry.py
python3 scripts/profile-map-gate.py
The Go command-line verifier is a fifth rail in this family and it does not live here, so it has no map. A map about a rail the repository does not carry could never be measured here.
The adversarial ratchet
docs/ADVERSARIAL-RATCHET.md publishes the open adversarial-mutation findings against these rails:
62 findings over 39 distinct vector and mutation pairs, across 25 vectors and 8 mutation families.
Every number in it is derived by scripts/ratchet_report.py and carries the ledger digest it came
from, because that count has been quoted by hand elsewhere and has been three different numbers
inside six weeks.
The report carries the vector id, the mutation family, the invariant axis, and the soundness class. The
mutator code does not, and the measured transition does not unless the report says
transitionsIncluded: true. A specification that names its own open attacks outranks one that does
not; a repository that publishes the code producing them is doing something else.
scripts/ratchet_report.py --check compares the report against the ledger. Without a ledger it
falls back to checking the document against its own JSON and says in the first line of its output
that it did so, because a self-consistency pass is not evidence that the findings are current.
Running the conformance harness
python3 conformance/run_policy_conformance.py --jobs 2
Needs opa, kyverno, and cue on the path. It takes about forty minutes. It runs every vector
through every rail and refuses on an undeclared divergence, on a Kyverno document whose condition
set has drifted from its siblings, and on a declared divergence that no longer reproduces, which is
what stops the declaration list becoming a place to file inconvenient results.
Continuous integration
Four jobs, on every push. hygiene checks that no tracked file names a host outside a
short allowlist, that no prose in the tree or anywhere in the history carries an
authorship mark or a form the style guide rules out, and that the two rendered documents
match the data they render from.
corpus clones the vectors at the pinned tag, refuses a tag that has moved off its
pinned commit, and re-derives the projection. oracle runs the module's 193 tests and
refuses a deployed manifest whose embedded copy has drifted from the module. rails
is the long one: it installs all three engines at pinned versions with pinned asset
digests, runs every rail over every vector, and fails on a map that is not the
measurement or a row that claims more than the measurement supports.
Citation and provenance
CITATION.cff is the citation. Cite the specification for what the predicate requires,
the vectors repository for what a corpus forces, and this repository only for what a
particular engine was measured to enforce.
PROVENANCE.md records where each policy artifact came from: the path it occupied in
the working tree it was derived from, the commit it was taken at, and the SHA-256 of
the source bytes. That tree is not published, so the digests are the part a reader can
hold onto, and the four classes of edit applied on derivation are listed there rather
than left to be inferred from a diff nobody can run.
License
Apache-2.0.