<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE rfc [
  <!ENTITY nbsp    "&#160;">
  <!ENTITY zwsp   "&#8203;">
  <!ENTITY nbhy   "&#8209;">
  <!ENTITY wj     "&#8288;">
]>
<rfc xmlns:xi="http://www.w3.org/2001/XInclude" ipr="trust200902" docName="draft-gruszka-audit-decision-receipts-00" category="info" submissionType="IETF" tocInclude="true" tocDepth="2" sortRefs="true" symRefs="true" version="3">
  <front>
    <title abbrev="AUDIT Decision Receipts">Decision Receipts for Approvals, Declined Actions and Executed Actions in Agent Audit Records</title>
    <seriesInfo name="Internet-Draft" value="draft-gruszka-audit-decision-receipts-00"/>
    <author initials="K." surname="Gruszka" fullname="Konrad Gruszka">
      <organization>b7n0de</organization>
      <address>
        <postal>
          <country>Germany</country>
        </postal>
        <email>kontakt@b7n0de.com</email>
      </address>
    </author>
    <date/>
    <area>Security</area>
    <keyword>AUDIT</keyword>
    <keyword>agent</keyword>
    <keyword>decision receipt</keyword>
    <keyword>in-toto</keyword>
    <keyword>DSSE</keyword>
    <abstract>
<t>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.</t>
    </abstract>
  </front>
  <middle>

<section anchor="introduction">
      <name>Introduction</name>
      <t>The architecture of <xref target="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).</t>
      <t>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 <xref target="PB-DECISION"/>, an in-toto Statement
<xref target="INTOTO-STATEMENT"/> in a DSSE envelope <xref target="DSSE"/>, signed with
Ed25519 <xref target="RFC8032"/> over a payload in the serialization of
<xref target="RFC8785"/>, supplies them together with its companion outcome receipt
<xref target="PB-OUTCOME"/>. It does so for three cases that Agent Guard, the gate of
the proofbundle plugin for two coding agent hosts, produces
(<xref target="implementation-status"/>):</t>
      <ol>
        <li>a human approval of one action (an Interaction Record of an approval and an
Authorization Transition Record with the trigger user_approval);</li>
        <li>a declined action, with its reason and without an effect;</li>
        <li>an executed action with its result (an Action Record).</li>
      </ol>
      <t>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.</t>
      <section anchor="requirements-language">
        <name>Requirements Language</name>
        <t>The key words "<bcp14>MUST</bcp14>", "<bcp14>MUST NOT</bcp14>", "<bcp14>REQUIRED</bcp14>", "<bcp14>SHALL</bcp14>", "<bcp14>SHALL
NOT</bcp14>", "<bcp14>SHOULD</bcp14>", "<bcp14>SHOULD NOT</bcp14>", "<bcp14>RECOMMENDED</bcp14>", "<bcp14>NOT RECOMMENDED</bcp14>",
"<bcp14>MAY</bcp14>", and "<bcp14>OPTIONAL</bcp14>" in this document are to be interpreted as
described in BCP&nbsp;14 <xref target="RFC2119"/> <xref target="RFC8174"/> when, and only when, they
appear in all capitals, as shown here.</t>
      </section>
      <section anchor="terms">
        <name>Terms</name>
        <dl newline="false" spacing="compact">
          <dt>Decision receipt, outcome receipt:</dt>
          <dd>A DSSE envelope whose payload is an in-toto Statement v1 with the predicate
type https://b7n0de.com/proofbundle/predicates/decision-receipt/v0.1 or
https://b7n0de.com/proofbundle/predicates/action-outcome/v0.1.</dd>
          <dt>Payload bytes:</dt>
          <dd>The bytes obtained by base64 decoding the payload member of the envelope. A DSSE signature covers the pre-authentication encoding of payloadType and these bytes.</dd>
          <dt>Content root:</dt>
          <dd>SHA-256 <xref target="RFC6234"/> over the payload bytes. Other digests commit to the byte strings identified by their respective fields.</dd>
          <dt>Approval record:</dt>
          <dd>The record of one human act that approves one action
(<xref target="case-approval"/>).</dd>
          <dt>Relying party:</dt>
          <dd>The component that lets an action run because of a decision.</dd>
          <dt>Consumption record:</dt>
          <dd>The durable record in which a relying party notes each approval it has
honoured.</dd>
        </dl>
      </section>
    </section>

