Agent Evidence

Predicate type: Done Receipt

Type URI: https://w3id.org/agent-evidence/predicate/v1/done-receipt

Version: 0.1.0

Predicate Name: Done Receipt

Status: DRAFT, issued under a self-hosted predicateType URI. The URI redirects to a page built from this file and served by the Pages site of probityai/agent-evidence, at https://probityai.github.io/agent-evidence/predicate/v1/done-receipt/. The URI will not move once a signed statement carries it, and no signed statement carries it before that page is live. The conformance corpora in this repository carry it in unsigned statements.

Purpose

Records the verdict of an outside verifier on one claim: that a coding agent finished its task. The receipt names the records the verdict rests on by digest, carries the three checks and the verdict they give, and carries what the agent did outside its workspace. A reader recomputes every one of them from the records, so the receipt asserts nothing a reader cannot check.

The claim is checked three ways, each against records made outside the agent:

claim type question
protected_paths/v1 did the agent leave the contract's protected paths alone?
acceptance_observed/v1 do the contract's acceptance commands pass when a separate verifier runs them on the agent's final tree?
tool_claim_match/v1 did every command the agent says it ran actually run, after its last edit?

Each returns supported, contradicted or not_established. not_established is an answer, not a failure: the records did not allow either of the others, and the receipt says which record was missing and why.

Use cases

Prerequisites

The in-toto Attestation Framework, done-contract and the record rules in done-records. Verify any DSSE envelope that carries the receipt or a record before reading it; the checks compare pinned bytes and do not verify signatures.

Model

The producer is an outside verifier. The subject is the agent's final workspace: one member, whose digest has one entry, sha256, equal to tree_sha256 of the tree record. The receipt is about that tree and no other.

predicateType MUST equal the Type URI above.

The receipt carries digests of the records it was computed from, in records. The records travel beside the receipt. A reader that does not hold a record named in records cannot verify the receipt, and MUST refuse it rather than trust the carried verdict.

Schema

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    { "name": "workspace-tree", "digest": { "sha256": "<tree_sha256 of the tree record>" } }
  ],
  "predicateType": "https://w3id.org/agent-evidence/predicate/v1/done-receipt",
  "predicate": {
    "schema_version": "probity-done-receipt/v1",
    "claim_id": "<the records' claim_id>",
    "invocation_id": "<the records' invocation_id>",
    "records": {
      "contract":    { "sha256": "<64-hex>" },
      "tree":        { "sha256": "<64-hex>" },
      "observation": { "sha256": "<64-hex>" },
      "capability":  { "sha256": "<64-hex>" },
      "grade":       { "sha256": "<64-hex>" },
      "claims":      { "sha256": "<64-hex>" },
      "launch":         { "sha256": "<64-hex>" },   // optional, with launch_receipt
      "launch_receipt": { "sha256": "<64-hex>" }    // optional, with launch
    },
    "checks": [
      { "claim_type": "protected_paths/v1", "decision": "supported",
        "reason": "protected_paths_untouched", "vantage": "independent",
        "witness_scope": "PEER" },
      { "claim_type": "acceptance_observed/v1", "...": "..." },
      { "claim_type": "tool_claim_match/v1", "...": "..." }
    ],
    "verdict": { "decision": "supported", "reason": "done_checks_supported",
                 "vantage": "independent", "witness_scope": "PEER" },
    "external_effects": [],
    "verifier": { "id": "<a URI naming the verifier that produced this receipt>" },
    "policy": { "table_sha256": "<64-hex>" },         // optional
    "stop_decision": { "action": "continue", "reason": "done_checks_supported" },  // optional
    "verifier_isolation": { "contract_sha256": "<64-hex>", "...": "..." }          // optional
  }
}

checks has exactly three entries in the order above. An entry carries witness_scope only when its decision is not not_established, and a not_established entry's vantage is unestablished. policy.table_sha256 is the digest of the policy table the harness acted on, when it acted; the receipt does not interpret it.

verifier.id is required. It is a URI (a scheme, a colon and a non-empty remainder, no white space) that names the verifier that wrote the receipt, for example https://github.com/probityai/probity for the reference command line. A reader checks the shape only and does not dereference it or decide which verifiers to trust; that is the consumer's policy. verifier has exactly the one member id.

