Skip to content

feat(provenance): make a refusal evidence, and demo it - #63

Merged
imran-siddique merged 4 commits into
mainfrom
feat/rejection-with-proof
Jul 27, 2026
Merged

feat(provenance): make a refusal evidence, and demo it#63
imran-siddique merged 4 commits into
mainfrom
feat/rejection-with-proof

Conversation

@imran-siddique

Copy link
Copy Markdown
Contributor

The gap

enforce_peer_call raised on an over-scoped call and emitted nothing. A denial left no artifact, so an auditor walking the DAG saw a gap where a hop should be, and the only account of why the call stopped was the operator's log. For a project whose pitch is "a third party can check this without trusting you", the refusal was the one thing you had to take on trust.

What changed

A refusal now emits a linked provenance record carrying the requested capability, the effective scope it fell outside, and the reason, attached to ScopeNotPermitted.record. The call still fails closed; the record is evidence, not a way to continue.

  • verify_dag treats a denial as terminal: nothing may be chained onto a refused hop, so a trail cannot claim work proceeded.
  • cross_check_chain matches only hop records positionally against the chain, and still requires a denial record to name a credential that is actually in it, so a refusal cannot be attributed to authority never granted.
  • ca2a verify-dag reports outcome: denied with the requested capability and effective scope, instead of printing verified: true and burying the interesting part.
  • Denial fields are omitted from an allow record's hashed body, so existing allow-record hashes are unchanged. Committed DAGs keep verifying.

The demo

examples/rejection-with-proof/. An agent asks for authority nobody delegated to it, is refused, and the refusal verifies offline through the shipped CLI against the committed chain and DAG.

ALLOW  tool:search    effective scope ['tool:search']
DENY   tool:purchase  capability 'tool:purchase' is not in the effective scope
{"verified": true, "records": 4, "outcome": "denied",
 "requested_capability": "tool:purchase", "effective_scope": ["tool:search"],
 "cross_checked": true}

The callee's local policy permits tool:purchase on purpose. It is refused anyway because the delegated scope does not carry it, which is the distinction transport-layer auth cannot express. A demo where the callee simply did not support purchasing would prove nothing.

The demo runs in CI (tests/unit/test_example_rejection_with_proof.py) so the README cannot promise something the code stopped doing, and both the README and the module docstring state that it makes no attestation claim: no sealing, no peer attestation, nothing about where the code ran.

Test plan

  • pytest tests/ — 205 passed, 2 skipped; 6 pre-existing test_cedar.py failures unrelated to this change (they fail identically on a clean tree and pass in CI)
  • ruff check clean
  • Demo run end to end, including the reparent-tamper check at the end

🤖 Generated with Claude Code

imran-siddique and others added 3 commits July 21, 2026 16:51
Removes ca2a's last duplicated attestation crypto. ca2a_verify.verify_cert_chain
(shared by the SEV-SNP VCEK, TDX PCK, and TPM AK chains) now delegates to
agent_manifest.verify_cert_chain, the org's shared generic algorithm-agnostic
verifier, re-raising CertChainError as AttestationFailed to preserve ca2a's
contract. ca2a keeps its own parsers, per-format signature checks, appraisal
shapes, trust-root pinning, and verify_offer semantics.

Dep: adds agent-manifest>=0.5. 201 tests pass unchanged; ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rdware

TdxQuote.parse threw on every genuine DCAP v4 quote. Real quotes nest the
Quoting Enclave material: the bytes after the attestation key are a
certification-data header of type 6 (QE_REPORT_CERTIFICATION_DATA) wrapping the
QE report, its PCK signature, the auth data and the type-5 PCK chain. The parser
read the QE report directly after the attestation key, six bytes early, so a real
GCP C3 quote failed with a bogus cert-length read.

The synthetic fixture emitted the same flat layout, so the tests validated the
defect. Failure was closed, so this is a false negative rather than an unsound
accept, but the TDX path had never worked against real evidence.

Both verifiers now run against genuine hardware evidence, env-gated because the
captures embed per-CPU identifiers: a real Azure CVM SEV-SNP report
(CA2A_SNP_FIXTURE_DIR) and a real GCP C3 TDX quote (CA2A_TDX_QUOTE), each also
asserted to fail closed on a tampered copy.

docs/hardware-validation.md records what is now verified and, as importantly,
what still is not: this validates the verifier, not a running attested peer. The
live handshake is still software mode, so cA2A is still not attested across trust
domains, and LIMITATIONS and ROADMAP say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enforce_peer_call raised on an over-scoped call and emitted nothing, so a denial
left no artifact: an auditor walking the DAG saw a gap where a hop should be, and
the only account of why the call stopped was the operator's log.

It now builds a linked denial record carrying the requested capability, the
effective scope it fell outside, and the reason, attached to
ScopeNotPermitted.record. The call still fails closed; the record is evidence of
the refusal, not a way to continue. verify_dag treats a denial as terminal so no
trail can claim work proceeded past it, cross_check_chain matches only hop
records positionally while still requiring a denial to name a credential that is
in the chain, and ca2a verify-dag surfaces outcome: denied. Denial fields are
omitted from an allow record's hashed body, so existing allow-record hashes are
unchanged.

examples/rejection-with-proof is the front door for it: an agent asks for
authority nobody delegated, is refused, and the refusal verifies offline through
the shipped CLI against the committed chain and DAG. The callee's own policy
permits the capability, so the refusal turns on delegation rather than on the
callee not supporting it. The demo runs in CI so the README cannot promise
something the code stopped doing, and it states plainly that it makes no
attestation claim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cred` was already bound as DelegationCredential by the positional loop above,
so reassigning the Optional from by_id.get shadowed it with an incompatible
type. Renamed to `under`, which also reads better: the credential the refusal
was issued under.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@imran-siddique
imran-siddique merged commit b566925 into main Jul 27, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant