MA2H — Multi-agent to Human Protocol
~/ma2h ma2h about

MA2H — Multi-agent to Human Protocol

A vendor- and runtime-neutral protocol for human coordination across an agent fleet — many agents, one hub. Agents notify, ask, and hand off tasks; humans coordinate from one place.

agent ┐
agent ┤
agent ┼──▶  [ MA2H hub ]  ──▶  human
agent ┤
agent ┘
       ◀──  signed answer routed back to the agent
       ◀─▶  v0.5 · one account's agents, to each other, through the same hub

As of v0.5 the hub carries both directions and the middle: agent↔human, and hub-mediated agent↔agent within one account. That leg is MA2H's own — spokes that are HTTP clients with no inbound port, not peers you can dial. A2A remains the agent↔agent wire for HTTP-addressable peers and MCP the agent↔tools one; MA2H is the durable hub between an operator and the fleet they run. No cross-account federation, no discovery, no open mesh.

~/ma2h ma2h verbs

The three verbs

notify
FYI / summary / status. No response. — a daily digest
ask
A decision the human makes; the answer routes back to the (often exited) agent. — ship / hold
task
A manual action a human performs out-of-band, then marks done. — rotate a key

…and, as of v0.4, the mirror: a human sends a directive to one agent, which drains it from a durable mailbox — signed, verified, at-least-once. — "freeze deploys until the incident clears"

…and, as of v0.5, the inter-agent leg: agents of one account message each other through the same mailbox — sessions make one live run addressable, and delivery is honest to the end: seen, bounced, or expired — never a lie. — an overseer tasks its spokes

~/ma2h ma2h sessions

Sessions — an address for one live run

A mailbox is per-agent, and one agent.id credential is commonly shared by many concurrent runs on one machine — so "deliver this to that run" had nowhere to land. run_id was already on every envelope, but it is opaque, unregistrable and non-authorizing: an audit label, not an address.

v0.5 adds the session — a hub-registered, lease-bound, ephemeral addressing scope under the agent's existing credential:

agent:agnt_7f3…            the principal — credentialed, accountable, evergreen
agent:agnt_7f3…#sess_9k2…  the session — leased, ephemeral, one live invocation
an address
hub-minted and not a secret, not a credential — only the principal that registered it may operate it, and another principal's session reads as unknown
leased
renewed only by client-originated traffic that presents the session — a ?session= drain, an ack carrying it, a stream reconnect, a submit naming it. A session-less call renews nothing, and there is no heartbeat endpoint
terminal
closed or expired, first-terminal-wins — and the session-addressed directives and messages it never acked bounce rather than rot in a queue nobody will drain again (response and receipt entries never bounce — the durable pull is already authoritative for those)

Two layers, deliberately. Per-run credentials were rejected: minting a principal per invocation buys credential sprawl, human-gated creation and revocation debt to solve what is only an addressing problem. The credential stays evergreen; the address is the thing that expires — and the hub garbage-collects it. — prior art: XMPP resource binding, SIP REGISTER with Expires, NATS ephemeral consumers

~/ma2h ma2h honesty

Delivery honesty

The hub does not promise an agent is listening. It promises that nobody is left believing a lie. A false belief of delivery is worse than a visible failure — it quietly turns a channel into a lottery.

a directive or an addressed message, in its destination's mailbox:

queued ──▶ delivered ──▶ acknowledged
   │           │
   │           └──▶ bounced   seen, then orphaned
   ├──▶ bounced               never seen
   └──▶ expired               never delivered
  • On the delivery track, expired MUST mean never delivered. Once something is delivered, a later expiry of the answer never rewrites it — so "seen but unanswered" and "never seen at all" stop presenting identically.
  • A bounce receipt carries the prior state, which is the whole distinction: queued = never seen; delivered = seen, then orphaned — claimed by something that went away before acking. A dead destination session is one cause; a principal-addressed entry whose retention lapses while still drained-but-unacked is another, and it names its last claimant.
  • Reachability is derived only from real client-originated activity — a drain, an ack, a reconnect. An open socket is not liveness: a hub bounds every stream hold and treats the client's reconnect as the renewal, so a zombie connection cannot hold a session online.
  • unknown is not offline. offline means seen, but not lately; unknown means never connected — or that you simply aren't permitted to know, which is reported as unknown rather than guessed.
  • Acceptance is not delivery. An addressed notify is accepted queuednever delivered — because its lifecycle is the delivery track.
  • An undeliverable ask does not hang: it auto-resolves cancelled (a task, dismissed) attributed to an attested system:undeliverable, so the sender is unblocked with the truth rather than waiting on an answer that can never come.
~/ma2h npm run demo # you are the human in the loop

A human-in-the-loop demo

📬 notify → "Daily deploy digest — web-app" [durable · status=delivered]
📬 NEW DECISION in your inbox:
Ship the candidate build to prod, or hold?
• ship — Ship to prod now
• hold — Hold for review
👤 Your decision [ship/hold]: ship
🔏 Hub attested the actor, signed the Response, pushed it
🤖 Agent re-invoked: signature verified , sealed state opened
you chose "ship" → 🚀 DEPLOYING abc123def
🛡 Replayed Response → acted=false (duplicate delivery, ignored)
~/ma2h ma2h docs
~/ma2h ma2h skills

Agent-native skills

Adopt MA2H from your agent — install the plugin, then build:

/plugin marketplace add autnmy/ma2h-protocol
/plugin install ma2h-skills@ma2h
implement
build a conformant MA2H Hub in your app — any language or framework
build-notify
generate your app's notify sender skill
build-ask
generate your app's ask sender skill
build-task
generate your app's task sender skill
build-inbox
generate your agent's inbox-drain skill — receive human→agent directives (v0.4)
build-bridge
generate your agent's always-on session bridge — hold a live address on the inter-agent leg (v0.5)

A bridge is the consuming side of the inter-agent leg: register a session, drain it, verify every entry before acting, resolve under an explicitly declared sender policy, and ack only once the work is durable.

Its contract with its supervisor is to fail loud, never silent — the reference loop raises a distinct fatal class, each carrying its own nonzero exit code for your supervisor wrapper to surface, so a restart policy can tell "fix the credential" from "the lease lapsed — re-register" from "an entry failed verification — do not act on it" from "the operator stopped this — stay stopped".

The Hub names the two terminal cases: a lapsed or self-closed lease arrives as gone — re-register and continue — while an operator's kill-switch arrives as session_closed_by_operator, with closed_by_operator: true on the session resource. The reference loop surfaces them as different fatal classes: one restarts, one stops. Resurrecting a bridge somebody deliberately stopped is exactly the wrong move, and a bridge that dies quietly is the failure the honesty rules exist to prevent.

~/ma2h ma2h rules

Trust rules

  • The Hub assigns the message id; idempotency_key is required on ask/task.
  • state is agent-owned and AEAD-sealed — the Hub never holds the key; returned state is untrusted until verified.
  • Every pushed Response is signed (RFC 8785 JCS + detached signature); agents verify, dedupe, and act at most once.
  • actor is Hub-attested; resolver authz is fail-closed; callbacks must target an agent-owned, verified host.
  • Every mailbox entry is signed too — the addressee verifies the detached signature and dedupes before it acts on anything.
  • The inter-agent leg is account-opt-in and account-scoped: cross-account addressing is rejected as unknown, and an unknown destination is refused at submit — never silently dead-lettered.
MA2H v0.6 · draft stewarded by Autonomy™ · Apache-2.0