A safe agent approval authorizes one identified actor to execute one canonical action, with exact parameters, under one policy version, before an expiry. A generic “looks good” message or a confirmation attached only to a conversation does not provide that binding.
This guide designs the approval boundary between an agent's proposal and a side-effecting tool. It applies to external messages, deployments, purchases, permission changes, deletion, and other consequential actions.
Approval is an authorization event
Treat the model's output as a proposal. The application:
- validates its structure and domain rules;
- computes the action's risk class;
- creates a human-readable preview from validated fields;
- binds the approval request to canonical action data;
- authenticates and authorizes the approver;
- records approve or reject;
- revalidates immediately before execution;
- executes once through an idempotent boundary.
OWASP recommends explicit approval for high-impact and irreversible operations, previews before execution, risk-based autonomy boundaries, audit trails, interruption, and rollback where possible. Its test matrix also calls for rejecting approvals that are invalid, expired, or not bound to parameters. OWASP AI Agent Security Cheat Sheet
Human presence alone is not a control. The person must see enough trustworthy information, have the right authority, and make a decision that execution can verify.
Define the proposed action
interface ProposedAction {
readonly id: string;
readonly runId: string;
readonly tenantId: string;
readonly tool: string;
readonly toolVersion: string;
readonly arguments: unknown;
readonly targetSummary: string;
readonly effectSummary: string;
readonly risk: "medium" | "high" | "critical";
readonly policyVersion: string;
readonly createdAt: string;
}
interface ApprovalRecord {
readonly id: string;
readonly actionId: string;
readonly actionDigest: string;
readonly approverId: string;
readonly decision: "approved" | "rejected";
readonly decidedAt: string;
readonly expiresAt: string;
readonly authenticationContext: string;
}
Resolve the tool from a server-side registry. Do not let the model supply executable code, an arbitrary endpoint, or the prose used to determine approver authority.
Risk classification should combine tool, operation, target, argument ranges, reversibility, audience, and tenant policy. A “send” tool is not uniformly risky: saving a private draft and sending to an external mailing list are different actions.
Generate the preview from trusted fields
The preview should answer:
- What exact action will occur?
- Which account, tenant, environment, recipient, or resource is targeted?
- What data leaves the system?
- What is the estimated monetary or operational impact?
- Is it reversible, and for how long?
- Which policy required approval?
- When does the request expire?
For a message, show recipients, subject, attachments, sending identity, and full final body. For a deployment, show repository, revision, environment, migration behavior, and rollback plan. For deletion, show resource identity, scope, counts from an authoritative query, retention implications, and whether recovery exists.
Never render raw untrusted HTML from the agent or a retrieved document in the approval interface. Escape text, make external links visually distinct, and prevent untrusted content from imitating application controls.
Do not hide changed fields in collapsed sections. A diff against the last approved version is useful, but the final full values remain authoritative.
Canonicalize and hash the action
Hash every field whose change would require a new decision:
approval_subject = {
action_id,
tenant_id,
tool,
tool_version,
arguments,
policy_version,
approver_role_requirement
}
action_digest = SHA-256(canonicalize(approval_subject))
Plain object serialization is not automatically a cross-language canonical format. RFC 8785 defines a JSON Canonicalization Scheme for repeatable hashing and signing, including deterministic property ordering and I-JSON constraints. It is an Informational RFC and has verified errata; adopt it only with conformant libraries, agreed data constraints, and test vectors. RFC 8785, RFC 8785 errata
Alternatively, define a different canonical binary or text encoding. What matters is that every producer and verifier computes identical bytes and rejects unsupported versions.
Immediately before execution:
- load the immutable proposed action;
- recompute its digest;
- compare it with the approval using a constant-time comparison where appropriate;
- verify approver authorization and approval expiry;
- re-check tenant policy and current target state;
- claim a one-time execution intent;
- invoke the tool with an idempotency key.
Any consequential parameter change creates a new action and requires a new approval.
Authenticate the right person
The application must answer two independent questions:
- Authentication: who made the decision?
- Authorization: may that actor approve this action for this tenant, target, value, and environment?
Do not infer approval authority from participation in the chat. Resolve it from the application's identity and policy systems at decision time and again at execution if permissions can change.
Use step-up authentication for actions whose organizational policy requires it. Record the authentication context without storing reusable secrets. Separate proposer and approver where policy requires dual control.
NIST AI RMF 1.0 emphasizes clearly defined and differentiated human roles in AI oversight. This is a risk-management principle, not a universal prescription for a particular authentication method. NIST AI RMF 1.0, Appendix C
Close time-of-check/time-of-use gaps
An approval can be exact yet stale. Between preview and execution:
- a resource may change owners;
- a recipient list may expand;
- a price or exchange rate may change;
- a deployment target may move to another revision;
- policy or approver membership may change.
Include stable resource versions or preconditions in the action when possible. For example, delete record X only if its version remains V. If a material precondition no longer holds, invalidate the approval and present a new preview.
Use short expiries proportional to risk and expected review time. Expiry prevents indefinite reuse but does not replace one-time execution claims.
Execute exactly once at the application boundary
Networks do not guarantee exactly-once side effects. Store a stable execution intent before invoking the external system:
interface ExecutionIntent {
readonly id: string;
readonly actionDigest: string;
readonly approvalId: string;
readonly idempotencyKey: string;
readonly status:
| "claimed"
| "succeeded"
| "failed"
| "outcome_unknown";
readonly externalReference?: string;
}
Atomically claim the action so two workers cannot execute it. Pass the idempotency key downstream where supported. If a timeout occurs after submission, mark outcome_unknown and reconcile through the external reference before retrying.
The durable-agent guide provides the full crash-recovery and reconciliation pattern. Approval says an action may happen; it does not make delivery reliable.
Distinguish clarification, approval, and acknowledgment
These interactions are not interchangeable:
- Clarification: supplies missing information.
- Approval: authorizes a bound future action.
- Acknowledgment: confirms the user saw an event that already occurred.
- Consent to an external flow: allows opening or continuing a separate interaction.
MCP's 2025-11-25 URL-mode elicitation makes the last distinction explicit: a response action of accept means consent to the interaction, not proof that the external flow completed. MCP elicitation specification
Use separate state and UI labels for each event. “Continue” is too vague for a critical action.
Design rejection and interruption
Rejection is a terminal decision for that action digest. The agent may propose a materially different action, but it must not repeatedly present the same rejected request with cosmetic wording changes.
Users need:
- a clear reject option with equal visual availability;
- a way to cancel pending work;
- a history of decisions;
- rollback or compensating action where genuinely supported;
- an incident path when an effect differs from the preview.
Never claim rollback exists unless the external system and your implementation were tested to provide it.
Test the boundary
Automate cases where:
- one argument changes after approval;
- an approval expires one moment before execution;
- the approver loses permission;
- the request is replayed;
- two workers race to execute;
- the tool times out after accepting the action;
- untrusted content imitates an approval button;
- a clarification is submitted where an approval is required;
- the resource version changes between preview and execution;
- a rejected action is proposed again unchanged.
The expected result should come from deterministic policy and storage invariants, not the model deciding to be cautious.
Approval checklist
- Validate and risk-classify before requesting approval.
- Generate previews from trusted, structured fields.
- Show targets, external data, impact, reversibility, and expiry.
- Canonicalize and hash every consequential field.
- Authenticate and authorize the specific approver.
- Re-check digest, policy, authority, expiry, and preconditions at execution.
- Atomically claim a one-time execution intent.
- Use downstream idempotency and reconcile ambiguous outcomes.
- Keep clarification, approval, acknowledgment, and external consent distinct.
- Test tampering, replay, races, stale state, and deceptive content.