Conversation
Standards Track, Draft. Defines one optional reserved _meta key (io.modelcontextprotocol/aiInvocation) carrying client-asserted input-audit context (invocationReason, model, userIntent, turnId) for AI-initiated MCP requests. Operationalizes discussion modelcontextprotocol#2704. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @hangum — this looks like the right SEP-0 boundary to me. From an implementer perspective, the valuable split is clear:
The latest clarifications around per-emitted-request I would keep the security boundary sharp: these fields can help explain how an invocation was produced, but they should not be treated as evidence that the invocation was authorized. That preserves a clean path for a follow-up decision-record SEP without overloading this first input-audit layer. |
|
Thanks @rpelevin — that matches the intended boundary. I agree that the most important part is keeping this layer as client-asserted input-audit metadata only. It can explain how an invocation was produced, but it must not be treated as authorization evidence. That should leave a clean path for a follow-up decision-record SEP to define the server-authoritative layer separately: stable tool-call identity, approval/policy outcome, and execution/decision semantics. |
|
Implementer note from the SEP-2787 side. We shipped a proxy that emits a signed execution receipt paired with 2787 attestation per The "explicitly not authorization evidence" line is the right call, and it's worth saying why. The other half is a server- or proxy-side signed record of what actually ran: the resolved tool, an arguments digest, a result digest, and the 2787 attestation, bound to the same turn. Neither half stands alone as evidence. Client-asserted context with no signed execution record is unverifiable. A signed execution record with no intent context is hard to interpret. Bound together they give an auditable trail from why the model made the call to what the server actually did, which is the gap your follow-up bullet on server-side decision records points at. Concretely, the If the receipt field layout we settled on is useful input for the stable-tool-call-identity follow-up, I can post it. |
|
+1 to @vaaraio’s framing. Using the If the SEP-2787 receipt field layout is ready to share, I think posting it here would be useful input for the follow-up stable-tool-call-identity / decision-record discussion. |
|
Thanks @vaaraio and @AgentGymLeader — this framing matches the intended boundary. I agree that Using If you can share the SEP-2787 receipt field layout, that would be useful prior art for the follow-up stable-tool-call-identity / decision-record SEP. |
|
As committed in #2704, here's how SEP-2817's The load-bearing point first: these fields enter the audit stage, never the policy stage. agent-guard's pipeline is Field mapping
agent-guard's existing Where the SEP-0 / follow-up boundary falls out naturally The signed outcome — Working sketch Input request {
"io.modelcontextprotocol/aiInvocation": {
"invocationReason": { "text": "User asked to view employee rows; employees table identified as target." },
"model": { "name": "example-model" },
"userIntent": { "text": "Show me 10 employees", "redacted": false },
"turnId": "opaque-client-generated-id"
}
}Resulting audit record ( {
"type": "tool_call",
"timestamp": "2026-05-30T00:00:00Z",
"request_id": "req-7f3a",
"session_id": "sess-abc",
"turn_id": "opaque-client-generated-id",
"agent_id": "coder-1",
"tool": "bash",
"payload_hash": "sha256:…",
"decision": "allow",
"policy_version": "0.2.0",
"matched_rule": "read_only_select",
"details": {
"aiInvocation": {
"invocationReason": { "text": "User asked to view employee rows; …" },
"model": { "name": "example-model" },
"userIntent": { "text": "Show me 10 employees", "redacted": false }
}
}
}
Happy to refine this against the SEP as the schema firms up, and to run the implementation pass once it's open for review. |
|
Thanks @XuebinMa — this is a very useful implementation mapping. The most important part is that The I will treat this as a cross-implementation reference for the PR discussion, without expanding SEP-2817’s scope. |
|
For another implementation data point, TadpoleDBHub / Tadpole AI CLI currently implements a similar audit shape, but with app-specific arguments and headers because MCP does not yet have a standard Current mapping:
For strict audit configurations, TadpoleDBHub can reject calls missing the user intent or per-call invocation reason, but these fields remain audit-completeness inputs. Authorization and approval decisions still rely on server-side user/service/DB policy and execution controls, not on client-asserted rationale. This is the implementation pressure behind SEP-2817: the context is operationally useful, but without a standard |
|
@hangum here's the layout. It's shipped in v0.42 ( Three blocks plus the signature, mirroring the SEP-2787 trust-surface layout so both envelopes verify with the same canonicalization (RFC 8785 JCS) and the same HS256/ES256/RS256 stack. A 2787 verifier needs no new crypto to check a receipt. {
"version": 0,
"alg": "ES256",
"backLink": {
"attestationDigest": "sha256:...", // over the full 2787 wire envelope, signature included
"attestationNonce": "..." // echo of issuerAsserted.nonce, for fast correlation
},
"receiptAsserted": {
"iss": "...", "sub": "...", "iat": "...",
"nonce": "...", "secretVersion": "...", "alg": "ES256"
},
"outcomeDerived": {
"status": "executed | refused | errored",
"completedAt": "...",
"resultCommitment": { } // optional; absent for a refused call
},
"signature": "..."
}The field that carries the follow-up SEP is No
@XuebinMa your Layout, vectors, and the offline verifier ( |
|
Thanks @vaaraio — this is very useful prior art for the follow-up decision-record / execution-receipt SEP. I agree with the split: SEP-2817 should keep That gives a clean composition: user turn context ( I will keep this out of SEP-2817’s normative scope, but treat the receipt layout and vectors as strong input for the follow-up server-authoritative execution/decision record work. |
|
Thanks, this is useful prior art for the follow-up layer. The part I would preserve most carefully is the split between the two joins:
That keeps SEP-2817’s boundary clean: client-asserted intent context remains input-audit metadata, while the server/proxy-side receipt proves what actually happened. I would be cautious about standardizing the full receipt envelope here, but the |
This comment was marked as spam.
This comment was marked as spam.
|
Two things worth separating in the composition, since they decide whether the follow-up SEP needs one new shape or two. An allow/deny decision and the execution outcome are the same server-authoritative record, not two stacked layers. A denied call is audit evidence, so the decision belongs as a field on the outcome rather than a slot above it. In the receipt that field is What you bind to matters more than where the decision sits. Binding a decision to On the SETTLED/REVERSED tail: settlement and reversal are transaction semantics, and most So the composition already on the table is the one I'd keep: |
|
Thanks both — this is useful follow-up material. My current read is that SEP-2817 should stay unchanged: For the follow-up decision/execution-record work, I agree the general MCP shape should stay small and domain-neutral: a server-authoritative record with a request backlink and an outcome status such as |
|
Agreed on the back-link — that's the right critique, and it's the gap. agent-guard's So the composition you've drawn is the one I'd build to as well: One implementer data point on that last part, since agent-guard already separates the two timestamps you mention. Its pre-execution decision is Keeping the core enum small and domain-neutral (as you and @hangum landed) is right; settlement lifecycles layer above. Happy to align |
|
@XuebinMa agreed, and REFER is the right thing to pin down before the enum freezes. I'd keep it off the outcome status. The cleaner cut is the two-record shape from earlier in the thread: a decision record written before the side effect, an outcome record written after, both bound to the same attestation by That keeps the normative outcome enum as small and domain-neutral as @hangum landed on, and gives the human-review lifecycle a home on the decision axis rather than the outcome axis. agent-guard's |
This comment was marked as spam.
This comment was marked as spam.
|
On the follow-up server-authoritative record work, two updates. Vaara v0.48.0 ships the external time anchor for the audit chain. The chain head gets a trusted timestamp (an RFC 3161 token by default, or an eIDAS qualified timestamp where one is required), so the head's existence is provable against a clock the runtime does not control, even after a signing-key compromise. That is the post-compromise backdating defense a server-authoritative execution record needs, and it verifies offline. I also drafted the follow-up signed-execution-record SEP this thread keeps pointing at: a decision record before the side effect, an outcome record after, paired by Draft: https://github.com/vaaraio/vaara/blob/main/docs/sep/sep-server-execution-record.md I can open it against the SEP process if that's useful. |
This comment was marked as spam.
This comment was marked as spam.
|
Thanks — this looks like useful material for a separate server-authoritative execution-record SEP. For this PR, I would keep SEP-2817 focused on client-asserted input-audit context only. If you open the signed execution record draft separately, I’d be happy to review it there so this PR can stay scoped. |
|
Opened it as #2828, kept to the server-authoritative half so SEP-2817 stays scoped to client-asserted input audit. It defines a decision record before the side effect (the allow/block/escalate verdict plus the risk basis) and an outcome record after (executed/refused/errored plus a result commitment), paired by Thanks for the offer to review there. |
|
Adoption evidence from a shipping host (Spring AI Playground, Apache-2.0) Disclosure: this comes from a maintainer of the project below, not a neutral observer. Offering it as a cross-implementation data point, since the thread is collecting them. The project already propagates identity and correlation context through tool-call execution for audit: session and conversation identifiers, a per-turn message id, trace/span correlation, and optional user identity context, carried in MDC and observability spans. The context already exists and is operationally useful. What is missing today is a standard protocol location to carry it across the host/server boundary: right now it lives only in the host's own logs and spans and never reaches an external MCP server, because there is no standard On the field mapping, the overlap is strongest at correlation. One thing that matches your framing directly: this is kept strictly as audit context, fully separate from the policy decision. Authorization and the human-in-the-loop approval gate run on a separate path and never read this context, so it is not visible to policy evaluation. "Not authorization evidence" is the right constraint, and it reflects how a shipping host already treats this context. Happy to share the exact field set and how it threads through tool execution if a concrete reference shape would help. Links:
|
|
Thanks @JM-Lab — this is a very helpful shipping-host data point. The Spring AI Playground mapping matches the intended boundary of SEP-2817 well: the context already exists operationally as audit/correlation data, but it does not currently have a standard protocol location across the MCP host/server boundary. I also appreciate the confirmation that this context is kept separate from authorization and approval policy. That is exactly the constraint this SEP is trying to preserve: Given this additional adoption evidence, I would appreciate maintainer guidance on whether SEP-2817 can move toward sponsorship/review, or whether any specific adjustment is needed before that step. |
|
Intermediary data point. Disclosure: I maintain mcp-audit-gateway, a transparent proxy between client and server that writes each The gateway now extracts "parties": [
{ "party": "gateway", "role": "witness", "scope": ["id","timestamp","method","toolName","namespace","upstream","principal","durationMs","success","errorCode","previousHash"] },
{ "party": "client", "role": "asserter", "scope": ["aiInvocation"] }
]One observation from implementing this: intermediaries that rewrite request bodies (policy proxies, body-logging gateways) could strip or mutate the |
|
Thanks @elang2 — this is a very useful intermediary/proxy data point. The I also agree with the intermediary concern. Since proxies and gateways may rewrite request bodies, it is worth making the forwarding expectation explicit so this context does not disappear at the first hop. I can add a short normative note along these lines:
That keeps the field useful for audit pipelines while still allowing intermediaries to enforce local policy when needed. |
|
@elang2 @hangum — the agent-guard is a standalone Rust execution-control runtime, not an MCP intermediary, so this is about record shape rather than SEP conformance. It has two paths: one where the runtime holds the executor start to finish, and one where it hands the action back to the host, which runs it outside the boundary and reports an exit code afterwards. Until last week both emitted the same audit record type. They now don't: ExecutionFinished(ExecutionEvent),
/// Outcome claimed by a host after execution left the Guard's boundary.
/// Unlike `ExecutionFinished`, this is transcribed rather than witnessed.
ExecutionReported(ExecutionEvent),Yours carries the distinction in a field ( We effectively had the field version first: the handoff record already carried @safal207 named the invariant that falls out of this, over in crewAIInc/crewAI#5888: a reduction used for an authority or outcome claim must preserve every evidence distinction that can change the strength of that claim. Two records can share a type and an exit code and still be non-interchangeable witnesses; if a reducer merges provenance classes, the resulting claim has to be bounded by the weaker class rather than silently inheriting the stronger one's meaning. So the question I'd put to the gateway, since the write side is clearly right: does anything downstream of the signed record reduce across the We pinned it with a regression that asserts the negative rather than the positive: no host-supplied result can ever produce a witnessed-execution record, verified by reading the audit stream back rather than by inspecting the emitter. Asserting that the new record gets emitted proves emission; it doesn't prevent reintroduction. Worth flagging too that #5888 has been formalizing this into invariants with an executable conformance suite — witness provenance, deterministic projection over the witness set, and the reduction rule above. Different ecosystem, same boundary, and further along on the theory than anything I have. |
|
@XuebinMa good question, and the honest answer is that the write side is correct but the read side hadn't been tested against this failure mode until now. Today So nothing downstream was reducing across the boundary, but only because nothing downstream was reducing at all. The bug you describe would appear the moment someone writes a consumer that counts outcomes. I just added the regression tests you described. Two assertions that verify the negative by reading the record back:
Commit: elang2/mcp-audit-gateway@a87b09b The invariant from crewAI #5888 ("a reduction used for an authority claim must preserve every evidence distinction that can change the strength of that claim") is exactly what the @hangum the intermediary forwarding note looks right. Thanks for picking that up. |
|
Сюэбин Ма оставил комментарий (modelcontextprotocol/modelcontextprotocol#2817) agent-guard — это автономная среда выполнения Rust для управления выполнением, а не посредник MCP, поэтому речь идёт о форме записи, а не о соответствии стандарту SEP. У неё два пути: один, где среда выполнения контролирует выполнение от начала до конца, и другой, где она передаёт действие обратно хосту, который выполняет его за пределами заданных границ и сообщает код завершения. До прошлой недели оба варианта генерировали один и тот же тип записи аудита. Теперь это не так: ExecutionFinished ( ExecutionEvent ) , По сути, у нас сначала была версия поля: запись о передаче уже содержала sandbox_type: "host-handoff" , с комментарием в документации, указывающим на то, что она существовала именно для того, чтобы потребители могли различать эти два события. Она была корректна на момент записи, и всё равно произошла ошибка, потому что потребитель её свернул. Наш верификатор сократил запись до счётчика — AuditRecord::ExecutionFinished(_) => report.executions_finished += 1 , отбросив тело записи — таким образом, подтвержденный хостом exit_code: 0 и подтвержденное во время выполнения выполнение попадали в один общий итог внутри инструмента, вся задача которого состоит в превращении журнала в доказательство. Различие было представимо, атрибутировано, задокументировано и исчезало к тому моменту, когда кто-либо читал сводку. @safal207 назвал инвариант, вытекающий из этого, в crewAIInc/crewAI#5888 : редукция, используемая для утверждения авторитета или результата, должна сохранять все различия в доказательствах, которые могут изменить силу этого утверждения. Две записи могут иметь общий тип и код завершения и при этом оставаться невзаимозаменяемыми свидетелями; если редуктор объединяет классы происхождения, результирующее утверждение должно быть ограничено более слабым классом, а не молчаливо наследовать значение более сильного класса. Итак, вопрос, который я бы задал шлюзу, поскольку сторона записи явно права: происходит ли какое-либо уменьшение количества данных после подписанной записи за пределами границ parties ? Панели мониторинга, правила оповещения, счетчик «успешных вызовов», экспорт, который сглаживает запись. Именно здесь у нас произошел сбой, и он произошел незаметно — ни один тест не провалился, потому что с записью все было в порядке. Мы зафиксировали это с помощью регрессионного анализа, который подтверждает скорее отрицательную, чем положительную сторону вопроса: ни один результат, предоставленный хостом, никогда не сможет создать запись о подтвержденном выполнении, которая проверяется путем считывания потока аудита, а не путем проверки отправителя. Утверждение о том, что новая запись отправлена, доказывает отправку; это не предотвращает повторное введение. Стоит также отметить, что в задаче #5888 это формализуется в виде инвариантов с помощью исполняемого набора проверок соответствия — происхождение свидетелей, детерминированная проекция на множество свидетелей и правило редукции, описанное выше. Другая экосистема, те же границы, и теория продвинулась дальше, чем в любом из моих проектов. — elang2 оставил комментарий (modelcontextprotocol/modelcontextprotocol#2817) Сегодня mcp-audit verify проверяет целостность цепочки (непрерывность хеша, действительность подписи, монотонные временные метки), но рассматривает каждую запись как единую единицу. Он не сообщает о классе происхождения отдельно. mcp-audit tail отображает все записи одинаково независимо от принадлежности к той или иной стороне. Отсутствовал слой редукции, который бы подсчитывал количество результатов. Таким образом, за границей ничего не уменьшалось, но только потому, что ничего не уменьшалось вообще. Описанная вами ошибка появляется в тот момент, когда кто-то пишет потребитель, который подсчитывает результаты. Я только что добавил описанные вами регрессионные тесты. Два утверждения, которые подтверждают отрицательное значение путем обратного считывания записи: Ни одно поле, подтвержденное клиентом, не может отображаться в области видимости шлюза (и наоборот). Инвариант из запроса crewAI #5888 («сокращение, используемое для утверждения об авторитетности, должно сохранять все различия в доказательствах, которые могут изменить силу этого утверждения») — это именно то, что делает машинно проверяемым массив parties . Теперь тесты гарантируют, что различие сохраняется на уровне записи. Потребителю, который их объединяет, потребуется явно удалить метаданные области видимости, чтобы это сделать. @hangum похоже, что уведомление о пересылке от посредника корректно. Спасибо, что обратили на это внимание. — |
…primitive
Introduces `projectByRole(record, role, party?)` and companion helpers as
the read-side primitive consumers call to preserve scope boundaries under
aggregation. The `parties[]` attribution shipped in v0.2.0 encodes
multi-party role/scope on write; without a scope-projecting read
primitive, consumers can silently collapse the witness/asserter
distinction when reducing across records.
Design decisions locked before implementation:
- Distinct WitnessProjection type, not a nulled AuditRecord. A nulled
record is type-indistinguishable from a legitimate partial record and
invites accidental rehash-as-record.
- Domain-tagged canonical digest. `projectionDigest(p)` reuses shipped
`canonicalizeValue` for the value canonicalization and wraps the
result as `JSON.stringify([PROJECTION_DOMAIN_TAG, canonical])`. Two
steps, both replicable by cross-language re-implementers: (a) the
shared canonical discipline, (b) the outer array wrap for domain
separation. A projection digest cannot collide with a record digest
by construction (records serialize as `{...}`; projections as `[...]`).
- Non-integer numbers, unsafe integers, and lone surrogates in projected
fields all throw via canonicalizeValue. This is the same producer
requirement Vector 2 of the C-REC harness enforces on records.
`durationMs` is currently assigned via `Date.now() - startTime` in
every production path (integer milliseconds by convention, not type);
a future change to a float source would surface here as an
`unsafe number` throw.
- Role-primary API with optional party refinement.
- Lossy for out-of-scope fields; in-scope fields preserved verbatim.
`projectionOf` carries the SHA-256 of the source record so a consumer
can locate the record when needed. `projectionOf` uses `hashRecord`
(raw `JSON.stringify` + sha256, matching how the chain hashes records
since v0.7.0's octets-first change); `projectionDigest` uses
`canonicalizeValue` on the projection body for cross-implementation
stability. The two hash outputs use different canonicalization
disciplines intentionally.
Runtime robustness:
- Imports hashRecord from audit-log directly (no duplication drift).
- Filters unknown role values in enumerators (defensive against
untrusted JSON input where the TypeScript type erases at runtime).
Referenced discussions:
- modelcontextprotocol/modelcontextprotocol#2817 (SEP-2817): parties[]
attribution integrated into the SEP text by hangum; XuebinMa's Aug-24
comment (issuecomment-5390870362) describes agent-guard's
ExecutionFinished vs ExecutionReported split after their consumer
collapsed the record-level distinction under a counter.
- Write-side regression tests at commit a87b09b lock scope-overlap and
cross-scope-leakage invariants at ingest time.
- CycloneDX/specification#1016 (Silentpartnercoding): a
field-mapping-verifier pattern that can call projectionDigest to
compute a scope-bounded digest independently.
Tests (36 total) cover: cross-scope leakage prevention, determinism,
digest-inequality domain separation (projection digest never equals
record digest), structural domain separation (projection canonical
bytes start with `[`, records with `{`), role vs party axis,
reduction-preserves-scope pattern, and edge cases (float durationMs
throws consistently with Vector 2, unknown role values filtered,
dotted-path scope entries return undefined, absent parties[],
deduplication, overlapping scopes).
No behavior changes to existing exports.
…primitive
Introduces `projectByRole(record, role, party?)` and companion helpers as
the read-side primitive consumers call to preserve scope boundaries under
aggregation. The `parties[]` attribution shipped in v0.2.0 encodes
multi-party role/scope on write; without a scope-projecting read
primitive, consumers can silently collapse the witness/asserter
distinction when reducing across records.
Design decisions locked before implementation:
- Distinct WitnessProjection type, not a nulled AuditRecord. A nulled
record is type-indistinguishable from a legitimate partial record and
invites accidental rehash-as-record.
- Domain-tagged canonical digest. `projectionDigest(p)` reuses shipped
`canonicalizeValue` for the value canonicalization and wraps the
result as `JSON.stringify([PROJECTION_DOMAIN_TAG, canonical])`. Two
steps, both replicable by cross-language re-implementers: (a) the
shared canonical discipline, (b) the outer array wrap for domain
separation. A projection digest cannot collide with a record digest
by construction (records serialize as `{...}`; projections as `[...]`).
- Non-integer numbers, unsafe integers, and lone surrogates in projected
fields all throw via canonicalizeValue. This is the same producer
requirement Vector 2 of the C-REC harness enforces on records.
`durationMs` is currently assigned via `Date.now() - startTime` in
every production path (integer milliseconds by convention, not type);
a future change to a float source would surface here as an
`unsafe number` throw.
- Role-primary API with optional party refinement.
- Lossy for out-of-scope fields; in-scope fields preserved verbatim.
`projectionOf` carries the SHA-256 of the source record so a consumer
can locate the record when needed. `projectionOf` uses `hashRecord`
(raw `JSON.stringify` + sha256, matching how the chain hashes records
since v0.7.0's octets-first change); `projectionDigest` uses
`canonicalizeValue` on the projection body for cross-implementation
stability. The two hash outputs use different canonicalization
disciplines intentionally.
Runtime robustness:
- Imports hashRecord from audit-log directly (no duplication drift).
- Filters unknown role values in enumerators (defensive against
untrusted JSON input where the TypeScript type erases at runtime).
Referenced discussions:
- modelcontextprotocol/modelcontextprotocol#2817 (SEP-2817): parties[]
attribution integrated into the SEP text by hangum; XuebinMa's Aug-24
comment (issuecomment-5390870362) describes agent-guard's
ExecutionFinished vs ExecutionReported split after their consumer
collapsed the record-level distinction under a counter.
- Write-side regression tests at commit a87b09b lock scope-overlap and
cross-scope-leakage invariants at ingest time.
- CycloneDX/specification#1016 (Silentpartnercoding): a
field-mapping-verifier pattern that can call projectionDigest to
compute a scope-bounded digest independently.
Tests (36 total) cover: cross-scope leakage prevention, determinism,
digest-inequality domain separation (projection digest never equals
record digest), structural domain separation (projection canonical
bytes start with `[`, records with `{`), role vs party axis,
reduction-preserves-scope pattern, and edge cases (float durationMs
throws consistently with Vector 2, unknown role values filtered,
dotted-path scope entries return undefined, absent parties[],
deduplication, overlapping scopes).
No behavior changes to existing exports.
ozereray
left a comment
There was a problem hiding this comment.
One security boundary I’d keep explicit in the split between invocation audit context and follow-up decision records: the audit envelope explains why the client says it invoked a tool, but it must remain cryptographically / structurally distinct from the server-authoritative execution decision.
For consequential tools, a useful follow-up invariant is: an execution receipt should bind the concrete tool name + canonical argument digest + caller/agent identity + policy/version + decision outcome to the resulting execution. That prevents a valid audit context from being mistaken for authorization, and makes replay or argument mutation detectable after approval.
That separation also maps cleanly to runtime enforcement systems: intent metadata is contextual evidence; the allow/deny/require-approval decision is the control point; the execution receipt is the proof of what actually happened.
Curious whether the follow-up SEP is expected to standardize that binding, or intentionally leave it to implementations.
|
@ozereray — I can't speak for where this SEP goes; that's @hangum's and the maintainers' call. But one fact bears on your question, and it isn't obvious from this thread. The follow-up that would have carried exactly that binding — #2828, "Server-Side Signed Execution Record for MCP Tool Calls" — was closed by its own author on 2026-07-18 and re-homed to an IETF Internet-Draft. So as of today nothing inside MCP is on track to standardize the execution-side binding, and #2817 is deliberately only the audit-context half. Your instinct to keep the two cryptographically distinct is, right now, the whole of the split that exists here. On the invariant itself I'd add one caveat from having shipped it and then broken it. Binding "tool name + canonical argument digest + caller identity + policy version + decision outcome" is necessary but not sufficient: each bound field has to be the value the executor actually consumes. In our own broker the approval bound a target URL resolved one way, while execution resolved the same target a second way and reached a different endpoint. Recompute-and-compare passed on every run, consistently, and the signed receipt described a destination the side effect never touched. The binding was self-consistent and wrong. So if a follow-up does standardize this, the part worth making normative is probably not the field list — that's the easy half, and everyone converges on it — but the recompute-immediately-before-execution step and, specifically, what a verifier is required to reject. A field list without that is checkable and still permits the failure above. (Scope on my side: agent-guard verifies its own Ed25519 receipts, not SEP-2817 records. I'm answering from implementation experience, not claiming coverage of this proposal.) |
|
This is a very useful clarification, especially the distinction between the
audit-context layer in #2817 and the execution-side binding that #2828
would have addressed.
I agree that the normative part should be the execution invariant rather
than simply a prescribed field list. A field list can describe what should
be bound, but it does not guarantee that the executor actually consumes
those values.
For Aegisora, the principle we are converging on is therefore:
*The decision is only valid for execution if the execution-time state
independently reconstructs the same security-relevant effect that the
decision authorized.*
That means the binding has to cover not only canonicalized inputs, but the
values at the final execution boundary. If the executor has a second
resolution path — such as the pushurl case you described — that path has to
be part of the binding or the execution must be rejected.
I also agree that the strongest adversarial test is not “the two paths
normally agree,” but deliberately creating a state where they diverge and
verifying that the runtime refuses to act.
The MCP split you pointed out is particularly relevant to us. We see a
clean architectural separation between:
*Audit context → Decision artifact → Enforcement binding → Execution
evidence*
with each layer proving a different property rather than one signed record
being treated as proof of the entire chain.
We will treat your pushurl failure mode as an explicit acceptance test for
Aegisora rather than assuming that digest recomputation alone is sufficient.
And thank you for pointing out the status of #2828. The fact that the
execution-side binding is currently outside the MCP standard makes this
separation even more important for runtimes that need to enforce policy
before side effects occur.
XuebinMa ***@***.***>, 10 Eyl 2026 Per, 18:37 tarihinde şunu
yazdı:
… *XuebinMa* left a comment (modelcontextprotocol/modelcontextprotocol#2817)
<#2817 (comment)>
@ozereray <https://github.com/ozereray> — I can't speak for where this
SEP goes; that's @hangum <https://github.com/hangum>'s and the
maintainers' call. But one fact bears on your question, and it isn't
obvious from this thread.
The follow-up that would have carried exactly that binding — #2828
<#2828>,
"Server-Side Signed Execution Record for MCP Tool Calls" — was *closed by
its own author on 2026-07-18* and re-homed to an IETF Internet-Draft. So
as of today nothing inside MCP is on track to standardize the
execution-side binding, and #2817
<#2817>
is deliberately only the audit-context half. Your instinct to keep the two
cryptographically distinct is, right now, the *whole* of the split that
exists here.
On the invariant itself I'd add one caveat from having shipped it and then
broken it. Binding "tool name + canonical argument digest + caller identity
+ policy version + decision outcome" is necessary but not sufficient: *each
bound field has to be the value the executor actually consumes.* In our
own broker the approval bound a target URL resolved one way, while
execution resolved the same target a second way and reached a different
endpoint. Recompute-and-compare passed on every run, consistently, and the
signed receipt described a destination the side effect never touched. The
binding was self-consistent and wrong.
So if a follow-up does standardize this, the part worth making normative
is probably not the field list — that's the easy half, and everyone
converges on it — but the *recompute-immediately-before-execution* step
and, specifically, what a verifier is required to reject. A field list
without that is checkable and still permits the failure above.
(Scope on my side: agent-guard verifies its own Ed25519 receipts, not
SEP-2817 records. I'm answering from implementation experience, not
claiming coverage of this proposal.)
—
Reply to this email directly, view it on GitHub
<#2817?email_source=notifications&email_token=A4KE3NSRIVZAULDLJJ6IXQD5OLKFHA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNRSGIYTCNBYG43KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5622114876>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/A4KE3NULUMOJAL6E4QTC4735OLKFHAVCNFSNUABFKJSXA33TNF2G64TZHM4DMMRVG4YDKMRTHNEXG43VMU5TINJVGAYTAMBRHE4KC5QC>
.
You are receiving this because you were mentioned.Message ID:
***@***.***
com>
|
|
@XuebinMa your read of the pushurl case matches mine: the recompute step is the property that matters, and a field list does not reach it. One data point from having built the execution-side gate, since you mentioned where #2828 went. That work continues as The proxy runs the recompute as a separate gate after the policy decision, and it can refuse a call the policy allowed. The order is: operator filter, policy decision, attestation and a grant minted over the argument digest, then the gateway re-verifies the runtime arguments against that digest, then forward upstream. The re-verification is the last thing before the forward. A constrained tool whose arguments changed after the grant was minted comes back refused, with the policy decision still recorded as allow, so the trail carries both. The gate sequence is recorded in order for the same reason you are arguing: a reader can tell a gate that ran and allowed from one that never ran, and a field list does not give you that. Your divergence still applies to this shape, in a relocated form. A proxy can bind the arguments it forwards. It cannot see a second resolution inside the upstream server. If the upstream re-resolves a target after receiving the call, the record describes what was sent and not what the server then did with it. For that case the executor has to attest, and the gate cannot reach it. The code is AGPL if anyone prefers checking the ordering to taking my word for it: |
|
Useful implementation data point. I think this adds one narrow conformance consequence to the #5888 framing: A compact negative vector would be:
while the original policy The upstream re-resolution caveat also keeps the boundary in the right place: the last component that resolves or consumes the side effect must bind or attest the values it actually used. |
|
Agreed. I think that is a very useful way to state the conformance case.
policy=ALLOW and execution=REFUSED should not be treated as contradictory
outcomes. They describe different stages of the same governed execution
attempt: the policy decision authorized the original intent, while the
enforcement gate later determined that the state reaching the execution
boundary no longer matched that authorization.
The important property is therefore preserving the ordered relationship
rather than reducing the whole trace to a single final decision:
ALLOW(A) → grant(A) → runtime args mutate to B → recompute mismatch →
REFUSED / no forward
For Aegisora, that maps directly to the distinction we are trying to
preserve between *decision*, *enforcement*, and *execution evidence*. The
original ALLOW remains an auditable fact, while the later refusal
establishes that the mutated action was not authorized by that decision.
I also agree with the upstream re-resolution boundary. The final component
that resolves or consumes the consequential value has to bind or validate
the value it actually uses; otherwise an apparently correct binding can
still describe the wrong side effect.
This gives us a strong framework-neutral invariant: *authorization may be
valid for A, while execution must still be refused when the actual
execution state is no longer A.*
That is a much more precise contract than simply asking whether a system
“has approval” or “has a signature.”
Aleksey Safonov ***@***.***>, 11 Eyl 2026 Cum, 17:01
tarihinde şunu yazdı:
… *safal207* left a comment (modelcontextprotocol/modelcontextprotocol#2817)
<#2817 (comment)>
Useful implementation data point. I think this adds one narrow conformance
consequence to the #5888 framing: policy=ALLOW and execution=REFUSED can
both be true, so a reducer must preserve gate identity and ordering rather
than collapse the trail into one verdict.
A compact negative vector would be:
ALLOW(A) -> grant(A) -> runtime args mutate to B -> recompute mismatch ->
REFUSED / no forward
while the original policy ALLOW remains independently observable.
The upstream re-resolution caveat also keeps the boundary in the right
place: the last component that resolves or consumes the side effect must
bind or attest the values it actually used.
—
Reply to this email directly, view it on GitHub
<#2817?email_source=notifications&email_token=A4KE3NVAMGL4J3OVZVL4AEL5OQHTBA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNRTGY2DANBSGE22M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5636404215>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/A4KE3NRZEK7XOPGZUAUHQ335OQHTBAVCNFSNUABFKJSXA33TNF2G64TZHM4DMMRVG4YDKMRTHNEXG43VMU5TINJVGAYTAMBRHE4KC5QC>
.
You are receiving this because you were mentioned.Message ID:
***@***.***
com>
|
|
@safal207 I ran your negative vector. Built it as a The short version: this verifier does not collapse the trail, which is what your rule asks for. But all three reports come back identical, including a control where the refusal record carries no Full numbers and the two controls are on a2aproject/A2A#1575, where the corpus lives — parking them there rather than growing this thread. @vaaraio your ordering point lands on the same spot from the proxy side, and your upstream-re-resolution caveat applies to us too: our pushurl failure was that divergence one layer down from yours. Neither of us can close it from the gate. |
|
The recompute step is the part worth writing into the spec. XuebinMa's pushurl case shows why a field list cannot cover it: every field was bound faithfully, the check passed every time, and the record still described an action that never happened. I build agent security tooling, so this comes from the build side. One addition on what a checker must reject. A check only helps someone who was not there if the record says which version of the rules it was made under. Ours carries a version label, and the checker takes the rule version from the record rather than assuming it. We had two versions of one signing rule live at once, the older one weaker; a checker assuming the rules it knew would have accepted the weaker one silently. The reverse holds too, where a checker on newer rules calls a sound record tampered because the format moved on. Which gives the sentence the spec needs. When a checker meets a rule version it does not recognise - refuse or may it use the one it knows? Refusing is safer and it has to be written down because assuming is easier to code. I would also keep the ordering point from safal207 and ozereray: a reader should be able to tell a gate that ran and allowed from one that never ran. |
…udit-context # Conflicts: # docs/seps/index.mdx
|
Updated the branch against current Thanks @ozereray and @damoclais. I agree with the separation. For SEP-2817 I would keep The recompute step, rule/policy version binding, unknown-version rejection, gate ordering, stable call correlation, approval lifecycle, and execution receipts all seem like the right scope for follow-up decision/receipt SEP work. |
|
Thanks, Hangum. I think that scope boundary makes SEP-2817 much cleaner.
Keeping aiInvocation explicitly client-asserted audit context avoids
turning metadata into an implicit authorization or execution claim. In
particular, I agree that the SEP should describe *why the invocation was
produced and how it can be correlated*, while leaving authority,
enforcement, and execution evidence to a separate layer.
The follow-up items you listed also form a useful security progression:
audit context → decision → policy/rule binding → enforcement gate →
execution → receipt
For me, the key property across those layers is that later stages must not
silently inherit trust from earlier metadata. The execution-side verifier
should establish what policy/version was applied, what action was actually
bound, which gates ran, and whether the resulting effect is consistent with
that authorization.
That separation is also central to what we’re exploring with Aegisora:
keeping *audit context, authorization, enforcement, and execution evidence
as distinct claims*, while making the binding between them explicit.
With #2817 now clearly scoped to the audit side, I think the follow-up
decision/receipt work has a much sharper foundation to build on.
Cho HyunJong ***@***.***>, 15 Eyl 2026 Sal, 02:33 tarihinde
şunu yazdı:
… *hangum* left a comment (modelcontextprotocol/modelcontextprotocol#2817)
<#2817 (comment)>
Updated the branch against current main and resolved the SEP index
conflict. The PR is mergeable again and all current checks are passing.
Thanks @ozereray <https://github.com/ozereray> and @damoclais
<https://github.com/damoclais>. I agree with the separation.
For SEP-2817 I would keep aiInvocation limited to client-asserted
input-audit context: why the model/client invoked the tool, which model
produced the invocation, user intent where appropriate, and a turn/request
correlation id. It should remain distinct from authorization, enforcement,
and execution evidence.
The recompute step, rule/policy version binding, unknown-version
rejection, gate ordering, stable call correlation, approval lifecycle, and
execution receipts all seem like the right scope for follow-up
decision/receipt SEP work.
—
Reply to this email directly, view it on GitHub
<#2817?email_source=notifications&email_token=A4KE3NXS2CKI2ALFMXKDVHT5PCE6BA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNRXGI4DINRRGA3KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5672846106>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/A4KE3NUHY4UWKE5OGAVPIAT5PCE6BAVCNFSNUABFKJSXA33TNF2G64TZHM4DMMRVG4YDKMRTHNEXG43VMU5TINJVGAYTAMBRHE4KC5QC>
.
You are receiving this because you were mentioned.Message ID:
***@***.***
com>
|
|
Thanks @ozereray — agreed. That layering matches the intended boundary for SEP-2817. I’ll keep this PR limited to client-asserted audit context and correlation, and leave authorization, enforcement, execution evidence, and receipt binding to follow-up decision/receipt work. |
This PR adds a new Standards Track SEP (Status: Draft, seeking a sponsor): AI Invocation Audit Context in Request
_meta.It standardizes one optional reserved
_metakey —io.modelcontextprotocol/aiInvocation— carrying optional, client-asserted input-audit context for AI-initiated MCP requests:invocationReason— why the AI/client made this callmodel— which model produced the invocationuserIntent— the user-level intent that caused the work (when safe to provide)turnId— groups MCP requests from the same user turn (servers MAY echo onlyturnIdin response_meta)All fields are optional and explicitly not authorization evidence. The proposal is deliberately minimal; server-side decision records, stable tool-call identity, agent/session correlation, and taxonomies are left to follow-up SEPs.
Operationalizes Discussion #2704: #2704
Prior art reconciled in the SEP: SEP-2787 (tool-call attestation), SEP-414 (OTel trace context), SEP-1788 / #775 / #2758 (
_metakey reservation), SEP-2643 (structured authorization denials), SEP-2061 (action security metadata), SEP-2448 / SEP-2028 (telemetry).AI assistance disclosure
This SEP was prepared with AI assistance for structure, wording, and review. The proposal direction, implementation experience, and final technical judgment were reviewed and edited by the author.