Answers · agent safety
How do I bind an approver and scope to an agent tool call, its result, and a timestamp?
Last updated
TL;DR: An approval that is not tied to the exact tool call, its result, and when it happened is weak evidence for audit. Build one outside-agent record that names the human approver (IdP subject, not a typed name), the scope that was granted, the tool name plus a hash of the canonical arguments, a digest of the tool result (or a typed error), and a wall-clock timestamp from a clock the agent cannot set. Fail closed if any of those fields is missing, mismatched, or arrives after the fact from chat memory. This is narrower than gateway grant-binding (which captures every tools/call under the live grant) and narrower than a full grant/spend/revocation trail: here the unit of evidence is one approved invocation sealed with who said yes, what scope covered it, what ran, what came back, and when. Pair request_human_approval for the yes, then record_ai_run (or your append-only store) for the sealed tuple; use precall_check before unfamiliar MCP servers.
What should I check, step by step?
1. Identify the human approver from your identity provider (subject, email, or employee id your IdP already asserts). Do not accept a free-text "approved by" string the agent typed.
- Freeze the granted scope on the same row: grant_id or approval_id, allowed actions/targets, policy_version, and expiry. Re-read it at execute time; do not copy it from the agent's paraphrase.
- Canonicalize the tool call: server/tool name, tool_call_id, and SHA-256 of sorted JSON arguments (redact secrets before hashing). Hash the same way at approve time and at seal time.
- After the tool returns, compute a result digest (SHA-256 of canonical success payload or of {isError, error_class}). Never seal "success" without a digest, and never let the model supply the digest.
- Stamp wall-clock time from your server or NTP-synced gateway (ISO-8601 with offset). Reject agent-supplied ran_at unless you treat it as advisory only and keep your own observed_at.
- Write one append-only record outside the session that joins: approver_id, scope/grant fields, tool+args hash, result digest, observed_at. Refuse to mark the action "approved" if any field is null.
- Call request_human_approval with summary plus action { tool, arguments_sha256, scope_grant_id, policy_version }; verify_human_approval again immediately before dispatch; then seal the post-result record.
- Fail closed on missing approver, missing scope, args drift, missing result digest, or clock skew beyond your policy. Log the denial the same way you would log a success.
Why the five fields have to travel together
MCP tools guidance: clients SHOULD show tool inputs before calling the server, prompt for confirmation on sensitive operations, and log tool usage for audit. Showing and logging still leave a gap if the yes is stored without the result digest and a trusted timestamp — an auditor cannot tell which output the human authorized, or when.
- OpenAI Agents SDK human-in-the-loop keeps approval state on the server for browser UIs and warns that RunState.fromString does not authenticate the snapshot: authenticate the reviewer, authorize against pending calls, and consume the decision atomically. Your sealed record should similarly bind identity + pending call, not a client-uploaded confirmed=true flag.
- OWASP Logging Cheat Sheet: each security event needs when, where, who, and what. For agent tool calls, "what" must include both the invocation (tool + args hash) and the outcome (result digest); "who" must be the IdP principal who approved, not the model.
- How to record MCP tool calls at the gateway with scope-grant binding covers capturing every call under the live grant. How to build an agent audit trail tied to grant, spend cap, and revocation covers the authority snapshot across many actions. This page is the single-invocation seal: approver + scope + call + result + time in one row.
Approve the call, then seal the result
request_human_approval before dispatch with the tool name and arguments_sha256 (and grant/scope ids) in the action object. Show the human the real arguments, not the model's summary.
- After tools/call returns, hash the result on the gateway or runner. Write the five-field record in the same durable transaction that marks the approval consumed (or that advances your audit cursor).
- record_ai_run can store a signed receipt of the run summary (model, prompt/output hashes, params that name tool_call_id, args_sha256, result_sha256, approver_id, grant_id, observed_at). verify_ai_run is free. The receipt proves what you recorded, not that the side effect was safe.
- precall_check before unfamiliar MCP servers reduces rug-pull risk; it does not replace binding the human yes to this call's args and result.
What can go wrong?
Approval lives in chat ("looks good") with no args hash, so the agent changes targets after the yes.
- You store tool name + timestamp but omit the result digest, so a later rewrite of the tool output still looks "approved".
- Approver is a display name the agent invented; IdP subject was never checked.
- Gateway grant-binding logs the call under a scope, but no human principal is on the row — useful for capture, incomplete for "who approved this exact outcome".
- Agent supplies ran_at from its session clock; after an incident the times no longer match your SIEM.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Before this tool runs, ask me to approve the exact tool name and argument hash under the current scope grant, then record a signed receipt that also hashes the tool result and the wall-clock time after it returns.”
{
"tool": "request_human_approval",
"arguments": {
"summary": "Approve mcp-crm.contacts.update (args sha256:9f3a…) under grant grnt_… policy_version 14; seal result digest after the call",
"action": {
"type": "tool_call_with_scope",
"tool": "contacts.update",
"server": "mcp-crm",
"arguments_sha256": "9f3a…",
"grant_id": "grnt_…",
"policy_version": 14,
"scope": "crm:contacts:write"
},
"expires_in_minutes": 10
}
}- Needs your API key. The action is hashed; decide once with decide_human_approval. verify_human_approval fails if tool or arguments_sha256 changed.
- After the tool returns, call record_ai_run with prompt_sha256/output_sha256 (or hashes of the call and result) and params naming approver_id, grant_id, tool_call_id, args_sha256, result_sha256, and observed_at.
- precall_check (free) probes an unfamiliar MCP server's tools/list fingerprint before you request approval.
- How to record MCP tool calls at the gateway with scope-grant binding and How to build an agent audit trail tied to grant, spend cap, and revocation cover gateway capture and the multi-action authority trail.
Rightshold records human approval requests, signed decisions, and AI-run receipts for agents that call it. It does not sit in your MCP gateway, assign IdP identities, hash your tool results, or stop a runner that never verifies. Enforce the five-field seal and fail-closed checks in your gateway and tool runner. Not legal advice.
What are the sources?
- Model Context Protocol specification (2025-11-25): Tools — human in the loop; log tool usage
- OpenAI Agents SDK (JS): Human-in-the-loop (keep approval state on the server; authenticate reviewer)
- OWASP Logging Cheat Sheet (when, where, who, what; protect log integrity)
- OWASP GenAI: LLM06:2025 Excessive Agency
- Human approvals on Rightshold
- Verified AI Runs on Rightshold
Related
- How to record MCP tool calls at the gateway with scope-grant binding
- How to build an agent audit trail tied to grant, spend cap, and revocation
- How to show a human the actual change before approving an AI agent action
- How to audit which human authorized an AI agent action
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
