DEV Community

Murthy G
Murthy G

Posted on

Human approval as a 2PC vote: a draft protocol for safe multi-tool agent action

MCP connects agents to tools and A2A connects agents to agents. Nothing defines how an agent finds the right human, does several tool calls all-or-nothing, or proves exactly what a human approved. Here is a draft spec that tries.

Picture an agent booking a business trip. It holds a flight seat, reserves a hotel, adds a calendar event, and charges the corporate card. Finance has to approve anything over $500.

Today that is four separate tool calls and a Slack message. If the hotel is sold out after the card is charged, or the approver says no after the seat is booked, you clean it up by hand. And when the approver clicks "Approve", nothing records which amount, recipient, or card they agreed to.

I've been writing a draft protocol for this gap. It's called HATP (Human-Agent Transaction Protocol). This post covers the core idea, the parts of the spec, and what works today.

The gap

Protocol What it connects
MCP agent → tools
A2A agent → agent
AP2 agent → payments
IETF HEM / CHEQ drafts, Twilio A2H agent → a known human, with no role-based routing or transaction binding

None of these define:

  1. how to find the right human by role, skill, spending authority, workload and availability;
  2. how to run several tool calls on different servers as one all-or-nothing action; or
  3. how to make a human's approval part of that action, with a signature that covers the exact arguments.

The core idea: a human approval is a vote in a two-phase commit

Databases solved "all or nothing across several systems" decades ago with two-phase commit (2PC). Phase 1: each participant prepares and votes yes or no. Phase 2: if all voted yes, everyone commits; otherwise everyone aborts.

HATP treats the human approver as one more participant. Their "Approve" is a yes vote, signed over a SHA-256 digest of the whole plan. If they say no, or don't answer by the deadline, the transaction aborts and the holds are released. No effect is left behind.

Initiator   Coordinator     Airline   Hotel   Calendar   Payments   HumanGW   Bob
   │ begin ─▶│                │        │        │          │          │        │
   │         │─prepare───────▶│ hold seat (lease 20m)      │          │        │
   │         │─prepare────────────────▶│ hold room          │          │        │
   │         │─prepare─────────────────────────▶│ validate  │          │        │
   │         │─prepare────────────────────────────────────▶│ validate │        │
   │         │─prepare(finance)───────────────────────────────────────▶│─ask──▶│
   │         │◀── yes ×4 ─────────────────────────────────────────────│◀approve (passkey)
   │         │◀── yes + signed approval────────────────────────────────│
   │         │ log COMMIT
   │         │─commit calendar → charge card → commit holds
   │◀ record │ COMMITTED
Enter fullscreen mode Exit fullscreen mode

Honest guarantees: participant tiers

Real tools can't all do proper 2PC. So each operation declares what it can actually guarantee:

Tier Prepare Commit Example
hold Places a hold with a lease Converts the hold; must succeed while the lease is valid Seat hold, card authorization
compensate Dry run only Does the effect and returns an undo token Calendar event, CRM update
final Dry run only Irreversible Card capture, sending an email, a wire

A plan may contain at most one final step. The coordinator commits in a fixed order: compensate steps first, then the single final step, then the holds. Running the one irreversible step while every hold is still guaranteed is the old XA "last resource commit" trick. If anything fails before the holds commit, the only effects left are ones that can be undone.

Finding the right human

HATP adds addresses for people and agents:

human://acme.com/bob                                  a person
human://acme.com/role/finance-approver?quorum=2-of-3  any 2 people holding a role
human://acme.com/pool/security-leads?skill=sox        a group, filtered by skill
agent://acme.com/travel-bot                           an agent
Enter fullscreen mode Exit fullscreen mode

The Human Card

A2A gives every agent an Agent Card that says what it can do. HATP gives every person a Human Card. Here's a shortened example (the full schema is in the spec):

