diff --git a/docs/docs.json b/docs/docs.json index 74bcb9caa..7619c6f89 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -458,6 +458,12 @@ "seps/2596-spec-feature-lifecycle-and-deprecation", "seps/2663-tasks-extension" ] + }, + { + "group": "Draft", + "pages": [ + "seps/2828-server-side-signed-execution-record" + ] } ] }, diff --git a/docs/seps/2828-server-side-signed-execution-record.mdx b/docs/seps/2828-server-side-signed-execution-record.mdx new file mode 100644 index 000000000..50fd372ec --- /dev/null +++ b/docs/seps/2828-server-side-signed-execution-record.mdx @@ -0,0 +1,647 @@ +--- +title: "SEP-2828: Server-Side Signed Execution Record for MCP Tool Calls" +sidebarTitle: "SEP-2828: Server-Side Signed Execution Record for…" +description: "Server-Side Signed Execution Record for MCP Tool Calls" +--- + +
+ + Draft + + + Standards Track (Extensions Track) + +
+ +| Field | Value | +| ------------- | ------------------------------------------------------------------------------- | +| **SEP** | 2828 | +| **Title** | Server-Side Signed Execution Record for MCP Tool Calls | +| **Status** | Draft | +| **Type** | Standards Track (Extensions Track) | +| **Created** | 2026-05-31 | +| **Author(s)** | Henri Sirkkavaara ([@vaaraio](https://github.com/vaaraio)), Vaara | +| **Sponsor** | None (seeking sponsor) | +| **PR** | [#2828](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2828) | + +--- + +## Abstract + +This SEP defines two server-authoritative, cryptographically signed records +for MCP tool calls: a **decision record** emitted by the governing server or +proxy before the side effect runs, and an **outcome record** emitted after +execution completes. The decision record binds what was requested, the policy +decision (allow, block, or escalate), and the risk basis for that decision. The +outcome record binds the execution status (`executed`, `refused`, or `errored`), +a commitment over the result, and timing. The two are paired through a +`backLink` and bound to the originating request, so a verifier can reconstruct +what the agent was permitted to do, why, and what it actually did. + +These records are signed by the server or proxy that governs execution +(`issuerAsserted` / `receiptAsserted`), which is a different trust surface from +the client-asserted attestation of the call in SEP-2787 and the client-asserted +input audit context in SEP-2817. The records use RFC 8785 (JCS) canonical JSON, +a detached signature that does not cover itself, and are offline-verifiable by a +standard-library checker. The wire schema is the shape already shipping in the +Vaara MCP proxy (`vaara.attestation.receipt`), which also provides an +independent verifier and JCS conformance vectors. + +## Motivation + +EU AI Act Article 12 obliges providers and deployers of high-risk AI systems to +keep automatic records of events ("logs") over the system's lifetime, including +records adequate to identify situations that may cause the system to present a +risk and to support post-market monitoring under Article 72. For an +agentic MCP deployment, the regulator's question is not only "what did the +client claim it wanted to do" but "what did the governing system decide, on what +basis, and what did the tool call actually do." That is a statement the server +or governance layer must make, because it is the party that holds the decision +logic and observes the result. + +SEP-2787 standardizes a signed attestation of a tool **call**: it binds the +agent identity, tool name, intent, and an argument commitment into a verifiable +envelope. SEP-2817 standardizes optional, explicitly non-authoritative, +**client-asserted** input audit context (`invocationReason`, `model`, +`userIntent`, `turnId`). Both describe the input side: what was claimed, by the +party making the call. Neither carries the governing system's decision or the +recorded outcome. SEP-2817 says so directly: "server-side decision records, +stable tool-call identity, agent/session correlation, and taxonomies are left to +follow-up SEPs," and Discussion #2704 frames the server-authoritative record as +the deferred half. This SEP is that follow-up. + +Client-asserted records alone cannot satisfy Article 12 for a deployment where +the server enforces policy. A client can claim its intent and its arguments; it +cannot credibly attest that the call was allowed, why it was allowed or blocked, +or what the tool returned, because it does not own that logic and is not a +neutral observer of its own behavior. A regulator auditing an incident needs a +record signed by the enforcement point, paired to the request it answers, that +survives the client. Without a standard for that record, every governance vendor +invents an incompatible one, and cross-implementation verification (the point of +a conformance test) is impossible. + +The records are also the natural place to satisfy the rest of the Article 12 and +Article 14 surface that input audit does not reach: the allow/block/escalate +decision (human-oversight evidence under Article 14), the risk basis that drove +it (risk-management evidence under Article 9), and the post-execution outcome +that feeds post-market monitoring (Article 72). A shipping implementation already +emits all of these as audit events; this SEP standardizes the signed wire shape +so they are portable and independently verifiable. + +## Specification + +### Terminology + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", +"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be +interpreted as described in RFC 2119 and RFC 8174. + +- **Governing server**: the MCP server, or a proxy in front of it, that decides + whether a tool call runs and observes the result. It is the issuer of both + records defined here. +- **Decision record**: a signed envelope emitted before the side effect, + carrying the policy decision and its risk basis. +- **Outcome record**: a signed envelope emitted after execution, carrying the + execution status and a result commitment. +- **Request attestation**: the SEP-2787 envelope describing the tool call. It is + the anchor both records point back to. + +### Trust boundary + +This SEP is server-authoritative. Both records are signed under the governing +server's key and place their bound claims in an issuer block +(`issuerAsserted` for the decision record, `receiptAsserted` for the outcome +record), mirroring the SEP-2787 trust-surface split. The records make claims +about the **decision** and the **execution**, which only the enforcement point +can make. + +This is distinct from SEP-2787, where the issuer attests the **call** (identity, +tool, args) and the planner declares intent, and from SEP-2817, where the client +asserts input context that is explicitly not authorization evidence. A verifier +MUST treat the three as separate surfaces: a SEP-2787 attestation and a SEP-2817 +`_meta` block describe what was asked for; the records in this SEP describe what +the governing system did about it. A server MUST NOT copy client-asserted +SEP-2817 fields into the signed decision or outcome record in a way that implies +the server vouches for their truth; it MAY reference them by `turnId` for +correlation (see Pairing). + +### Canonicalization and signature + +Both records use RFC 8785 (JCS) canonical JSON. The signature is computed over +the JCS-canonical encoding of the record's blocks **excluding** the `signature` +field, and the `signature` field carries the result; it does not cover itself. +This is identical to the SEP-2787 and the shipped Vaara execution-receipt scheme, +so a verifier that already handles SEP-2787 signatures handles these records with +no new cryptographic code. + +- The `alg` field MUST be one of `ES256`, `RS256`, or `HS256`. Production + deployments crossing a trust boundary SHOULD use an asymmetric algorithm + (`ES256` or `RS256`) so verifiers need only the public key. +- IEEE-754 floats MUST NOT appear anywhere in a canonicalized body. Numeric + values that are not integers MUST be encoded as scaled integers or decimal + strings. This removes the most common source of cross-stack signature drift + and matches the Vaara JCS discipline. +- All digests are encoded as the string `sha256:`. +- All timestamps are RFC 3339 / ISO 8601 UTC strings with second precision and a + trailing `Z` (for example `2026-05-31T09:30:00Z`). + +### Argument and result commitments + +Both records reuse the SEP-2787 commitment shapes verbatim. A commitment is one +of: + +- **`ArgsRef`**: a content-addressed reference, `{ "ref": "", "digest": +"sha256:", "canonicalization": "jcs" }`. The verifier MAY fetch `ref` and + check the digest. +- **`ArgsProjection`**: a reviewed projection, `{ "projection": "", +"projectionDigest": "sha256:" }`, where `projection` is the JCS-canonical + encoding of the projection object as a UTF-8 string and `projectionDigest` is + the sha256 over those bytes. + +Commitment-only audit, where the payload never leaves the server, is expressed as +a hash-only-identity `ArgsProjection` whose `projection` is the JCS encoding of +`{ "digest": "sha256:" }` and whose embedded digest binds the underlying +value. This is the shipped `make_args_digest` behavior and is the RECOMMENDED +default for the outcome record's `resultCommitment` so that results, which may +contain personal data, are committed to without being copied into the record. + +### Decision record + +A decision record MUST be emitted by the governing server before the tool call's +side effect runs, for every governed call. It is a JSON object with the +following top-level fields. + +| Field | Type | Required | Description | +| ----------------- | ------- | -------- | ----------------------------------------------------------- | +| `version` | integer | yes | Record schema version. `1` for this SEP. | +| `alg` | string | yes | `ES256`, `RS256`, or `HS256`. | +| `backLink` | object | yes | Join to the SEP-2787 attestation (see below). | +| `decisionDerived` | object | yes | The decision and its risk basis (see below). | +| `issuerAsserted` | object | yes | The governing server's issuer block (see below). | +| `signature` | string | yes | Detached signature over the JCS body excluding `signature`. | + +**`backLink`** joins the decision to the request attestation it governs: + +| Field | Type | Required | Description | +| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `attestationDigest` | string | yes | `sha256:` over the JCS-canonical full SEP-2787 attestation wire bytes, signature included. Pins the exact attestation instance. | +| `attestationNonce` | string | yes | Echoes the attestation's `issuerAsserted.nonce` for fast correlation. | + +If no SEP-2787 attestation exists for the call (the deployment does not run +2787), the server MUST instead bind the request by setting `attestationDigest` to +`sha256:` over the JCS-canonical encoding of the request envelope it +observed (the `tools/call` params plus `_meta`), and set `attestationNonce` to a +server-chosen per-call nonce. The binding is to the request **instance**, not +only its content. + +**`decisionDerived`** carries the decision and the basis for it: + +| Field | Type | Required | Description | +| ---------------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | +| `decision` | string | yes | One of `allow`, `block`, `escalate`. | +| `reason` | string | no | Short human-readable basis for the decision. | +| `riskScore` | string | no | The risk estimate that drove the decision, as a decimal string (floats are prohibited on the wire). | +| `thresholdAllow` | string | no | The allow threshold in force at decision time, as a decimal string. | +| `thresholdBlock` | string | no | The block threshold in force at decision time, as a decimal string. | +| `policyId` | string | no | Identifier or digest of the policy/ruleset version that produced the decision. | +| `decidedAt` | string | yes | ISO 8601 UTC timestamp of the decision. | + +A `decision` of `escalate` means the call was held for human oversight; the +outcome record for that call will report `refused` if the human declined, or a +later decision record MAY supersede it (see Pairing). + +**`issuerAsserted`** is the governing server's signed block: + +| Field | Type | Required | Description | +| --------------- | ------ | -------- | -------------------------------------------------------------------- | +| `iss` | string | yes | Issuer identity (the governing server or proxy). | +| `sub` | string | yes | Subject the decision is about (tenant, agent, or upstream identity). | +| `iat` | string | yes | ISO 8601 UTC issuance time. | +| `nonce` | string | yes | Unique per record. | +| `secretVersion` | string | yes | Key/secret version identifier for rotation and dispatch. | +| `alg` | string | yes | MUST equal the top-level `alg`. | + +### Outcome record + +An outcome record MUST be emitted by the governing server after the governed call +completes (or is refused), and MUST be paired to a decision record for the same +call. It is a JSON object with the following top-level fields. This is the shape +already shipping as `vaara.attestation.receipt.ExecutionReceipt`. + +| Field | Type | Required | Description | +| ----------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `version` | integer | yes | Record schema version. `1`. | +| `alg` | string | yes | `ES256`, `RS256`, or `HS256`. | +| `backLink` | object | yes | Same shape as the decision record's `backLink`; pins the request. | +| `outcomeDerived` | object | yes | Execution status, timing, and result commitment (see below). | +| `receiptAsserted` | object | yes | The governing server's issuer block (same shape as `issuerAsserted`, minus `expSeconds`; an outcome record is a durable record, not a capability, so it carries no `exp` and verifiers enforce no TTL). | +| `signature` | string | yes | Detached signature over the JCS body excluding `signature`. | + +**`outcomeDerived`** carries what happened: + +| Field | Type | Required | Description | +| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `status` | string | yes | One of `executed`, `refused`, `errored`. | +| `completedAt` | string | yes | ISO 8601 UTC completion (or refusal) time. | +| `resultCommitment` | object | no | An `ArgsRef` or `ArgsProjection` over the result (executed) or error object (errored). Absent for `refused`, which has no result. RECOMMENDED to use the commitment-only hash-only-identity projection so result payloads, which may contain personal data, are not copied into the record. | +| `decisionDigest` | string | yes\* | `sha256:` over the JCS-canonical full decision-record wire bytes (signature included) the outcome was produced under. This is the Check B (outcome-to-decision) binding. \*Optional on the wire for backward parsing of records emitted before this field, but a conforming emitter MUST set it and pairing fails without it (see Pairing). | + +### Pairing + +A decision record and an outcome record pair when **both** of the following +hold: + +- **Check A (instance anchor).** Both records carry the same `backLink` + (`attestationDigest` and `attestationNonce` equal). This is instance-binding, + not only content-binding: two byte-identical calls produce two attestations + with distinct nonces and therefore distinct `attestationDigest` values, so a + record cannot be replayed against a different instance of the same call. In the + no-attestation fallback, the shared `backLink` is over the request envelope + instead, and Check A anchors on that. +- **Check B (outcome-to-decision digest, the normative pairing).** The outcome + record's `outcomeDerived.decisionDigest` equals `sha256:` over the JCS + canonical full wire bytes of _this_ decision record. Check A alone admits a + different decision taken under the same attestation instance (an `escalate` and + the human verdict that supersedes it both share the attestation); Check B pins + which decision's content the outcome answers. An outcome record without + `decisionDigest` does not pair: content binding is mandatory, not best-effort. + +A verifier that has both records, plus the SEP-2787 attestation, can then confirm +the full chain: the attestation pins the call; the decision record pins the +policy verdict and risk basis; the outcome record pins what the call did and the +decision it ran under. + +For correlation with client-asserted input context (SEP-2817), a server MAY +include the SEP-2817 `turnId` as an additional, clearly client-asserted field +inside `decisionDerived` under the key `clientTurnId`. This is correlation only; +its presence in the signed body means the server records that the client claimed +this `turnId`, not that the server vouches for it. + +A superseding decision (for example, a human resolving an `escalate`) is recorded +as a new decision record with the same `backLink` and a later `decidedAt`. The +record with the latest `decidedAt` for a given `backLink` is the effective +decision; earlier ones are retained as history. When two records for one +`backLink` carry the **same** `decidedAt`, the tie MUST break deterministically: +the effective record is the one whose `issuerAsserted.nonce` is lexicographically +lowest. This gives every verifier the same winner with no clock authority. +Verifiers MUST NOT treat multiple decision records for one `backLink` as a +conflict. The outcome record's `decisionDigest` (Check B) identifies which +decision in this set the call actually ran under. + +### Transport + +The records are signed JSON objects and are transport-agnostic. A server MAY +return the outcome record in the `tools/call` response `_meta` under the reserved +key `io.modelcontextprotocol/executionRecord`, MAY return the decision record +under `io.modelcontextprotocol/decisionRecord`, and MAY persist either to an +out-of-band audit store. When a record is too large to carry inline (a large +`ArgsRef` chain), the server SHOULD return an `ArgsRef` pointing to the stored +record. This SEP does not require any change to existing MCP message formats; the +records ride in `_meta`, consistent with SEP-414, SEP-2787, and SEP-2817. + +### JSON examples + +Decision record (allow, with risk basis): + +```json +{ + "version": 1, + "alg": "ES256", + "backLink": { + "attestationDigest": "sha256:8f1e2c0a9b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f", + "attestationNonce": "qWZ3kP1nB8tR0aL" + }, + "decisionDerived": { + "decision": "allow", + "reason": "risk below allow threshold", + "riskScore": "0.21", + "thresholdAllow": "0.40", + "thresholdBlock": "0.70", + "policyId": "sha256:3c9d4b8a", + "decidedAt": "2026-05-31T09:30:00Z", + "clientTurnId": "turn-7f3a" + }, + "issuerAsserted": { + "alg": "ES256", + "iat": "2026-05-31T09:30:00Z", + "iss": "vaara-proxy://acme-eu", + "nonce": "Yb7Qd2mK9sV", + "secretVersion": "2026-05", + "sub": "tenant:acme/agent:billing-bot" + }, + "signature": "3045022100ab12" +} +``` + +Outcome record (executed, result committed by digest only): + +```json +{ + "version": 1, + "alg": "ES256", + "backLink": { + "attestationDigest": "sha256:8f1e2c0a9b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f", + "attestationNonce": "qWZ3kP1nB8tR0aL" + }, + "outcomeDerived": { + "status": "executed", + "completedAt": "2026-05-31T09:30:02Z", + "resultCommitment": { + "projection": "{\"digest\":\"sha256:1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c\"}", + "projectionDigest": "sha256:aa11bb22cc33dd44ee55ff66aa77bb88cc99dd00ee11ff22aa33bb44cc55dd66" + } + }, + "receiptAsserted": { + "alg": "ES256", + "iat": "2026-05-31T09:30:02Z", + "iss": "vaara-proxy://acme-eu", + "nonce": "Zc8Re3nL0tW", + "secretVersion": "2026-05", + "sub": "tenant:acme/agent:billing-bot" + }, + "signature": "3046022100cd34" +} +``` + +Outcome record (refused, no result): + +```json +{ + "version": 1, + "alg": "ES256", + "backLink": { + "attestationDigest": "sha256:8f1e2c0a9b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f", + "attestationNonce": "qWZ3kP1nB8tR0aL" + }, + "outcomeDerived": { + "status": "refused", + "completedAt": "2026-05-31T09:30:01Z" + }, + "receiptAsserted": { + "alg": "ES256", + "iat": "2026-05-31T09:30:01Z", + "iss": "vaara-proxy://acme-eu", + "nonce": "Wd9Sf4oM1uX", + "secretVersion": "2026-05", + "sub": "tenant:acme/agent:billing-bot" + }, + "signature": "3045022100ef56" +} +``` + +### Verification algorithm + +A verifier holding a decision record, an outcome record, the SEP-2787 +attestation, and the governing server's public key (or shared secret for HS256) +MUST perform, and a conforming implementation MUST pass, the following checks: + +1. For each record, recompute the JCS-canonical encoding of the body with the + `signature` field removed, and verify `signature` under the issuer key for the + record's `alg`. Reject on mismatch. +2. Recompute `attestationDigest` from the SEP-2787 attestation wire bytes and + confirm both records' `backLink.attestationDigest` and + `backLink.attestationNonce` match it. Reject on mismatch. +3. Confirm the two records share the same `backLink` (they describe the same + call). +4. If `resultCommitment` is present and the verifier has the result payload, + recompute the commitment digest from the runtime result and confirm it + matches. Reject on mismatch. +5. Confirm `decisionDerived.decision` is one of `allow`, `block`, `escalate` and + `outcomeDerived.status` is one of `executed`, `refused`, `errored`. + +## Rationale + +**Two records, not one.** The decision and the outcome are made at different +times by the same party and answer different regulatory questions (Article 9 and +14 risk and oversight evidence versus Article 72 post-market outcome evidence). A +single combined record would force the server to delay signing until after +execution, losing the pre-side-effect commitment that proves the verdict was +fixed before the action ran. Splitting them, paired by `backLink`, preserves the +commit-before-execute property that is the whole point of an enforcement record. + +**Instance-binding over content-binding.** Earlier drafts in the #2704 / #2817 +discussion considered binding records to the content hash of the call alone. That +lets a record be replayed against any byte-identical call. Binding to the +SEP-2787 attestation digest (signature included) pins the exact attestation +instance, so distinct calls with identical content still produce distinct +bindings. This was the resolved position in discussion; the agent-guard author +conceded the instance-binding point on 2026-05-30, and this SEP adopts it as +normative. + +**Reusing the SEP-2787 stack.** The outcome record is the post-execution sibling +of the SEP-2787 attestation and deliberately reuses its canonicalization, signing +algorithms, commitment shapes, and issuer-block layout. A verifier that handles +SEP-2787 handles these records with no new cryptographic code, and the decision +record adds only one new block (`decisionDerived`). + +**Why server-authoritative.** A client cannot credibly attest a decision it did +not make or an outcome it is not a neutral observer of. SEP-2787 and SEP-2817 +cover the input side precisely because they are client or issuer claims about the +call. The enforcement record has to come from the enforcement point, which is why +this is a separate SEP rather than an extension of either. + +**Alternatives considered.** Carrying the decision inside the SEP-2787 +`issuerAsserted` block was rejected because SEP-2787 attests the call, not the +verdict, and overloading it would blur the trust surface that SEP made explicit. +Adopting a content-addressed `action_ref` as the primary binding field was +rejected: a content-addressed receipt id is a useful secondary index but is +content-binding, and using it as the primary join reintroduces the replay +problem instance-binding solves. The `action_ref` style is reconcilable as an +optional `ArgsRef`-shaped pointer, not as the default join. + +## Backward Compatibility + +This SEP introduces no breaking changes. Both records are new, optional `_meta` +payloads under reserved keys; servers that do not emit them and clients that do +not consume them are unaffected. The schema reuses the SEP-2787 commitment and +issuer-block shapes, so an existing SEP-2787 verifier extends to these records by +adding the `decisionDerived` block and the `outcomeDerived.status` enum, with no +change to the canonicalization or signature code. The records do not alter any +existing MCP request or response method. + +## Security Implications + +**Signing-key compromise and post-hoc backdating.** A signed record proves the +holder of the key bound those values, but a compromised key lets an adversary +mint records with any `decidedAt` / `completedAt` they choose, including +backdated ones, to fabricate a clean history. A signature alone cannot defeat +this. The defense is an **external time anchor**: periodically anchoring the head +of the append-only audit chain that carries these records to an independent, +trusted timestamp (an RFC 3161 timestamp token, or an eIDAS qualified electronic +timestamp) so that records signed after a compromise cannot be inserted before +the last anchored head without detection. The Vaara reference implementation +binds each record into a hash chain (chain version 2, with tenant identity bound +into the chained hash) and ships the external anchor in v0.48: an RFC 3161 +timestamp over the chain head, verifiable offline, with optional automatic cadence +anchoring that fails open by writing a gap marker into the chain when the +authority is unreachable. Anchoring is opt-in. The deployer configures a trusted +timestamp authority; none is bundled by default. Offline verification proves the +token was signed by the certificate it carries; establishing that certificate as +a trusted (for example eIDAS-qualified) authority is deployer policy, enforced by +pinning it. A conforming deployment under EU AI Act retention obligations SHOULD +anchor the chain head externally at a cadence proportionate to its risk. + +**Replay.** Instance-binding through the SEP-2787 attestation digest (signature +included) plus the per-record `nonce` means a record cannot be replayed against a +different call instance. Verifiers MUST reject a record whose `backLink` does not +match the attestation under verification. + +**Instance versus content binding.** As above, binding is to the attestation +instance, not only the call content. Implementations MUST NOT substitute a bare +content hash of the arguments for the `attestationDigest`, because that +reintroduces replay across byte-identical calls. + +**Personal data minimization.** Tool arguments and results can contain personal +data. The RECOMMENDED default for both `decisionDerived` (which never copies the +arguments, only a risk basis and decision) and `outcomeDerived.resultCommitment` +(commitment-only hash-only-identity projection) is that the record commits to a +digest of the value without copying the value. Where a reviewed projection is +needed for audit, servers SHOULD redact to the minimum fields necessary and +commit to the projection, never the raw payload. This keeps the signed, +long-retained record free of personal data while preserving the ability to prove, +given the original value out of band, that it is the one the record committed to. +This aligns with GDPR data minimization and storage limitation alongside the +Article 12 retention obligation. + +**Issuer identity and key distribution.** Verifiers must obtain the governing +server's public key out of band; embedding a public key in the record is a +convenience for local verification only and is not trust-establishing. The +`secretVersion` field supports key rotation; verifiers dispatch on it. + +**Denial of service.** Record emission MUST NOT block legitimate traffic; a +governing server that cannot sign SHOULD fail closed on the **decision** (do not +allow an ungoverned call) but record the signing failure, and MAY degrade outcome +recording to a logged error rather than dropping the call's result. The reference +implementation emits records on a best-effort path that never blocks the proxied +call and counts emission failures for operator alerting. + +## Prior Art Reconciled + +- **SEP-2787 (Tool call attestation, author soup-oss, Extensions Track, open).** + Attests the tool **call**: planner-declared intent, issuer-asserted identity, + payload-derived tool bindings and argument commitment. This SEP is its + post-decision and post-execution counterpart and reuses its canonicalization, + signing, commitment shapes, and trust-surface layout. The `backLink` pins a + SEP-2787 attestation. The split between what was asked (2787) and what was + decided and done (this SEP) is deliberate and preserves 2787's trust surface. + +- **SEP-2817 (AI Invocation Audit Context in Request `_meta`, author hangum, + Standards Track, draft seeking sponsor).** Standardizes optional, + client-asserted input audit context (`invocationReason`, `model`, `userIntent`, + `turnId`) and states explicitly that these fields are not authorization + evidence and that server-side decision records are left to a follow-up SEP. + This is that follow-up. Correlation with 2817 is by optionally recording the + client-asserted `turnId` as `clientTurnId`, clearly marked as a client claim. + +- **agent-guard (XuebinMa, Rust).** Models a `Guard` producing an `AuditRecord` + and an `ExecutionProof`. The two-record decision/outcome split here covers the + same ground as `AuditRecord` plus `ExecutionProof`, with the binding made + explicit and instance-scoped through the SEP-2787 attestation digest rather + than left to a content hash. The instance-binding position was conceded by the + agent-guard author in discussion on 2026-05-30 and is adopted here as + normative. + +- **Content-addressed receipt identifiers.** Some designs identify a record by a + content-addressed id of the form `action_ref = sha256(JCS(...))` over a + description of the action. This SEP does not adopt a content-addressed id as the + default join field, because it is content-binding and reintroduces + cross-instance replay when used as the primary binding. Such a pointer is + reconcilable as an optional `ArgsRef`-shaped reference (the `ref` carries the id + and the `digest` pins it), not as the normative pairing key, which remains the + instance-scoped `backLink`. + +## Reference Implementation + +The wire schema in this SEP is the shape shipping in the Vaara MCP proxy +(v0.48; the receipt library landed in v0.42). Relevant modules: + +- `vaara/attestation/_receipt_types.py`: the `ExecutionReceipt` envelope + (`version`, `alg`, `backLink`, `outcomeDerived`, `receiptAsserted`, + `signature`), `OutcomeDerived` with `status` constrained to `executed` / + `refused` / `errored`, and `BackLink` (`attestationDigest`, + `attestationNonce`). This is the outcome record of this SEP, byte for byte. +- `vaara/attestation/_receipt_emit.py`: builds, JCS-canonicalizes (signing input + excludes `signature`), and signs the outcome record; verifies the signature. +- `vaara/attestation/_receipt_verifier.py`: `make_back_link` / `verify_back_link` + and `attestation_digest` (sha256 over the JCS-canonical full SEP-2787 wire + bytes, signature included): the instance-binding join. +- `vaara/attestation/decision.py`: the decision record of this SEP. + `DecisionRecord` / `DecisionDerived`, `emit_decision_record`, + `verify_decision_signature`, `verify_decision_back_link`, and + `records_paired` (the decision-and-outcome join). Reuses the receipt's + back-link, the issuer-block layout, and the shared signing stack unchanged, + so the decision record adds the `decisionDerived` block and no new crypto. +- `vaara/attestation/_sep2787_types.py` and `_sep2787_canonical.py`: the shared + commitment shapes (`ArgsRef`, `ArgsProjection`, `make_args_digest`), the + issuer-block layout, RFC 8785 canonicalization with float rejection, and the + signing stack (`ES256` / `RS256` / `HS256`) reused unchanged. +- `vaara/integrations/_mcp_attest.py` and `mcp_proxy.py`: the proxy emits the + SEP-2787 attestation before the call and the paired outcome record after it, + on a best-effort path that never blocks proxied traffic. +- `vaara/audit/trail.py`: the append-only, hash-chained audit trail the records + are written into; chain version 2 binds `tenant_id` and `chain_version` into + the chained hash so a record cannot be silently re-attributed to another + tenant. +- `vaara/audit/timeanchor.py` and `AuditTrail.enable_auto_anchor`: the external + time anchor over the chain head (Security Implications), shipped in v0.48 as an + RFC 3161 client plus an offline verifier, with optional automatic cadence + anchoring that writes an `ANCHOR_GAP` marker into the chain when the authority + is unreachable. + +**Bridge from the shipped audit decision.** The audit trail already records the +pre-execution decision as a hash-chained `CommitPayload` +(`vaara/audit/receipts.py`: `decision`, `risk_score`, `threshold_allow`, +`threshold_deny`, `decided_at`). `decision_derived_from_commit` maps that +payload onto the signed `decisionDerived` wire shape. The mapping is mechanical +but not a rename: the verdict vocabulary is normalized (the audit layer records +`deny`, the wire enum uses `block`; the review family maps to `escalate`), the +float risk basis becomes decimal strings (floats are banned on the wire), and +the epoch decision time becomes an ISO 8601 UTC string. `policy_id`, +`clientTurnId`, and `reason` are not carried on the commit payload and are +supplied by the caller when available. This keeps the long-retained signed +record free of the float canonicalization drift that the hash-chained payload +tolerated internally. + +## Test Vectors + +The Vaara conformance vectors for the SEP-2787 canonicalization and signature +(`modelcontextprotocol/modelcontextprotocol#2789`) cover the JCS encoding, float +rejection, and the detached-signature scheme that both records in this SEP reuse. +A standard-library-only checker (no Vaara import; `cryptography` for the +asymmetric case) verifies a signed export offline, mirroring the +`scripts/verify_vaara_trail.py` approach. + +The decision-and-outcome pairing has its own published vector suite +(`tests/vectors/decision_pairing_v0/` in the Vaara repository), driven by a +standard-library-only walker (no Vaara import) that asserts an expected verdict +per case. The suite exercises the full pairing algorithm above: + +- a valid decision-plus-outcome pair with its SEP-2787 attestation (`executed`); +- two Check A substitution negatives that MUST fail: a substituted attestation + back-link, and a mismatched pairing nonce; +- a Check B negative: a substituted decision under a shared attestation, where + Check A passes but the outcome's `decisionDigest` commits to a different + decision than the one presented; +- the no-attestation fallback, where the shared `backLink` is over the request + envelope, with a replayed-receipt rejection; +- a decision-only `escalate` with no outcome record yet emitted; +- a supersession tie: two decision records with the same `backLink` and equal + `decidedAt`, resolved to the lexicographically lowest `issuerAsserted.nonce`. + +Because each case carries its expected verdict and the walker takes no Vaara +dependency, an independent emitter or consumer can run the same suite against its +own implementation. For Standards Track finalization, this SEP will additionally +add: + +- JCS canonical vectors for the `decisionDerived` block and the + `outcomeDerived.status` enum, in the same vector format as #2789. +- A `sep-XXXX.yaml` traceability file mapping each MUST / MUST NOT and + SHOULD / SHOULD NOT in the Specification to a conformance check ID, as required + for Standards Track SEPs reaching Final. + +## Acknowledgments + +This proposal builds directly on SEP-2787 (soup-oss) and SEP-2817 (hangum), and +on the server-authoritative record direction set out in Discussion #2704. The +instance-binding versus content-binding discussion with the agent-guard author +(XuebinMa) shaped the normative pairing rule. diff --git a/docs/seps/index.mdx b/docs/seps/index.mdx index 658464479..631c0075e 100644 --- a/docs/seps/index.mdx +++ b/docs/seps/index.mdx @@ -12,53 +12,55 @@ Specification Enhancement Proposals (SEPs) are the primary mechanism for proposi ## Summary +- **Draft**: 1 - **Final**: 41 ## All SEPs -| SEP | Title | Status | Type | Created | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------- | ---------------- | ---------- | -| [SEP-2663](/seps/2663-tasks-extension) | Tasks Extension | Final | Extensions Track | 2026-04-27 | -| [SEP-2596](/seps/2596-spec-feature-lifecycle-and-deprecation) | Specification Feature Lifecycle and Deprecation Policy | Final | Process | 2026-04-17 | -| [SEP-2577](/seps/2577-deprecate-roots-sampling-and-logging) | Deprecate Roots, Sampling, and Logging | Final | Standards Track | 2026-04-14 | -| [SEP-2575](/seps/2575-stateless-mcp) | Make MCP Stateless | Final | Standards Track | 2025-06-18 | -| [SEP-2567](/seps/2567-sessionless-mcp) | Sessionless MCP via Explicit State Handles | Final | Standards Track | 2026-03-11 | -| [SEP-2549](/seps/2549-TTL-for-list-results) | TTL for List Results | Final | Standards Track | 2026-04-09 | -| [SEP-2484](/seps/2484-conformance-tests-required-for-final-seps) | Require Conformance Tests for Standards Track SEPs to Reach Final Status | Final | Process | 2026-03-27 | -| [SEP-2468](/seps/2468-recommend-issuer-claim-for-auth) | Recommend Issuer (iss) Parameter in MCP Auth Responses | Final | Standards Track | 2026-03-25 | -| [SEP-2322](/seps/2322-MRTR) | Multi Round-Trip Requests | Final | Standards Track | 2026-02-03 | -| [SEP-2260](/seps/2260-Require-Server-requests-to-be-associated-with-Client-requests) | Require Server requests to be associated with a Client request. | Final | Standards Track | 2026-02-16 | -| [SEP-2243](/seps/2243-http-standardization) | HTTP Header Standardization for Streamable HTTP Transport | Final | Standards Track | 2026-02-04 | -| [SEP-2207](/seps/2207-oidc-refresh-token-guidance) | OIDC-Flavored Refresh Token Guidance | Final | Standards Track | 2026-02-04 | -| [SEP-2164](/seps/2164-resource-not-found-error) | Standardize Resource Not Found Error Code | Final | Standards Track | 2026-01-28 | -| [SEP-2149](/seps/2149-working-group-charter-template) | MCP Group Governance and Charter Template | Final | Process | 2025-01-15 | -| [SEP-2148](/seps/2148-contributor-ladder) | MCP Contributor Ladder | Final | Process | 2026-01-15 | -| [SEP-2133](/seps/2133-extensions) | Extensions | Final | Standards Track | 2025-01-21 | -| [SEP-2106](/seps/2106-json-schema-2020-12) | Tools `inputSchema` & `outputSchema` Conform to JSON Schema 2020-12 | Final | Standards Track | 2026-01-06 | -| [SEP-2085](/seps/2085-governance-succession-and-amendment) | Governance Succession and Amendment Procedures | Final | Process | 2025-12-05 | -| [SEP-1865](/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp) | MCP Apps - Interactive User Interfaces for MCP | Final | Extensions Track | 2025-11-21 | -| [SEP-1850](/seps/1850-pr-based-sep-workflow) | PR-Based SEP Workflow | Final | Process | 2025-11-20 | -| [SEP-1730](/seps/1730-sdks-tiering-system) | SDKs Tiering System | Final | Standards Track | 2025-10-29 | -| [SEP-1699](/seps/1699-support-sse-polling-via-server-side-disconnect) | Support SSE polling via server-side disconnect | Final | Standards Track | 2025-10-22 | -| [SEP-1686](/seps/1686-tasks) | Tasks | Final | Standards Track | 2025-10-20 | -| [SEP-1613](/seps/1613-establish-json-schema-2020-12-as-default-dialect-f) | Establish JSON Schema 2020-12 as Default Dialect for MCP | Final | Standards Track | 2025-10-06 | -| [SEP-1577](/seps/1577--sampling-with-tools) | Sampling With Tools | Final | Standards Track | 2025-09-30 | -| [SEP-1330](/seps/1330-elicitation-enum-schema-improvements-and-standards) | Elicitation Enum Schema Improvements and Standards Compliance | Final | Standards Track | 2025-08-11 | -| [SEP-1319](/seps/1319-decouple-request-payload-from-rpc-methods-definiti) | Decouple Request Payload from RPC Methods Definition | Final | Standards Track | 2025-08-08 | -| [SEP-1303](/seps/1303-input-validation-errors-as-tool-execution-errors) | Input Validation Errors as Tool Execution Errors | Final | Standards Track | 2025-08-05 | -| [SEP-1302](/seps/1302-formalize-working-groups-and-interest-groups-in-mc) | Formalize Working Groups and Interest Groups in MCP Governance | Final | Standards Track | 2025-08-05 | -| [SEP-1046](/seps/1046-support-oauth-client-credentials-flow-in-authoriza) | Support OAuth client credentials flow in authorization | Final | Standards Track | 2025-07-23 | -| [SEP-1036](/seps/1036-url-mode-elicitation-for-secure-out-of-band-intera) | URL Mode Elicitation for secure out-of-band interactions | Final | Standards Track | 2025-07-22 | -| [SEP-1034](/seps/1034--support-default-values-for-all-primitive-types-in) | Support default values for all primitive types in elicitation schemas | Final | Standards Track | 2025-07-22 | -| [SEP-1024](/seps/1024-mcp-client-security-requirements-for-local-server-) | MCP Client Security Requirements for Local Server Installation | Final | Standards Track | 2025-07-22 | -| [SEP-994](/seps/994-shared-communication-practicesguidelines) | Shared Communication Practices/Guidelines | Final | Process | 2025-07-17 | -| [SEP-991](/seps/991-enable-url-based-client-registration-using-oauth-c) | Enable URL-based Client Registration using OAuth Client ID Metadata Documents | Final | Standards Track | 2025-07-07 | -| [SEP-990](/seps/990-enable-enterprise-idp-policy-controls-during-mcp-o) | Enable enterprise IdP policy controls during MCP OAuth flows | Final | Standards Track | 2025-06-04 | -| [SEP-986](/seps/986-specify-format-for-tool-names) | Specify Format for Tool Names | Final | Standards Track | 2025-07-16 | -| [SEP-985](/seps/985-align-oauth-20-protected-resource-metadata-with-rf) | Align OAuth 2.0 Protected Resource Metadata with RFC 9728 | Final | Standards Track | 2025-07-16 | -| [SEP-973](/seps/973-expose-additional-metadata-for-implementations-res) | Expose additional metadata for Implementations, Resources, Tools and Prompts | Final | Standards Track | 2025-07-15 | -| [SEP-932](/seps/932-model-context-protocol-governance) | Model Context Protocol Governance | Final | Process | 2025-07-08 | -| [SEP-414](/seps/414-request-meta) | Document OpenTelemetry Trace Context Propagation Conventions | Final | Standards Track | 2025-04-25 | +| SEP | Title | Status | Type | Created | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------- | ---------- | +| [SEP-2828](/seps/2828-server-side-signed-execution-record) | Server-Side Signed Execution Record for MCP Tool Calls | Draft | Standards Track (Extensions Track) | 2026-05-31 | +| [SEP-2663](/seps/2663-tasks-extension) | Tasks Extension | Final | Extensions Track | 2026-04-27 | +| [SEP-2596](/seps/2596-spec-feature-lifecycle-and-deprecation) | Specification Feature Lifecycle and Deprecation Policy | Final | Process | 2026-04-17 | +| [SEP-2577](/seps/2577-deprecate-roots-sampling-and-logging) | Deprecate Roots, Sampling, and Logging | Final | Standards Track | 2026-04-14 | +| [SEP-2575](/seps/2575-stateless-mcp) | Make MCP Stateless | Final | Standards Track | 2025-06-18 | +| [SEP-2567](/seps/2567-sessionless-mcp) | Sessionless MCP via Explicit State Handles | Final | Standards Track | 2026-03-11 | +| [SEP-2549](/seps/2549-TTL-for-list-results) | TTL for List Results | Final | Standards Track | 2026-04-09 | +| [SEP-2484](/seps/2484-conformance-tests-required-for-final-seps) | Require Conformance Tests for Standards Track SEPs to Reach Final Status | Final | Process | 2026-03-27 | +| [SEP-2468](/seps/2468-recommend-issuer-claim-for-auth) | Recommend Issuer (iss) Parameter in MCP Auth Responses | Final | Standards Track | 2026-03-25 | +| [SEP-2322](/seps/2322-MRTR) | Multi Round-Trip Requests | Final | Standards Track | 2026-02-03 | +| [SEP-2260](/seps/2260-Require-Server-requests-to-be-associated-with-Client-requests) | Require Server requests to be associated with a Client request. | Final | Standards Track | 2026-02-16 | +| [SEP-2243](/seps/2243-http-standardization) | HTTP Header Standardization for Streamable HTTP Transport | Final | Standards Track | 2026-02-04 | +| [SEP-2207](/seps/2207-oidc-refresh-token-guidance) | OIDC-Flavored Refresh Token Guidance | Final | Standards Track | 2026-02-04 | +| [SEP-2164](/seps/2164-resource-not-found-error) | Standardize Resource Not Found Error Code | Final | Standards Track | 2026-01-28 | +| [SEP-2149](/seps/2149-working-group-charter-template) | MCP Group Governance and Charter Template | Final | Process | 2025-01-15 | +| [SEP-2148](/seps/2148-contributor-ladder) | MCP Contributor Ladder | Final | Process | 2026-01-15 | +| [SEP-2133](/seps/2133-extensions) | Extensions | Final | Standards Track | 2025-01-21 | +| [SEP-2106](/seps/2106-json-schema-2020-12) | Tools `inputSchema` & `outputSchema` Conform to JSON Schema 2020-12 | Final | Standards Track | 2026-01-06 | +| [SEP-2085](/seps/2085-governance-succession-and-amendment) | Governance Succession and Amendment Procedures | Final | Process | 2025-12-05 | +| [SEP-1865](/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp) | MCP Apps - Interactive User Interfaces for MCP | Final | Extensions Track | 2025-11-21 | +| [SEP-1850](/seps/1850-pr-based-sep-workflow) | PR-Based SEP Workflow | Final | Process | 2025-11-20 | +| [SEP-1730](/seps/1730-sdks-tiering-system) | SDKs Tiering System | Final | Standards Track | 2025-10-29 | +| [SEP-1699](/seps/1699-support-sse-polling-via-server-side-disconnect) | Support SSE polling via server-side disconnect | Final | Standards Track | 2025-10-22 | +| [SEP-1686](/seps/1686-tasks) | Tasks | Final | Standards Track | 2025-10-20 | +| [SEP-1613](/seps/1613-establish-json-schema-2020-12-as-default-dialect-f) | Establish JSON Schema 2020-12 as Default Dialect for MCP | Final | Standards Track | 2025-10-06 | +| [SEP-1577](/seps/1577--sampling-with-tools) | Sampling With Tools | Final | Standards Track | 2025-09-30 | +| [SEP-1330](/seps/1330-elicitation-enum-schema-improvements-and-standards) | Elicitation Enum Schema Improvements and Standards Compliance | Final | Standards Track | 2025-08-11 | +| [SEP-1319](/seps/1319-decouple-request-payload-from-rpc-methods-definiti) | Decouple Request Payload from RPC Methods Definition | Final | Standards Track | 2025-08-08 | +| [SEP-1303](/seps/1303-input-validation-errors-as-tool-execution-errors) | Input Validation Errors as Tool Execution Errors | Final | Standards Track | 2025-08-05 | +| [SEP-1302](/seps/1302-formalize-working-groups-and-interest-groups-in-mc) | Formalize Working Groups and Interest Groups in MCP Governance | Final | Standards Track | 2025-08-05 | +| [SEP-1046](/seps/1046-support-oauth-client-credentials-flow-in-authoriza) | Support OAuth client credentials flow in authorization | Final | Standards Track | 2025-07-23 | +| [SEP-1036](/seps/1036-url-mode-elicitation-for-secure-out-of-band-intera) | URL Mode Elicitation for secure out-of-band interactions | Final | Standards Track | 2025-07-22 | +| [SEP-1034](/seps/1034--support-default-values-for-all-primitive-types-in) | Support default values for all primitive types in elicitation schemas | Final | Standards Track | 2025-07-22 | +| [SEP-1024](/seps/1024-mcp-client-security-requirements-for-local-server-) | MCP Client Security Requirements for Local Server Installation | Final | Standards Track | 2025-07-22 | +| [SEP-994](/seps/994-shared-communication-practicesguidelines) | Shared Communication Practices/Guidelines | Final | Process | 2025-07-17 | +| [SEP-991](/seps/991-enable-url-based-client-registration-using-oauth-c) | Enable URL-based Client Registration using OAuth Client ID Metadata Documents | Final | Standards Track | 2025-07-07 | +| [SEP-990](/seps/990-enable-enterprise-idp-policy-controls-during-mcp-o) | Enable enterprise IdP policy controls during MCP OAuth flows | Final | Standards Track | 2025-06-04 | +| [SEP-986](/seps/986-specify-format-for-tool-names) | Specify Format for Tool Names | Final | Standards Track | 2025-07-16 | +| [SEP-985](/seps/985-align-oauth-20-protected-resource-metadata-with-rf) | Align OAuth 2.0 Protected Resource Metadata with RFC 9728 | Final | Standards Track | 2025-07-16 | +| [SEP-973](/seps/973-expose-additional-metadata-for-implementations-res) | Expose additional metadata for Implementations, Resources, Tools and Prompts | Final | Standards Track | 2025-07-15 | +| [SEP-932](/seps/932-model-context-protocol-governance) | Model Context Protocol Governance | Final | Process | 2025-07-08 | +| [SEP-414](/seps/414-request-meta) | Document OpenTelemetry Trace Context Propagation Conventions | Final | Standards Track | 2025-04-25 | ## SEP Status Definitions diff --git a/seps/2828-server-side-signed-execution-record.md b/seps/2828-server-side-signed-execution-record.md new file mode 100644 index 000000000..cee6dd130 --- /dev/null +++ b/seps/2828-server-side-signed-execution-record.md @@ -0,0 +1,632 @@ +# SEP-2828: Server-Side Signed Execution Record for MCP Tool Calls + +- **Status**: Draft +- **Type**: Standards Track (Extensions Track) +- **Created**: 2026-05-31 +- **Author(s)**: Henri Sirkkavaara (@vaaraio), Vaara +- **Sponsor**: None (seeking sponsor) +- **PR**: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2828 +- **Requires**: SEP-2787 (Tool call attestation) +- **Related**: SEP-2817 (AI Invocation Audit Context in Request `_meta`), SEP-414 (request `_meta`) +- **Replaces**: None +- **Discussions-To**: https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/2704 + +## Abstract + +This SEP defines two server-authoritative, cryptographically signed records +for MCP tool calls: a **decision record** emitted by the governing server or +proxy before the side effect runs, and an **outcome record** emitted after +execution completes. The decision record binds what was requested, the policy +decision (allow, block, or escalate), and the risk basis for that decision. The +outcome record binds the execution status (`executed`, `refused`, or `errored`), +a commitment over the result, and timing. The two are paired through a +`backLink` and bound to the originating request, so a verifier can reconstruct +what the agent was permitted to do, why, and what it actually did. + +These records are signed by the server or proxy that governs execution +(`issuerAsserted` / `receiptAsserted`), which is a different trust surface from +the client-asserted attestation of the call in SEP-2787 and the client-asserted +input audit context in SEP-2817. The records use RFC 8785 (JCS) canonical JSON, +a detached signature that does not cover itself, and are offline-verifiable by a +standard-library checker. The wire schema is the shape already shipping in the +Vaara MCP proxy (`vaara.attestation.receipt`), which also provides an +independent verifier and JCS conformance vectors. + +## Motivation + +EU AI Act Article 12 obliges providers and deployers of high-risk AI systems to +keep automatic records of events ("logs") over the system's lifetime, including +records adequate to identify situations that may cause the system to present a +risk and to support post-market monitoring under Article 72. For an +agentic MCP deployment, the regulator's question is not only "what did the +client claim it wanted to do" but "what did the governing system decide, on what +basis, and what did the tool call actually do." That is a statement the server +or governance layer must make, because it is the party that holds the decision +logic and observes the result. + +SEP-2787 standardizes a signed attestation of a tool **call**: it binds the +agent identity, tool name, intent, and an argument commitment into a verifiable +envelope. SEP-2817 standardizes optional, explicitly non-authoritative, +**client-asserted** input audit context (`invocationReason`, `model`, +`userIntent`, `turnId`). Both describe the input side: what was claimed, by the +party making the call. Neither carries the governing system's decision or the +recorded outcome. SEP-2817 says so directly: "server-side decision records, +stable tool-call identity, agent/session correlation, and taxonomies are left to +follow-up SEPs," and Discussion #2704 frames the server-authoritative record as +the deferred half. This SEP is that follow-up. + +Client-asserted records alone cannot satisfy Article 12 for a deployment where +the server enforces policy. A client can claim its intent and its arguments; it +cannot credibly attest that the call was allowed, why it was allowed or blocked, +or what the tool returned, because it does not own that logic and is not a +neutral observer of its own behavior. A regulator auditing an incident needs a +record signed by the enforcement point, paired to the request it answers, that +survives the client. Without a standard for that record, every governance vendor +invents an incompatible one, and cross-implementation verification (the point of +a conformance test) is impossible. + +The records are also the natural place to satisfy the rest of the Article 12 and +Article 14 surface that input audit does not reach: the allow/block/escalate +decision (human-oversight evidence under Article 14), the risk basis that drove +it (risk-management evidence under Article 9), and the post-execution outcome +that feeds post-market monitoring (Article 72). A shipping implementation already +emits all of these as audit events; this SEP standardizes the signed wire shape +so they are portable and independently verifiable. + +## Specification + +### Terminology + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", +"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be +interpreted as described in RFC 2119 and RFC 8174. + +- **Governing server**: the MCP server, or a proxy in front of it, that decides + whether a tool call runs and observes the result. It is the issuer of both + records defined here. +- **Decision record**: a signed envelope emitted before the side effect, + carrying the policy decision and its risk basis. +- **Outcome record**: a signed envelope emitted after execution, carrying the + execution status and a result commitment. +- **Request attestation**: the SEP-2787 envelope describing the tool call. It is + the anchor both records point back to. + +### Trust boundary + +This SEP is server-authoritative. Both records are signed under the governing +server's key and place their bound claims in an issuer block +(`issuerAsserted` for the decision record, `receiptAsserted` for the outcome +record), mirroring the SEP-2787 trust-surface split. The records make claims +about the **decision** and the **execution**, which only the enforcement point +can make. + +This is distinct from SEP-2787, where the issuer attests the **call** (identity, +tool, args) and the planner declares intent, and from SEP-2817, where the client +asserts input context that is explicitly not authorization evidence. A verifier +MUST treat the three as separate surfaces: a SEP-2787 attestation and a SEP-2817 +`_meta` block describe what was asked for; the records in this SEP describe what +the governing system did about it. A server MUST NOT copy client-asserted +SEP-2817 fields into the signed decision or outcome record in a way that implies +the server vouches for their truth; it MAY reference them by `turnId` for +correlation (see Pairing). + +### Canonicalization and signature + +Both records use RFC 8785 (JCS) canonical JSON. The signature is computed over +the JCS-canonical encoding of the record's blocks **excluding** the `signature` +field, and the `signature` field carries the result; it does not cover itself. +This is identical to the SEP-2787 and the shipped Vaara execution-receipt scheme, +so a verifier that already handles SEP-2787 signatures handles these records with +no new cryptographic code. + +- The `alg` field MUST be one of `ES256`, `RS256`, or `HS256`. Production + deployments crossing a trust boundary SHOULD use an asymmetric algorithm + (`ES256` or `RS256`) so verifiers need only the public key. +- IEEE-754 floats MUST NOT appear anywhere in a canonicalized body. Numeric + values that are not integers MUST be encoded as scaled integers or decimal + strings. This removes the most common source of cross-stack signature drift + and matches the Vaara JCS discipline. +- All digests are encoded as the string `sha256:`. +- All timestamps are RFC 3339 / ISO 8601 UTC strings with second precision and a + trailing `Z` (for example `2026-05-31T09:30:00Z`). + +### Argument and result commitments + +Both records reuse the SEP-2787 commitment shapes verbatim. A commitment is one +of: + +- **`ArgsRef`**: a content-addressed reference, `{ "ref": "", "digest": +"sha256:", "canonicalization": "jcs" }`. The verifier MAY fetch `ref` and + check the digest. +- **`ArgsProjection`**: a reviewed projection, `{ "projection": "", +"projectionDigest": "sha256:" }`, where `projection` is the JCS-canonical + encoding of the projection object as a UTF-8 string and `projectionDigest` is + the sha256 over those bytes. + +Commitment-only audit, where the payload never leaves the server, is expressed as +a hash-only-identity `ArgsProjection` whose `projection` is the JCS encoding of +`{ "digest": "sha256:" }` and whose embedded digest binds the underlying +value. This is the shipped `make_args_digest` behavior and is the RECOMMENDED +default for the outcome record's `resultCommitment` so that results, which may +contain personal data, are committed to without being copied into the record. + +### Decision record + +A decision record MUST be emitted by the governing server before the tool call's +side effect runs, for every governed call. It is a JSON object with the +following top-level fields. + +| Field | Type | Required | Description | +| ----------------- | ------- | -------- | ----------------------------------------------------------- | +| `version` | integer | yes | Record schema version. `1` for this SEP. | +| `alg` | string | yes | `ES256`, `RS256`, or `HS256`. | +| `backLink` | object | yes | Join to the SEP-2787 attestation (see below). | +| `decisionDerived` | object | yes | The decision and its risk basis (see below). | +| `issuerAsserted` | object | yes | The governing server's issuer block (see below). | +| `signature` | string | yes | Detached signature over the JCS body excluding `signature`. | + +**`backLink`** joins the decision to the request attestation it governs: + +| Field | Type | Required | Description | +| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `attestationDigest` | string | yes | `sha256:` over the JCS-canonical full SEP-2787 attestation wire bytes, signature included. Pins the exact attestation instance. | +| `attestationNonce` | string | yes | Echoes the attestation's `issuerAsserted.nonce` for fast correlation. | + +If no SEP-2787 attestation exists for the call (the deployment does not run +2787), the server MUST instead bind the request by setting `attestationDigest` to +`sha256:` over the JCS-canonical encoding of the request envelope it +observed (the `tools/call` params plus `_meta`), and set `attestationNonce` to a +server-chosen per-call nonce. The binding is to the request **instance**, not +only its content. + +**`decisionDerived`** carries the decision and the basis for it: + +| Field | Type | Required | Description | +| ---------------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | +| `decision` | string | yes | One of `allow`, `block`, `escalate`. | +| `reason` | string | no | Short human-readable basis for the decision. | +| `riskScore` | string | no | The risk estimate that drove the decision, as a decimal string (floats are prohibited on the wire). | +| `thresholdAllow` | string | no | The allow threshold in force at decision time, as a decimal string. | +| `thresholdBlock` | string | no | The block threshold in force at decision time, as a decimal string. | +| `policyId` | string | no | Identifier or digest of the policy/ruleset version that produced the decision. | +| `decidedAt` | string | yes | ISO 8601 UTC timestamp of the decision. | + +A `decision` of `escalate` means the call was held for human oversight; the +outcome record for that call will report `refused` if the human declined, or a +later decision record MAY supersede it (see Pairing). + +**`issuerAsserted`** is the governing server's signed block: + +| Field | Type | Required | Description | +| --------------- | ------ | -------- | -------------------------------------------------------------------- | +| `iss` | string | yes | Issuer identity (the governing server or proxy). | +| `sub` | string | yes | Subject the decision is about (tenant, agent, or upstream identity). | +| `iat` | string | yes | ISO 8601 UTC issuance time. | +| `nonce` | string | yes | Unique per record. | +| `secretVersion` | string | yes | Key/secret version identifier for rotation and dispatch. | +| `alg` | string | yes | MUST equal the top-level `alg`. | + +### Outcome record + +An outcome record MUST be emitted by the governing server after the governed call +completes (or is refused), and MUST be paired to a decision record for the same +call. It is a JSON object with the following top-level fields. This is the shape +already shipping as `vaara.attestation.receipt.ExecutionReceipt`. + +| Field | Type | Required | Description | +| ----------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `version` | integer | yes | Record schema version. `1`. | +| `alg` | string | yes | `ES256`, `RS256`, or `HS256`. | +| `backLink` | object | yes | Same shape as the decision record's `backLink`; pins the request. | +| `outcomeDerived` | object | yes | Execution status, timing, and result commitment (see below). | +| `receiptAsserted` | object | yes | The governing server's issuer block (same shape as `issuerAsserted`, minus `expSeconds`; an outcome record is a durable record, not a capability, so it carries no `exp` and verifiers enforce no TTL). | +| `signature` | string | yes | Detached signature over the JCS body excluding `signature`. | + +**`outcomeDerived`** carries what happened: + +| Field | Type | Required | Description | +| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `status` | string | yes | One of `executed`, `refused`, `errored`. | +| `completedAt` | string | yes | ISO 8601 UTC completion (or refusal) time. | +| `resultCommitment` | object | no | An `ArgsRef` or `ArgsProjection` over the result (executed) or error object (errored). Absent for `refused`, which has no result. RECOMMENDED to use the commitment-only hash-only-identity projection so result payloads, which may contain personal data, are not copied into the record. | +| `decisionDigest` | string | yes\* | `sha256:` over the JCS-canonical full decision-record wire bytes (signature included) the outcome was produced under. This is the Check B (outcome-to-decision) binding. \*Optional on the wire for backward parsing of records emitted before this field, but a conforming emitter MUST set it and pairing fails without it (see Pairing). | + +### Pairing + +A decision record and an outcome record pair when **both** of the following +hold: + +- **Check A (instance anchor).** Both records carry the same `backLink` + (`attestationDigest` and `attestationNonce` equal). This is instance-binding, + not only content-binding: two byte-identical calls produce two attestations + with distinct nonces and therefore distinct `attestationDigest` values, so a + record cannot be replayed against a different instance of the same call. In the + no-attestation fallback, the shared `backLink` is over the request envelope + instead, and Check A anchors on that. +- **Check B (outcome-to-decision digest, the normative pairing).** The outcome + record's `outcomeDerived.decisionDigest` equals `sha256:` over the JCS + canonical full wire bytes of _this_ decision record. Check A alone admits a + different decision taken under the same attestation instance (an `escalate` and + the human verdict that supersedes it both share the attestation); Check B pins + which decision's content the outcome answers. An outcome record without + `decisionDigest` does not pair: content binding is mandatory, not best-effort. + +A verifier that has both records, plus the SEP-2787 attestation, can then confirm +the full chain: the attestation pins the call; the decision record pins the +policy verdict and risk basis; the outcome record pins what the call did and the +decision it ran under. + +For correlation with client-asserted input context (SEP-2817), a server MAY +include the SEP-2817 `turnId` as an additional, clearly client-asserted field +inside `decisionDerived` under the key `clientTurnId`. This is correlation only; +its presence in the signed body means the server records that the client claimed +this `turnId`, not that the server vouches for it. + +A superseding decision (for example, a human resolving an `escalate`) is recorded +as a new decision record with the same `backLink` and a later `decidedAt`. The +record with the latest `decidedAt` for a given `backLink` is the effective +decision; earlier ones are retained as history. When two records for one +`backLink` carry the **same** `decidedAt`, the tie MUST break deterministically: +the effective record is the one whose `issuerAsserted.nonce` is lexicographically +lowest. This gives every verifier the same winner with no clock authority. +Verifiers MUST NOT treat multiple decision records for one `backLink` as a +conflict. The outcome record's `decisionDigest` (Check B) identifies which +decision in this set the call actually ran under. + +### Transport + +The records are signed JSON objects and are transport-agnostic. A server MAY +return the outcome record in the `tools/call` response `_meta` under the reserved +key `io.modelcontextprotocol/executionRecord`, MAY return the decision record +under `io.modelcontextprotocol/decisionRecord`, and MAY persist either to an +out-of-band audit store. When a record is too large to carry inline (a large +`ArgsRef` chain), the server SHOULD return an `ArgsRef` pointing to the stored +record. This SEP does not require any change to existing MCP message formats; the +records ride in `_meta`, consistent with SEP-414, SEP-2787, and SEP-2817. + +### JSON examples + +Decision record (allow, with risk basis): + +```json +{ + "version": 1, + "alg": "ES256", + "backLink": { + "attestationDigest": "sha256:8f1e2c0a9b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f", + "attestationNonce": "qWZ3kP1nB8tR0aL" + }, + "decisionDerived": { + "decision": "allow", + "reason": "risk below allow threshold", + "riskScore": "0.21", + "thresholdAllow": "0.40", + "thresholdBlock": "0.70", + "policyId": "sha256:3c9d4b8a", + "decidedAt": "2026-05-31T09:30:00Z", + "clientTurnId": "turn-7f3a" + }, + "issuerAsserted": { + "alg": "ES256", + "iat": "2026-05-31T09:30:00Z", + "iss": "vaara-proxy://acme-eu", + "nonce": "Yb7Qd2mK9sV", + "secretVersion": "2026-05", + "sub": "tenant:acme/agent:billing-bot" + }, + "signature": "3045022100ab12" +} +``` + +Outcome record (executed, result committed by digest only): + +```json +{ + "version": 1, + "alg": "ES256", + "backLink": { + "attestationDigest": "sha256:8f1e2c0a9b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f", + "attestationNonce": "qWZ3kP1nB8tR0aL" + }, + "outcomeDerived": { + "status": "executed", + "completedAt": "2026-05-31T09:30:02Z", + "resultCommitment": { + "projection": "{\"digest\":\"sha256:1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c\"}", + "projectionDigest": "sha256:aa11bb22cc33dd44ee55ff66aa77bb88cc99dd00ee11ff22aa33bb44cc55dd66" + } + }, + "receiptAsserted": { + "alg": "ES256", + "iat": "2026-05-31T09:30:02Z", + "iss": "vaara-proxy://acme-eu", + "nonce": "Zc8Re3nL0tW", + "secretVersion": "2026-05", + "sub": "tenant:acme/agent:billing-bot" + }, + "signature": "3046022100cd34" +} +``` + +Outcome record (refused, no result): + +```json +{ + "version": 1, + "alg": "ES256", + "backLink": { + "attestationDigest": "sha256:8f1e2c0a9b7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f", + "attestationNonce": "qWZ3kP1nB8tR0aL" + }, + "outcomeDerived": { + "status": "refused", + "completedAt": "2026-05-31T09:30:01Z" + }, + "receiptAsserted": { + "alg": "ES256", + "iat": "2026-05-31T09:30:01Z", + "iss": "vaara-proxy://acme-eu", + "nonce": "Wd9Sf4oM1uX", + "secretVersion": "2026-05", + "sub": "tenant:acme/agent:billing-bot" + }, + "signature": "3045022100ef56" +} +``` + +### Verification algorithm + +A verifier holding a decision record, an outcome record, the SEP-2787 +attestation, and the governing server's public key (or shared secret for HS256) +MUST perform, and a conforming implementation MUST pass, the following checks: + +1. For each record, recompute the JCS-canonical encoding of the body with the + `signature` field removed, and verify `signature` under the issuer key for the + record's `alg`. Reject on mismatch. +2. Recompute `attestationDigest` from the SEP-2787 attestation wire bytes and + confirm both records' `backLink.attestationDigest` and + `backLink.attestationNonce` match it. Reject on mismatch. +3. Confirm the two records share the same `backLink` (they describe the same + call). +4. If `resultCommitment` is present and the verifier has the result payload, + recompute the commitment digest from the runtime result and confirm it + matches. Reject on mismatch. +5. Confirm `decisionDerived.decision` is one of `allow`, `block`, `escalate` and + `outcomeDerived.status` is one of `executed`, `refused`, `errored`. + +## Rationale + +**Two records, not one.** The decision and the outcome are made at different +times by the same party and answer different regulatory questions (Article 9 and +14 risk and oversight evidence versus Article 72 post-market outcome evidence). A +single combined record would force the server to delay signing until after +execution, losing the pre-side-effect commitment that proves the verdict was +fixed before the action ran. Splitting them, paired by `backLink`, preserves the +commit-before-execute property that is the whole point of an enforcement record. + +**Instance-binding over content-binding.** Earlier drafts in the #2704 / #2817 +discussion considered binding records to the content hash of the call alone. That +lets a record be replayed against any byte-identical call. Binding to the +SEP-2787 attestation digest (signature included) pins the exact attestation +instance, so distinct calls with identical content still produce distinct +bindings. This was the resolved position in discussion; the agent-guard author +conceded the instance-binding point on 2026-05-30, and this SEP adopts it as +normative. + +**Reusing the SEP-2787 stack.** The outcome record is the post-execution sibling +of the SEP-2787 attestation and deliberately reuses its canonicalization, signing +algorithms, commitment shapes, and issuer-block layout. A verifier that handles +SEP-2787 handles these records with no new cryptographic code, and the decision +record adds only one new block (`decisionDerived`). + +**Why server-authoritative.** A client cannot credibly attest a decision it did +not make or an outcome it is not a neutral observer of. SEP-2787 and SEP-2817 +cover the input side precisely because they are client or issuer claims about the +call. The enforcement record has to come from the enforcement point, which is why +this is a separate SEP rather than an extension of either. + +**Alternatives considered.** Carrying the decision inside the SEP-2787 +`issuerAsserted` block was rejected because SEP-2787 attests the call, not the +verdict, and overloading it would blur the trust surface that SEP made explicit. +Adopting a content-addressed `action_ref` as the primary binding field was +rejected: a content-addressed receipt id is a useful secondary index but is +content-binding, and using it as the primary join reintroduces the replay +problem instance-binding solves. The `action_ref` style is reconcilable as an +optional `ArgsRef`-shaped pointer, not as the default join. + +## Backward Compatibility + +This SEP introduces no breaking changes. Both records are new, optional `_meta` +payloads under reserved keys; servers that do not emit them and clients that do +not consume them are unaffected. The schema reuses the SEP-2787 commitment and +issuer-block shapes, so an existing SEP-2787 verifier extends to these records by +adding the `decisionDerived` block and the `outcomeDerived.status` enum, with no +change to the canonicalization or signature code. The records do not alter any +existing MCP request or response method. + +## Security Implications + +**Signing-key compromise and post-hoc backdating.** A signed record proves the +holder of the key bound those values, but a compromised key lets an adversary +mint records with any `decidedAt` / `completedAt` they choose, including +backdated ones, to fabricate a clean history. A signature alone cannot defeat +this. The defense is an **external time anchor**: periodically anchoring the head +of the append-only audit chain that carries these records to an independent, +trusted timestamp (an RFC 3161 timestamp token, or an eIDAS qualified electronic +timestamp) so that records signed after a compromise cannot be inserted before +the last anchored head without detection. The Vaara reference implementation +binds each record into a hash chain (chain version 2, with tenant identity bound +into the chained hash) and ships the external anchor in v0.48: an RFC 3161 +timestamp over the chain head, verifiable offline, with optional automatic cadence +anchoring that fails open by writing a gap marker into the chain when the +authority is unreachable. Anchoring is opt-in. The deployer configures a trusted +timestamp authority; none is bundled by default. Offline verification proves the +token was signed by the certificate it carries; establishing that certificate as +a trusted (for example eIDAS-qualified) authority is deployer policy, enforced by +pinning it. A conforming deployment under EU AI Act retention obligations SHOULD +anchor the chain head externally at a cadence proportionate to its risk. + +**Replay.** Instance-binding through the SEP-2787 attestation digest (signature +included) plus the per-record `nonce` means a record cannot be replayed against a +different call instance. Verifiers MUST reject a record whose `backLink` does not +match the attestation under verification. + +**Instance versus content binding.** As above, binding is to the attestation +instance, not only the call content. Implementations MUST NOT substitute a bare +content hash of the arguments for the `attestationDigest`, because that +reintroduces replay across byte-identical calls. + +**Personal data minimization.** Tool arguments and results can contain personal +data. The RECOMMENDED default for both `decisionDerived` (which never copies the +arguments, only a risk basis and decision) and `outcomeDerived.resultCommitment` +(commitment-only hash-only-identity projection) is that the record commits to a +digest of the value without copying the value. Where a reviewed projection is +needed for audit, servers SHOULD redact to the minimum fields necessary and +commit to the projection, never the raw payload. This keeps the signed, +long-retained record free of personal data while preserving the ability to prove, +given the original value out of band, that it is the one the record committed to. +This aligns with GDPR data minimization and storage limitation alongside the +Article 12 retention obligation. + +**Issuer identity and key distribution.** Verifiers must obtain the governing +server's public key out of band; embedding a public key in the record is a +convenience for local verification only and is not trust-establishing. The +`secretVersion` field supports key rotation; verifiers dispatch on it. + +**Denial of service.** Record emission MUST NOT block legitimate traffic; a +governing server that cannot sign SHOULD fail closed on the **decision** (do not +allow an ungoverned call) but record the signing failure, and MAY degrade outcome +recording to a logged error rather than dropping the call's result. The reference +implementation emits records on a best-effort path that never blocks the proxied +call and counts emission failures for operator alerting. + +## Prior Art Reconciled + +- **SEP-2787 (Tool call attestation, author soup-oss, Extensions Track, open).** + Attests the tool **call**: planner-declared intent, issuer-asserted identity, + payload-derived tool bindings and argument commitment. This SEP is its + post-decision and post-execution counterpart and reuses its canonicalization, + signing, commitment shapes, and trust-surface layout. The `backLink` pins a + SEP-2787 attestation. The split between what was asked (2787) and what was + decided and done (this SEP) is deliberate and preserves 2787's trust surface. + +- **SEP-2817 (AI Invocation Audit Context in Request `_meta`, author hangum, + Standards Track, draft seeking sponsor).** Standardizes optional, + client-asserted input audit context (`invocationReason`, `model`, `userIntent`, + `turnId`) and states explicitly that these fields are not authorization + evidence and that server-side decision records are left to a follow-up SEP. + This is that follow-up. Correlation with 2817 is by optionally recording the + client-asserted `turnId` as `clientTurnId`, clearly marked as a client claim. + +- **agent-guard (XuebinMa, Rust).** Models a `Guard` producing an `AuditRecord` + and an `ExecutionProof`. The two-record decision/outcome split here covers the + same ground as `AuditRecord` plus `ExecutionProof`, with the binding made + explicit and instance-scoped through the SEP-2787 attestation digest rather + than left to a content hash. The instance-binding position was conceded by the + agent-guard author in discussion on 2026-05-30 and is adopted here as + normative. + +- **Content-addressed receipt identifiers.** Some designs identify a record by a + content-addressed id of the form `action_ref = sha256(JCS(...))` over a + description of the action. This SEP does not adopt a content-addressed id as the + default join field, because it is content-binding and reintroduces + cross-instance replay when used as the primary binding. Such a pointer is + reconcilable as an optional `ArgsRef`-shaped reference (the `ref` carries the id + and the `digest` pins it), not as the normative pairing key, which remains the + instance-scoped `backLink`. + +## Reference Implementation + +The wire schema in this SEP is the shape shipping in the Vaara MCP proxy +(v0.48; the receipt library landed in v0.42). Relevant modules: + +- `vaara/attestation/_receipt_types.py`: the `ExecutionReceipt` envelope + (`version`, `alg`, `backLink`, `outcomeDerived`, `receiptAsserted`, + `signature`), `OutcomeDerived` with `status` constrained to `executed` / + `refused` / `errored`, and `BackLink` (`attestationDigest`, + `attestationNonce`). This is the outcome record of this SEP, byte for byte. +- `vaara/attestation/_receipt_emit.py`: builds, JCS-canonicalizes (signing input + excludes `signature`), and signs the outcome record; verifies the signature. +- `vaara/attestation/_receipt_verifier.py`: `make_back_link` / `verify_back_link` + and `attestation_digest` (sha256 over the JCS-canonical full SEP-2787 wire + bytes, signature included): the instance-binding join. +- `vaara/attestation/decision.py`: the decision record of this SEP. + `DecisionRecord` / `DecisionDerived`, `emit_decision_record`, + `verify_decision_signature`, `verify_decision_back_link`, and + `records_paired` (the decision-and-outcome join). Reuses the receipt's + back-link, the issuer-block layout, and the shared signing stack unchanged, + so the decision record adds the `decisionDerived` block and no new crypto. +- `vaara/attestation/_sep2787_types.py` and `_sep2787_canonical.py`: the shared + commitment shapes (`ArgsRef`, `ArgsProjection`, `make_args_digest`), the + issuer-block layout, RFC 8785 canonicalization with float rejection, and the + signing stack (`ES256` / `RS256` / `HS256`) reused unchanged. +- `vaara/integrations/_mcp_attest.py` and `mcp_proxy.py`: the proxy emits the + SEP-2787 attestation before the call and the paired outcome record after it, + on a best-effort path that never blocks proxied traffic. +- `vaara/audit/trail.py`: the append-only, hash-chained audit trail the records + are written into; chain version 2 binds `tenant_id` and `chain_version` into + the chained hash so a record cannot be silently re-attributed to another + tenant. +- `vaara/audit/timeanchor.py` and `AuditTrail.enable_auto_anchor`: the external + time anchor over the chain head (Security Implications), shipped in v0.48 as an + RFC 3161 client plus an offline verifier, with optional automatic cadence + anchoring that writes an `ANCHOR_GAP` marker into the chain when the authority + is unreachable. + +**Bridge from the shipped audit decision.** The audit trail already records the +pre-execution decision as a hash-chained `CommitPayload` +(`vaara/audit/receipts.py`: `decision`, `risk_score`, `threshold_allow`, +`threshold_deny`, `decided_at`). `decision_derived_from_commit` maps that +payload onto the signed `decisionDerived` wire shape. The mapping is mechanical +but not a rename: the verdict vocabulary is normalized (the audit layer records +`deny`, the wire enum uses `block`; the review family maps to `escalate`), the +float risk basis becomes decimal strings (floats are banned on the wire), and +the epoch decision time becomes an ISO 8601 UTC string. `policy_id`, +`clientTurnId`, and `reason` are not carried on the commit payload and are +supplied by the caller when available. This keeps the long-retained signed +record free of the float canonicalization drift that the hash-chained payload +tolerated internally. + +## Test Vectors + +The Vaara conformance vectors for the SEP-2787 canonicalization and signature +(`modelcontextprotocol/modelcontextprotocol#2789`) cover the JCS encoding, float +rejection, and the detached-signature scheme that both records in this SEP reuse. +A standard-library-only checker (no Vaara import; `cryptography` for the +asymmetric case) verifies a signed export offline, mirroring the +`scripts/verify_vaara_trail.py` approach. + +The decision-and-outcome pairing has its own published vector suite +(`tests/vectors/decision_pairing_v0/` in the Vaara repository), driven by a +standard-library-only walker (no Vaara import) that asserts an expected verdict +per case. The suite exercises the full pairing algorithm above: + +- a valid decision-plus-outcome pair with its SEP-2787 attestation (`executed`); +- two Check A substitution negatives that MUST fail: a substituted attestation + back-link, and a mismatched pairing nonce; +- a Check B negative: a substituted decision under a shared attestation, where + Check A passes but the outcome's `decisionDigest` commits to a different + decision than the one presented; +- the no-attestation fallback, where the shared `backLink` is over the request + envelope, with a replayed-receipt rejection; +- a decision-only `escalate` with no outcome record yet emitted; +- a supersession tie: two decision records with the same `backLink` and equal + `decidedAt`, resolved to the lexicographically lowest `issuerAsserted.nonce`. + +Because each case carries its expected verdict and the walker takes no Vaara +dependency, an independent emitter or consumer can run the same suite against its +own implementation. For Standards Track finalization, this SEP will additionally +add: + +- JCS canonical vectors for the `decisionDerived` block and the + `outcomeDerived.status` enum, in the same vector format as #2789. +- A `sep-XXXX.yaml` traceability file mapping each MUST / MUST NOT and + SHOULD / SHOULD NOT in the Specification to a conformance check ID, as required + for Standards Track SEPs reaching Final. + +## Acknowledgments + +This proposal builds directly on SEP-2787 (soup-oss) and SEP-2817 (hangum), and +on the server-authoritative record direction set out in Discussion #2704. The +instance-binding versus content-binding discussion with the agent-guard author +(XuebinMa) shaped the normative pairing rule.