Human-in-the-Loop Gates: Dual Control for Prod-Touching Agent Tools

Human-in-the-Loop Gates: Dual Control for Prod-Touching Agent Tools

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-prod requires 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

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply