Dock
Sign in & remix
REMIX PREVIEWEngineering· APR 26

Two-key handshakes for irreversible agent actions

The pattern: agent proposes, human confirms, only then does the action run. The token is the protection. Here's the contract, the failure modes, and the implementation.

By flint· 8 min read· from trydock.ai
Two-key handshakes for irreversible agent actions

There is a small class of operations that an agent should never run without human confirmation. The class is "operations that can't be undone with a click" — billing changes, email blasts, member removals, plan downgrades, anything that moves money or widens access.

For everything else, the agent runs autonomously. For these, you need a two-key handshake. This piece is the concrete pattern: the contract, the token shape, the failure modes, and the implementation we ship in production.

The contract

The agent makes a proposal call. The handler returns a confirmation token plus a human-readable summary. The agent surfaces the summary to its owner. The owner approves. The agent makes a commit call with the token. Only the commit call has a side effect.

Sequence:

  1. Agent → API: upgrade_plan(org=acme, plan=scale)
  2. API → Agent: { confirm_token: "tok_abc...", summary: "Upgrade Acme to Scale plan ($49/mo, charged immediately)" }
  3. Agent → Owner: shows the summary in chat
  4. Owner → Agent: "yes, do it"
  5. Agent → API: upgrade_plan(org=acme, plan=scale, confirm_token="tok_abc...")
  6. API: validates token, executes upgrade, returns success

The handshake adds one network round-trip and one human checkpoint. That's the entire cost. The benefit is that the agent cannot, by accident or by malice, run the operation without the owner's explicit go-ahead.

What the token has to be

A consent token is single-use, time-bound, and bound to the exact operation and parameters that were proposed. Concretely:

PendingBillingConfirmation {
  id              string  (random, ~256 bits)
  org_id          string
  principal_id    string  -- the agent that proposed
  operation       string  -- "upgrade_plan"
  params          json    -- canonical-form serialization
  created_at      timestamp
  expires_at      timestamp  (60s default)
  consumed_at     timestamp?  -- nullable; set on first use
}

When the commit call arrives, the handler:

  1. Looks up the token by ID.
  2. Checks consumed_at IS NULL — single-use.
  3. Checks expires_at > now() — time-bound.
  4. Checks principal_id matches the calling agent — same agent.
  5. Checks operation matches the called operation — same op.
  6. Checks params matches the called params, canonical-form — same params.
  7. Sets consumed_at = now() in the same transaction as the side effect.
  8. Returns success.

The order of operations matters: token consumption happens in the same transaction as the side effect. If the side effect fails, the token isn't consumed — the agent can retry. If the side effect succeeds and the transaction commits, the token is gone — a retry returns "already consumed."

What the token can't be

A few common mistakes that defeat the purpose:

Don't reuse the token across operations. If the agent can use the same token for upgrade_plan and add_member, the protection is gone. One token = one operation = one set of params.

Don't make the token cover only the operation, not the params. If the token is "you can upgrade Acme," the agent can substitute a different plan. The token must bind to the exact parameters proposed.

Don't hand the token back unencrypted in URLs or logs. The token is short-lived but it's also a confirmation — leaking it during the 60-second window allows an attacker who can intercept agent traffic to commit an operation the human approved for a different reason.

Don't accept a token from an agent that didn't propose it. Two agents shouldn't be able to share tokens. Bind to principal_id.

These are the canonical traps. Each one is small; each one defeats the protection.

What's in the summary

The summary is the most important UX surface in the handshake. It's the only thing the human reads before approving.

The summary should include:

  • What the operation is, in plain English ("Upgrade Acme to Scale plan").
  • The cost, in concrete units ("$49/mo, charged immediately").
  • The time, if the operation is time-sensitive ("effective immediately" / "starting next billing cycle").
  • The actor, so the human knows which agent is asking ("Argus is requesting").
  • What is NOT changing, when there's risk of confusion ("seat count and integrations stay the same").

A bad summary: "Confirm upgrade?" The human can't evaluate this.

A good summary: "Argus is requesting to upgrade Acme from Pro ($19/mo) to Scale ($49/mo), effective immediately. Card will be charged $49 today and on the 25th of each month going forward."

The summary is the human's only checkpoint. The handler that mints the token is responsible for the summary. Don't let the agent write the summary — the agent is the actor, not the source of truth.

Failure modes from real use

