Engineering · July 31, 2026 · 8 min read
Adding passkey-backed human approval to high-risk MCP actions
An AI agent should not be able to approve its own irreversible action. Here is the architecture we built and tested in production on a Mac with Touch ID.
The missing boundary
Tool permissions are often decided before an agent starts. That works for broad access, but it is weak for actions whose risk depends on the exact payload: deploying a migration, sending funds, rotating a credential, or exporting customer data.
GoodRoom.verify adds a narrow boundary immediately before execution. The agent describes the action and submits a SHA-256 digest. A human reviews that request and proves physical presence with WebAuthn. Only then does the requesting process receive a short-lived proof for that exact digest and audience.
Agent
Hashes the exact action locally
Edge
Creates a 120-second challenge
Operator
Approves with a passkey
Wrapper
Verifies the action-bound proof
Capabilities, not guessable identifiers
A challenge ID identifies state; it does not authorize access. Creation returns two independent random capabilities: one for the browser approval flow and one for polling. The service stores only SHA-256 hashes of both. The API key never enters an approval URL.
Each challenge maps to one Cloudflare Durable Object. Its storage transaction is the serialization point that moves a request from PENDING to exactly one terminal decision. An alarm removes unfinished state after 120 seconds and completed delivery state after a short retention window.
A proof the tool wrapper can verify offline
After approval, the edge signs an Ed25519 JWT with a 30-second lifetime. Its claims bind the account, challenge, action hash, risk level, audience, and a unique proof ID. The MCP client fetches the public JWKS, checks the key ID, then verifies the signature and claims locally.
This distinction matters: the proof says a human approved an action. It does not claim the action ran. The protected wrapper remains responsible for matching the digest and audience, rejecting replayed proof IDs, and executing only after verification.
Auditing without collecting agent context
The persistent audit record contains the action hash, audience, risk, decision, account, timestamp, and credential reference. It deliberately excludes the action summary, prompts, tool arguments, source code, raw assertions, and conversation context.
D1 and Durable Objects cannot share one transaction, so the completed Durable Object result carries a minimal audit outbox. The request writes eagerly to D1, while proof delivery and the object alarm retry the same idempotent event. A transient database failure cannot silently release an unaudited proof.
What we tested
The production test created a challenge through the MCP client, opened the approval page on macOS, enrolled Touch ID, issued the proof, delivered it once, verified it locally against the public JWKS, and persisted the verified audit event in D1.
Current limits
This is a private beta, not a sandbox or general policy engine. API keys are issued manually, the MCP package is built from the repository, and protected tools still need an enforcement wrapper. The API currently uses a Cloudflare workers.dev hostname until the custom API domain is moved into the same DNS account.
Try the private beta
Connect an MCP client and put Touch ID in front of one risky tool.