| Internet-Draft | AUDIT Decision Receipts | November 2026 |
| Gruszka | Expires 5 May 2027 | [Page] |
The architecture for auditing agent delegation and interactions proposed for the AUDIT effort names Interaction Records, Action Records and Authorization Transition Records, and leaves their encoding open. This document shows how an existing signed record, the decision receipt of the open-source proofbundle project, together with its companion outcome receipt, gives three such records exact bytes, a signature and digest bindings that a third party can check offline. The three cases are a human approval, a declined action and an executed action with its result. For each case the document maps the receipt members onto the record types of the architecture, states what the receipt adds and what it does not show, and gives an offline verification procedure. It states one requirement for approvals, that an approval is honoured at most once, also after the record of consumed approvals is rotated. Test vectors for all three cases, each with a counterexample, are included. This document defines no new format, predicate or registry.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 5 May 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
The architecture of [AUDIT-ARCH] asks that auditing link user intent, delegation, authorization and execution, and treats authorization as a state that changes over time. Three of its four record types (its Section 5) matter here. Interaction Records capture user-facing events such as approvals and refusals, Action Records capture events at the boundary where an action took effect, and Authorization Transition Records capture changes in permission state. Its field names are placeholders (its Section 7), and a canonical data model is a work item (WI-1).¶
An Auditor in that architecture does not trust the Agent (its Section 8), so a record helps only when a third party can check it. That needs exact bytes, a signature over them and bindings to inputs and evidence by digest. This document shows that the decision receipt of proofbundle [PB-DECISION], an in-toto Statement [INTOTO-STATEMENT] in a DSSE envelope [DSSE], signed with Ed25519 [RFC8032] over a payload in the serialization of [RFC8785], supplies them together with its companion outcome receipt [PB-OUTCOME]. It does so for three cases that Agent Guard, the gate of the proofbundle plugin for two coding agent hosts, produces (Section 5):¶
The document maps and verifies. It does not propose the decision receipt as the data model of the architecture and does not change the receipts it describes.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
All decision receipts here use the decisionType preActionAuthorization and carry validity with audience, nonce and expiresAt. A decision receipt is the gate's signed view of an event; the human act itself is the approval record, bound by digest. In the tables, IR stands for Interaction Record, ATR for Authorization Transition Record and AR for Action Record, followed by a placeholder field of the architecture. The fields common to all cases map as follows.¶
A gate asks a person before an action and records the answer. An earlier receipt for the same request, such as one with the verdict ESCALATE, is a separate record; this receipt does not reference it. After the answer the gate decides ALLOW for exactly the approved action. The approval record carries approvalId, approver, session, actionDigest (SHA-256 of the approved action bytes), approvedAt and validUntil. The decision receipt names it in inputSnapshot under the name "approval record".¶
| Record, field | Decision receipt |
|---|---|
| IR, approval | approval record, by its digest in inputSnapshot |
| IR, actor | approver in the approval record, equal to principal.id |
| ATR, previous_state | not recorded in this receipt; preceding state must be established separately |
| ATR, new_state | verdict ALLOW for parametersDigest |
| ATR, trigger | reasonCodes contains user_approval |
| ATR, actor | approver in the approval record |
The scope of the new state is one action, identified by its bytes. A second action, even one with the same name and target, needs a new approval.¶
A gate declines an action because a rule or a person says no. The receipt records the verdict DENY with its reasons. It may say in actionOutcome that the call was blocked, which is the decision maker's own statement.¶
| Record, field | Decision receipt |
|---|---|
| IR, refusal | verdict DENY or REFUSE |
| reason | reasonCodes |
| action | proposedAction with parametersDigest |
| no effect | actionOutcome.status blocked (self-asserted); no outcome with status executed names this decision in the supplied set |
| ATR | none; the state is unchanged |
A receipt cannot prove that something did not happen. An observer at the boundary can separately report that it observed no effect. The check in Section 3.5 detects a conflicting executed outcome in the supplied set; it does not establish absence of an effect.¶
A gate allows an action, the action runs, and an executor or an observer at the boundary records what happened in an outcome receipt signed with its own key. The outcome receipt names the decision by its content root in decisionRef.¶
| Record, field | Outcome and decision receipt |
|---|---|
| AR, event_id | outcomeId |
| AR, parent_id | traceContext.parentId of the referenced decision |
| decision binding | decisionRef, the content root of the decision |
| AR, timestamp | performedAt |
| AR, actor | executor.id |
| AR, action | requestedActionDigest, equal to parametersDigest |
| result | status executed, effectDigest |
| authorization | the decision (ALLOW, validity.expiresAt) |
| evidence | evidenceRefs of the decision |
The architecture asks that Action Records be produced at the boundary where the action took effect. Its WI-1-3 distinguishes the Recorder's signing identity from the recorded Agent's identity where they are operationally separate. Section 3.4 checks that the outcome and decision use different keys and that executor.id differs from decisionMaker.id. Those checks alone do not establish operational separation or that the outcome signer observed the boundary.¶
A decision may rest on an evaluation result, such as a test suite that passed. A Signed Evaluation Receipt [SER] records such a result. It appears in a decision receipt only as an entry of evidenceRefs whose digest is the receipt digest, SHA-256 over its payload bytes, as in [SER-MAPPINGS]. A verifier MUST NOT treat a valid Signed Evaluation Receipt, or its presence in evidenceRefs, as permission for an action. In the records of this document only a decision receipt with the verdict ALLOW, from a key the relying party trusts for decisions, records a permission.¶
The procedure below needs no network access. Each step either passes or ends the procedure with a failure named by the step. A later step never repairs an earlier failure.¶
The verifier needs the envelope bytes of each receipt and the Ed25519 public keys selected by its trust policy for the decision signer and, for Case 3, the outcome signer. It also needs the expected decisionMaker.id and, for Case 3, the expected executor.id whose assertion that policy permits under the outcome-signing key. An outcome signer may be an observer rather than the executor.¶
The verifier supplies an evaluation time T in UTC; it does not take T from a receipt. Expected audience and nonce and the action and effect byte strings are optional inputs. Case 1 requires the approval record bytes. Inputs or evidence whose bytes are not checked remain digest references, not verified contents.¶
Members. predicate MUST be a JSON object satisfying the field constraints of the strict v0.1 form in [PB-DECISION]. It MUST contain schemaVersion, decisionId, decisionType, decidedAt, decisionMaker, agent, principal, proposedAction, inputSnapshot, policyBoundary (with policyDigest), evidenceRefs, decision, notChecked, decisionChangeConditions, privacy and validity. Other predicate members are limited to recordedAt, delegationRefs, actionOutcome and traceContext.¶
decisionMaker, agent, principal, proposedAction, policyBoundary, decision, privacy and validity MUST be objects. decisionId, decisionMaker.id, agent.id and principal.id MUST be non-empty strings. inputSnapshot, evidenceRefs, notChecked and decisionChangeConditions MUST be arrays. decision.reasonCodes MUST be a non-empty array of non-empty strings.¶
schemaVersion MUST consist of "0.1." followed by one or more ASCII decimal digits. decisionType MUST be preActionAuthorization. Every timestamp, when non-null, MUST be an [RFC3339] UTC timestamp with a trailing "Z". Every non-null digest MUST be an object with exactly one member, sha256, whose value is 64 lowercase hexadecimal digits. A null is allowed only where the applicable field rules or Section 3.3 explicitly permit it.¶
decision.verdict MUST be ALLOW and decision.reasonCodes MUST contain user_approval. inputSnapshot MUST contain exactly one entry named "approval record". The approval record bytes A MUST be supplied, MUST hash to that entry's digest, and MUST be their own [RFC8785] serialization.¶
A MUST be a JSON object containing approvalId, approver, session, actionDigest, approvedAt and validUntil. approvalId, approver and session MUST be non-empty strings. actionDigest MUST have the digest form of Section 3.2 and MUST equal proposedAction.parametersDigest. approver MUST equal principal.id. approvedAt and validUntil MUST be [RFC3339] UTC timestamps with a trailing "Z". The bounds approvedAt <= decidedAt <= T < validity.expiresAt <= validUntil MUST hold.¶
Members. predicate MUST be a JSON object satisfying the field constraints of [PB-OUTCOME]. It MUST contain schemaVersion, outcomeId, decisionRef, executor, requestedActionDigest, status and performedAt. Other predicate members are limited to effectDigest, actualActionDigest, responseDigest, policyPurpose, receiverRefs, sequence, recordedAt, limitations, traceContext and validity.¶
schemaVersion MUST consist of "0.1." followed by one or more ASCII decimal digits. outcomeId MUST be a non-empty string. executor MUST be an object with a non-empty id string. status MUST be executed, refused, failed or partial. Each non-null timestamp MUST be an [RFC3339] UTC timestamp with a trailing "Z". Each non-null digest MUST be an object with exactly one member, sha256, whose value is 64 lowercase hexadecimal digits. A null is allowed only where the applicable field rules permit it.¶
An Auditor that holds the receipts and approval records of a run can check two properties that no single receipt shows. The Auditor MUST obtain the relying party's consumption scope from its audit context or trust policy; that scope is not inferred from an approvalId alone. First, within that scope, no two distinct ALLOW decision payloads, identified by different content roots, may cite approval records with the same identity (approvalId, actionDigest and session). Such a pair violates Section 4; additional copies or signatures of one payload do not.¶
Second, no outcome receipt with status executed may name, in decisionRef, the content root of a receipt with the verdict DENY or REFUSE. Both checks are only as complete as the set of receipts the Auditor holds. The first check does not detect two executions that reuse one ALLOW receipt without producing another decision receipt.¶
A pass shows that the holder of the decision maker's key signed exactly these payload bytes, that they name the action, approval, inputs and evidence by the digests checked, and, for Case 3, that the holder of a different key, which the trust policy permits to attest the expected executor.id, signed an outcome with status executed bound to that decision. It does not show that the signer's statements are true, that the approval came from the named person, that the outcome signer observed the boundary, that no other receipt exists, or that the keys were not misused. Those questions belong to the trust policy, to signed approvals, to observers and to transparency registration.¶
An approval in Case 1 frees one action once. For live use, the relying party MUST apply the time bounds of Section 3.3 with its own current time as T. A historical audit time MUST NOT make an expired approval usable. A relying party that lets an action run because of an approval:¶
Rotation is the step where this requirement is most easily lost. A record that is full is moved aside so that new approvals can be written; if the relying party then reads only the new generation, approvals of the old one may become usable again. Items 4 and 5 preserve the consumed state without requiring a particular storage format. A decision receipt for Case 1 binds the approval record by digest, so Section 3.5 can expose distinct ALLOW decisions for the same approval identity. Preventing reuse of an approval or of an already issued ALLOW remains the relying party's duty.¶
This section is given per [RFC7942] and is to be removed by the RFC Editor before publication.¶
proofbundle 6.4.0 is on PyPI [PB-PYPI] with the classifier "Development Status :: 4 - Beta"; its tag v6.4.0 is commit 699545521ec5ef479bb84e61fe7461fc79ed558c of [PB-REPO]. There the decision receipt predicate is described as shipped since version 2.1.0 and the outcome predicate as experimental. In the seven command line runs reported in Appendix A, the four passing vectors returned exit code 0, the invalid signature of V1-bad returned 1, and the non-canonical payload of V2-bad and the decision binding mismatch of V3-bad each returned 2. These results do not specify the command line interface's complete exit-code contract.¶
Agent Guard is the name used here for the preview in plugins/proofbundle of the same repository, inspected at commit 8bb289c8b41d32c54734155fc00756231093bc2a. This inspected revision is not the release tag v6.4.0. Its public README names the preview the proofbundle plugin for Claude Code and Codex.¶
In the inspected code, the configured receipt pilot uses a separate signing service for preActionAuthorization decisions with verdict ALLOW, DENY or ESCALATE for the writes it supports. An observer signs outcome receipts for effects it matches to decisions. The runbook assigns the observer to an operator account separate from the agent session. The approval path requires an explicit operator configuration and is not enabled under Codex. It binds an approval to the command bytes and its session. Its time check accepts the approval from 60 seconds before the recorded answer, to allow for clock skew, through 15 minutes after it. For a write covered by the receipt pilot, the hook obtains a signed ALLOW before recording its own use of the approval and allowing the command. These are observations of the source and runbook, not a live Agent Guard execution measured for this document.¶
The test vectors of Appendix A were produced with proofbundle 6.4.0 from PyPI and test keys, not by Agent Guard. For an approval, the inspected pilot writes released_by_owner among its reason codes where the vectors write user_approval. It does not put the approval record's digest into inputSnapshot, may add the digest of configured closing rules there, sets no traceContext or actionOutcome, and leaves evidenceRefs empty. The vectors therefore do not demonstrate that the preview produces Case 1 receipts conforming to this document.¶
At the inspected commit, the signing service keeps its record of honoured approvals without eviction and refuses a new approval when the record holds 4096. The runbook has an operator rotate a full record, with the service stopped, by copying it into the folder honoured-releases of the records and then removing it. The service treats an approval named in the current record or in any file of that folder as honoured, before its decision and again under its lock before it releases the signature. If that folder or a file in it is a symbolic link, is owned by an account other than root or the service's own, or is writable by another account, or if a file there does not read as a record, the service releases no signature in the configuration in which it honours approvals.¶
The hook keeps its own record of the approvals it has used, keyed by an identity that excludes the answer time, and treats a copy of a used approval as used whatever answer time it carries. When the record holds 1024 uses, the hook removes only uses recorded more than 15 minutes and 60 seconds earlier, after which the copy that was used is no longer within its window, and keeps a digest of each removed identity, which it does not remove; a copy of such an identity is refused. If the record is still full, would exceed 512 KiB, contains a field the hook does not know, or cannot be read or written, the hook does not honour the approval.¶
In both layers the validity window is taken from the answer time of the copy in use, and neither keeps an original validity per identity. For an identity that has not yet been used, a copy with a later answer time therefore opens a later window; item 2 of Section 4 does not permit that.¶
At the earlier commit ba8466d1a04334a5d83b3eaa12e30a464cc9d686, the signing service did not read archived records and the hook removed its oldest use without checking that entry's age; both were gaps relative to Section 4.¶
No conformance of Agent Guard to Section 4 and no enforcement of single use in a running host is claimed from these source-level observations.¶
The separate Go checker supplied with the vectors and the proofbundle 6.4.0 command line interface agree on acceptance or rejection of all seven vectors. This comparison does not establish complete equivalence of the verifiers, conformance of Agent Guard to Sections 3 and 4, or enforcement of single use in a running host. No later implementation revision is assessed here.¶
This information is given per [RFC7942] and does not constitute endorsement.¶
Key binding. A receipt is only as trustworthy as the verifier's choice of keys and its policy for the identities those keys may attest. Section 3.2 checks decisionMaker.id; Section 3.4 checks the expected executor.id under the outcome-signing key. An observer's signature over an executor identifier is not the executor's own signature. A key held in the recorded Agent's process gives an Auditor that does not trust that Agent little assurance. The inspected Agent Guard design uses separate service accounts as described in Section 5.¶
Exact bytes. A verifier that re-serializes a payload before checking it can be led to accept bytes that were never signed, or to compute another content root. Step 4 refuses such payloads, and vector V2-bad tests it.¶
Replay. An ALLOW for one action must not free another, and an outcome that names another decision must not count as the result of this one. parametersDigest binds the action bytes, validity binds audience, nonce and expiry, decisionRef binds the outcome, and Section 4 requires the relying party to prevent reuse of an approval. V1-bad tests rejection of changed payload bytes under an unchanged signature. V3-bad tests rejection of an outcome bound to a different decision. The seven vectors do not test consumption-state retention or rotation.¶
Self-assertion. actionOutcome in a decision receipt and an outcome receipt whose effect cannot be resolved are statements of their signers. A digest that is present but cannot be resolved to bytes was chosen by the signer.¶
Suppression. A decision maker can fail to write a receipt for a decision it does not want seen. Independent observers at the boundary, as the architecture requires for Action Records, and registration with a Transparency Service reduce this risk; this document does not remove it.¶
Clocks. decidedAt and performedAt come from the signers' clocks. Ordering against outside events needs a timestamp or a transparency registration.¶
The receipts of this document carry digests of action bytes, approval records and effects instead of their content, and state rawInputsIncluded false. A digest of a short or predictable input, such as a command line, can be found by trying candidate inputs; a deployment that must hide such inputs keeps the receipts as private as the inputs, or adds a secret salt to the digested bytes. Identifiers such as principal.id, agent.id and traceContext link a person's actions across services, as Section 9 of [AUDIT-ARCH] describes; pairwise or opaque identifiers reduce that linkage. Registering a receipt reveals that a decision took place at a time, even when only its content root is registered.¶
This document has no IANA actions.¶
The vectors were generated with proofbundle 6.4.0 from PyPI and are for testing only. Each key seed is SHA-256 over a public ASCII label, so that every byte can be rebuilt; these keys MUST NOT be used for any real receipt. Ed25519 is deterministic, so the signatures below follow from the seeds and the payloads.¶
Each key is given by its label, Ed25519 seed (hex), public key (base64) and keyid. The key labelled gate-1 signs the decision receipts; the key labelled observer-1 signs the outcome receipts. For these tests, the trust policy accepts decisionMaker.id gate-1 under the gate-1 key and permits the observer-1 key to attest executor.id forge:agent-42-bot. The key label observer-1 does not assert that the observer is the executing account.¶
gate-1 draft-gruszka-audit-decision-receipts test key gate-1 100fa92fa6464c141f791a96327f555c1c0cfbf3095af986537e50c3c4335f8c TWfs75IN1In3CNI8TnyIOUHgtwilOPGcRFQLJ670CJc= SHA256:NjZMAelLq9XON3pxV5tR5hZjX99NkQyw4gGJSRc2gP8 observer-1 draft-gruszka-audit-decision-receipts test key observer-1 f03ad5b668ad225ea15fd9fd272fc8df84be59f467da86b0f2ad0a5ad26f4ddd 4hDsi5TCL+A+9SB7k2l4EkY7XePUheE+zE9mBazt2Fs= SHA256:q/d0VZCREBObUFi8PWKWXEF4lVNeYPuGn0GuOqd1ij0¶
Each input is given by a name, its bytes, its size and its SHA-256. "\n" stands for one line feed byte. The bytes of ap, a3 and e3 are the serialization under [RFC8785] of the JSON value shown, on one line and without white space. In ap, SHA-256(a1) stands for the JSON string of the 64 lowercase hexadecimal digits of the SHA-256 of a1. pol is the policy named by every policyDigest. B is the payload of vector P1 of [SER], Appendix A.2.1, cited by V3-decision.¶
a1 "git push origin refs/heads/topic:refs/heads/topic\n" 50 bytes, SHA-256 dec54f771352454097310ae4bd6608c0070ebebb3955c6be013c1b3a8e310bd0¶
a1x "git push --force origin refs/heads/topic:refs/heads/main\n" 57 bytes, SHA-256 b683418806875ff79c6fa190c50edab0b96577ad8062de5ba7b41dc1d9c363fb¶
a2 "gh pr merge 7 --repo acme/widgets --merge\n" 42 bytes, SHA-256 5a7549948a1441addb710f5ada59e06dbce25cc5a59accf14f140379f47adb5b¶
pol "example gate rules, revision 1\n" 31 bytes, SHA-256 43789b41915ae0febcd13263d293101af5ab041eb7c234e1cbe625d6312815f6¶
ap, 231 bytes, SHA-256
88e5b6d69b3e4bc250a42aeffea0414d406e5f04ba4f991ee432a78231da1170
{
"actionDigest": {
"sha256": SHA-256(a1)
},
"approvalId": "appr-001",
"approvedAt": "2026-10-15T09:00:00Z",
"approver": "user-123",
"session": "sess-1",
"validUntil": "2026-10-15T09:15:00Z"
}
¶
a3, 81 bytes, SHA-256
9165c850b7e03cd59578efa1769be74c5386cff5ce96df6b59e25d38ad6f88b4
{
"body": "All required checks passed for 4f1c2a9.",
"pull": 7,
"repo": "acme/widgets"
}
¶
e3, 93 bytes, SHA-256
d3d824cf55168298c0f651244853ec9d9620ea09087abe899243ceb8f9df3a6d
{
"author": "agent-42-bot",
"body": "All required checks passed for 4f1c2a9.",
"id": 9001,
"pull": 7
}
¶
B, 458 bytes, SHA-256 8cd849a6dcce08aee334511d7b2f31c54e478885dda1795d3c8f972dd0d4811c¶
The payload bytes of each vector are the serialization under [RFC8785] of a Statement {"_type": "https://in-toto.io/Statement/v1", "predicateType": P, "predicate": D, "subject": [{"name": N, "digest": {"sha256": H}}]}, where H is SHA-256 over the serialization of D. For a decision receipt P is the decision receipt type and N is "decision:" followed by decisionId; for an outcome receipt P is the outcome receipt type and N is "outcome:" followed by outcomeId.¶
Each predicate is given as a list of assignments that starts from the predicate named in its first line, {} being the empty object. An assignment sets the member at the path to the JSON value shown. A path is a sequence of member names joined by dots, where a decimal index i in square brackets after a name selects element i of an array; missing objects on the path are created, and an assignment to element i of an array with i elements appends it. Members not named keep their value. SHA-256(x) stands for the JSON string of the 64 lowercase hexadecimal digits of the SHA-256 of input x, and R(V) for that of the payload bytes of vector V.¶
V1-good, from {}
agent.id = "agent-42"
decidedAt = "2026-10-15T09:00:05Z"
decision.reasonCodes = ["user_approval"]
decision.verdict = "ALLOW"
decisionChangeConditions = []
decisionChangeConditions[0].conditionType = "ruleChange"
decisionChangeConditions[0].description =
"a changed gate, mode or rule of the gate"
decisionId = "dec-0001"
decisionMaker.id = "gate-1"
decisionType = "preActionAuthorization"
evidenceRefs = []
inputSnapshot = []
inputSnapshot[0].digest.sha256 = SHA-256(a1)
inputSnapshot[0].mediaType = "text/plain"
inputSnapshot[0].name = "approved bytes"
inputSnapshot[1].digest.sha256 = SHA-256(ap)
inputSnapshot[1].mediaType = "application/json"
inputSnapshot[1].name = "approval record"
notChecked = []
notChecked[0].field = "content"
notChecked[0].impact = "an approved text can be wrong"
notChecked[0].reason =
"the gate does not judge whether the text is true"
policyBoundary.bundleRevision = "1"
policyBoundary.decisionPath = "outbound-gate/allow-write"
policyBoundary.policyDigest.sha256 = SHA-256(pol)
policyBoundary.policyEngine = "proofbundle-pilot/github-write"
policyBoundary.policyId = "outbound-gate-rules"
principal.id = "user-123"
privacy.rawInputsIncluded = false
proposedAction.actionType = "git.push"
proposedAction.method = "write"
proposedAction.parametersDigest.sha256 = SHA-256(a1)
proposedAction.target.name =
"https://git.example/acme/widgets refs/heads/topic"
proposedAction.target.uri = "https://git.example/acme/widgets"
schemaVersion = "0.1.0"
traceContext.parentId = "int-002"
traceContext.traceId = "trace-abc"
validity.audience = ["observer-1"]
validity.expiresAt = "2026-10-15T09:05:05Z"
validity.nonce = "dec-0001"
¶
V2-good, from V1-good actionOutcome.outcomeRef = null actionOutcome.performedAt = null actionOutcome.status = "blocked" decidedAt = "2026-10-15T09:05:00Z" decision.reasonCodes = ["required_check_failed"] decision.verdict = "DENY" decisionId = "dec-0002" inputSnapshot = [] inputSnapshot[0].digest.sha256 = SHA-256(a2) inputSnapshot[0].mediaType = "text/plain" inputSnapshot[0].name = "proposed bytes" policyBoundary.decisionPath = "outbound-gate/deny-write" proposedAction.actionType = "github.merge" proposedAction.parametersDigest.sha256 = SHA-256(a2) proposedAction.target.name = "https://git.example/acme/widgets/pull/7" proposedAction.target.uri = "https://git.example/acme/widgets/pull/7" traceContext.parentId = "int-003" validity.expiresAt = "2026-10-15T09:10:00Z" validity.nonce = "dec-0002"¶
V3-decision, from V1-good decidedAt = "2026-10-15T09:10:00Z" decision.reasonCodes = ["evidence_verified"] decisionId = "dec-0003" evidenceRefs = [] evidenceRefs[0].digest.sha256 = SHA-256(B) evidenceRefs[0].relation = "evaluationResult" inputSnapshot = [] inputSnapshot[0].digest.sha256 = SHA-256(a3) inputSnapshot[0].mediaType = "application/json" inputSnapshot[0].name = "approved bytes" proposedAction.actionType = "github.conversationComment" proposedAction.parametersDigest.sha256 = SHA-256(a3) proposedAction.target.name = "https://git.example/acme/widgets/pull/7" proposedAction.target.uri = "https://git.example/acme/widgets/pull/7" traceContext.parentId = "int-004" validity.expiresAt = "2026-10-15T09:15:00Z" validity.nonce = "dec-0003"¶
V3-good, from {}
decisionRef.sha256 = R(V3-decision)
effectDigest.sha256 = SHA-256(e3)
executor.id = "forge:agent-42-bot"
outcomeId = "out-0001"
performedAt = "2026-10-15T09:10:02Z"
requestedActionDigest.sha256 = SHA-256(a3)
schemaVersion = "0.1.0"
status = "executed"
¶
V3-bad, from V3-good decisionRef.sha256 = R(V1-good) outcomeId = "out-0002"¶
V1-bad has the payload bytes of V1-good with every occurrence of the hexadecimal SHA-256 of a1 replaced by that of a1x, and keeps the signature of V1-good. V2-bad has as payload bytes the output of Python's json.dumps(s, sort_keys=True) for the Statement s of V2-good, with a space after each comma and colon, and is validly signed over them by gate-1.¶
Every envelope is {"payload": base64 of the payload bytes, "payloadType": "application/vnd.in-toto+json", "signatures": [{"keyid": keyid of the signer, "sig": signature}]}. Each block below gives the vector, the evaluation time T, the expected result with the exit code of the proofbundle 6.4.0 command line interface, the payload size and signer, the SHA-256 of the payload bytes (the content root where the payload is canonical) and the signature in base64 on two lines. The expected audience is observer-1 and the expected nonce is the decisionId. decision verify ran with --strict, --aud, --nonce and --verification-time T; outcome verify with --strict, --expected-decision-ref R(V3-decision) and --decision-maker-id gate-1.¶
V1-good, T 2026-10-15T09:01:00Z, pass, exit 0 1829 bytes, signed by gate-1 b9dfd26537542632013d0df166141a788551b4987dfa33b060d5b5ebba78dfb1 GkvzPjklV5ZbtomNRXbS4+O4MIOSw8xGmA0diAEYKEm0 s4Q9qLkL1j7+e5RZnT9l56tfB2utDoNycACsSvSoDw==¶
V1-bad, T 2026-10-15T09:01:00Z, fail at step 3, exit 1 1829 bytes, signed by gate-1 69eb87fb33a29153182b0e0c89a444023cc00c82fe7c8db1c7e0893363b2c926 GkvzPjklV5ZbtomNRXbS4+O4MIOSw8xGmA0diAEYKEm0 s4Q9qLkL1j7+e5RZnT9l56tfB2utDoNycACsSvSoDw==¶
V2-good, T 2026-10-15T09:06:00Z, pass, exit 0 1765 bytes, signed by gate-1 ed149a1c7383842eebf8304001da9de97d130b0b15080d1315ab8fbf1274ed44 66CUa1kz6aOm6gbdctRg4l97fW8x185nxpsB5rkCFj+a HZVjRjc5hDnQu6v4J/bHzaufyHhPCPdM6XUAWiylAQ==¶
V2-bad, T 2026-10-15T09:06:00Z, fail at step 4, exit 2 1866 bytes, signed by gate-1 646d4b2229a55c54848361030bf6d57816597a96bd7d1fb00d7da0f03b009bd2 2yjE3MxaVXdureYhh50+d3XdDAsJDhW3nacud4WCbzBM R/NLHyxeqIvbsDKQTabl+X+5O4CSXu17rA7lFYN0Cw==¶
V3-decision, T 2026-10-15T09:11:00Z, pass, exit 0 1827 bytes, signed by gate-1 0014b1c419798125c3984e85a4b322ee619a3227978840cac5f9d282b189521f DDLIUtX6uk5A3AqcvYP9f9zYeHJAWNTrdBZwpXmEQq4y u0bivvsiNEjx4u2Kw2U6Mr11lcWoRZnSo1w5MX2PCA==¶
V3-good, pass, exit 0 694 bytes, signed by observer-1 f0ff90cf25e2db1e989fa79869d4b3b46899112e8c2269d9a18a64b751aa7274 MCUgSY+fGUO2cm5DnxBVKEC/BBSyeS359aj2rYowwkHT j+CskAgraX5TirUBic2i2KyUUaxBYMMdVafhu3tcAQ==¶
V3-bad, fail at binding, exit 2 694 bytes, signed by observer-1 48be708cf3f959556323474f4a5c9193b417d2bfea5c88bb852daf00b64827b4 uEgT4TFv7jEgmH7pwJcPBEEZM+n2wGjic6U6yYWOU3Dh Ta0tMuimQ8q4Ha1lgw4zWNR9YBag4GmVt9SdsfzwCQ==¶
The payload bytes of V1-good, V2-good, V3-decision, V3-good and V3-bad and the inputs ap, a3, e3 and B are each equal to three serializations of their parsed value: Python's json.dumps with sort_keys=True, separators=(",", ":") and either setting of ensure_ascii; the rfc8785 package 0.1.4 for Python; and the independent JavaScript implementation [JCS-JS]. The values contain no floating-point numbers and no characters outside ASCII, which keeps these serializations equal. The payload of V2-bad differs from all of them, as intended.¶
The author used AI tools to help draft this document and to generate and cross-check its examples. The author reviewed the entire text and is responsible for it.¶
Citing adjacent work in the References does not imply that its authors endorse this document.¶