<section anchor="mapping">
      <name>Mapping the Three Cases onto AUDIT Records</name>
      <t>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.</t>
      <dl newline="false" spacing="compact" indent="26">
        <dt>event_id</dt><dd>decisionId (outcomeId in an outcome receipt)</dd>
        <dt>trace_id, parent_id</dt><dd>traceContext.traceId, traceContext.parentId</dd>
        <dt>timestamp</dt><dd>decidedAt (performedAt in an outcome receipt)</dd>
        <dt>actor, on_behalf_of</dt><dd>agent.id, principal.id</dd>
        <dt>action</dt><dd>proposedAction (actionType, target, method)</dd>
        <dt>exact action bytes</dt><dd>proposedAction.parametersDigest</dd>
        <dt>authorization.expires_at</dt><dd>validity.expiresAt</dd>
        <dt>delegation_chain</dt><dd>none (delegationRefs is not used here)</dd>
        <dt>record signer</dt><dd>decisionMaker.id, bound to the key by a trust policy</dd>
      </dl>

      <section anchor="case-approval">
        <name>Case 1, a Human Approval</name>
        <t>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".</t>
        <table anchor="tab-approval">
          <name>Case 1</name>
          <thead><tr><th>Record, field</th><th>Decision receipt</th></tr></thead>
          <tbody>
            <tr><td>IR, approval</td><td>approval record, by its digest in inputSnapshot</td></tr>
            <tr><td>IR, actor</td><td>approver in the approval record, equal to principal.id</td></tr>
            <tr><td>ATR, previous_state</td><td>not recorded in this receipt; preceding state must be established separately</td></tr>
            <tr><td>ATR, new_state</td><td>verdict ALLOW for parametersDigest</td></tr>
            <tr><td>ATR, trigger</td><td>reasonCodes contains user_approval</td></tr>
            <tr><td>ATR, actor</td><td>approver in the approval record</td></tr>
          </tbody>
        </table>
        <t>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.</t>
      </section>

      <section anchor="case-declined">
        <name>Case 2, a Declined Action</name>
        <t>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.</t>
        <table anchor="tab-declined">
          <name>Case 2</name>
          <thead><tr><th>Record, field</th><th>Decision receipt</th></tr></thead>
          <tbody>
            <tr><td>IR, refusal</td><td>verdict DENY or REFUSE</td></tr>
            <tr><td>reason</td><td>reasonCodes</td></tr>
            <tr><td>action</td><td>proposedAction with parametersDigest</td></tr>
            <tr><td>no effect</td><td>actionOutcome.status blocked (self-asserted); no outcome with status executed names this decision in the supplied set</td></tr>
            <tr><td>ATR</td><td>none; the state is unchanged</td></tr>
          </tbody>
        </table>
        <t>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 <xref target="sets"/> detects a conflicting executed outcome in the supplied set; it does not establish absence of an effect.</t>
      </section>

      <section anchor="case-executed">
        <name>Case 3, an Executed Action and Its Result</name>
        <t>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.</t>
        <table anchor="tab-executed">
          <name>Case 3</name>
          <thead><tr><th>Record, field</th><th>Outcome and decision receipt</th></tr></thead>
          <tbody>
            <tr><td>AR, event_id</td><td>outcomeId</td></tr>
            <tr><td>AR, parent_id</td><td>traceContext.parentId of the referenced decision</td></tr>
            <tr><td>decision binding</td><td>decisionRef, the content root of the decision</td></tr>
            <tr><td>AR, timestamp</td><td>performedAt</td></tr>
            <tr><td>AR, actor</td><td>executor.id</td></tr>
            <tr><td>AR, action</td><td>requestedActionDigest, equal to parametersDigest</td></tr>
            <tr><td>result</td><td>status executed, effectDigest</td></tr>
            <tr><td>authorization</td><td>the decision (ALLOW, validity.expiresAt)</td></tr>
            <tr><td>evidence</td><td>evidenceRefs of the decision</td></tr>
          </tbody>
        </table>
        <t>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. <xref target="verify-outcome"/> 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.</t>
      </section>

      <section anchor="adds">
        <name>What the Receipt Adds</name>
        <ul>
          <li>Exact bytes. The payload is its own serialization under <xref target="RFC8785"/>. A verifier uses the received payload bytes for the signature input and content root. Canonical serialization is used to check equality and to derive the predicate's subject digest, never to repair or replace the received payload.</li>
          <li>A signature. Ed25519 over the DSSE pre-authentication encoding of the payload
type and the payload bytes.</li>
          <li>Binding by digest. The action bytes (parametersDigest), the approval record and
other inputs (inputSnapshot), the policy (policyBoundary.policyDigest), evidence
(evidenceRefs), the decision itself (decisionRef in the outcome) and the observed
effect (effectDigest) are named by SHA-256.</li>
          <li>Stated limits. The member notChecked lists what the decision maker did not
check, and decisionChangeConditions lists what would change the decision.</li>
        </ul>
      </section>

      <section anchor="not-adds">
        <name>What the Receipt Does Not Do</name>
        <ul>
          <li>It does not show that a decision was correct, safe or lawful.</li>
          <li>It does not authenticate the approval. The approval record in this document is not signed by the person it names. A deployment can additionally cite a separately signed approval in inputSnapshot. Validating that signature and establishing its correspondence to the approval record require rules beyond this document. The signed approvals of <xref target="IZMAYLOV"/> are one possible source of such evidence.</li>
          <li>It does not show that an effect happened. An outcome receipt shows that its
signer stated an effect with a given digest. A digest that cannot be resolved
to bytes shows only that a value was written.</li>
          <li>It does not register anything. Transparency in the sense of
<xref target="RFC9943"/> needs a separate registration and a SCITT Receipt.</li>
          <li>It does not detect a receipt that was never written, unless an observer or a
sequence counter is added.</li>
          <li>Its times are the signer's clock.</li>
          <li>It is not an access token. A verified ALLOW is a record of a decision, and the