{
  "id": "human://acme.com/bob",
  "displayName": "Bob Chen",
  "roles": ["finance-approver"],
  "skills": ["travel-policy", "sox-controls"],
  "authority": [
    { "action": "approve.spend", "maxAmount": { "value": 10000, "currency": "USD" } }
  ],
  "channels": ["slack", "sms", "voice"],
  "availability": { "timezone": "America/New_York", "status": "available" },
  "sla": { "decideSeconds": 900 },
  "capacity": { "maxOpen": 10, "open": 2 },
  "delegates": ["human://acme.com/carol"],
  "issuer": "https://directory.acme.com",
  "signature": { "alg": "Ed25519", "kid": "acme-directory-1", "sig": "…" }
}
Enter fullscreen mode Exit fullscreen mode

What each part is for:

  • roles and skills let an agent ask for "a finance approver who knows SOX" instead of hard-coding a name.
  • authority says what this person may approve, for example spend up to $10,000. A $25,000 request is never routed to Bob.
  • availability, sla and capacity let routing prefer someone who is online, not overloaded, and likely to answer before the deadline.
  • delegates says who takes over if Bob doesn't decide within the SLA.
  • channels lists channel types only. Bob's phone number and Slack ID stay inside the Human Gateway; agents never see them.
  • signature: the company's directory signs every card, so an agent can't invent an approver or give someone more authority.

The card guides routing, but it isn't the security boundary. The Human Gateway checks authority again against company policy when the request arrives.

Routing

A Directory resolves an address to ranked candidates using these cards. The Human Gateway then delivers the request and enforces separation of duties: the person the agent acts for can't approve their own spend. It escalates to a delegate when someone misses their SLA, and records the whole routing chain in the approval, so auditors see who was asked as well as who decided.

Evidence anyone can verify

  • Every vote, receipt and approval is signed with Ed25519 over its canonical JSON (RFC 8785), and each one includes the plan digest.
  • An approval records who decided, how they authenticated (the spec recommends WebAuthn/passkeys; the demo simulates the click), and who was asked before them.
  • The coordinator emits a hash-chained Transaction Record. Anyone with the public keys can check offline that every party agreed to the same plan and that no event was removed or reordered.

Gateways must render the approval screen from the structured plan, not from text the agent wrote, and label agent-written text as such. That shuts off the most obvious prompt-injection path: an agent describing a $10,000 wire as "a small refund".

Designed as an MCP extension

An existing MCP server becomes a participant by advertising one capability and implementing a few extra methods:

{ "capabilities": { "experimental": { "hatp": {
    "version": "0.1",
    "roles": ["participant"],
    "operations": { "book_seat": { "tier": "hold", "maxLeaseSeconds": 1800 } }
} } } }
Enter fullscreen mode Exit fullscreen mode

Methods: hatp/directory/resolve, hatp/txn/{begin,prepare,quote,commit,abort,compensate,renew,status,voted}, and hatp/lock/{acquire,renew,release} with fencing tokens.

Status: what works today

  • Spec v0.1 (draft): Discovery, Transactions, Evidence.
  • JSON Schema for all the core types.
  • TypeScript reference SDK: coordinator with a durable decision log, participant wrapper, Directory, Human Gateway, signing and offline record verification. It currently runs in a single process; the JSON-RPC/MCP transport is not wired up yet.
  • Tests cover all-yes commit, commit ordering, a no vote releasing every hold, human rejection, SLA escalation to a delegate, separation of duties, 2-of-N quorum, rollback when the irreversible step fails, approval-first mode catching a price jump at hold time, lease renewal before commit, record tampering, and unsigned-card rejection.
  • Not yet implemented: locks, deferred (pending) votes, and continuous lease renewal while waiting for humans.

Next (v0.2): asynchronous asks outside transactions, and SIP-style live calls where any human or agent can dial any other (voice with humans, a structured data channel between agents), including turning a spoken "yes" into a passkey-confirmed approval.

I'd like feedback on

  1. Is "approval as a 2PC vote" the right model? The spec also has an approval-first mode: humans approve quoted prices before any holds are placed, and the transaction aborts if a held price moves beyond a set tolerance. Should that be the default instead?
  2. Would you implement hold / compensate / final in your MCP server?
  3. How should a Human Gateway in one company trust a coordinator in another?

Comments and issues welcome.

Top comments (0)