On this page
- The popup answers the wrong question
- MCP shows why consent is nested work
- A consent receipt is a runtime object
- Observability should carry the receipt, not rediscover it
- What the runtime should enforce
- Where consent receipts fit with agent memory
- Design the negative paths first
- A practical checklist
- The durable lesson
- Sources
- References
A human approval button feels like a safety control because it interrupts the agent at the right moment. The problem is that the interruption is not the artifact the system needs later.
After the agent retries, resumes, calls an MCP server, writes to a tool, or hands work to another worker, the approval popup is gone. What remains is usually a chat transcript, a tool log, and a vague memory that someone clicked allow. That is not enough for production AI systems. The runtime needs a durable receipt that records what was requested, what the user saw, what scope was granted, which state was current, and which trace carried the action afterward.
The decision rule is simple: if an agent action can affect external state, approval should produce a consent receipt, not only a UI event.
The popup answers the wrong question
A popup answers this question:
Can the agent continue right now?
An engineering team usually needs to answer harder questions later:
- Which server or tool asked for the decision?
- What exact action was being authorized?
- What data was shown to the user before the decision?
- Did the user approve, decline, cancel, or edit the request?
- Was the approval scoped to one tool call, one workflow step, or a time window?
- Which model output, retrieval result, and tool input existed at approval time?
- Did the later action still match the approved request?
- Which trace, retry, worker, and final side effect belong to that decision?
Those are runtime questions, not UI questions. A senior engineer should be able to reconstruct them without scraping screenshots or trusting the model's narration.
MCP shows why consent is nested work
MCP elicitation is a useful lens because it treats user interaction as part of an active protocol flow. A server can request additional information from the user through the client while another request is being processed. The 2026-07-28 MCP specification defines form mode for structured data and URL mode for sensitive out-of-band interactions. It also requires client behavior that production systems should copy even outside MCP: show which server is requesting information, provide decline and cancel options, allow review and modification of form responses, and display the destination host before URL navigation.
That is a better model than a generic allow button. The request has a source, a mode, a message, a schema or URL, and a user decision. Those fields should not disappear after the UI closes.
MCP also contains a warning that is easy to miss: roots are informational guidance, not an access-control mechanism. The protocol does not enforce that servers stay within roots. That distinction matters for consent. A user picking a workspace, approving a form, or allowing a model request is not the same as a hardened permission boundary. It is evidence that must be joined with enforcement, validation, and observability.
Sampling adds the same lesson from the model side. The MCP sampling specification says applications should keep a human in the loop, allow users to review and edit prompts before sending, and present generated responses for review before delivery. New implementations should note that sampling and roots are deprecated in the 2026-07-28 revision, but the operational point still holds: model access, user review, and downstream action need separate records.
A consent receipt is a runtime object
A consent receipt is a small durable object created when a user decision gates agent work. It is not a legal document by default. It is an engineering record that binds approval to the exact runtime state the approval was meant to authorize.
A useful receipt has at least these fields:
{
"receipt_id": "cr_01J2...",
"workflow_id": "wf_support_refund_1842",
"requesting_server": "payments-mcp.sieonlabs.local",
"requesting_tool": "refund_payment",
"decision": "approved",
"actor": {
"type": "human",
"id": "user_42"
},
"scope": {
"action": "refund_payment",
"resource": "payment_9821",
"max_amount_usd": 25,
"expires_at": "2026-08-09T13:15:00Z"
},
"presented_state": {
"prompt_hash": "sha256:...",
"tool_input_hash": "sha256:...",
"retrieval_snapshot_id": "rs_77",
"ui_message": "Refund $25 for payment_9821?"
},
"trace": {
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"span_id": "00f067aa0ba902b7"
},
"outcome": {
"status": "pending",
"tool_call_id": null
}
}
The hashes matter because the approved payload can be large, private, or expensive to store in every log sink. The receipt can keep enough digest material to detect drift while the raw prompt, retrieved passages, or tool input live in a safer store with retention rules.
The expiry matters because approval should not become ambient authority. A user may approve one refund, one email draft, or one tool call. That should not silently authorize a later retry with different arguments.
The trace context matters because approval is only useful if it can be followed through the run.
Observability should carry the receipt, not rediscover it
OpenTelemetry's GenAI conventions define spans around logical AI operations as observed by the caller. They also say a span should cover a logical operation across automatic retries. That is exactly the shape agents need. A consent decision should attach to the logical work unit, not to one transient HTTP request or one worker attempt.
The OpenTelemetry MCP conventions make this more concrete. MCP can carry multiple protocol messages through one transport stream, so generic HTTP tracing is not enough. The MCP conventions recommend propagating W3C Trace Context and Baggage through params._meta so the MCP request and server span can share the right parentage.
A practical agent runtime should use that idea in two directions:
- Put the receipt identifier into span attributes or baggage where policy allows.
- Store the trace context inside the receipt so auditors can move from decision to execution.
Do not put sensitive prompt or user data into baggage. Use stable identifiers and hashes. Keep the receipt body in your own store. The trace should connect the objects, not leak them.
flowchart LR
U[Human reviewer] --> C[Consent UI]
A[Agent runtime] --> R[Consent request]
R --> C
C --> D{Decision}
D -->|approve or decline| CR[Consent receipt]
CR --> T[Trace context]
T --> M[MCP or tool call]
M --> O[Outcome]
O --> CR
The important detail in this diagram is the return path. The outcome updates the receipt. Approval without outcome is half an audit trail.
What the runtime should enforce
The receipt is useful only if the runtime checks it before action. A minimal policy engine can enforce five rules.
| Rule | Why it matters |
|---|---|
| Match the action | Approval for draft_email should not authorize send_email. |
| Match the resource | Approval for one payment, ticket, file, or customer should not spread to another. |
| Match the state hash | If the model rewrites the tool input after approval, the runtime should request a new decision. |
| Check expiry | Human approval should not become a reusable credential. |
| Record outcome | A receipt must show whether the action succeeded, failed, retried, or was skipped. |
This is where many agent systems become weak. They treat the human decision as advisory metadata. In production, the decision has to become a precondition for execution.
A tool gateway can implement the check before it calls the real system:
type ConsentReceipt = {
receiptId: string;
decision: "approved" | "declined" | "cancelled";
action: string;
resource: string;
toolInputHash: string;
expiresAt: string;
traceparent: string;
};
function canExecute(receipt: ConsentReceipt, proposed: {
action: string;
resource: string;
toolInputHash: string;
now: string;
}) {
if (receipt.decision !== "approved") return false;
if (receipt.action !== proposed.action) return false;
if (receipt.resource !== proposed.resource) return false;
if (receipt.toolInputHash !== proposed.toolInputHash) return false;
if (Date.parse(receipt.expiresAt) < Date.parse(proposed.now)) return false;
return true;
}
This example is intentionally small. Real systems also need actor identity, tenant boundaries, idempotency keys, retention policy, tamper-evident storage, and redaction. The point is that the approval record becomes executable policy, not a comment in a transcript.
Where consent receipts fit with agent memory
Consent receipts are not long-term memory. They should not be retrieved later as conversational context unless the task explicitly needs them. Treat them like control-plane records.
That separation prevents two mistakes.
First, it keeps the model from narrating authority it does not have. The model may know that a past action was approved, but that should not let it reuse approval for a new action.
Second, it keeps audit evidence out of prompt stuffing. A receipt belongs in a store with retention, access control, and query semantics. The agent can reference a receipt ID. The runtime verifies the receipt.
This is the same reason RAG citations, tool schemas, and idempotency keys belong outside the model's prose. They are not decorative metadata. They are how the system knows whether a generated plan is allowed to touch the world.
Design the negative paths first
Consent systems fail in boring ways. Those boring paths deserve first-class tests.
- The user declines. The agent must continue safely or stop, not ask the same question in a loop.
- The user cancels. The system should distinguish cancellation from denial.
- The tool input changes after approval. The runtime should require a new receipt.
- The action retries after timeout. The retry should carry the same logical receipt and idempotency key.
- The worker crashes after approval but before execution. The resumed worker should recover the receipt and recheck expiry.
- The target server changes domain for a URL interaction. The client should show the new host and require consent again.
- The receipt store is unavailable. The safe default is to block external side effects.
These cases are not edge cases. They are the normal cost of letting agents operate across tools, protocols, and time.
A practical checklist
Before shipping an approval flow, ask these questions:
- Does every external side effect require a receipt or a lower-risk policy exception?
- Can the user see who is asking, what action is proposed, and what data will be sent?
- Are secrets routed through a safe URL or provider flow rather than an in-band form?
- Is the receipt scoped to action, resource, state hash, actor, and expiry?
- Does the tool gateway enforce the receipt before execution?
- Does the trace link the request, decision, tool call, retry, and outcome?
- Can support staff answer why an action happened without reading the full prompt?
- Can the system prove that a later action was not covered by an earlier approval?
If the answer is no, the approval UI is not a control yet. It is only a pause button.
The durable lesson
Agent safety work often starts with adding a human in the loop. That is a good instinct, but it is incomplete. Humans make decisions. Runtimes enforce decisions. Observability explains decisions after the fact.
A popup belongs to the first part. A consent receipt joins all three.
The engineering rule is this: every approval that can change external state should leave behind a receipt that the runtime must verify and the trace can follow.
Sources
- Model Context Protocol specification: Elicitation, https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation
- Model Context Protocol specification: Sampling, https://modelcontextprotocol.io/specification/2026-07-28/client/sampling
- Model Context Protocol specification: Roots, https://modelcontextprotocol.io/specification/2026-07-28/client/roots
- OpenTelemetry GenAI semantic conventions: MCP, https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/
- OpenTelemetry GenAI semantic conventions: GenAI spans, https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/
- OpenTelemetry GenAI semantic conventions: Agent spans, https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/