stackchain-lab-loop/docs/path-proof-threat-boundary.md
vincent f61ef22f48 [Vincent] #28: executable replay/forgery gate for path-proof receipts
Dependency-free path_proof package under a dedicated module. Bounded to this
issue only: no changes to existing lab_loop tests or scripts.

Deliverables:
1. path_proof/ package (receipt.py, keys.py, __init__.py) — verifier with
   canonical JSON, deterministic build/verify/verify_and_accept, in-memory
   KeyRegistry + SpentRegistry. No I/O, no wall clock, no randomness.
2. Each receipt binds action kind/target/request/result, model id+invocation,
   tool trace, policy bundle, code identity, evidence uri, plus action_id,
   verifier nonce, issued_at/expires_at, and signer.
3. Deterministic spent-nonce/idempotency registry: the same valid receipt is
   accepted exactly once; a replay is rejected on the nonce OR action_id axis.
4. Positive + negative tests: valid passes; replay fails; request/result
   substitution fails; wrong target fails; expired fails; unknown/revoked
   signer fails; malformed structures fail closed with no exception. Every
   negative case asserts rejection AND a specific reason.
5. docs/path-proof-threat-boundary.md documents the exact threat boundary: a
   verified receipt proves a bound, fresh, single-use, trusted-attested
   commitment — NOT runtime execution unless runtime evidence is supplied.

Clean-checkout verification (commands actually run):
- /home/vincent/seedvault-inventory/venv/bin/python3 -m pytest tests/ -q
  -> 36 passed (31 path_proof + 5 pre-existing lab_loop)
- git diff --cached --name-only origin/main -> 6 files, zero .pyc
- __pycache__ gitignored; clean checkout stays clean after tests

Closes #28
Refs: #3 (Vincent path-proof critique), #19/PR #42 (canon validator r2)
[HANDOFF] from=vincent to=timmy
2026-08-15 16:44:58 -04:00

67 lines
3.4 KiB
Markdown

# Path Proof Receipt — Threat Boundary
This document states, precisely, what the `path_proof` verification gate proves
and does not prove. It is the contract the verifier enforces and the limit a
consumer must respect. The field rationale lives in
`docs/path-proof-receipt.md` (branch `timmy/3-path-proof-receipt`); this file
is the executable-side boundary statement required by issue #28.
## What a verified, freshly-accepted receipt proves
A receipt that passes `verify_receipt` and is accepted by `verify_and_accept`
proves exactly the following, and nothing more:
1. **Binding.** The receipt commits, as a single signed unit, to:
- the consequential action (`action.kind`, `action.target`),
- the exact request and observed result it claims
(`action.request_sha256`, `action.result_sha256`),
- the model invocation identity (`path.model.id`, `path.model.invocation`),
- the ordered tool path and effective policy
(`path.tools.trace_sha256`, `path.policy.bundle_sha256`),
- the immutable code identity (`path.code.repo`, `path.code.commit`,
`path.code.entrypoint`), and
- the retrievable evidence bundle (`evidence.uri`).
2. **Trust.** The attestation resolves to a signer that is present in the
caller's trusted key registry, is not revoked, and is within its validity
window at the time of verification.
3. **Freshness.** The receipt is inside its `[issued_at, expires_at]` window.
4. **Single-use.** The receipt's `nonce` and `action_id` have not been
previously consumed, so an identical receipt cannot be accepted a second
time (idempotency / replay protection).
## What a verified receipt does NOT prove
- **It does not prove runtime execution of the consequential effect.**
`result_sha256` is the hash of an *observed* result the recorder claims it
saw. Proving the side effect actually took place requires the verifier to
independently re-observe it (for example, read the Gitea label state back and
compare it to `result_sha256`). Until that happens the effect is *attested*,
not *confirmed*.
- **It does not prove the provider ran the advertised weights.** `path.model.id`
is an identity string, not a cryptographic attestation of model provenance.
Only a separately trusted provider model attestation could close that gap.
- **It does not survive compromise of both the recorder and its signing key.**
The trust anchor is the key registry. If both are compromised, a forged
receipt with a valid signature is indistinguishable from a genuine one.
- **It does not prove the evidence bundle was not selectively redacted
ambiguously.** The redaction manifest is part of the bundle; verifying the
bundle digest proves integrity of what is present, not that nothing material
was omitted.
## Failure semantics
- Any single failed check yields status **`unverified`** with specific reasons
— never a partial pass.
- A structurally unreadable receipt yields status **`malformed`**.
- A rejected receipt has **no side effects**: it does not consume a nonce or
action_id. Only a verified acceptance marks identities as spent.
## Determinism guarantees
- Verification takes `now` (epoch seconds) as an explicit argument and never
reads the wall clock.
- The key registry and spent-identity registry are in-memory objects the
caller controls; the module performs no I/O, no randomness, and no network.
- Therefore the gate is fully reproducible from a clean checkout and leaves no
generated artifacts (test caches are gitignored).