Superseded. The current version is v0.6. Rendered from /spec/v0.1.md.
MA2H — Multi-agent to Human Protocol
Version: 0.1 (Draft) · Date: 2026-06-03 · License: Apache-2.0 · Steward: Autonomy
⚠️ Superseded by v0.2 (2026-06-03). v0.1 is retained for history only. It has known trust-model, return-leg, concurrency, and durability gaps that v0.2 fixes — do not implement against it. See the CHANGELOG.
0. Abstract
MA2H is a vendor- and runtime-neutral protocol by which agents reach humans and receive decisions
back. It defines a hub-and-spoke model: heterogeneous Agents POST one of three message types —
notify, ask, task — to a central Hub; a Human triages them from one inbox; and answers to
ask/task messages are routed back to the originating agent via push (webhook) or pull
(polling). MA2H is the agent↔human complement to A2A (agent↔agent) and MCP (agent↔tools).
1. Conformance and Terminology
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, MAY are to be interpreted as described in RFC 2119 / RFC 8174.
- Agent — a software process (any runtime) that originates MA2H messages. A "spoke."
- Hub — the central server that ingests messages, presents the human inbox, and routes responses. The "center."
- Human — the operator who triages the inbox and resolves
ask/taskmessages. - Caller — the specific agent run/invocation that produced a message; identified by
agent.run_id. - Resolution — the terminal outcome of a message (e.g.,
answered,completed,expired).
A conformant Hub MUST accept the message envelope (§4–§5), expose the transport bindings (§8), and emit the response envelope (§6) on resolution. A conformant Agent MUST send a valid envelope and MUST be able to receive a resolution by at least one of push or pull.
2. Goals and Non-Goals
Goals
- Runtime-agnostic: anything that speaks HTTPS + JSON can participate. No SDK required.
- Minimal core: three verbs, one envelope.
- First-class support for ephemeral agents (a CLI run, a GitHub Action) via the pull binding, and for resumable agents via the push binding.
- Async by default; a human is never assumed to be online.
- Reuse established conventions (§11) rather than inventing new ones.
Non-Goals (v0.1) — deferred to later versions, called out in §10.
- The human-facing inbox UI (only the wire contract is normative).
- Human identity / SSO and authn of operators.
- Multi-human assignment, escalation, SLA routing (the ITSM layer). v0.2 candidate.
- Streaming partial agent output (AG-UI's domain).
- Multi-turn threaded asks (a resolution is single-shot in v0.1).
3. Architecture
┌─────────┐ POST /v1/messages ┌──────────────────┐ inbox ┌────────┐
│ Agent │ ────────────────────▶ │ Hub │ ─────────▶ │ Human │
│ (spoke) │ ◀──────────────────── │ (ingest + inbox │ ◀───────── │ │
└─────────┘ push webhook / or │ + router) │ resolve └────────┘
│ agent polls poll_url └──────────────────┘
└── GET /v1/messages/{id}
A notify terminates at the inbox. An ask or task produces a Response (§6) when the human
resolves it; the Hub delivers that Response to the agent by the binding the agent selected (§8).
4. Message Envelope (common fields)
Every message is a JSON object with these common fields:
| Field | Type | Req | Notes |
|---|---|---|---|
ma2h_version |
string | MUST | Protocol version, e.g. "0.1". |
id |
string | MUST | Stable message id (ULID/UUID). Agent-supplied or Hub-assigned; if agent-supplied it MUST be unique per agent. |
type |
enum | MUST | notify | ask | task. Discriminator. |
created_at |
string | MUST | RFC 3339 timestamp. |
agent |
object | MUST | Origin descriptor (§4.1). |
title |
string | MUST | One line for the inbox list. ≤ 200 chars. |
body |
string | SHOULD | Human-readable detail. Markdown. |
priority |
enum | SHOULD | low | normal | high | urgent. Default normal. |
tags |
string[] | MAY | Free-form labels for filtering (["deploy","web-app"]). |
context |
Part[] | MAY | Attachments: text, structured data, or files (§4.2). |
expires_at |
string | MAY | RFC 3339. After this, an unresolved ask/task auto-resolves (§7). |
idempotency_key |
string | MAY | Hub MUST treat a repeat key from the same agent as the same message. |
4.1 agent descriptor
{
"id": "deploybot/dev-team", // stable logical agent identity
"run_id": "run_01H...", // this specific invocation (correlation for resume)
"runtime": "github-actions", // github-actions | cli | cloud | desktop | openclaw | other
"project": "web-app", // optional grouping key (≈ A2A contextId)
"labels": { "model": "opus-4.8" } // optional free-form
}
agent.id + agent.run_id together identify the Caller to route a response back to.
4.2 Part (borrowed from A2A)
A context entry is exactly one of:
{ "kind": "text", "text": "..." }
{ "kind": "data", "data": { "...": "..." } } // structured JSON, e.g. a diff summary
{ "kind": "file", "file": { "uri": "https://...", "name": "diff.patch", "mime_type": "text/x-diff" } }
Each Part MAY carry metadata.
5. The Three Verbs
5.1 notify
Informational. No response is expected; the message is terminal on delivery. Used for summaries, status
updates, the daily executive digest. Carries only the common envelope (§4). A Hub MUST NOT route a
response for a notify.
5.2 ask
A decision the human must make. The agent is blocked on, or will act on, the answer. Adds a request
block:
"request": {
"mode": "select", // select | input | confirm
"options": [ // REQUIRED when mode=select (MCP enumNames + HumanLayer ResponseOption)
{ "value": "ship", "label": "Ship to prod now", "description": "Deploy immediately." },
{ "value": "hold", "label": "Hold for review", "description": "Wait for a human PR review." },
{ "value": "abort", "label": "Abort the change", "description": "Revert and close." }
],
"schema": { // REQUIRED when mode=input — FLAT JSON Schema (string/number/bool/enum only)
"type": "object",
"properties": { "reason": { "type": "string" } },
"required": ["reason"]
},
"permissions": { // borrowed from LangGraph HumanInterruptConfig
"allow_accept": true, "allow_edit": true, "allow_respond": true, "allow_ignore": true
},
"default_on_expire": "hold", // value applied if expires_at passes unresolved; null = no default
"callback": { /* §8.3 */ }
}
mode=confirm is sugar for a two-option select (approve/deny).
5.3 task
A manual action a human performs in the world, outside the agent. Distinct from ask: the human does
work, not just picks an answer. Adds an action block:
"action": {
"instructions": "Rotate API_SIGNING_KEY in the prod vault and confirm the webhook still signs.",
"checklist": [ // optional sub-steps the human can tick
{ "text": "Generate a new key in the secret manager", "done": false },
{ "text": "Update prod secret", "done": false }
],
"verification": "Webhook test event returns 200 with a valid signature.",
"callback": { /* §8.3 — optional; notify the agent when marked done */ }
}
6. Response Envelope (human → Hub → agent)
Emitted by the Hub when an ask/task reaches a terminal state. Delivered via §8.
{
"ma2h_version": "0.1",
"in_reply_to": "msg_01H...", // the original message id
"agent": { "id": "deploybot/dev-team", "run_id": "run_01H..." },
"resolution": "answered", // see §7 per type
"response": { // present for ask; present-if-captured for task
"value": "hold", // chosen option value, or the input object
"edited": false, // true if the human edited an agent-proposed value
"actor": "human:alice", // who resolved it
"resolved_at": "2026-06-03T18:04:11Z",
"comment": "Let's get a human eye on the migration first."
},
"state": { "...": "..." } // OPAQUE agent state, round-tripped verbatim (HumanLayer pattern).
// The Hub MUST store and return it unmodified and MUST NOT inspect it.
}
The state field lets a stateless agent reconstruct its context on resume without the Hub understanding it.
7. Lifecycle / State Machines
notify: created ──▶ delivered (terminal)
ask: open ──┬─▶ answered (terminal) ← human chose/responded
├─▶ declined (terminal) ← human refused (allow_respond/permissions)
├─▶ cancelled (terminal) ← agent or Hub withdrew it
└─▶ expired (terminal) ← expires_at passed; default_on_expire applied if set
task: open ──┬─▶ completed (terminal) ← human marked done
├─▶ dismissed (terminal) ← human declined to do it
└─▶ expired (terminal)
Terminal states are irreversible (A2A rule). An agent MAY cancel its own open message (§8.4); the Hub
MUST mark it cancelled and stop soliciting the human.
8. Transport Bindings (HTTP/JSON)
All endpoints are HTTPS, Content-Type: application/json. Base path /v1.
8.1 Submit — POST /v1/messages
Body: a message envelope (§4–§5). On success the Hub returns 202 Accepted (HITL-Protocol handshake):
{
"id": "msg_01H...",
"status": "open", // or "delivered" for notify
"poll_url": "https://hub.example/v1/messages/msg_01H...",
"review_url": "https://hub.example/inbox/msg_01H..."// human-facing deep link (out-of-band share)
}
8.2 Pull — GET /v1/messages/{id}
Returns the message plus its current status and, once terminal, the response (§6). The Hub SHOULD
support long-poll via ?wait=<seconds> (cap 60): hold the connection until resolution or timeout, then
return current state. This is the binding for ephemeral agents that cannot be called back.
8.3 Push — agent-supplied callback
An ask/task MAY include a callback so the Hub delivers the Response actively (A2A PushNotificationConfig):
"callback": {
"mode": "push", // push | pull (pull = omit url; agent polls 8.2)
"url": "https://agent.example/ma2h/resolve",
"auth": { "scheme": "hmac", "secret_ref": "env:MA2H_CALLBACK_SECRET" } // hmac | bearer | apikey
}
On resolution the Hub POSTs the Response envelope (§6) to url. The Hub MUST sign the request (§9), MUST
retry with exponential backoff on non-2xx (≥ 5 attempts), and MUST include the message id so delivery is
idempotent. An agent MAY register both push and pull; push is best-effort, pull is the source of truth.
8.4 Cancel — POST /v1/messages/{id}/cancel
Withdraws an open message. Idempotent.
9. Security and Privacy
- Agent → Hub auth: bearer token or API key scoped to an
agent.id. The Hub MUST reject envelopes whoseagent.iddoes not match the credential. - Hub → Agent callback signing: when
auth.scheme = hmac, the Hub MUST sendMA2H-Signature: t=<unix>,v1=<hex(HMAC-SHA256(secret, "<t>.<raw-body>"))>(Stripe/Slack style). The agent MUST verify and MUST reject timestamps outside a ±5-minute window (replay defense). - Transport: HTTPS REQUIRED. Plaintext bindings MUST NOT be offered.
- Secret hygiene (CHEQ idea): when an
askcollects a sensitive value the agent must use but should not see in its LLM context, the responsevalueMAY be a reference ({"secret_ref":"vault://..."}) resolved out-of-band, never the raw secret. The Hub MUST allow flagging an input field assensitiveso it is excluded from any inbox export or telemetry. - PII in telemetry: Hub logs MUST NOT include
body/response.valueof messages flaggedsensitive.
10. Versioning and Roadmap
- Versioning:
ma2h_versioncarries the spec version. Breaking changes bump the number; the Hub MUST reject an unknown major it cannot satisfy with400. - RFC-2119 normative language; spec text Apache-2.0; contributions under DCO (
Signed-off-by).
Roadmap (explicitly out of v0.1):
- v0.2 — assignment / escalation / SLA (the ITSM layer):
assignee,escalation_policy, on-call routing. The lesson from PagerDuty + enterprise ITSM is that routing metadata matters as much as payload. - v0.2 — multi-turn threads (an
askwhose answer spawns a follow-upaskon the samecontext/thread). - v0.3 — streaming agent progress into the inbox item (AG-UI event interop).
- v0.x — channel fan-out (deliver an inbox item to Slack/email/SMS), modeled on HumanLayer
ContactChannel.
11. Provenance (what MA2H reuses, and from where)
MA2H is intentionally non-NIH. Conventions adopted from prior art:
| From | Reused in MA2H |
|---|---|
| A2A (Linux Foundation) | Part union (text/data/file), callback ← PushNotificationConfig auth schemes, project ≈ contextId, terminal-state irreversibility, input-required → the ask lifecycle |
HITL Protocol (rotorstar/hitl-protocol) |
202 Accepted + poll_url/review_url submit handshake (§8.1) |
| MCP elicitation (Anthropic) | select options as enum+enumNames, flat input schema for renderability, accept/decline/cancel resolutions |
| HumanLayer | opaque state round-trip for stateless agents, typed contact channels (roadmap §10) |
| LangGraph | request.permissions ← HumanInterruptConfig (allow_accept/edit/respond/ignore) |
| CHEQ (IETF draft) | keep human-entered secrets out of the agent's LLM context (§9) |
Appendix A — Worked Examples
See /examples: the daily executive digest as a notify, a dev-team ship/hold decision as
an ask, a manual key-rotation as a task, and the corresponding response envelope.