executing system makes its own authorization check.</li>
        </ul>
      </section>

      <section anchor="evaluation-receipts">
        <name>Evaluation Receipts Are Evidence, Never Authorization</name>
        <t>A decision may rest on an evaluation result, such as a test suite that
passed. A Signed Evaluation Receipt <xref target="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
<xref target="SER-MAPPINGS"/>. A verifier <bcp14>MUST NOT</bcp14> 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.</t>
      </section>
    </section>

<section anchor="verification">
      <name>Offline Verification</name>
      <t>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.</t>
      <section anchor="verify-inputs">
        <name>Inputs</name>
        <t>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.</t>
        <t>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.</t>
      </section>
      <section anchor="verify-decision">
        <name>Decision Receipt</name>
        <ol>
          <li>Envelope. Parse the envelope as a JSON object with the members payload,
payloadType and signatures. payloadType <bcp14>MUST</bcp14> be
application/vnd.in-toto+json.</li>
          <li>Payload bytes. Decode payload as base64 <xref target="RFC4648"/> into the
payload bytes P.</li>
          <li>Signature. Form PAE = "DSSEv1" SP LEN(type) SP type SP LEN(P) SP P, where SP
is one space, LEN is the length in bytes as ASCII decimal without leading zeros, and
type is the payloadType. At least one entry of signatures <bcp14>MUST</bcp14> carry a
base64 sig that verifies as an Ed25519 signature over PAE under the expected key. The
keyid member is not used to select a key.</li>
          <li>Canonical form. P <bcp14>MUST</bcp14> be valid UTF-8 and parse as one JSON
value with no duplicate member names. The serialization of the parsed value under
<xref target="RFC8785"/> <bcp14>MUST</bcp14> be byte for byte equal to P. A verifier
<bcp14>MUST NOT</bcp14> continue with a re-serialized form.</li>
          <li>Content root. Compute R = SHA-256(P).</li>
          <li>Statement. _type <bcp14>MUST</bcp14> be https://in-toto.io/Statement/v1 and
predicateType the decision receipt type of <xref target="terms"/>. subject
<bcp14>MUST</bcp14> have one entry whose digest.sha256 is SHA-256 over the
<xref target="RFC8785"/> serialization of predicate.</li>
          <li><t>Members. predicate <bcp14>MUST</bcp14> be a JSON object satisfying the field constraints of the strict v0.1 form in <xref target="PB-DECISION"/>. It <bcp14>MUST</bcp14> 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.</t><t>decisionMaker, agent, principal, proposedAction, policyBoundary, decision, privacy and validity <bcp14>MUST</bcp14> be objects. decisionId, decisionMaker.id, agent.id and principal.id <bcp14>MUST</bcp14> be non-empty strings. inputSnapshot, evidenceRefs, notChecked and decisionChangeConditions <bcp14>MUST</bcp14> be arrays. decision.reasonCodes <bcp14>MUST</bcp14> be a non-empty array of non-empty strings.</t><t>schemaVersion <bcp14>MUST</bcp14> consist of "0.1." followed by one or more ASCII decimal digits. decisionType <bcp14>MUST</bcp14> be preActionAuthorization. Every timestamp, when non-null, <bcp14>MUST</bcp14> be an <xref target="RFC3339"/> UTC timestamp with a trailing "Z". Every non-null digest <bcp14>MUST</bcp14> 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 <xref target="verify-cases"/> explicitly permit it.</t></li>
          <li>Signer. decisionMaker.id <bcp14>MUST</bcp14> be the identifier the trust
policy binds to the key of step 3.</li>
          <li>Validity. validity <bcp14>MUST</bcp14> carry a non-empty array of non-empty audience strings, a non-empty nonce string and an expiresAt timestamp. decidedAt <bcp14>MUST NOT</bcp14> be later than T, and T <bcp14>MUST</bcp14> be strictly earlier than expiresAt. Where an expected audience is given, audience <bcp14>MUST</bcp14> contain it; where an expected nonce is given, nonce <bcp14>MUST</bcp14> equal it.</li>
          <li>Action bytes. Where the action bytes are given, their SHA-256
<bcp14>MUST</bcp14> equal proposedAction.parametersDigest.sha256.</li>
        </ol>
      </section>
      <section anchor="verify-cases">
        <name>Checks per Case</name>
        <dl newline="true" spacing="normal">
          <dt>Case 1, approval.</dt>
          <dd><t>decision.verdict <bcp14>MUST</bcp14> be ALLOW and decision.reasonCodes <bcp14>MUST</bcp14> contain user_approval. inputSnapshot <bcp14>MUST</bcp14> contain exactly one entry named "approval record". The approval record bytes A <bcp14>MUST</bcp14> be supplied, <bcp14>MUST</bcp14> hash to that entry's digest, and <bcp14>MUST</bcp14> be their own <xref target="RFC8785"/> serialization.</t><t>A <bcp14>MUST</bcp14> be a JSON object containing approvalId, approver, session, actionDigest, approvedAt and validUntil. approvalId, approver and session <bcp14>MUST</bcp14> be non-empty strings. actionDigest <bcp14>MUST</bcp14> have the digest form of <xref target="verify-decision"/> and <bcp14>MUST</bcp14> equal proposedAction.parametersDigest. approver <bcp14>MUST</bcp14> equal principal.id. approvedAt and validUntil <bcp14>MUST</bcp14> be <xref target="RFC3339"/> UTC timestamps with a trailing "Z". The bounds approvedAt &lt;= decidedAt &lt;= T &lt; validity.expiresAt &lt;= validUntil <bcp14>MUST</bcp14> hold.</t></dd>
          <dt>Case 2, declined.</dt>
          <dd>decision.verdict <bcp14>MUST</bcp14> be DENY or REFUSE. Where