stop_decision is what the harness's policy table did with the verdict: action is one of continue, replan, escalate and stop, and reason is the reason code that drove it. A reader checks its shape only; the table is the harness's.

verifier_isolation is the grading verifier's account of its own isolation, written at the end of the grading run. Its members, types and the obligations they close (vi-1 to vi-9) are defined by the agent-evidence-admission document docs/VERIFIER-ISOLATION.md, and the Rego module there reads this block by this name. A receipt reader requires only that the block is an object whose contract_sha256 equals the digest of the contract record the receipt names (receipt_isolation_contract_differs, receipt_isolation_malformed); judging the facts is the admission rule's job. The report is the verifier's word about itself, and a consumer that needs more pins an independent observation of the grading run as well.

The source paths that other documents use for these members are predicate.verdict, predicate.checks[claim_type=acceptance_observed/v1].decision and predicate.stop_decision.

The three checks

Each check pins its witnesses through the consumer's case and policy (see done-records).

protected_paths/v1

The bounded absence rule of event_absence/v2, applied to file mutation. Witnesses: contract, capability, observation, tree. The assessment names the observed party, the invocation and the interval.

  1. A recorded mutation of a protected path, or a rename or link onto one, contradicts the claim (protected_path_mutated), even under incomplete coverage. This catches an edit the agent reverted before the end.
  2. Otherwise, an independent final tree whose protected digests differ from the contract's contradicts it (protected_path_digest_changed). This catches a change whose events were lost.
  3. An empty record supports the claim only when the capability record makes every mutation type visible, coverage is complete and the independent final tree matches the contract (protected_paths_untouched).

Otherwise the claim is not_established, with the first of these reasons that applies: contract_unavailable_or_unbound, observation_unavailable_or_unbound, observation_context_mismatch, observation_vantage_unestablished, capability_unavailable_or_unbound, capability_context_mismatch, field_visibility_unestablished, observation_coverage_unestablished, final_tree_unestablished. A path excluded by name is not protected, so the verifier that grades the run MUST reset those names along with the protected paths.

acceptance_observed/v1

Witnesses: contract, tree, grade. Both the tree and the grade must be independent of the observed party.

  1. The grade must name the pinned contract's digest (grade_contract_mismatch) and the final tree's digest (grade_tree_unbound). A grade run on any other tree says nothing about what the agent left.
  2. The grade must answer every acceptance command, with the contract's exact argv, and nothing else (acceptance_result_missing, acceptance_argv_mismatch, acceptance_result_unexpected).
  3. Any nonzero exit contradicts the claim (acceptance_failed_in_verifier).
  4. All commands passing supports it (acceptance_passed_in_verifier) only when the contract killed at least one mutant. A contract with killed: 0 cannot tell a fix from a no-op (contract_adequacy_zero, not_established).

The other not_established reasons are contract_unavailable_or_unbound, final_tree_unestablished, grade_unavailable_or_unbound, grade_context_mismatch and grade_vantage_unestablished.

tool_claim_match/v1

Witnesses: claims, capability, observation; the observation must be independent. For each claim:

  1. Candidates are the exec events with exactly the claimed argv. None means the run never happened (tool_claim_unmatched), which is how a fabricated 12 passed shows up.
  2. The last edit is the latest mutation event outside the process trees (by origin) of runs with that argv. A test run's own cache writes therefore do not make it stale, while a later write by the agent or by any other command does. A run is fresh when its step follows the last edit. Candidates that exist but none fresh is tool_claim_precedes_last_edit.
  3. A claimed exit must match a fresh run's recorded exit (tool_claim_exit_mismatch). A fresh run whose exit the observer did not see leaves the claim not_established (tool_exit_unobserved).

Every claim matched supports the record (tool_claims_matched), and so does an empty claim list. Because both a match and a mismatch rest on what did not happen after a run, every verdict except not_established needs exec and every mutation type visible, and complete coverage. A command that edits files inside the same shell invocation as a claimed run shares its process tree, so its writes are not counted as edits; this is a stated limit of the check.

Vantage and witness scope

A supported or contradicted check reports two fields:

Effects outside the workspace

Observation records hold workspace paths only. A mutation anywhere else, and every network connection, is carried in external_effects, copied from sensors.external_effects of the launch receipt, or empty when there is no launch receipt.

