Answers · agent safety
How do I record a named human approver, the exact tool call, and the policy version in a tamper-evident ledger entry for every AI agent approval?
Last updated
TL;DR: Write one append-only entry per approval, and make each entry carry four things: the named person who decided (a stable subject id from your identity provider, not a name the agent typed), the exact tool call (tool name plus a SHA-256 of the canonical arguments), the version of the policy that required the approval (a version id or a hash of the policy document as it stood at that moment), and the decision with a timestamp from a clock the agent cannot set. Then make the log tamper-evident: chain each entry to the hash of the one before it, sign it, and periodically publish or anchor the chain head so a later edit or deleted row breaks verification. The policy version is the field most trails leave out, and it is the one an auditor asks for first: "which rule said this needed a human, and which edition of that rule?" request_human_approval hashes the action object you send, so put approver_id, tool, args_sha256 and policy_version inside it; decide_human_approval returns a signed, timestamped decision receipt on Rightshold's hash-chained ledger, and verify_human_approval and ledger_verify are free.
What should I check, step by step?
1. Give every policy an immutable version: a version id plus the SHA-256 of the policy text. Never edit a policy in place; publish a new version and keep the old one readable.
- When a call needs approval, build the action object from the real call: tool name, sha256 of RFC 8785 (JCS) canonical arguments, target or environment, and the policy_version and rule id that triggered the approval.
- Resolve the approver from your SSO or IdP session at decision time (stable subject id, plus display name for humans). Put approver_id into the record server-side; never accept it from agent output.
- Store one entry per decision (approve or deny), with decided_at from your server clock, in an append-only table or ledger the agent's credentials cannot UPDATE or DELETE.
- Chain entries: each one includes prev_hash (the hash of the previous entry) and is signed. Publish or anchor the latest head on a schedule so a rewrite after the fact is detectable by anyone holding an earlier head.
- At execution, recompute args_sha256 and check it against the approved entry, and check that the policy version in force now is the one that was approved under, or that the new version still allows the call. Fail closed otherwise.
- Record denials and expiries too. A trail that only contains approvals cannot show that the gate ever said no.
- Test verification regularly: recompute the chain from the first entry, check every signature, and compare the head with the one you published.
What should one AI agent approval ledger entry contain?
Who: approver_id from the identity provider (subject or employee id), the role or group that made them eligible to approve, and how they authenticated (SSO session, passkey, hardware key).
- What: the tool name, args_sha256 over canonical JSON, the target (account, repo, environment, recipient), and an evidence_ref to the preview the person actually saw.
- Why it needed a person: policy_id, policy_version (or policy_sha256) and the rule that fired, for example "payments over $500 need finance approval, v14".
- Outcome: decision, decided_at, expiry, approval_id, prev_hash and a signature. The agent's session should hold only approval_id.
Why record the policy version with every agent approval?
Policies change. If the threshold moved from $1,000 to $500 last week, an entry that says only "approved" cannot show whether this call needed approval under the rule in force at the time, or whether the approver saw the rule that applied.
- Pinning the version also stops a quiet downgrade: an agent or operator who edits the policy to remove a rule cannot make past calls look as if they never needed a person, because each entry names the version and the hash of that version.
- Rightshold's guardrail gateway (POST /v1/guard/check, rs-guard/0.1) records the policy version with every decision in the owner-only decision log and puts a hash of each decision on the public ledger stream guard_decisions, so the agent sees only allow or deny while the owner keeps the full reasons.
How do I make an agent approval log tamper-evident?
A database row with an audit column is editable by whoever holds the database. Tamper-evident means an edit is detectable, not impossible: hash chaining, signatures and a published head, in the style of Certificate Transparency logs (RFC 6962), do that.
- Keep the log outside the agent's reach. The agent's API key should be able to request an approval and read its status, never to write decisions or rewrite history.
- Rightshold's ledger chains every entry and signs it; ledger_verify recomputes every payload, leaf and block hash, Merkle root, chain link and signature without a key, so a reviewer does not have to rely on Rightshold's word for the history.
- How to audit which human authorized an AI agent action covers the grant and delegation side (token exchange, act claims, scopes). This page is the per-approval entry and the policy version that required it.
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 my agent runs this payment tool call, ask me to approve it, and bind the approval to my approver id, the tool name, a hash of the exact arguments, and the policy version that requires approval.”
{
"tool": "request_human_approval",
"arguments": {
"summary": "pay_invoice INV-2207 for $740 needs finance approval under payments policy v14 (rule over-500)",
"action": {
"type": "tool_call",
"tool": "pay_invoice",
"args_sha256": "sha256:9c1e…",
"target": "vendor:acme-supplies",
"policy_id": "payments",
"policy_version": "v14",
"policy_sha256": "sha256:51ab…",
"rule": "over-500",
"approver_id": "idp:dana@example.com"
},
"expires_in_minutes": 30
}
}- Needs your API key. You decide once with decide_human_approval (owner account key); the signed, timestamped decision receipt goes on the ledger.
- verify_human_approval is free: send the receipt and the action, and it fails if the tool, args_sha256, policy_version or approver_id no longer match what was approved.
- ledger_verify is free and keyless and recomputes the whole chain, so an edited or removed entry shows up.
- record_ai_run can add a signed receipt of what the agent's model produced for the same call; verify_ai_run is free.
Rightshold records approval requests, owner decisions and guardrail decisions for agents that call it, on a signed, hash-chained ledger. It does not run your identity provider, version your internal policies for you, or stop a tool call that never asks. Rightshold runs the service; the ledger lets anyone check the history independently. Not legal advice.
What are the sources?
- NIST SP 800-53 Rev. 5: AU-9 (protection of audit information) and AU-10 (non-repudiation)
- RFC 6962: Certificate Transparency (append-only Merkle hash logs)
- RFC 8785: JSON Canonicalization Scheme (JCS)
- EU AI Act, Regulation (EU) 2024/1689: Article 12 (record-keeping) and Article 14 (human oversight)
- Human approvals on Rightshold
- Agent guardrails on Rightshold
Related
- How to audit which human authorized an AI agent action
- How to bind an approver and scope to an agent tool call, its result, and a timestamp
- How to keep an agent risk approval record outside the agent's editable session
- How to make a named human the accountable owner of an AI agent on the record
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}}'