actionOutcome is present, its status <bcp14>MUST</bcp14> be notAttempted, blocked
or refused, and performedAt and outcomeRef <bcp14>MUST</bcp14> be absent or
null.</dd>
          <dt>Case 3, executed.</dt>
          <dd>decision.verdict <bcp14>MUST</bcp14> be ALLOW. The outcome receipt <bcp14>MUST</bcp14> have status executed and <bcp14>MUST</bcp14> pass <xref target="verify-outcome"/> against this decision.</dd>
        </dl>
      </section>
      <section anchor="verify-outcome">
        <name>Outcome Receipt</name>
        <ol>
          <li>Apply steps 1 to 5 of <xref target="verify-decision"/> separately to the outcome envelope with the expected outcome-signing key, obtaining payload bytes Q. Retain the decision receipt's content root R for the binding check below.</li>
          <li>_type <bcp14>MUST</bcp14> be https://in-toto.io/Statement/v1 and predicateType the outcome receipt type, with a subject derived from predicate as in step 6 of <xref target="verify-decision"/>.</li>
          <li><t>Members. predicate <bcp14>MUST</bcp14> be a JSON object satisfying the field constraints of <xref target="PB-OUTCOME"/>. It <bcp14>MUST</bcp14> 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.</t><t>schemaVersion <bcp14>MUST</bcp14> consist of "0.1." followed by one or more ASCII decimal digits. outcomeId <bcp14>MUST</bcp14> be a non-empty string. executor <bcp14>MUST</bcp14> be an object with a non-empty id string. status <bcp14>MUST</bcp14> be executed, refused, failed or partial. Each non-null timestamp <bcp14>MUST</bcp14> be an <xref target="RFC3339"/> UTC timestamp with a trailing "Z". Each non-null digest <bcp14>MUST</bcp14> 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.</t></li>
          <li>Binding. decisionRef.sha256 <bcp14>MUST</bcp14> equal the content root R of the decision receipt that passed <xref target="verify-decision"/>, and requestedActionDigest <bcp14>MUST</bcp14> equal its parametersDigest.</li>
          <li>Identity and separation. executor.id <bcp14>MUST</bcp14> equal the expected executor identifier supplied under <xref target="verify-inputs"/>. The verifier's trust policy <bcp14>MUST</bcp14> permit the outcome-signing key to attest that identifier. That key <bcp14>MUST</bcp14> differ from the decision maker's key, and executor.id <bcp14>MUST</bcp14> differ from decisionMaker.id. These checks do not establish that the two keys are controlled by operationally independent parties.</li>
          <li>Result. status executed <bcp14>MUST</bcp14> come with a non-null effectDigest. Where effect bytes are supplied, effectDigest <bcp14>MUST</bcp14> be present and their SHA-256 <bcp14>MUST</bcp14> equal effectDigest.sha256.</li>
          <li>Time. performedAt <bcp14>MUST NOT</bcp14> be earlier than the decision's decidedAt and <bcp14>MUST</bcp14> be strictly earlier than its validity.expiresAt.</li>
        </ol>
      </section>
      <section anchor="sets">
        <name>Checks over a Set of Receipts</name>
        <t>An Auditor that holds the receipts and approval records of a run can check two properties that no single receipt shows. The Auditor <bcp14>MUST</bcp14> 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 <xref target="approvals-once"/>; additional copies or signatures of one payload do not.</t>
        <t>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.</t>
      </section>
      <section anchor="verify-meaning">
        <name>What a Pass Shows</name>
        <t>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.</t>
      </section>
    </section>

<section anchor="approvals-once">
      <name>Approvals Are Honoured At Most Once</name>
      <t>An approval in Case 1 frees one action once. For live use, the relying party <bcp14>MUST</bcp14> apply the time bounds of <xref target="verify-cases"/> with its own current time as T. A historical audit time <bcp14>MUST NOT</bcp14> make an expired approval usable. A relying party that lets an action run because of an approval:</t>
      <ol>
        <li><bcp14>MUST</bcp14> honour each approval at most once;</li>
        <li><bcp14>MUST</bcp14> key its consumption state by the identity of the approval (approvalId, actionDigest and session), not by the bytes of one copy of the approval record. Copies or reissues with other metadata count as the same approval and <bcp14>MUST NOT</bcp14> extend its original validUntil. Extending that validity requires a new approval act and a new approvalId;</li>
        <li><bcp14>MUST</bcp14> mark the approval as consumed durably before it either allows the action to run or releases a signed ALLOW that cites the approval;</li>
        <li><bcp14>MUST</bcp14> treat an approval recorded as consumed in any current, rotated, archived or compacted representation as consumed for as long as item 6 requires that state to be retained;</li>
        <li><bcp14>MUST NOT</bcp14> honour an approval unless it can consult a complete, durable view of the consumption state required by this section. Rotation, archiving or compaction <bcp14>MAY</bcp14> replace earlier generations if all entries still required by item 6 are preserved. Missing or unreadable state whose required entries have not been preserved elsewhere <bcp14>MUST</bcp14> cause refusal;</li>
        <li><bcp14>MAY</bcp14> drop an entry only after the approval's original validUntil, plus the clock skew it allows, has passed and no copy of that approval can still be honoured.</li>
      </ol>
      <t>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 <xref target="sets"/> 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.</t>
    </section>

