microsoft / microsoft/agent-governance-toolkit
RFC: define the verified approver assertion (MCP-SECURITY-GATEWAY-1.0 §4.3 and ADR-0030)
- Dominant language
- Python
- Stars
- 6.3k
- Forks
- 1.1k
- Avg merge
- 5d 11h
- Merged PRs (30d)
- 142
Description
### Summary
Two places in AGT stop a risky action and wait for a person. The MCP gateway calls `approval_callback` and blocks. ADR-0030 suspends the decision and records the outcome in an approval chain. Both surfaces store who approved, and neither defines a way to check it. What stands behind the recorded name is the approval service's word for it. Authenticated transport proves which service sent the message. It does not prove which human, if any, was at the other end. The hash-linked chain then computes over that name faithfully, which makes the arithmetic sound and the input unverified.
This RFC proposes carrying a signature made by the approver's own authenticator, so a reader of the record can check it later without asking the approval service anything. Three separable pieces:
1. A timeout rule for `approval_callback`. Two paragraphs of spec text, independent of the rest.
2. A result object in place of the bare `ApprovalStatus` string, which makes `PENDING` usable.
3. An optional `approver_evidence` object on both surfaces, covering refusals as well as approvals, with one normative rule about how a runtime reads it.
Piece 3 is optional, and a deployment that omits it behaves exactly as it does today. Piece 1 is the only change that constrains an existing implementation, and it constrains it toward the fail-closed posture the spec already takes everywhere else.
### Motivation
Three reasons this is worth doing now.
**1. The repository already left this blank on purpose, and wrote down that it had.**
ADR-0030 (#2953, merged 2026-06-11), from the proposing PR's own summary of what the ADR does:
> - requires verified approver identity, durable pending state, replay protection, and execution-time revalidation;
> - fails closed on timeout, restart, malformed responses, or binding mismatch;
> - **defines linked approval evidence without requiring signed receipts in the first implementation;**
So verified approver identity is required, and the mechanism that would make it verifiable was left for later. This RFC is that later piece.
A reviewer on the same PR named the gap as a test case. Roman Pelevin, listing the negative matrix he wanted driving the implementation:
> - return an approver string that is not bound to authenticated transport;
That test asks a good question and stops one step short. An approver string bound to authenticated transport is still a string the approval service chose.
The merged webhook contract (#3097) then stated the rule and left the mechanism open:
> ADR-0030 section 5 supersedes that: a webhook is a transport, not an approver identity type, and the contract must carry the binding and refuse body-supplied identities that are not backed by a verified assertion.
The implementation honors that by refusing to trust an unverified identity, and delegates the judgment to a caller-supplied hook:
> an `approve` is honoured only when the approver identity is verified by `response_verifier`
Nothing in the specification says what a verified assertion is, so `response_verifier` is a hole every operator fills privately. This RFC proposes the first defined format that fits it.
The audit schema already carries the field, unbacked. #2473, merged 2026-05-22:
> - `approver_did` — DID of the approving principal (approval-chain identity)
and, from the same PR:
> A new spec subsection §4.3.1 documents the v1.0 caveat (fields recorded but not yet hash-covered)
An identifier for the approver is already persisted. What is missing is anything that makes it more than a label.
**2. Three things an operator cannot do today.**
An auditor holding the sealed record can confirm the digest matches the action, the policy version, the chain integrity, and the expiry. Those checks are real, and they all pass while the approver claim goes unchecked.
First, nobody can prove a human was involved. An automated process holding the approval service's credentials satisfies every check on both surfaces. ADR-0030 keeps an LLM out of terminal resolution and the implementation enforces it, which is the right intent, and neither reaches a compromised or misconfigured approval service. #3805 raises the same defect against a different AGT record, the verifiable-compliance-receipt proposal, and states the consequence plainly:
> every stated verification check (signature validity, chain integrity, policy binding) can pass in full for a receipt chain that is entirely self-signed [...] an auditor reading "signature valid, chain intact, policy hash matches" could reasonably read that as "this was checked," when the honest claim is closer to "this agent process didn't (visibly) lie to itself."
Read that against an approval chain instead of a receipt chain and nothing about it changes.
Second, the assurance label is self-issued. `identity_assurance` is a value the approval service applies to itself. A verifier cannot tell a strong credential from a weak one, or from none.
Third, a refusal carries no more weight than an approval. A `DENIED` result is as unbacked as an `APPROVED` one, which matters in the case where somebody later claims they refused.
**3. The same distinction is already argued in this repository, for a different field.**
#3658, on data residency:
> ADR-0008 models residency as declared metadata (`PolicyMetadata.processing_regions: list[str]`, `AgentPolicyClaims.data_residency`). A declared region string is unverifiable. Nothing binds it to evidence, and nothing carries it across more than one hop.
> The declared-versus-verified distinction is where sovereignty claims actually fail. Deploying in an EU region is necessary and not sufficient.
Substitute the approver name for the region string and the passage needs no other edit. That RFC also says why leaving the weak form in place is worse than leaving the field empty:
> Rejected because an unverifiable residency claim is worse than none: it creates documented, auditable confidence in a property nobody checked.
`identity_assurance` is that claim today. The compliance obligation sits in the same family, and #3658 made that point too:
> AGT ships an Annex IV exporter (`governance/annex_iv.py`) and an evidence pipeline (`governance/evidence_pipeline.py`), so the toolkit already takes conformity documentation seriously. Residency is the one obligation in that family with no mechanism behind it.
Approver identity is the sibling case.
**Worked example, which is the acceptance test I would want.**
A gateway lists `payments.transfer` in `sensitive_tools`. An agent proposes a transfer of 40,000 to account A. Policy escalates. A person approves on a hardware authenticator.
- Re-presenting that approval for a transfer of 40,000 to account B fails, and fails at the digest rather than at a lookup.
- A verifier holding the sealed record, a published public key, and the action confirms that a specific enrolled credential authorized that specific action, with no live call to the gateway, the approval service, or any vendor.
- The same verifier reads approve or refuse out of the signature, not out of a field sitting beside it.
- A compromised approval service cannot manufacture a record that passes those checks.
- If the callback never answers, the call is blocked.
Where those stand today. The first already holds under ADR-0030, which binds the approval to the action digest and revalidates at execution. It has no equivalent on the gateway callback in §4.3, which returns a bare status with nothing to bind. The second, third, and fourth fail on both surfaces, because all three depend on evidence that does not exist. The fifth is unspecified.
So this is not a repair of the binding. The binding is correct. It is the approver claim inside it that nothing checks.
### Detailed Design
**What to add.**
Two schema additions and one normative rule. The timeout rule is a fourth, separate item, and it has its own section below.
**1. The callback returns an object instead of a bare status.** `PENDING` blocks correctly today, and the caller cannot learn which approval it waits on, when the wait ends, or how to resume. A `reason_code` also separates three cases a blocked call currently collapses: a person refused, the request expired unanswered, and no eligible approver was reachable. The third is a staffing failure that presents identically to a security decision. A callback that returns a bare status string keeps working, read as `{"status": ""}`.
**2. That object carries optional `approver_evidence`, and so does `ApprovalChainEntry`.** An array, because one stage can require several approvers. The first registered format is a WebAuthn assertion. The array is the extension point, so the design does not hard-code one credential technology.
**Normative rule.** When `approver_evidence` is present and verifies, the runtime MUST derive `identity_assurance` from the evidence and MUST NOT accept a transport-asserted value for that entry. Where policy requires evidence, absent, malformed, or failing evidence resolves to deny.
Properties the design must satisfy
Stated as properties first, following the convention #3658 used here, which gives its detailed design "deliberately specified as required properties and interface surface rather than a chosen mechanism." One concrete format is proposed below as the first registry entry. Mechanism detail for other credential classes is a follow-up.
1. **Attested, not declared.** An approver assertion must be backed by evidence a verifier can check, not a value the approval service supplies. #3658 states the same property for jurisdiction: "A jurisdiction assertion must be backed by evidence, not a configuration string."
2. **Bound to the action, and to the decision.** The evidence covers the exact action and whether it was approved or refused. An approval must not verify as a denial of the same action.
3. **Fails closed.** Where policy requires evidence, absent, malformed, or failing evidence resolves to deny. A configured callback that does not answer resolves to deny.
4. **Offline-verifiable.** #3658, on the equivalent requirement: "A relying party must be able to recompute the residency verdict from the record with no live call to any participant." Same requirement here, and the Merkle chaining in `governance/audit.py` is the same substrate.
5. **Graduated conformance.** #3658: "A declared region and an attested one must not be indistinguishable to a verifier. The design needs explicit levels so a deployer cannot read the weakest form as the strongest." The assurance tiers below are that requirement applied to approver evidence.
6. **Credential-agnostic.** The evidence slot must not hard-code one credential technology, which is why `approver_evidence` is an array of registered formats rather than a WebAuthn field.
Field detail: the result object and the evidence object
```jsonc
// approval_callback(agent_id, tool_name, params) -> ApprovalResult
{
"status": "APPROVED", // APPROVED | DENIED | PENDING, semantics unchanged
"approval_id": "", // REQUIRED when status is PENDING
"expires_at": "",
"redrive_url": "",
"reason_code": "", // low-cardinality
"approver_evidence": [ ] // optional
}
```
```jsonc
"approver_evidence": [
{
"format": "webauthn.assertion/1", // registry key
"decision": "APPROVED", // or DENIED, and inside the digest
"canonicalization_profile": "",
"credential_id": "",
"client_data_json": "", // carries challenge = nonce || op_hash
"authenticator_data": "", // rpIdHash, flags (UP/UV/BE/BS), signCount
"signature": "",
"aaguid": "",
"attestation_ref": "",
"nonce": "",
"op_hash": "",
"assertion_at": "",
"co_signatures": [ ] // optional
}
]
```
No change to `ActionBinding`, `PolicyDecision`, `ApprovalRequest`, or `ApprovalResolution`. Schema version bump on `ApprovalChainEntry` only.
How the signature is bound to the action
```
op_hash = SHA-256( JCS( canonicalize({ profile, decision, fields }) ) ) // Unicode NFC
nonce = 32 random bytes
challenge = nonce || op_hash
```
WebAuthn lets the relying party choose the challenge freely, so the authenticator signs whatever meaning the challenge carries.
Both halves do work. A challenge equal to the digest alone binds the shape of an action rather than the instance, so two transfers of the same amount to the same account produce an identical challenge and a captured assertion replays against the next one. The nonce makes each ceremony unique.
The decision sits inside the hashed structure. Approve and refuse over the same action therefore produce different digests, which is what lets a verifier read the decision out of the person's signature instead of out of a neighboring field.
The challenge is issued when the approver opens the ceremony, not when the request is created, so assertion freshness stays tight while the human window stays realistic.
Why the signed field set has to be named and versioned
The action object reaches the gateway in different shapes from an MCP `tools/call`, an external-authorization check, a gateway plugin, and a host hook. Signing over "the action" without saying which fields, in which order, is how a signature ends up covering less than the operator believes.
This repository has already shipped that bug. #3507:
> `|` is legal inside an MCP payload and inside a sender id, so this encoding is not injective: distinct (nonce, timestamp, sender_id, payload) tuples collapse to the same canonical string, and therefore to the same HMAC.
>
> An attacker holding one valid envelope can forge others without the signing key, by moving text across a field boundary and reusing the signature verbatim.
The fix in #3508 length-prefixes every field, and its compatibility note states the principle that applies here too:
> That is unavoidable -- the old format cannot be accepted as a fallback without keeping the forgery available.
The mild failure mode is a false reject. The dangerous one is a false accept. So:
1. A named profile per tool schema or host surface.
2. The profile ID inside the hashed structure.
3. Re-verify using the profile ID carried in the evidence, never from ambient configuration.
4. A registry that rejects an unknown profile ID rather than defaulting.
5. Any material field present at execution but absent from the signed set is a hard failure.
Verification procedure for webauthn.assertion/1
1. Parse `client_data_json`. Confirm `type` is `webauthn.get`.
2. Split the challenge into `nonce` (leading 32 bytes) and `op_hash` (trailing 32 bytes). Confirm the nonce matches the one issued for this ceremony and has not been used.
3. Resolve `canonicalization_profile` against the registry. Reject an unknown profile.
4. Recompute `op_hash` from the fields present at execution and the `decision` value, using that profile.
5. Constant-time compare against the digest in the challenge.
6. Confirm `origin` and `rpIdHash` match the configured approval relying party.
7. Confirm the UP flag, and the UV flag if policy requires it.
8. Verify `signature` over `authenticator_data || SHA-256(client_data_json)` using the registered public key.
9. Check `signCount` where the authenticator increments it.
10. Resolve `aaguid` against the FIDO Metadata Service and apply configured policy.
11. Confirm the assertion is inside the freshness window.
12. Verify any `co_signatures` cover the same `op_hash`.
13. Consume exactly once, safe under concurrency, reusing the existing `consume()` guard.
Deriving an assurance tier, non-normative
| Evidence state | Suggested tier |
|---|---|
| UV=1, BE=0, AAGUID resolves to an MDS entry offering biometric user verification | Highest |
| UV=1, BE=0, non-biometric user verification | High |
| UV=1, BE=1 (backup-eligible, replicated credential) | Medium |
| No `approver_evidence` | Lowest, and current behavior |
BE and BS are WebAuthn Level 3 flags recording whether a credential can exist on more than one device. Organizations answer that differently per action class and should stay free to. The point is that the framework cannot record the difference at all right now.
Two clocks, and an optional co-signature
| Clock | Scope | Typical |
|---|---|---|
| Pending-approval TTL | How long the request stays outstanding and re-drivable | Minutes to hours |
| Assertion-freshness window | Maximum age of the assertion at execution | Tens of seconds |
Pending state has to survive a restart of whatever holds it. Otherwise every restart is a silent denial that presents as correct fail-closed behavior, which is an availability bug wearing a security costume. Worth stating in the spec rather than leaving implementations to discover it.
Some deployments hold a second credential whose value is a portable identity binding rather than proof of human presence. A smart-card signature under FIPS 201-3 and NIST SP 800-73 is the common case, chaining to an enterprise PKI a third party already knows how to validate. `co_signatures` carries such a signature over the same `op_hash`, alongside the assertion and never instead of it. A smart-card signature carries no evidence that a biometric verification happened, so a verifier that finds a co-signature and no assertion MUST treat the evidence as invalid. Sketched rather than specified. Say if a concrete second format is wanted.
Interface surface affected
`[VERIFY: paths below come from PR descriptions in #3200 and #2363. Confirm against main before filing.]`
- `docs/specs/MCP-SECURITY-GATEWAY-1.0.md` §4.3 and §4.4: `ApprovalStatus` becomes a result object, and the callback gains a timeout rule.
- `docs/adr/0030-action-bound-approval-protocol.md`: `ApprovalChainEntry` gains one optional field and one normative rule.
- `agent-governance-python/agent-mesh/src/agentmesh/governance/approval_protocol/models.py`
- `agent-governance-typescript/src/approval-protocol/models.ts`, for SDK parity per #3083.
- `agent-governance-*/.../approval_protocol/coordinator.*`: `validateForExecution` gains evidence verification alongside its existing digest, version, integrity, expiry, and consume-once checks.
- `agent-os/mcp_gateway.py`: the callback result shape and the timeout.
- `agent-os/tests/test_spec_mcp_gateway_conformance.py`: new conformance cases, in the style of the 127 already there.
**The timeout rule, which can land on its own.**
§4.2 fails closed when no callback is configured. §4.4 fails closed on `DENIED` and `PENDING`. The spec says nothing about a configured callback that does not answer, which is the normal case when a human sits at the other end.
#3106 took this position for agent-mesh and merged it on 2026-07-01:
> `CallbackApproval.on_timeout` implied timeout auto-approval, which is unsafe at a governance boundary (and was already a dead parameter: a timeout always denies).
Proposed text extends that to this spec:
> A gateway MUST define a timeout for `approval_callback`. On timeout the gateway MUST treat the result as `DENIED` and MUST NOT permit the invocation to proceed. Where the configured approval mechanism involves a human, a gateway SHOULD return `PENDING` rather than holding the callback open, and SHOULD NOT require a synchronous response within a fixed sub-second budget.
This is separable from everything above and could merge alone.
**Staging.**
1. The timeout rule. Two paragraphs, independent of everything below.
2. The result object, with `approver_evidence` unpopulated. Makes `PENDING` usable and gives `reason_code` a home.
3. The evidence object and the normative rule, plus the `webauthn.assertion/1` format and the profile registry.
4. SDK parity across the ports, following the pattern in #3083.
5. Optional co-signature, and the rendering conformance profile if wanted.
**Open questions.** These are design details, not blockers. I would rather settle them in review than guess.
1. Should `decision` sit inside the hashed structure as proposed, or alongside it with a separate binding rule?
2. Where do the evidence-format registry and the canonicalization-profile registry live? These specs, a separate registry document, or the ACS manifest schema?
3. Where does the credential-to-identity mapping live? AgentMesh already binds a `sponsor_email` to every agent identity, and reusing that beats standing up a second registry.
4. Is a FIDO Metadata Service snapshot a runtime dependency the toolkit will take, or should AAGUID policy be host-supplied?
**What we would contribute.**
Token (`tokencore.com`) builds FIDO2/WebAuthn biometric hardware authenticators with match-on-chip fingerprint verification and device-bound credentials.
We will publish, under Apache 2.0, a reference `approval_callback` implementation and a conformance suite in the style of the 127 tests that shipped with #2363. The suite is the more useful artifact, because it lets a partner or an assessor hold any implementation to the same bar without reading anyone's source.
The fourteen checks the suite proves
1. Pending is honored, so nothing executes before an approval exists.
2. Fail-closed holds under a host timeout, including on hosts that fail open.
3. The canonicalization profile round-trips: bound field set equals executed field set.
4. Replay is rejected, and field substitution is rejected.
5. Single use is consumed exactly once under concurrency.
6. Both clocks expire independently and correctly.
7. A partial multi-approver state does not release, and separation of duties holds.
8. An unsigned or wrongly scoped eligibility assertion is rejected.
9. The evidence signature verifies against the published public key, and a tampered record fails.
10. Action content is unreachable before the authentication ceremony completes.
11. A notification token issued for one candidate approver does not authenticate another.
12. An edit to routing data changes who is notified and never who is authorized.
13. Where a co-signature is enabled, both signatures cover the same `op_hash`.
14. An assertion produced for approval does not verify as a denial of the same action, and the reverse.
The proposal is vendor-neutral and satisfiable by any WebAuthn authenticator, including platform authenticators, at the assurance tier the evidence supports. We are offering the implementation and the test vectors, not asking for a vendor-specific path.
**What I am asking for.**
Three answers. The first is the only one that blocks anything.
**1. A direction call: is verifiable approver evidence wanted in this repository at all?** A no is a useful answer and I will stop there rather than keep refining it. A yes does not commit you to the field names above, which are a starting point for review and not the thing I am asking you to accept.
**2. Where it belongs: this issue, or #3668.** #3668 is open and asks for a principal on audit entries, which is the same problem from the audit side. I opened separately because this touches the gateway spec and ADR-0030 rather than the audit schema, and because the timeout rule has nothing to do with audit entries. If you would rather see one thread, say so and I will move the evidence half into #3668 and leave the timeout rule here.
**3. Whether the timeout rule can proceed on its own.** It is two paragraphs, it extends a position #3106 already merged, and it does not depend on anything else in this RFC. If the answer to question 1 is no, I would still like to know whether that piece is worth a PR.
If the direction call is yes, the next artifact from me is a `docs(adr): propose ADR-00NN` PR carrying the spec text for staging items 1 and 2, with the evidence object following once the registry questions above are settled. The Apache 2.0 conformance suite lands with it.
One timing question, so I do not land across work in flight. Is there anything currently moving through `approval_protocol` or `mcp_gateway.py` that this should sequence behind? I would rather wait than merge into a file somebody is mid-refactor on.
No urgency on my side. Happy to take any of this to a call if that is easier than issue comments.
### Alternatives Considered
**Declared-only, as the specification stands.** The cheapest option, and it is what §4.3 and ADR-0030 specify today. Rejected for the reason #3658 gave when it faced the same choice for residency: an unverifiable claim is worse than none, because it creates documented, auditable confidence in a property nobody checked. If the project prefers this anyway, my strong suggestion is the one #3658 made in the same position, which is to label the field explicitly as declared-only in the spec text so `identity_assurance` cannot be mistaken for enforcement.
**Leave it to implementation-level extensions, which §4.3 already allows.** No spec change, no maintenance burden, and an operator who wants approver evidence can build it now. Rejected because that is the status quo, and the status quo is what produces the problem. Every operator who answers "who approved this" invents a private response shape, and none of them interoperate. #2473 added `approver_did`, #3668 proposes reason, argument digest and principal, and that is the same pressure surfacing twice in the audit schema. The hook already exists in `response_verifier`. The thing a private extension cannot supply is a shared definition of what that hook verifies, and the shared definition is the whole value.
**Have the approval service sign a receipt, rather than the person.** Much cheaper to deploy: no credential enrollment, no ceremony, no metadata service, and it does add real tamper-evidence over the current state. Rejected as an answer to this particular problem because it moves the signature without moving the trust. The approval service is the party under evaluation. #3805 quotes `draft-etcheverry-action-ref-03` §5.2 on exactly this point: "The signer of the Decision record MUST be independent of the actor/executor whose action it authorizes." A service-signed receipt is a reasonable intermediate step, and it cannot carry the assurance tiers, so it should not be recorded as though it can.
**Detect after the fact rather than bind before it.** Reconciling approvals against executed actions afterwards is cheaper and needs no change to the approval path. Rejected because the hash-linked chain already provides tamper-evidence, and tamper-evidence is not the missing property. A chain proves nobody edited the record. It does not prove that anyone other than the acting service ever looked at the decision, and no amount of after-the-fact reading recovers a signature that was never taken.
**Fold the whole thing into #3668.** Attractive, and this one is a live question rather than a settled rejection, so it appears in the ask above as well. #3668 asks for reason, argument digest and principal on audit entries, which reaches the same problem from the audit side. I opened separately because the callback result shape and the timeout rule are not audit-schema changes, and because #3668 is scoped to `agent-governance-opencode` while this touches the gateway spec and ADR-0030. If maintainers would rather have one thread, say so and I will move the evidence half there and leave the timeout rule here.
### Security Implications
**What this does not prove.**
#3658 set the convention for this section by stating its own limit in one line: "Hardware attestation proves what code is running. It does not prove where the machine is." The equivalent limit here is the display, not the geography.
A user-verification flag proves that an enrolled human, holding the credential, consented to a bound challenge. It does not prove the human saw the true action, because the action renders on a display outside the authenticator's trust boundary. A compromised approver host can misrepresent what is being approved. That is a property of any authenticator without a trusted display. W3C attempted transaction display in WebAuthn twice and abandoned it both times, which is why this scopes binding to named profiles instead of promising clear-signing of arbitrary API calls.
#3658 lands on "non-repudiation rather than prevention" as the honest description of what its split claim buys. The claim here is stronger than that, because the key sits with the person rather than with an operator who could sign either way, and it is still not proof that the person saw the true action. Two controls narrow the gap and are worth stating as expectations. Action content stays unreachable until the approver completes an authenticated ceremony, so a notification link on its own discloses nothing. And the notification carries a reference, a severity, and an expiry, with no action parameters.
Closing the remainder honestly takes a conformance profile for the rendering surface: what gets rendered, from which authority, in what order, with what truncation and homoglyph handling, plus a correspondence rule that every displayed material field is signed and every signed material field is displayed. Happy to contribute that if it is in scope.
**New attack surface introduced.**
Three things this adds that need treatment rather than assumption.
The canonicalization-profile registry becomes an integrity dependency inside the verification path. A substituted or wrongly resolved profile silently narrows what the signature covers, which is the false-accept case, and it is why the profile ID is inside the hashed structure and re-resolved from the evidence rather than from ambient configuration.
The FIDO Metadata Service becomes an availability dependency wherever AAGUID policy is enforced. A fail-closed posture there turns an MDS outage into a denial of every approval, which is a security control behaving as an availability incident. Open question 4 asks whether the toolkit wants that dependency at all, or whether AAGUID policy should be host-supplied.
Enrollment becomes the soft target. Evidence is only as good as the binding between a credential and a person, so whatever registers credentials inherits the trust that used to sit with the approval service. This RFC does not specify enrollment, and a deployment that gets enrollment wrong gets no benefit from the rest of it.
**Out of scope, and worth naming so it is not assumed.**
Per-action human approval does not fit workflows that propose actions at machine rate. The mechanism that resolves it is a bounded standing grant, where the digest covers a scope expression instead of a single action, with a maximum use count, a magnitude cap, a TTL, and a severity ceiling, all revocable, and no unbounded grant representable. It composes with the evidence object without changing it. Left out here to keep this reviewable.
### Migration / Backward Compatibility
No breaking changes intended.
- `approver_evidence` is optional on both surfaces. Omitting it preserves current behavior exactly.
- A callback that returns a bare `ApprovalStatus` string keeps working, read as `{"status": ""}`.
- Schema version bump on `ApprovalChainEntry` only. No change to `ActionBinding`, `PolicyDecision`, `ApprovalRequest`, or `ApprovalResolution`.
- Existing sealed entries keep verifying. An optional field that is absent changes no digest.
- The timeout rule is the only change that constrains an existing implementation. A gateway with no timeout today has to define one.
If that last item needs a migration path rather than a flag day, #3106 already set the shape for one in this project: warn by default, enforce under an opt-in strict mode, let an environment variable act as a floor that a per-handler setting cannot loosen, and make strict the default in a later major. Reusing that pattern rather than inventing a second one seems right.
### Scope
Cross-package (2-3 packages)
### Target Placement
Core (agent-os, agent-mesh, agent-hypervisor)
### Prior Art
| Reference | What it says | Relationship |
|---|---|---|
| #2953 | "defines linked approval evidence without requiring signed receipts in the first implementation" | This is the deferred piece |
| #3097 | "refuse body-supplied identities that are not backed by a verified assertion" | Defines what a verified assertion is, so `response_verifier` has something to check |
| #2473 | "`approver_did` — DID of the approving principal" | The field exists. This proposes what would populate it verifiably |
| #3106 | Deprecated timeout auto-approval as "unsafe at a governance boundary" | The timeout rule here extends that position to the gateway spec, and its strict-mode pattern is the migration shape |
| #3507, #3508 | A non-injective canonical string produced a real forgery | Motivates the profile registry |
| #3668 | Asks for reason, argument digest, and principal on audit entries | Overlaps `reason_code` and the evidence field. Happy to fold into that issue instead if maintainers prefer |
| #3658 | "A declared region string is unverifiable. Nothing binds it to evidence." | Same distinction, applied to the approver. This RFC follows its structure and its honesty convention |
| #3805 | Quotes `draft-etcheverry-action-ref-03` §5.2: "The signer of the Decision record MUST be independent of the actor/executor whose action it authorizes [...] the signer of record must be independent BEFORE the outcome is final, not verifying it afterward." | An approver assertion satisfies both halves by construction. The key sits with the person, not the operator's infrastructure, and the signature exists before execution |
| #3015, #3076, #3096, #3200 | ADR-0030 implementation and SDK parity | One optional field added to the model those ports share |
| #2363 | The specification this amends | |
On #3668, the overlap is worth quoting, since it reaches the same problem from the audit side:
> When an agent acts under delegated user identity, the target system may record the action as the human's.
Outside this repository, the relevant standards are W3C WebAuthn Level 3 for the assertion and its BE and BS flags, RFC 8785 JCS for canonicalization, which ADR-0030 already uses, and FIPS 201-3 with NIST SP 800-73 for the optional smart-card co-signature. Those are cited as credential and encoding standards, not as validation claims about any product.
Relationship to TRACE
The AgenTrust TRACE specification (`agentrust-io/trace-spec`, v0.2) binds workload identity, model, runtime measurement, policy hash, and tool transcript to a hardware root of trust, and verifies offline. Human approval is the one link in that chain with no root of trust. TRACE already defines External Execution Evidence, out-of-band receipts matched by `linked_call_id` and canonicalized per RFC 8785, and a `webauthn.assertion/1` entry fits that slot unmodified. Both specifications already use JCS, so the digests line up. Noted because defining the object once here makes it portable instead of AGT-specific.
### Checklist
- [x] I have searched existing issues and RFCs for duplicates
- [x] I have read the ADR index (adr/index.md) for related decisions
- [x] I am willing to implement this RFC or help review an implementation
Contributor guide
Research direction
Start by reading ADR-0030 and MCP-SECURITY-GATEWAY-1.0 §4.3, then inspect governance/audit.py and the existing approval-chain and webhook contracts referenced in the issue. Define the result and evidence interfaces, timeout behavior, and verification requirements without assuming a single credential format. Done means the RFC resolves the stated gaps and its worked example and failure cases are testable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend-api-design, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100