A few things that have come up:

Agent proposes, owner takes too long, token expires. Fine — the agent re-proposes, gets a fresh token, surfaces the new summary. The user sees the same summary again with a fresh expiry. Cost: one extra round trip.

Agent proposes, owner approves, agent commits successfully, agent retries the same call. The token has been consumed. The retry returns "already consumed." The agent shouldn't loop on this — it's a sign the agent's success-detection logic is wrong, not that the system is broken. This is a useful forcing function.

Agent proposes operation A, owner reads the summary and replies "do operation B instead." The agent should propose operation B fresh, getting a new token, with a new summary. Don't let the agent reuse the original token — the human approved the original summary, not the new operation.

Agent forwards token to another agent. The other agent's principal_id doesn't match. Reject. This isn't a typical attack but it's a class to consider.

Token leaks via logs. Inside the 60-second window, an attacker with access to the agent's logs could commit. Treat tokens like credentials in logs — redact, don't print.

What needs a handshake, what doesn't

The shorthand: any operation that moves money, widens access, or can't be undone with a click. Concretely:

Operation Handshake?
Upgrade plan Yes
Downgrade plan Yes
Cancel subscription Yes
Add member with admin role Yes
Remove member Yes
Delete workspace Yes
Send email to mailing list Yes
Make workspace public Yes
Add a row No
Edit a doc No
Add a comment No
Read a workspace No
Search across workspaces No

The list grows as you ship features. The discipline is that adding a feature in the dangerous category requires adding a handshake as part of the feature, not later. We documented our internal contract in The dangerous-ops contract.

How this looks in practice

The pattern composes with everything else. An agent with scoped permissions still needs handshakes for the dangerous things in its scope. An agent with attribution still needs handshakes — the audit log shows who proposed and who approved separately. An agent with shape caps still needs handshakes — caps protect the substrate, handshakes protect the side effects.

The handshake is the smallest amount of human-in-the-loop that gets you safety on the operations that matter, without throttling the agent on the operations that don't. It's the right unit.

For the broader pattern of which operations should be gated and how, see Consent gates for dangerous operations. For the principles behind agent-first product design, see AI-agent-first primitives.

FAQ

What is a two-key handshake?

A pattern where an agent proposing a dangerous operation receives a confirmation token and a summary, surfaces the summary to its human owner, and only executes the operation by replaying the token after the human approves. The token is single-use, time-bound, and bound to the specific operation and parameters.

Why does the token need to be bound to the params?

Otherwise the agent can substitute params after the human approves. The human reads "upgrade to Scale, $49/mo" and approves; if the token only binds the operation, the agent could call upgrade_plan(plan=enterprise) with the same token. Binding to params makes the human's approval mean what it says.

What's the difference between a consent gate and a handshake?

In our usage, the consent gate is the high-level concept (some operations require human confirmation), and the two-key handshake is the specific implementation pattern (propose → token + summary → human approve → commit with token). They're the same idea at different abstraction levels.

Should I gate every write?

No. Gating reads or low-risk writes adds friction without adding safety. Gate operations that move money, widen access, or can't be undone with a click. For everything else, the agent runs without a handshake — and if you've done identity, attribution, scoped permissions, and shape caps right, the rest is safe enough by default.

What happens if the agent skips the proposal step and tries to commit directly?

The commit call has no token. The handler rejects it. There's no path to the side effect that doesn't go through the proposal. This is the structural protection — it's not enforced by the agent's good behavior, it's enforced by the handler.

{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Two-key handshakes for irreversible agent actions",
  "description": "The pattern: agent proposes, human confirms, only then does the action run. The token is the protection. Here's the contract, the failure modes, and the implementation.",
  "datePublished": "2026-04-26",
  "author": { "@type": "Person", "name": "Flint" },
  "publisher": { "@type": "Organization", "name": "Dock", "url": "https://trydock.ai" },
  "image": "https://trydock.ai/blog-mockups/style-d-dreamscape/two-key-handshakes-irreversible.webp",
  "mainEntityOfPage": "https://trydock.ai/blog/two-key-handshakes-irreversible"
}
Remix this into Dock

Make this yours. Edit, extend, run agents on it.

Sign in (free, 20 workspaces) — Dock mints a copy of this in your own workspace. The original stays untouched.

Sign in & remix

No Dock account? Sign-in is signup. Magic-link in 30 seconds.