<section anchor="implementation-status">
      <name>Implementation Status</name>
      <t>This section is given per <xref target="RFC7942"/> and is to be removed by the RFC
Editor before publication.</t>
      <t>proofbundle 6.4.0 is on PyPI <xref target="PB-PYPI"/> with the classifier "Development Status :: 4 - Beta"; its tag v6.4.0 is commit 699545521ec5ef479bb84e61fe7461fc79ed558c of <xref target="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 <xref target="vectors"/>, 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.</t>
      <t>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.</t>
      <t>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.</t>
      <t>The test vectors of <xref target="vectors"/> 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.</t>
      <t>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.</t>
      <t>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.</t>
      <t>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 <xref target="approvals-once"/> does not permit that.</t>
      <t>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 <xref target="approvals-once"/>.</t>
      <t>No conformance of Agent Guard to <xref target="approvals-once"/> and no enforcement of single use in a running host is claimed from these source-level observations.</t>
      <t>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.</t>
      <t>This information is given per <xref target="RFC7942"/> and does not constitute endorsement.</t>
    </section>

<section anchor="related">
      <name>Relation to Other Work</name>
      <t><xref target="GILDA"/> defines one in-toto predicate type, signed in DSSE with <xref target="RFC8785"/> canonical bytes, that carries an authorization decision and the observed effect together with a derived agreement between them, and offers it as a data model for AUDIT records. This document keeps decision and outcome in two receipts signed under different keys and maps three cases onto the architecture; the consumption-state input that <xref target="GILDA"/> names concerns the same kind of state as <xref target="approvals-once"/>.</t>
      <t><xref target="IZMAYLOV"/> specifies permissions that a principal signs with a passkey and action receipts signed by the agent, linked to earlier receipts and appended to a log by a recorder. It also defines a passkey-signed approval of one action. Such an approval could supply additional evidence for Case 1. Citing it by digest does not replace the approval record or the checks of <xref target="verify-cases"/>; this document defines no mapping from that format to the approval record.</t>
      <t><xref target="WRIGHT"/> defines a verdict record that states a result on a claim and
cites its evidence by digest, in any receipt format. A decision or outcome receipt of
this document can be such evidence, and a verdict record can state the result of the
procedure of <xref target="verification"/>.</t>
      <t><xref target="BIRKHOLZ"/> defines a data format for agent conversation records protected by COSE signatures, which <xref target="AUDIT-ARCH"/> names as a candidate for Interaction Records. A signed conversation record can be cited by digest as additional input. Establishing which entry records a human approval and how it corresponds to the action, approver, session and validity of Case 1 requires a mapping not defined here. The approval record and the checks of <xref target="verify-cases"/> still apply.</t>
      <t>SCITT <xref target="RFC9943"/> makes the existence of a signed statement
undeniable through registration and Receipts. A decision or outcome receipt can be
registered as the payload of a Signed Statement, or by its content root, which adds
what <xref target="not-adds"/> says the receipt lacks.</t>
    </section>

<section anchor="security-considerations">
      <name>Security Considerations</name>
      <t>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. <xref target="verify-decision"/> checks decisionMaker.id; <xref target="verify-outcome"/> 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 <xref target="implementation-status"/>.</t>
      <t>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.</t>
      <t>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 <xref target="approvals-once"/> 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.</t>
      <t>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.</t>
      <t>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.</t>
      <t>Clocks. decidedAt and performedAt come from the signers' clocks. Ordering against outside events needs a timestamp or a transparency registration.</t>
    </section>

<section anchor="privacy-considerations">
      <name>Privacy Considerations</name>
      <t>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 <xref target="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.</t>
    </section>

