A single engineer approving their own agent’s prod.dynamodb.DeleteItem is not a control — it is theater. Dual control means two distinct humans, a change ID, and an explicit blast radius before any prod-touching tool returns credentials or executes. Accountability stays human; the agent only proposes.
⚡ TL;DR: Classify tools
read/write-nonprod/write-prod.write-prodrequires Slack dual-ack from different users + Change Management ID + TTL’d capability token. Log every decision. Pair with Agent Tool Allowlists and Step Functions multi-agent approval gates.
Tool risk classes
// policy/tool-class.ts
export type ToolClass = "read" | "write_nonprod" | "write_prod";
export const TOOLS: Record<string, ToolClass> = {
"repo.search": "read",
"db.query.staging": "read",
"db.mutate.staging": "write_nonprod",
"db.mutate.prod": "write_prod",
"iam.putRolePolicy": "write_prod",
};
// gate/dual-control.ts
export type Approval = {
changeId: string; // CHG######
blastRadius: string; // free text, required min length
approverA: string;
approverB: string;
expiresAt: number; // epoch ms
};
export function assertDual(a: Approval) {
if (a.approverA === a.approverB) throw new Error("same_human");
if (!/^CHG\d{6,}$/.test(a.changeId)) throw new Error("bad_change_id");
if (a.blastRadius.trim().length < 40) throw new Error("blast_radius_too_thin");
if (Date.now() > a.expiresAt) throw new Error("approval_expired");
}
❌ Single emoji reaction from the requester as “approval.”
Slack dual-ack flow
1. Agent requests write_prod tool with plan + blast radius draft
2. Bot posts message to #agent-prod-gates with buttons Approve/Deny
3. Approver A (not requester) Approve
4. Approver B (not A, not requester) Approve
5. Gate mints TTL capability token (5–15 min) bound to tool+args hash
6. Agent invokes tool with token; executor verifies signature + expiry
# executor/verify_token.py
def verify(token: str, tool: str, args_hash: str) -> None:
payload = jwt.decode(token, PUB, algorithms=["RS256"])
assert payload["tool"] == tool
assert payload["args_hash"] == args_hash
assert payload["class"] == "write_prod"
# ✅ requester not in approver set
assert payload["requester"] not in payload["approvers"]
Blast radius template agents must fill
| Field | Example |
|---|---|
| Target | prod payments table pk=order#* |
| Rows estimate | < 20 |
| Reversible | yes — PITR restore |
| Customers | segment: canary-eu |
| Rollback | replay undo stream |
Same interruptibility as Secure AI Sandboxes on ECS — short-lived creds, no standing prod roles for agents.
Closing checklist
✅ Dos
– ✅ Classify every tool; default deny prod writes
– ✅ Dual distinct humans + change ID + blast radius
– ✅ TTL capability tokens bound to args hash
– ✅ Audit log requester, approvers, tool, args hash
– ✅ Expire unused approvals automatically
❌ Don’ts
– ❌ Don’t accept self-approval
– ❌ Don’t reuse tokens across tools
– ❌ Don’t grant standing prod IAM to the agent role
– ❌ Don’t allow empty blast radius
– ❌ Don’t skip deny-path testing in staging
Related reading
- Agent Tool Allowlists: Least Privilege for Filesystem and Shell Access
- Step Functions: Multi-Agent Coding Pipelines With Human Approval Gates
- Secure AI Sandboxes: Ephemeral ECS Tasks for Agent Tool Execution
- AI On-Call Copilots: Suggest Runbooks Without Mutating Production
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
