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

pathwhat it is
rego/execution_evidence.regoThe 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-*.yamlThree 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-*.yamlThe same policy on Kyverno's CEL rail.
policy-controller/clusterimagepolicy-*.yamlThree 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.pyRuns 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.json93 obligations, one row per rail per obligation, measured.
docs/ADVERSARIAL-RATCHET.mdThe open adversarial-mutation findings against these rails, as vector ids, families, axes, and soundness classes.
docs/CONSUMER-POLICY.mdThe 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.mdPer lifted file: where it came from, at which commit, and the SHA-256 of the source bytes.
.github/workflows/ci.ymlThe 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.json names 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.