<section anchor="iana-considerations">
      <name>IANA Considerations</name>
      <t>This document has no IANA actions.</t>
    </section>
  </middle>
  <back>
    <references>
      <name>References</name>
      <references>
        <name>Normative References</name>
        <reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
          <front>
            <title>Key words for use in RFCs to Indicate Requirement Levels</title>
            <author initials="S." surname="Bradner"><organization/></author>
            <date year="1997" month="March"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="2119"/>
          <seriesInfo name="DOI" value="10.17487/RFC2119"/>
        </reference>
        <reference anchor="RFC8174" target="https://www.rfc-editor.org/info/rfc8174">
          <front>
            <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
            <author initials="B." surname="Leiba"><organization/></author>
            <date year="2017" month="May"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="8174"/>
          <seriesInfo name="DOI" value="10.17487/RFC8174"/>
        </reference>
        <reference anchor="RFC8785" target="https://www.rfc-editor.org/info/rfc8785">
          <front>
            <title>JSON Canonicalization Scheme (JCS)</title>
            <author initials="A." surname="Rundgren"><organization/></author>
            <author initials="B." surname="Jordan"><organization/></author>
            <author initials="S." surname="Erdtman"><organization/></author>
            <date year="2020" month="June"/>
          </front>
          <seriesInfo name="RFC" value="8785"/>
          <seriesInfo name="DOI" value="10.17487/RFC8785"/>
        </reference>
        <reference anchor="RFC8032" target="https://www.rfc-editor.org/info/rfc8032">
          <front>
            <title>Edwards-Curve Digital Signature Algorithm (EdDSA)</title>
            <author initials="S." surname="Josefsson"><organization/></author>
            <author initials="I." surname="Liusvaara"><organization/></author>
            <date year="2017" month="January"/>
          </front>
          <seriesInfo name="RFC" value="8032"/>
          <seriesInfo name="DOI" value="10.17487/RFC8032"/>
        </reference>
        <reference anchor="RFC6234" target="https://www.rfc-editor.org/info/rfc6234">
          <front>
            <title>US Secure Hash Algorithms (SHA and SHA-based HMAC and HKDF)</title>
            <author initials="D." surname="Eastlake 3rd"><organization/></author>
            <author initials="T." surname="Hansen"><organization/></author>
            <date year="2011" month="May"/>
          </front>
          <seriesInfo name="RFC" value="6234"/>
          <seriesInfo name="DOI" value="10.17487/RFC6234"/>
        </reference>
        <reference anchor="RFC4648" target="https://www.rfc-editor.org/info/rfc4648">
          <front>
            <title>The Base16, Base32, and Base64 Data Encodings</title>
            <author initials="S." surname="Josefsson"><organization/></author>
            <date year="2006" month="October"/>
          </front>
          <seriesInfo name="RFC" value="4648"/>
          <seriesInfo name="DOI" value="10.17487/RFC4648"/>
        </reference>
        <reference anchor="RFC3339" target="https://www.rfc-editor.org/info/rfc3339">
          <front>
            <title>Date and Time on the Internet: Timestamps</title>
            <author initials="G." surname="Klyne"><organization/></author>
            <author initials="C." surname="Newman"><organization/></author>
            <date year="2002" month="July"/>
          </front>
          <seriesInfo name="RFC" value="3339"/>
          <seriesInfo name="DOI" value="10.17487/RFC3339"/>
        </reference>
        <reference anchor="DSSE" target="https://github.com/secure-systems-lab/dsse/blob/v1.0.2/protocol.md">
          <front>
            <title>DSSE: Dead Simple Signing Envelope, protocol specification, version 1.0.2</title>
            <author><organization>Secure Systems Lab</organization></author>
            <date year="2024" month="May" day="28"/>
          </front>
        </reference>
        <reference anchor="INTOTO-STATEMENT" target="https://github.com/in-toto/attestation/blob/v1.2.0/spec/v1/statement.md">
          <front>
            <title>in-toto Attestation Framework: Statement layer, v1 (release v1.2.0)</title>
            <author><organization>in-toto project</organization></author>
            <date year="2026" month="March" day="18"/>
          </front>
        </reference>
        <reference anchor="PB-DECISION" target="https://github.com/b7n0de/proofbundle/blob/v6.4.0/docs/predicates/decision-receipt.md">
          <front>
            <title>Decision Receipt predicate decision-receipt/v0.1</title>
            <author><organization>proofbundle project</organization></author>
            <date year="2026" month="October"/>
          </front>
          <refcontent>proofbundle, tag v6.4.0, docs/predicates/decision-receipt.md</refcontent>
        </reference>
        <reference anchor="PB-OUTCOME" target="https://github.com/b7n0de/proofbundle/blob/v6.4.0/docs/predicates/action-outcome.md">
          <front>
            <title>Action Outcome predicate action-outcome/v0.1</title>
            <author><organization>proofbundle project</organization></author>
            <date year="2026" month="October"/>
          </front>
          <refcontent>proofbundle, tag v6.4.0, docs/predicates/action-outcome.md</refcontent>
        </reference>
      </references>
      <references>
        <name>Informative References</name>
        <reference anchor="AUDIT-ARCH" target="https://datatracker.ietf.org/doc/html/draft-kuehlewind-audit-architecture-01">
          <front>
            <title>An Architecture for Auditing Agent Delegation and Interactions</title>
            <author initials="M." surname="Kuehlewind"><organization/></author>
            <author initials="H." surname="Birkholz"><organization/></author>
            <date year="2026" month="September" day="7"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-kuehlewind-audit-architecture-01"/>
        </reference>
        <reference anchor="GILDA" target="https://datatracker.ietf.org/doc/html/draft-gilda-audit-agent-audit-record-03">
          <front>
            <title>An Audit Record Format for AI Agent Authorization Decisions</title>
            <author initials="S." surname="Gilda"><organization/></author>
            <date year="2026" month="October" day="29"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-gilda-audit-agent-audit-record-03"/>
        </reference>
        <reference anchor="IZMAYLOV" target="https://datatracker.ietf.org/doc/html/draft-izmaylov-agent-permission-receipts-01">
          <front>
            <title>Signed Permissions and Action Receipts for Automated Agents</title>
            <author initials="P." surname="Izmaylov"><organization/></author>
            <date year="2026" month="October" day="24"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-izmaylov-agent-permission-receipts-01"/>
        </reference>
        <reference anchor="WRIGHT" target="https://datatracker.ietf.org/doc/html/draft-wright-verdict-record-01">
          <front>
            <title>Verdict Records: Results on Claims that Cite Receipts as Evidence</title>
            <author initials="D." surname="Wright"><organization/></author>
            <date year="2026" month="November" day="1"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-wright-verdict-record-01"/>
        </reference>
        <reference anchor="BIRKHOLZ" target="https://datatracker.ietf.org/doc/html/draft-birkholz-verifiable-agent-conversations-01">
          <front>
            <title>Verifiable Agent Conversation Records</title>
            <author initials="H." surname="Birkholz"><organization/></author>
            <author initials="T." surname="Heldt"><organization/></author>
            <author initials="O." surname="Steele"><organization/></author>
            <date year="2026" month="August" day="31"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-birkholz-verifiable-agent-conversations-01"/>
        </reference>
        <reference anchor="SER" target="https://datatracker.ietf.org/doc/html/draft-gruszka-signed-evaluation-receipts-04">
          <front>
            <title>Signed Evaluation Receipts</title>
            <author initials="K." surname="Gruszka"><organization/></author>
            <date year="2026" month="October" day="26"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-gruszka-signed-evaluation-receipts-04"/>
        </reference>
        <reference anchor="SER-MAPPINGS" target="https://datatracker.ietf.org/doc/html/draft-gruszka-evaluation-receipt-mappings-04">
          <front>
            <title>Citing Signed Evaluation Receipts from Existing Envelopes</title>
            <author initials="K." surname="Gruszka"><organization/></author>
            <date year="2026" month="October" day="26"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-gruszka-evaluation-receipt-mappings-04"/>
        </reference>
        <reference anchor="RFC9943" target="https://www.rfc-editor.org/info/rfc9943">
          <front>
            <title>An Architecture for Trustworthy and Transparent Digital Supply Chains</title>
            <author initials="H." surname="Birkholz"><organization/></author>
            <author initials="A." surname="Delignat-Lavaud"><organization/></author>
            <author initials="C." surname="Fournet"><organization/></author>
            <author initials="Y." surname="Deshpande"><organization/></author>
            <author initials="S." surname="Lasker"><organization/></author>
            <date year="2026" month="June"/>
          </front>
          <seriesInfo name="RFC" value="9943"/>
          <seriesInfo name="DOI" value="10.17487/RFC9943"/>
        </reference>
        <reference anchor="RFC7942" target="https://www.rfc-editor.org/info/rfc7942">
          <front>
            <title>Improving Awareness of Running Code: The Implementation Status Section</title>
            <author initials="Y." surname="Sheffer"><organization/></author>
            <author initials="A." surname="Farrel"><organization/></author>
            <date year="2016" month="July"/>
          </front>
          <seriesInfo name="BCP" value="205"/>
          <seriesInfo name="RFC" value="7942"/>
          <seriesInfo name="DOI" value="10.17487/RFC7942"/>
        </reference>
        <reference anchor="PB-PYPI" target="https://pypi.org/project/proofbundle/6.4.0/">
          <front>
            <title>proofbundle 6.4.0</title>
            <author><organization>Python Package Index</organization></author>
            <date year="2026" month="October" day="28"/>
          </front>
        </reference>
        <reference anchor="PB-REPO" target="https://github.com/b7n0de/proofbundle">
          <front>
            <title>proofbundle source repository</title>
            <author><organization>proofbundle project</organization></author>
            <date year="2026"/>
          </front>
        </reference>
        <reference anchor="JCS-JS" target="https://www.npmjs.com/package/canonicalize/v/2.1.0">
          <front>
            <title>canonicalize 2.1.0, a JavaScript implementation of RFC 8785</title>
            <author initials="S." surname="Erdtman"><organization/></author>
            <date year="2025" month="March" day="18"/>
          </front>
        </reference>
      </references>
    </references>