A mutation outside the workspace at a path a grader loads, or writes its result to, is an effect on the grader whatever the three checks say. Those paths are: any file named sitecustomize.py, usercustomize.py, reward.json or reward.txt, any file ending .pth, and any path with a site-packages, dist-packages or verifier segment. The first group is what a Python interpreter imports at start; the second is where a harness reads a result back, so that an agent able to write there can write its own grade. A rename or link counts when either path is one of these.

The list is part of this version of the predicate. A consumer that needs another path reads external_effects itself; a receipt's verdict is derived by the list above.

The done verdict

verdict combines the three checks, in the order protected paths, acceptance, tool claims:

  1. If any check is contradicted, the verdict is contradicted with that check's reason (the first, in order).
  2. Otherwise, if external_effects holds a mutation at one of those paths, the verdict is contradicted with reason external_effect_on_grader_path.
  3. Otherwise, if all three are supported, the verdict is supported with reason done_checks_supported.
  4. Otherwise the verdict is not_established with the reason of the first check that is.

A supported or contradicted verdict has vantage: independent and a witness_scope that is EXTERNAL only when every check that is not not_established says EXTERNAL. A not_established verdict has vantage: unestablished and no scope.

A consumer that needs a second operator holds a verdict that does not rest on EXTERNAL scope as not_established. That is the consumer's policy, not a member of the receipt.

Verifying a receipt

A reader applies these in order and reports the first that fails. Rules 1 to 4 and the launch rules read the receipt and the records it names; rules 8 and 9 recompute the checks.

# Rule Code
1 The statement is strict JSON with _type, subject, predicateType, predicate; the first two and the predicate type are as above; subject is one member with a sha256 digest receipt_json_invalid, receipt_json_duplicate_key, receipt_json_number, receipt_fields, receipt_type_wrong, receipt_predicate_type_wrong, receipt_subject_malformed
2 The predicate has exactly the members above (policy, stop_decision and verifier_isolation optional), schema_version is probity-done-receipt/v1, verifier.id is a URI, and the policy digest is well formed receipt_schema_unsupported, receipt_predicate_malformed, receipt_verifier_malformed, receipt_predicate_fields, receipt_stop_decision_malformed, receipt_isolation_malformed
3 records names exactly the six core records, or those and both launch records; a carried isolation report names the contract record's digest receipt_records_shape, receipt_isolation_contract_differs
4 Every named record is present and its bytes have the named digest receipt_record_missing, receipt_record_digest_mismatch
5 The tree and observation records parse; the subject digest equals the tree record's tree_sha256 the record's code, receipt_subject_unbound
6 With launch records: both parse; the launch receipt names this run, the launch record's digest, the observation, capability and tree records' digests, and the same tree digest; its witnessScope equals both records' witness_scope; its coverage equals the observation's; and the observation does not say complete where the sensors report a lost event receipt_launch_context_mismatch, receipt_launch_digest_unbound, receipt_launch_records_unbound, receipt_launch_tree_unbound, receipt_launch_scope_differs, receipt_launch_coverage_differs, receipt_launch_coverage_overstated
7 external_effects equals the launch receipt's, or is empty without one receipt_external_effects_differ
8 The three checks, recomputed, equal checks receipt_check_not_recomputed
9 The verdict, derived, equals verdict receipt_verdict_not_derived

A record that breaks a record rule is refused with its own code and no receipt verdict is reached: a receipt is judged against records that parse.

Rule 6 reads a lost event as: the tracer was lost, the inotify queue overflowed or a watch failed, an escape (io_uring_setup, ptrace, process_vm_writev) or an unresolved call or an unparsed trace line was reported, or the host saw a change the trace did not.

What a receipt does not establish

Conformance

The corpora in this repository exercise the receipt and the checks it carries:

corpus what its members show
vectors-done-receipt/ whole runs and the receipts for them, honest and not, with a defect for every receipt rule
vectors-spoofed-tool-output/ fabricated and stale tool output against the observer's record
vectors-test-tampering/ edits of protected paths, the ones an observer sees and the ones it loses
vectors-grader-isolation/ reaching what grades the run: its output files, its tree, its interpreter

A reader conforms when it reaches every declared decision, reason, vantage and scope.