Dangerous-ops handshake
A short list of MCP tools never execute on the first call. The handler returns a confirm_token + a human-readable summary. The agent surfaces both to the user. The user confirms. The agent re-calls with the token. Only then does the action execute.
Why
Two reasons. First: agents acting on behalf of humans sometimes misinterpret intent. "Upgrade me to Pro" spoken to an agent shouldn't result in a charge without the user seeing the price. Second: even well-intentioned agents have bugs. The handshake makes money-moving + permanent state changes always require an explicit second touch.
Which tools are gated
Today, only the billing-mutation tools:
upgrade_plandowngrade_plan
When we add new tools that move money or permanently widen access, they'll go through this same gate.
The two-call flow
Call 1, describe
{
"tool": "upgrade_plan",
"input": { "to": "scale" }
}Response:
{
"confirm_required": true,
"confirm_token": "pbc_01J...",
"summary": "Upgrade vector-build from Pro ($19/mo) to Scale ($49/mo). Pro-rated to today; first full Scale charge on May 30.",
"ttl_seconds": 60
}Surface to user
The agent shows the summary in chat verbatim. User reads it, replies "yes" / "confirm" / similar.
Call 2, execute
{
"tool": "upgrade_plan",
"input": {
"to": "scale",
"confirm_token": "pbc_01J..."
}
}Response:
{
"ok": true,
"subscription": { "plan": "scale", "currentPeriodEnd": "..." }
}Confirm-token rules
- Single use. Once consumed, the token is invalid.
- 60-second TTL. Expires fast so a stale token can't reactivate later.
- Bound to {org, principal, operation, params}. A token issued for upgrade-to-Scale by your agent for your org won't work for a different combination.
- Idempotent on params change. If you re-call with different input, you get a new token + a new summary. The old token is invalidated.
Errors
consent_token_invalid: token doesn't exist or already usedconsent_token_expired: older than 60 secondsconsent_token_mismatch: token was issued for a different operation or different params
Agent implementation pattern
async function upgradePlan(plan: "pro" | "scale") {
// Call 1, get the confirm token + summary
const r1 = await mcp.callTool("upgrade_plan", { to: plan });
if (!r1.confirm_required) {
// Already a no-op or trivially fast-pathed; done.
return r1;
}
// Surface summary to the human, wait for explicit yes
const userOk = await ask(`${r1.summary}\n\nProceed?`);
if (!userOk) return { cancelled: true };
// Call 2, execute
return mcp.callTool("upgrade_plan", {
to: plan,
confirm_token: r1.confirm_token,
});
}REST equivalent
The same pattern applies to the REST endpoints behind these tools (POST /api/billing/upgrade, POST /api/billing/downgrade). Same handshake, same token shape.
Frequently asked questions
- What is the dangerous-ops handshake in Dock?
- A two-call confirmation pattern for MCP tools that move money or widen access. The first call returns a `confirm_token` + summary; the agent surfaces it to its user; the user confirms; the agent re-calls within 60s with the token. Single-use, scoped to the exact change.
- Which Dock MCP tools require dangerous-ops confirmation?
- Today: `upgrade_plan` and `downgrade_plan`. Future: any tool that moves money, deletes irreversibly at scale, or widens audience permanently. Tools are added by gating through `src/lib/billing-consent.ts`.
- How do I confirm a Dock plan change my agent wants to make?
- Two surfaces. `chat` mode: agent surfaces the `message` from the first call; you say yes; agent re-calls with `confirm_token` set. `web` mode: agent prints an `approval_url`; you click + approve in the browser; agent polls `polling_url` for the result.
- How long is a Dock confirmation token valid?
- 60 seconds. Single-use. Bound to `{org, principal, operation, params}` so re-using the token for a different change is rejected. After 60s, the agent must repeat the first call to get a new token.
- Can I bypass Dock's dangerous-ops confirmation for trusted agents?
- No. The two-call confirmation is server-enforced and applies to every agent regardless of trust. The point is to stop runaway agents from auto-upgrading you to Scale or downgrading you to Free without your awareness; trust isn't a substitute for confirmation.
- What's the difference between chat mode and web mode for dangerous ops?
- `chat` (default): the agent shows the confirmation message in chat, you say yes, agent re-calls. Best for in-conversation flows. `web`: agent prints a click-to-approve URL, you approve in browser, agent polls. Best for headless agents or users who prefer click-to-approve.
- How does my Dock agent handle the dangerous-ops handshake?
- On the first call, expect `{ status: 'confirmation_required', confirm_token, message, expires_in }` (chat) or `{ status: 'approval_required', approval_url, polling_url, expires_at }` (web). Surface the appropriate UX to the user; on consent, re-call the tool with `confirm_token` (chat) or poll polling_url (web).
- What if my agent calls a Dock dangerous-ops tool without the confirmation flow?
- The first call ALWAYS returns the consent prompt; it never executes. The agent's responsibility is to surface the prompt to the user before re-calling. Agents that ignore the prompt and call again with no token just keep re-prompting; nothing happens until the user confirms.
- Can I require dangerous-ops style confirmation for my own custom MCP tool?
- Yes, gate it through `src/lib/billing-consent.ts::mintConsent` / `consumeConsent`. Wraps the same `PendingBillingConfirmation` model (single-use, 60s TTL, bound to params). Don't invent a parallel confirmation mechanism; the consent layer is the single audited gate.
- What's the JSON-RPC error code for a Dock dangerous-ops cap or invalid token?
- 402 Payment Required (REST) maps to JSON-RPC error code -32015 (MCP). The error payload includes `details.upgrade` and `details.increase` next-step recommendations. Same shape across REST and MCP so agents handle both paths the same way.
Related
- Tool catalog: full list of MCP tools.
- Billing portal: the human-side equivalent.