<section anchor="vectors">
      <name>Test Vectors</name>
      <t>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 <bcp14>MUST NOT</bcp14> be used for any real receipt. Ed25519 is
deterministic, so the signatures below follow from the seeds and the payloads.</t>
      <section anchor="vectors-keys">
        <name>Test Keys and Inputs</name>
        <t>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.</t>
        <artwork><![CDATA[
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
]]></artwork>
        <t>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
<xref target="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
<xref target="SER"/>, Appendix A.2.1, cited by V3-decision.</t>
        <artwork><![CDATA[
a1 "git push origin refs/heads/topic:refs/heads/topic\n"
50 bytes, SHA-256
dec54f771352454097310ae4bd6608c0070ebebb3955c6be013c1b3a8e310bd0
]]></artwork>
        <artwork><![CDATA[
a1x "git push --force origin refs/heads/topic:refs/heads/main\n"
57 bytes, SHA-256
b683418806875ff79c6fa190c50edab0b96577ad8062de5ba7b41dc1d9c363fb
]]></artwork>
        <artwork><![CDATA[
a2 "gh pr merge 7 --repo acme/widgets --merge\n"
42 bytes, SHA-256
5a7549948a1441addb710f5ada59e06dbce25cc5a59accf14f140379f47adb5b
]]></artwork>
        <artwork><![CDATA[
pol "example gate rules, revision 1\n"
31 bytes, SHA-256
43789b41915ae0febcd13263d293101af5ab041eb7c234e1cbe625d6312815f6
]]></artwork>
        <artwork><![CDATA[
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"
}
]]></artwork>
        <artwork><![CDATA[
a3, 81 bytes, SHA-256
9165c850b7e03cd59578efa1769be74c5386cff5ce96df6b59e25d38ad6f88b4
{
 "body": "All required checks passed for 4f1c2a9.",
 "pull": 7,
 "repo": "acme/widgets"
}
]]></artwork>
        <artwork><![CDATA[
e3, 93 bytes, SHA-256
d3d824cf55168298c0f651244853ec9d9620ea09087abe899243ceb8f9df3a6d
{
 "author": "agent-42-bot",
 "body": "All required checks passed for 4f1c2a9.",
 "id": 9001,
 "pull": 7
}
]]></artwork>
        <artwork><![CDATA[
B, 458 bytes, SHA-256
8cd849a6dcce08aee334511d7b2f31c54e478885dda1795d3c8f972dd0d4811c
]]></artwork>
      </section>
      <section anchor="vectors-statements">
        <name>Statements</name>
        <t>The payload bytes of each vector are the serialization under
