Agent Wranglers

AGT-21Required

Confirm risky actions with a token bound to the exact request

An agent call that pays, deletes, sends to many people or cannot be undone first returns a confirmation token. Only a second call with that token, the same arguments and the person's yes carries it out.

How to do it

The client's own approval step is outside your control, so enforce the pause on the server. Return a preview and a token tied to the user, the tool and a hash of the arguments. Let it expire after 10 minutes. Get the person's yes through an input_required round, as Sign the state you hand back for a second round says, or through your own app. Refuse a token used twice, used by someone else or used with changed arguments. Set the money and recipient thresholds in one place, and keep them below the caps in Cap what each agent grant may spend. The client side of the same idea is Ask a person before the agent spends or deletes.

Examples

JavaScript example

import { createHash, randomUUID } from 'node:crypto';
const CONFIRM_TTL_MS = 600_000;
const argsHash = args => createHash('sha256').update(JSON.stringify(args, Object.keys(args).sort())).digest('hex');

export async function confirmOrPreview(tool, args, user, preview, run) {
  const { confirmation, ...rest } = args;
  if (!confirmation) {
    const id = randomUUID();
    await confirmations.insert({ id, userId: user.id, tool, hash: argsHash(rest), expiresAt: Date.now() + CONFIRM_TTL_MS });
    return { needsConfirmation: true, confirmation: id, preview: await preview(rest) };
  }
  const used = await confirmations.consume(confirmation, { userId: user.id, tool, hash: argsHash(rest), now: Date.now() });
  if (!used) return { isError: true, content: [{ type: 'text', text: 'Confirmation is missing, expired or for other arguments. Ask again.' }] };
  return run(rest);
}

Python example

import hashlib, json, time, uuid

CONFIRM_TTL_SECONDS = 600

def args_hash(args: dict) -> str:
    return hashlib.sha256(json.dumps(args, sort_keys=True).encode()).hexdigest()

def confirm_or_preview(tool: str, args: dict, user, preview, run):
    rest = {key: value for key, value in args.items() if key != "confirmation"}
    if "confirmation" not in args:
        token = str(uuid.uuid4())
        confirmations.insert(id=token, user_id=user.id, tool=tool, hash=args_hash(rest), expires_at=time.time() + CONFIRM_TTL_SECONDS)
        return {"needsConfirmation": True, "confirmation": token, "preview": preview(rest)}
    if not confirmations.consume(args["confirmation"], user_id=user.id, tool=tool, hash=args_hash(rest), now=time.time()):
        return {"isError": True, "content": [{"type": "text", "text": "Confirmation is missing, expired or for other arguments. Ask again."}]}
    return run(rest)

How to check

A test calls refund_order without a token and gets a preview, not a refund. A second call with the token and a changed amount is refused. The same token used twice refunds once. A token older than 10 minutes is refused.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/agents-as-users/safe-actions/confirmation-token">Agent Wranglers agents as users playbook</a>. In code you write, add a comment next to it with the same link.