<xref target="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. </t>
        <t>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.</t>
        <artwork><![CDATA[
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"
]]></artwork>
        <artwork><![CDATA[
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"
]]></artwork>
        <artwork><![CDATA[
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"
]]></artwork>
        <artwork><![CDATA[
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"
]]></artwork>
        <artwork><![CDATA[
V3-bad, from V3-good
decisionRef.sha256 = R(V1-good)
outcomeId = "out-0002"
]]></artwork>
        <t>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.</t>
      </section>
      <section anchor="vectors-table">
        <name>Results</name>
        <t>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.</t>
        <artwork><![CDATA[
V1-good, T 2026-10-15T09:01:00Z, pass, exit 0
1829 bytes, signed by gate-1
b9dfd26537542632013d0df166141a788551b4987dfa33b060d5b5ebba78dfb1
GkvzPjklV5ZbtomNRXbS4+O4MIOSw8xGmA0diAEYKEm0
s4Q9qLkL1j7+e5RZnT9l56tfB2utDoNycACsSvSoDw==
]]></artwork>
        <artwork><![CDATA[
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==
]]></artwork>
        <artwork><![CDATA[
V2-good, T 2026-10-15T09:06:00Z, pass, exit 0
1765 bytes, signed by gate-1
ed149a1c7383842eebf8304001da9de97d130b0b15080d1315ab8fbf1274ed44
66CUa1kz6aOm6gbdctRg4l97fW8x185nxpsB5rkCFj+a
HZVjRjc5hDnQu6v4J/bHzaufyHhPCPdM6XUAWiylAQ==
]]></artwork>
        <artwork><![CDATA[
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==
]]></artwork>
        <artwork><![CDATA[
V3-decision, T 2026-10-15T09:11:00Z, pass, exit 0
1827 bytes, signed by gate-1
0014b1c419798125c3984e85a4b322ee619a3227978840cac5f9d282b189521f
DDLIUtX6uk5A3AqcvYP9f9zYeHJAWNTrdBZwpXmEQq4y
u0bivvsiNEjx4u2Kw2U6Mr11lcWoRZnSo1w5MX2PCA==
]]></artwork>
        <artwork><![CDATA[
V3-good, pass, exit 0
694 bytes, signed by observer-1
f0ff90cf25e2db1e989fa79869d4b3b46899112e8c2269d9a18a64b751aa7274
MCUgSY+fGUO2cm5DnxBVKEC/BBSyeS359aj2rYowwkHT
j+CskAgraX5TirUBic2i2KyUUaxBYMMdVafhu3tcAQ==
]]></artwork>
        <artwork><![CDATA[
V3-bad, fail at binding, exit 2
694 bytes, signed by observer-1
48be708cf3f959556323474f4a5c9193b417d2bfea5c88bb852daf00b64827b4
uEgT4TFv7jEgmH7pwJcPBEEZM+n2wGjic6U6yYWOU3Dh
Ta0tMuimQ8q4Ha1lgw4zWNR9YBag4GmVt9SdsfzwCQ==
]]></artwork>
        <t>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 <xref target="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.</t>
      </section>
    </section>
    <section numbered="false" anchor="acknowledgments">
      <name>Acknowledgments</name>
      <t>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.</t>
      <t>Citing adjacent work in the References does not imply that its authors endorse this
document.</t>
    </section>
  </back>
</rfc>
