# MA2H — Multi-agent to Human Protocol

**Version:** 0.4 (Draft) · **Date:** 2026-06-30 · **License:** Apache-2.0 · **Steward:** Autonomy

> **Superseded by [v0.5](v0.5.md)** (2026-08-10) — implement against v0.5.
> Supersedes [v0.3](v0.3.md). v0.4 is **additive and backward-compatible**: it adds a **human→agent**
> messaging leg (the inbound **directive**, §13) alongside the unchanged v0.3 agent→human legs. No v0.3
> wire format, envelope, lifecycle, or signature changes. See [CHANGELOG](../CHANGELOG.md) and
> [MIGRATION](../MIGRATION.md).

## 0. Abstract

MA2H is a vendor- and runtime-neutral protocol by which **agents reach humans** and **receive decisions
back**, and — as of v0.4 — by which **a human reaches a specific agent**. 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 (often ephemeral) agent via
**push** (webhook) or **pull** (polling). The v0.4 **inbound leg** mirrors that return leg in reverse: a
Hub-attested human authors a **directive** addressed to one agent, and the agent drains it from a **durable
per-agent mailbox** by the same pull-first / webhook-optional mechanism (§13). v0.4 also adds two
**cross-cutting** primitives that enrich *both* directions: an **acknowledgment/receipt** (§14) closing the
loop on a delivered message or answer, and a derived **presence** signal (§15) telling a human whether an
agent is listening. MA2H is the agent↔human complement to A2A (agent↔agent) and MCP (agent↔tools).

This version (0.4) builds on the v0.3 payload-bound-signature pass — which itself built on the v0.2
hardening that closed the trust-model, return-leg, concurrency, and durability gaps found in the v0.1
design review — and **adds** the human→agent directive leg (§13) **without changing any v0.3 leg**. A 0.4
Hub remains backward-compatible with 0.3 agent→human envelopes. The new leg reuses v0.3's envelope
conventions, RFC 8785 JCS canonicalization, detached Hub signature, `jti` replay dedup, SSRF controls
(§9.4), and durability model (§3.1) rather than inventing new machinery. As in prior versions, it
specifies candidate controls for the security-critical requirements; final closure of those is proven
against a conformant Hub (see §12).

## 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, routes responses, and
  performs the security and lifecycle duties in this spec. The "center."
- **Human** — an authenticated operator who triages the inbox and resolves `ask`/`task` messages.
- **Caller** — the specific agent invocation that produced a message, identified by the tuple
  `(agent.id, agent.run_id)`. Responses route to the Caller.
- **Resolution** — the terminal outcome of an `ask`/`task`, carried in the Response (§6).
- **Directive** — a **human→agent** message (v0.4, §13): a Hub-attested `human:<id>` sends an instruction or
  FYI addressed to one `agent:<id>`, delivered from that agent's **mailbox**. The inbound mirror of `notify`.
- **Mailbox** — the durable, per-`agent.id` queue of pending directives an agent drains (§8.7, §13).
- **Acknowledgment** (receipt) — a one-shot, terminal confirmation that the receiving party got the other
  party's message or answer, optionally with a note (v0.4, §14). Cross-cutting; distinct from a `notify`.
- **Presence** — a derived, best-effort signal of whether an `agent:<id>` is currently reachable/listening,
  from its existing poll/subscription activity (v0.4, §15). Owner-only.

Three words that v0.1 overloaded are kept distinct in v0.3 and MUST NOT be conflated:

| Term | Meaning |
|------|---------|
| **`status`** | The lifecycle phase of a message (§7): `open`, `delivered`, or a terminal value. The wire field on the submit ack (§8.1) and GET body (§8.2). |
| **`resolution`** | The *terminal* `status` of an `ask`/`task`, surfaced in the Response (§6). Its value space is a subset of `status`. |
| **`state`** | The opaque, agent-owned blob round-tripped verbatim (§4, §6, §9). Never a lifecycle concept. |

A **conformant Hub** MUST accept the message envelope (§4–§5), expose the transport bindings (§8), enforce
the security model (§9), apply the lifecycle rules (§7), and emit the Response (§6) on resolution. A
**conformant Agent** MUST send a valid envelope, MUST be able to receive a resolution by at least one of
push or pull, and MUST honor the agent-side duties marked in §6, §8, and §9 (dedup, signature
verification, state-integrity verification).

The inbound leg (§13) is **OPTIONAL to offer and to consume**: a conformant Hub MAY omit it entirely (and
then MUST NOT advertise `inbound` in §8.0), and a conformant Agent need not drain a mailbox. A Hub that
**does** offer it MUST implement the §13 mailbox semantics, sign every delivery per §9.7, and advertise
`inbound` (§8.0); an Agent that **does** consume it MUST verify the §9.7 signature and deduplicate on the
directive `id` (§13.4). Offering or consuming the inbound leg is independent of full v0.3 conformance —
neither implies the other.

## 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 **ephemeral** agents (a CLI run, a GitHub Action) via pull and the resume pattern (§2.1).
- Async by default; a human is never assumed online.
- Server-authoritative trust: the Hub decides; the agent never trusts the return leg without verifying it.

**Non-Goals (v0.4)**
- The human-facing inbox **UI** (only the wire contract is normative).
- Human **identity / SSO** mechanics. MA2H requires the Hub to authenticate the operator and attest
  `actor`/`from`, but does not specify the authentication method.
- Multi-human **assignment, escalation, SLA** routing. (Post-v0.4.)
- **Streaming** partial agent output; multi-turn **threaded** asks. (Post-v0.4.)
- **Channel fan-out** (Slack/email/SMS delivery of an inbox item).
- **Human→agent `ask`/`task`.** v0.4's inbound leg is deliberately scoped to the one-way **directive** (the
  `notify` mirror). An inbound message that expects a routed *agent response* — the mirror of §6 — is a
  materially larger design (agent-side resolution, an attested `agent:` actor on the return leg, a second
  signed leg) and is deferred to a future SCP (§10).
- **Agent liveness / push-to-wake.** Delivering a directive to a mailbox does not start, wake, or guarantee
  the existence of a consuming agent; the mailbox is durable and drained whenever the agent next polls. How
  an agent is scheduled to poll (or is re-invoked by the optional webhook) is out of scope.

### 2.1 Ephemeral Agent Resume Pattern (normative)

The defining MA2H flow: an agent that **exits** after asking, and a **new** process that resumes when the
human answers. This is how headless agents (GitHub Actions, CLI runs) participate without holding a
process open.

```
  Run #1 (exits)                  Hub                    Human         Run #2 (re-invoked)
  ─────────────                   ───                    ─────         ──────────────────
  1. seal state  ── POST ask ───▶ ingest, validate
     (encrypt-then-MAC,            store, 202 {id,...}
      key NOT in state)  ◀──────── 202 ack
  2. exit (nothing held open)
                                   present to inbox ───▶ resolve
                                   atomic terminal CAS
                                   stamp resolution_id
                                   sign Response ──────────────────────▶ 3. POST signed Response
                                                                            (push) to re-invoke URL
                                                                         4. verify signature + replay jti
                                                                         5. verify+open state MAC
                                                                            (untrusted until verified)
                                                                         6. dedup (in_reply_to,
                                                                            resolution_id); act once
```

Normative requirements:

1. Before exiting, an agent that needs to resume MUST populate `state` (§4) with enough context to
   reconstruct itself in a new process, and MUST integrity-protect it per §9.3.
2. The agent MUST set `callback.url` (§5.2) to an endpoint that **re-invokes** the agent process (e.g., a
   workflow-dispatch URL), not merely a data sink. The callback host MUST satisfy §9.4.
3. On resolution the Hub POSTs the Response (§6), including `state` verbatim, to `callback.url`.
4. The re-invoked process MUST verify the Response signature (§9.2) and reject replays, MUST verify the
   integrity of `state` it applied (§9.3) and treat it as untrusted until it does, and MUST deduplicate on
   `(in_reply_to, resolution_id)` (§6, §7) and act at most once.
5. The state-seal key MUST be sourced per §9.3 — pre-positioned in the agent runtime, distinct from the
   callback credential, and never carried in `state` or through the Hub.

`actor` MAY be `agent:<agent.id>` — an agent may resolve an ask on behalf of its operator (autonomous
approval). Any action a human resolver can take, an authorized agent resolver can take (§6, §9.1).

## 3. Architecture

```
  ┌─────────┐   POST /v1/messages   ┌──────────────────┐   inbox    ┌────────┐
  │  Agent  │ ────────────────────▶ │       Hub        │ ─────────▶ │ Human  │
  │ (spoke) │ ◀──────────────────── │ ingest · atomic  │ ◀───────── │        │
  └─────────┘  signed push  /  or   │ lifecycle · sign │  resolve   └────────┘
       │       agent polls poll_url │ · SSRF-guard     │  (attested actor,
       └── GET /v1/messages/{id}    └──────────────────┘   authz checked)
```

Trust boundaries: the **request leg** is authenticated by per-`agent.id` credentials (§9.1); the **return
leg** is integrity-protected by a Hub signature the agent verifies (§9.2); **`state`** is opaque to the
Hub and integrity-sealed by the agent (§9.3) — the Hub is never trusted with state integrity.

### 3.1 Durability and Conformance

A conformant Hub MUST durably persist message lifecycle such that a Hub process restart does not lose:
- any **open** `ask`/`task`,
- any **committed resolution**,
- any **`delivered` notify** (it MUST remain pull-checkable per §8.2),
- any **pending push-delivery obligation** (it MUST be re-attempted after restart while non-terminal),
- (inbound leg, §13) any **un-acked directive** in a mailbox (it MUST remain drainable until acked or
  expired), and any **pending directive webhook obligation** (re-attempted after restart while non-terminal).

The guarantee is stated as an observable outcome; the storage mechanism is implementation-defined.
In-memory-only storage that loses the above on restart is **non-conformant**.

## 4. Message Envelope (common fields)

Every message is a JSON object. The schema is `schema/v0.4/message.schema.json`.

| Field | Type | Req | Notes |
|-------|------|-----|-------|
| `ma2h_version` | string | MUST | `"0.4"` (a 0.3 agent still sends `"0.3"` — accepted, §10). Version handling per §10. |
| `type` | enum | MUST | `notify` \| `ask` \| `task`. Discriminator. |
| `created_at` | string | MUST | RFC 3339 timestamp (agent clock; informational — the Hub clock is authoritative for expiry, §9.5). |
| `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. Treated as untrusted (§9.6). |
| `priority` | enum | SHOULD | `low` \| `normal` \| `high` \| `urgent`. Default `normal`. |
| `tags` | string[] | MAY | Free-form filter labels. |
| `context` | Part[] | MAY | Attachments (§4.2). |
| `state` | object | MAY | **Opaque, agent-owned** resume blob. The Hub MUST store and return it verbatim in the Response and MUST NOT inspect or log it (§9.3). Required in practice for the resume pattern (§2.1). |
| `client_ref` | string | MAY | Opaque, agent-chosen correlation **label**. Never used for dedup. Not exposed to resolvers (§9.1). |
| `idempotency_key` | string | see §8.1 | Dedup key. **REQUIRED for `ask`/`task`**, MAY for `notify`. Scope and semantics in §8.1. |
| `expires_at` | string | MAY | RFC 3339. Auto-resolution boundary for `ask`/`task` (§7). MUST be in the future at submit or the Hub rejects with 422 (§8.5). |

The `id` is **not** an agent input — the Hub assigns it (§8.1, KTD: Hub-canonical ids).

Cross-type fields (`request`, `action`) are constrained by the discriminated `oneOf` in the schema: a
`notify` MUST NOT carry `request` or `action`; an `ask` carries `request` and MUST NOT carry `action`; a
`task` carries `action` and MUST NOT carry `request`.

### 4.1 `agent` descriptor

```json
{
  "id": "deploybot/dev-team",        // stable logical agent identity (credential is scoped to this)
  "run_id": "run_01H...",         // this specific invocation; (id, run_id) = the Caller
  "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 string map
}
```

`run_id` is opaque to the Hub and MUST NOT be used to authorize cross-run access (§9.1).

### 4.2 `Part` (convention borrowed from A2A)

A `context` entry is exactly one of:

```json
{ "kind": "text", "text": "..." }
{ "kind": "data", "data": { "...": "..." } }
{ "kind": "file", "file": { "uri": "https://...", "name": "diff.patch", "mime_type": "text/x-diff" } }
```

Each Part MAY carry `metadata`. A Hub MUST NOT server-side-fetch a `file.uri` unless it passes the same
host controls as a callback URL (§9.4).

## 5. The Three Verbs

### 5.1 `notify`

Informational. No response is expected. Used for summaries, status updates, the daily digest. Carries only
the common envelope (§4). A Hub MUST NOT route a Response for a `notify`.

`notify` is **fire-and-forget but not droppable**: a `notify` reaches `delivered` on acceptance (the 202
ack carries `status: delivered`; there is no observable `created` state and no undeliverable state
machine). A `delivered` notify MUST be durable (§3.1) and pull-checkable via §8.2 for the retention TTL,
so a sender (e.g., a daily-digest agent) can confirm it landed.

### 5.2 `ask`

A decision the human must make. Adds a `request` block:

```json
"request": {
  "mode": "select",            // select | input | confirm
  "options": [                 // REQUIRED when mode=select
    { "value": "ship",  "label": "Ship to prod now",  "description": "Deploy immediately." },
    { "value": "hold",  "label": "Hold for review",   "description": "Wait for a human PR review." }
  ],
  "schema": {                  // REQUIRED when mode=input — FLAT JSON Schema (string/number/bool/enum)
    "type": "object",
    "properties": { "reason": { "type": "string", "x-ma2h-sensitive": false } },
    "required": ["reason"]
  },
  "permissions": {             // which affordances/terminals the human is offered (§7)
    "allow_accept": true, "allow_edit": true, "allow_respond": true, "allow_ignore": true
  },
  "default_on_expire": "hold", // applied on expiry; MUST be a member of options[].value (select) or
                               // null, or an object validating against `schema` (input). See §7.
  "allowed_resolvers": ["human:alice"],  // optional authz; default fail-closed (§9.1)
  "callback": { /* §8.3 */ }
}
```

- `mode=confirm` is sugar: when `options` is omitted the Hub MUST synthesize exactly two options with
  values `approve` and `deny`; `response.value` is one of those. If `options` is supplied it MUST have
  exactly two entries.
- `permissions` → lifecycle mapping (§7): `allow_respond:false` removes the answer affordance (the human
  may only `decline`); `allow_ignore:false` removes the decline/ignore affordance. The v0.1 `ignored`
  resolution does **not** exist — a human ignoring an ask resolves it as `declined` (§6, §7).

### 5.3 `task`

A manual action a human performs in the world. Adds an `action` block:

```json
"action": {
  "instructions": "Rotate API_SIGNING_KEY in the prod vault and confirm the webhook still signs.",
  "checklist": [
    { "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.",
  "allowed_resolvers": ["human:alice"],
  "callback": { /* §8.3 — optional; notify the agent when marked done */ }
}
```

A `task` has no `default_on_expire`: on expiry it resolves `expired` with no default applied (§7).

## 6. Response Envelope (Hub → agent)

Emitted by the Hub when an `ask`/`task` reaches a terminal state. Delivered via §8. The schema is
`schema/v0.4/response.schema.json`.

```json
{
  "ma2h_version": "0.3",
  "in_reply_to": "msg_01H...",          // the original Hub-assigned message id
  "resolution_id": "res_01H...",        // Hub-assigned, opaque, random; identical on every channel/retry
  "agent": { "id": "deploybot/dev-team", "run_id": "run_01H..." },  // routing only (responseAgent: id+run_id)
  "resolution": "answered",             // §7 terminal value for the verb
  "response": {                         // present for ask; for task only when a comment/checklist is captured
    "value": "hold",                    // ask only: chosen option value, or the input object (union by mode)
    "edited": false,
    "actor": "human:alice",          // Hub-attested; ^(human|agent|system):.+$ (§9.1)
    "resolved_at": "2026-06-04T18:04:11Z",
    "comment": "Let's get a human eye on the migration first."
  },
  "defaulted": false,                   // true iff resolution came from default_on_expire (actor=system:...)
  "state": { "...": "..." }             // opaque agent blob, round-tripped verbatim (§9.3)
}
```

- **`resolution` value space** (the single canonical list; identical in §1, §7, the schema, and §11):
  - `ask` → `answered` | `declined` | `cancelled` | `expired`
  - `task` → `completed` | `dismissed` | `expired`
- `response.value` is a discriminated union keyed on the original `request.mode` (string for
  select/confirm, object for input). It is absent for `task`.
- `response` presence: REQUIRED for `ask`; for `task` present only to carry a human `comment` and/or final
  `checklist` state (never `value`); never present for `notify` (no Response is emitted).
- An **expiry-defaulted** Response MUST set `resolution: "expired"`, `defaulted: true`, and
  `response.actor: "system:default_on_expire"` — never a human `actor`. Agents SHOULD fail-closed on
  `expired` for high-impact decisions.
- The agent MUST treat `(in_reply_to, resolution_id)` as the at-most-once key and `state` as untrusted
  until verified (§9.3).

## 7. Lifecycle / State Machines

```
notify:   delivered (terminal on acceptance; durable, §3.1)

ask:      open ──┬─▶ answered    ← human answered (allow_respond)
                 ├─▶ declined    ← human refused / ignored (allow_ignore maps here)
                 ├─▶ cancelled   ← agent or Hub withdrew (ask only; §8.4)
                 └─▶ expired     ← expires_at passed; default_on_expire applied if set

task:     open ──┬─▶ completed   ← human marked done
                 ├─▶ dismissed   ← human declined to act
                 └─▶ expired     ← expires_at passed (no default)
```

Normative rules:

- **Atomic single-writer.** Every transition is a single atomic compare-and-set against the current
  status. The **first** transition to a terminal status wins and is **immutable**; later resolve/expiry/
  cancel attempts are no-ops that return the existing Response.
- **Expiry vs answer.** A human resolution accepted **at or before** `expires_at` (Hub clock, §9.5) beats
  `default_on_expire`. Expiry and answer are evaluated against the same clock in the one atomic transition.
- **Resolve vs cancel.** First terminal wins. A cancel arriving after a terminal status is a no-op that
  returns the existing terminal Response (not a fake success), so the agent learns it lost the race. A
  cancel arriving strictly **after** `expires_at` likewise loses to `default_on_expire` — evaluated against
  the same clock as expiry-vs-answer — so an overdue `ask` resolves to `expired`, never `cancelled`. A
  `cancelled` message still emits a Response so push/pull agents get closure. `cancelled` is **ask-only**.
- **At-most-once delivery semantics.** Once a message is terminal, the Hub MUST NOT deliver a *different*
  resolution. In-flight delivery attempts for the terminal resolution MAY complete; they carry the same
  `resolution_id`, and the agent deduplicates (§6).
- `default_on_expire` MUST validate against the request: a member of `options[].value` for `select`, an
  object validating against `schema` for `input`, or `null`. (For `select` this is schema-enforceable; for
  `input` and for the cross-field `options` membership, the Hub enforces it and a conformance vector
  covers it — see §12.)

## 8. Transport Bindings (HTTP/JSON)

All endpoints are HTTPS, `Content-Type: application/json`, base path `/v1`. Plaintext MUST NOT be offered.

### 8.0 Discovery — `GET /.well-known/ma2h`

Returns a capability document (schema `schema/v0.4/capability.schema.json`) so an agent can configure
itself without out-of-band knowledge:

```json
{
  "ma2h_version": "0.4",
  "max_body_bytes": 65536,
  "max_part_bytes": 262144,
  "max_context_parts": 16,
  "auth_schemes": ["bearer", "apikey"],
  "callback_auth_schemes": ["hmac", "bearer", "apikey"],
  "signature_algs": ["hmac-sha256", "ed25519"],
  "rate_limit": { "requests_per_minute": 60, "inbox_depth": 1000 },
  "retention_days": 30,
  "replay_window_seconds": 120,
  "inbound": {
    "enabled": true,
    "poll_url": "https://hub.example/v1/inbox",
    "ack_url": "https://hub.example/v1/inbox/ack",
    "max_batch": 32,
    "visibility_timeout_seconds": 60,
    "retention_days": 30,
    "signature_algs": ["hmac-sha256", "ed25519"],
    "webhook_supported": true
  },
  "ack": { "enabled": true, "signature_algs": ["hmac-sha256", "ed25519"] },
  "presence": { "enabled": true, "freshness_seconds": 90 }
}
```

The `inbound` object (v0.4, §13) advertises the human→agent leg. A Hub that does not offer inbound
directives MUST omit `inbound` (or set `inbound.enabled: false`); its absence means "no mailbox here," and
a pre-0.4 Hub omits it entirely. The `ack` object (v0.4, §14) advertises the acknowledgment/receipt
primitive (`signature_algs` are the algs the Hub uses to sign a **pushed** ack, §14.4); the `presence`
object (v0.4, §15) advertises the presence signal (`freshness_seconds` is the window after which a
`last_seen` reads `offline`, §15.2). A Hub offering neither omits them. All fields beyond `ma2h_version`
remain SHOULD.

### 8.1 Submit — `POST /v1/messages`

Body: a message envelope (§4–§5). On success the Hub returns **`202 Accepted`** (schema
`schema/v0.4/submit-ack.schema.json`):

```json
{ "id": "msg_01H...", "status": "open", "poll_url": "https://hub.example/v1/messages/msg_01H...",
  "review_url": "https://hub.example/inbox/msg_01H..." }
```

For `notify` the ack carries `status: "delivered"`.

**`idempotency_key`** (REQUIRED for `ask`/`task`). The Hub deduplicates on `(agent.id, idempotency_key)`,
**run-independent**, for at least a stated TTL (RECOMMENDED ≥ 24h). A replay with the **same** key and an
identical payload MUST return the original ack including the original `id` and current `status` (so a
crashed agent recovers its outcome). A replay with the same key but a **differing** payload MUST be
rejected with `409` (§8.5). The agent retry recipe (lost ack): retry with the **same** `idempotency_key`
until a 202 or 409 is observed — this is what prevents a duplicate human decision when the 202 is lost and
the agent (holding no `id` yet) must blindly retry.

### 8.2 Pull — `GET /v1/messages/{id}`

Returns the message plus its current `status` and, once terminal, the embedded `response` (schema
`schema/v0.4/get-message.schema.json`). Reads are idempotent: a terminal message returns the same body on
every GET. The Hub SHOULD support long-poll via `?wait=<seconds>` (server-max, cap 60): hold until
resolution or timeout, then return current state (a `200` with `status: open` on timeout is normal, not an
error). A `?since=<RFC3339>` cursor MAY be used; the Hub SHOULD return `304` when status is unchanged.

A resolved message MUST remain pull-available for the retention TTL (RECOMMENDED 30 days; advertised in
§8.0). A **deleted** message returns `410 Gone`; an **unknown** id returns `404`. This is the source of
truth for ephemeral agents and the fallback when push fails (§8.3).

On a Hub that offers the ack primitive (§14), the GET body additionally carries an optional `delivery`
object — the receipt track (`delivered-to-agent` → `acknowledged`) — which the human reads to see whether
the agent got and acknowledged the answer. It is additive and orthogonal to `status`/`response` (§14.2).

### 8.3 Push — agent-supplied `callback`

An `ask`/`task` MAY include a callback so the Hub delivers the Response actively:

```json
"callback": {
  "mode": "push",                       // push | pull (pull = omit callback or mode:pull; agent polls 8.2)
  "url": "https://agent.example/ma2h/resolve",
  "auth": { "scheme": "hmac", "secret_ref": "env:MA2H_CALLBACK_SECRET" }  // see §9.2
}
```

Omitting `callback` (or `mode:pull`) means the agent is in pull mode and MUST use §8.2. On resolution the
Hub POSTs the Response (§6) to `url`, **signed** per §9.2. Delivery is **at-least-once**: the Hub MUST
retry on `5xx`/network errors with exponential backoff (≥ 5 attempts), MUST NOT retry on `4xx`, and MUST
cap total attempts and total duration (advertised). The agent's callback receiver MUST deduplicate on
`(in_reply_to, resolution_id)` and act at most once. After retry exhaustion the resolution MUST remain
pull-available (§8.2) — it is never lost. Push is best-effort; pull is the source of truth, and on any
push/pull disagreement the GET value is authoritative.

### 8.4 Cancel — `POST /v1/messages/{id}/cancel`

Withdraws an open `ask`. **Only the submitting principal may cancel** (§9.1); a cancel from any other
authenticated agent is refused as if the id were unknown (`404`), never applied. On success returns
`200 { "id", "status": "cancelled" }` and emits the terminal `cancelled` Response, delivered like a resolve
(push and/or embedded for pull) so the agent gets closure. If the message already reached a **different**
terminal (e.g. `answered`), returns `409` with the existing `{ "id", "status", "resolution" }` (the agent
reads the real outcome). Idempotent: re-cancelling an already-`cancelled` message returns the same `200`.
Tasks are not cancellable in v0.3.

### 8.5 Error Model

Errors use a canonical envelope: `{ "error": { "code": "<machine_code>", "message": "<human>" } }`.

| Status | `code` | When |
|--------|--------|------|
| 400 | `validation_error` / `version_not_supported` | malformed envelope; unknown major (§10) |
| 401 | `unauthenticated` | missing/invalid agent credential |
| 403 | `agent_id_mismatch` / `not_authorized` | credential ↔ `agent.id` mismatch; resolver not in `allowed_resolvers` |
| 404 | `not_found` | unknown message id |
| 409 | `idempotency_conflict` / `already_terminal` | replay with differing payload; cancel/resolve after terminal |
| 410 | `gone` | message deleted (vs 404 unknown) |
| 422 | `invalid_field` | e.g., `expires_at` in the past; `default_on_expire` ∉ options |
| 429 | `rate_limited` | over a published limit; includes `Retry-After` |

Callback (Hub→agent) delivery follows the same 4xx-no-retry / 5xx-retry rule (§8.3).

### 8.6 Rate Limits and Quotas

A Hub MAY enforce, and advertises (§8.0): a per-`agent.id` submit rate and a max inbox depth (over-limit →
`429` + `Retry-After`), a max envelope and per-Part byte size (over-limit → `422`), and a cap on total
callback attempts and duration. These bound both the inbox denial-of-attention and the callback-amplifier
risks. Agents MUST respect `Retry-After` and MUST NOT retry immediately on `429`.

### 8.7 Inbox — the agent mailbox (v0.4, inbound leg)

The transport for the human→agent leg (§13). Available only on a Hub advertising `inbound` (§8.0). Both
endpoints are authenticated by the agent's existing **bearer token / API key scoped to one `agent.id`**
(§9.1); the credential — not any request field — selects the mailbox, so an agent can only ever drain and
ack **its own** mailbox.

**Drain — `GET /v1/inbox`.** Returns the caller's pending directives (§13.1) in FIFO order, each paired
with its detached signature (§9.7):

```json
{ "messages": [ { "directive": { /* §13.1 */ }, "signature": "MA2H-Signature: t=..,jti=..,v1=.." } ] }
```

- `?max=<n>` caps the batch (Hub-max advertised as `inbound.max_batch`); `?wait=<seconds>` long-polls
  (server-max, cap 60) exactly like §8.2 — hold until a directive is available or timeout, then return
  (an empty `messages` on timeout is normal, not an error).
- A returned directive is marked **in-flight** for the `inbound.visibility_timeout_seconds` window: it is
  hidden from subsequent drains until either acked (removed) or the window lapses (**redelivered** — this
  is the at-least-once guarantee). The Hub MUST re-sign each delivery with a fresh `t`/`jti` (§9.7).
- FIFO is guaranteed on first delivery; redelivery MAY reorder relative to fresh directives, so the agent
  MUST NOT depend on strict ordering for correctness — it deduplicates on the directive `id` (§13.4).

**Consume / ack — `POST /v1/inbox/ack`.** Removes directives the agent has durably processed:

```json
{ "ids": ["dir_01H...", "dir_01H..."] }
```

Returns `200 { "acked": <count> }`. Idempotent: acking an already-acked or unknown id is a no-op (counted
or not, Hub's choice), never an error. Acking is the agent's explicit **consume**; an un-acked directive is
redelivered after the visibility window and retained until acked or expired (§13.3). Ack is submitter-bound
like drain: an ack referencing an id not in the caller's own mailbox has no effect (and MUST NOT reveal
whether that id exists elsewhere).

**Webhook (optional).** If the agent has pre-registered a §9.4-verified inbound callback, the Hub MAY also
POST each signed directive to it, following the §8.3 at-least-once retry rules (5xx/network retry with
backoff, no 4xx retry, capped attempts/duration). Push is best-effort; the mailbox (drain/ack) remains the
source of truth and the fallback — a directive delivered only by webhook is still drainable until acked or
expired. Registration of the agent's inbound callback URL is out-of-band and implementation-defined (the
same host-ownership verification as §9.4).

## 9. Security and Privacy

### 9.1 Authentication and authorization

- **Agent → Hub.** Bearer token or API key scoped to one `agent.id`. The Hub MUST reject an envelope whose
  `agent.id` does not match the credential (`403`). `run_id` is opaque and MUST NOT be used to authorize
  cross-run access; the Hub MUST bind a message's poll (§8.2), callback (§8.3), **and cancel (§8.4)** access
  to the principal that submitted it. A non-submitting principal MUST NOT poll, receive the callback for, or
  cancel a message by learning or guessing its `id` — cancel is state-terminating, so an unbound cancel lets
  one agent withdraw another's open `ask`. To that principal the message SHOULD be indistinguishable from an
  unknown id (`404` over `403`), so the binding also serves as an id-enumeration guard.
- **Resolver authz.** `actor` MUST be **Hub-attested** — derived from the Hub's authenticated operator
  session, never from the resolving request body; format `<type>:<id>`, `type ∈ {human, agent, system}`.
  A message's optional `allowed_resolvers` lists who may resolve it. **When absent, the default is
  fail-closed**: only the submitting `agent.id`'s own identity may resolve. (A fail-open default would
  ship the unauthenticated-resolver hole as opt-in.) `client_ref` MUST NOT be exposed to resolvers.

### 9.2 Response integrity (all callback schemes)

The Hub MUST sign every pushed Response, for **every** `callback.auth.scheme` (not only hmac). The
signature is **detached** and computed over a canonical `signed_context`:

```
signed_context = {
  "ma2h_version", "callback_url", "id", "in_reply_to", "jti",
  "payload_sha256", "resolution", "resolution_id", "resolved_at", "t"
}
```

serialized with **RFC 8785 JSON Canonicalization Scheme (JCS)** (deterministic key order, IEEE-754 number
formatting, canonical minimal string escaping — RFC 8785 §3.1 preserves string content and does **not**
normalize Unicode), then signed with `hmac-sha256` (secret from `secret_ref`) or `ed25519` (advertised in
§8.0). Wire header:

```
MA2H-Signature: t=<unix>,jti=<nonce>,v1=<base64url(signature)>
```

**`payload_sha256` (v0.3) binds the response payload.** It is the lowercase-hex SHA-256 of the JCS
serialization of the fixed-key object `{ "response": <the Response `response` object, or `null`>, "state":
<the Response `state`, or `null`> }`. This binds the entire `response` detail the agent consumes —
`value`, `comment`, `actor`, `edited`, `resolved_at` — and the round-tripped `state`, so altering any of
them in transit invalidates the signature. The Hub MUST compute `payload_sha256` over the payload it
delivers; the agent MUST **recompute** it from the payload it actually received — never trusting a
transmitted digest — and include it in the `signed_context` it reconstructs before verifying. Numeric
payload values MUST be canonicalized per RFC 8785 §3.2.2.3 so a conformant signer and verifier agree
byte-for-byte. RFC 8785 serializes a number as its IEEE-754 double value (the `Number::toString` form), so
ordinary decimals (`0.1`, `1e-7`, `-0.001`) and in-range integers all canonicalize deterministically;
implementations MUST use a conformant JCS implementation (a JS `JSON.stringify` of a number already
conforms — a non-JS implementation MUST use a vetted JCS library, and MUST preserve string content as-is:
RFC 8785 §3.1 does **not** normalize Unicode). The one caveat is *exactness*, not conformance: because a
JSON number is a double, an application that must preserve an integer beyond ±(2^53−1) exactly MUST carry
it as a **string** (a bare number would silently round). The `dp-004` conformance vector pins the canonical
bytes + digest for a numeric payload as the cross-language interop target. `payload_sha256` is carried only
inside the `signed_context` covered by the `MA2H-Signature`
header; it is intentionally **not** a field of the Response body schema (a body-borne digest would be
attacker-suppliable, defeating the recompute requirement above).

`resolution`, `resolution_id`, and `resolved_at` are additionally bound as top-level fields. The Response's
top-level `defaulted` flag and its routing-only top-level `agent` object (`agent.id` / `agent.run_id`) are
**not** bound and MUST be treated as advisory: a conformant agent MUST derive the human-vs-expiry-default
distinction from the **bound** `actor` (`system:default_on_expire` vs a `human:`/`agent:` identity) and
`resolution` — never from `defaulted` — and MUST NOT use the top-level `agent` (e.g. `agent.id`) for
authorization, correlation, or state-key selection (the bound `in_reply_to` + `callback_url` are the
authenticated routing identity). Before v0.3 the answer `value` for a
`select`/`input` ask was unsigned — a terminating proxy could flip it and verification still passed.

The agent MUST verify the signature against the canonical `signed_context` it reconstructs, MUST reject a
`t` outside ±120s of its own clock (replay window), and MUST maintain a **replay cache** of seen `jti`
values with a TTL **≥ the replay window** (this is independent of the 30-day message retention; a stable
pull response is not protected by `jti` and is instead trusted via §8.2 transport + the immutable terminal
record). Binding the signature to `id` + `resolution_id` + `callback_url` prevents cross-message and
cross-endpoint replay; `payload_sha256` binds the decision payload itself. The body-signing key MAY be the same as the callback-auth credential or distinct;
either way rotation MUST be supported (the agent MAY hold two valid keys during overlap).

### 9.3 State integrity (agent-owned) and key provenance

Returned `state` is **UNTRUSTED input**. An agent MUST NOT act on `state` contents without verifying
integrity it applied itself. The Hub storing `state` opaquely and never inspecting it (§4) is necessary
but not sufficient — it does not stop a malicious Hub or resolver from tampering with the blob.

- An agent SHOULD AEAD-seal `state` (encrypt-then-MAC) and MUST verify the MAC before use; a minimal HMAC
  is permitted for low-sensitivity state.
- **Key provenance (REQUIRED).** The state-seal key MUST be a per-`agent.id` secret pre-positioned in the
  agent runtime (e.g., a CI/Actions secret, a vault env var), **distinct from the callback credential**,
  surviving re-invocation independently of any Hub-held value. The key MUST NOT be embedded in `state`
  (circular — zero integrity) and MUST NOT transit the Hub. If this key co-resides with a compromised
  callback host, the guarantee is void.
- **Anti-pattern (do not do this):** placing the seal key inside the `state` blob, or deriving it from
  data the Hub can see. Either makes "verify before use" pass on attacker-controlled state.

### 9.4 Callback destination safety (SSRF)

Before attaching a referenced credential to a callback, the Hub MUST verify the `callback.url` host is
agent-owned (verification method implementation-defined: DNS-TXT, HTTP well-known, or operator approval).
The Hub MUST refuse private/link-local/metadata IP ranges, MUST apply that refusal **at delivery time**
(not only at registration — DNS-rebinding defense), MUST NOT follow redirects, and MUST only attach a
credential when the URL host matches a registered binding. A dev-mode relaxation MAY exist but MUST
fail closed in production. (v0.1's GitHub-PAT-to-`/dispatches` example is a confused-deputy anti-pattern;
see `examples/callback-anti-pattern.md`.)

### 9.5 Clock authority

The **Hub clock** is authoritative for `expires_at` evaluation and for the §9.2 signature `t` window
(the agent applies the window against its own clock for the inbound signature; the Hub uses its own clock
for expiry). Agents SHOULD synchronize via NTP.

### 9.6 Content safety and telemetry minimization

- A Hub MUST NOT render raw HTML from `body` in the inbox (Markdown is sanitized to a no-raw-HTML profile;
  external links visibly marked, not auto-followed; remote images not auto-fetched). It MUST NOT
  server-side-fetch `context.file.uri` unless the URI passes §9.4 host controls.
- **Telemetry.** Hub logs SHOULD exclude `body`, `context`, `response.value`, and `response.comment` by
  default, and MUST NOT log `state`. A property marked `x-ma2h-sensitive: true` (or a message-level
  `sensitive: true`) strengthens the SHOULDs to MUST and excludes the value from any export/telemetry. A
  sensitive value MAY be carried as a reference (`{ "secret_ref": "vault://..." }`) so the raw secret
  never enters the agent's LLM context (the CHEQ idea).

### 9.7 Directive integrity (inbound leg, v0.4)

The inbound mirror of §9.2. A directive drives agent behavior, so the agent MUST authenticate its origin
and integrity before acting — a forged or tampered directive is a prompt-/command-injection vector. The Hub
MUST sign **every** directive delivery (drain and webhook alike), for **every** advertised
`inbound.signature_algs` scheme, with a **detached** signature over a canonical `inbound_signed_context`:

```
inbound_signed_context = { "from", "id", "jti", "ma2h_version", "payload_sha256", "t", "to" }
```

serialized with **RFC 8785 JCS** (§9.2) and signed with `hmac-sha256` or `ed25519`. The wire header reuses
the §9.2 form: `MA2H-Signature: t=<unix>,jti=<nonce>,v1=<base64url(signature)>`.

- **`payload_sha256`** is the lowercase-hex SHA-256 of the JCS serialization of the fixed-key object
  `{ "directive": <the human-authored content object> }`, where the content object carries exactly the
  directive's present `title`, `body`, `priority`, `tags`, and `context` fields. This binds the instruction
  the agent consumes, so altering (or adding/stripping) any of it in transit invalidates the signature.
  Numbers canonicalize per RFC 8785 §3.2.2.3 exactly as in §9.2. As in §9.2, `payload_sha256` is carried
  **only** inside the signed context (never as a body field), and the agent MUST **recompute** it from the
  directive it actually received rather than trusting any transmitted digest.
- **`from`**, **`id`**, and **`to`** are bound as top-level fields: `from` authenticates the attested author,
  `id` is the stable at-most-once key, and `to` binds the directive to its addressed agent — so a directive
  signed for `agent:X` cannot be re-addressed to `agent:Y` without breaking the signature (and the receiving
  agent additionally confirms `to` is itself, §13.4).
- **Unbound metadata (advisory).** `created_at`, `expires_at`, and `sensitive` are **not** bound — neither
  top-level nor inside `payload_sha256` (which covers only the `title`/`body`/`priority`/`tags`/`context`
  content). They are **Hub-authoritative advisory** fields and MUST NOT drive an agent's security decisions:
  the Hub — not the agent — enforces `expires_at` (it drops an expired directive before delivery, §13.3), and
  `sensitive` governs Hub-side telemetry (§9.6). An agent MUST NOT, for example, extend trust based on a
  received `expires_at` or `created_at`. (This mirrors §9.2 treating the top-level `defaulted`/`agent` as
  advisory.)
- **Per-delivery signing (REQUIRED).** Unlike a §6 Response (delivered promptly on resolution), a directive
  MAY rest in a mailbox for a long time before the agent polls. The Hub MUST therefore compute a **fresh**
  `t` and `jti` **at each delivery**, so a legitimately-old directive still lands inside the agent's replay
  window. Only `t`/`jti` change per delivery; the bound `from`/`id`/`to`/`payload_sha256` are stable.

The agent MUST verify the signature against the `inbound_signed_context` it reconstructs, MUST reject a `t`
outside its replay window (±`replay_window_seconds`, §8.0) against its own clock, MUST maintain a `jti`
replay cache (TTL ≥ the window, §9.2), and MUST deduplicate acting on the directive `id` (§13.4). Because
every directive delivery is signature-verified on **both** channels — the pull mailbox is an unsolicited
inbound command surface, not a response the agent solicited — this is a MUST on pull as well as webhook,
which is intentionally stronger than the §8.2 "authenticated GET is trusted transport" stance for the
return leg. The signing key MAY be the same as, or distinct from, the §9.2 body-signing key; rotation MUST
be supported (the agent MAY hold two valid keys during overlap).

## 10. Versioning and Roadmap

- `ma2h_version` is `MAJOR.MINOR`. The **major** is the integer before the first dot. A Hub MUST reject a
  message whose **major** it does not recognize with `400 version_not_supported`. Within a recognized
  major, a Hub SHOULD accept any minor and MUST **ignore unknown fields** (robustness principle). The
  v0.4 schema accepts a **canonical** `0.x` via `pattern: "^0\\.(0|[1-9]\\d*)$"` — a leading-zero minor
  (e.g. `0.03`) is a non-canonical spelling and is rejected as a `validation_error`, so it can never be
  mistaken for `0.3` and slip a push past the parity gate below. (The historical v0.3 schema used the looser
  `^0\\.\\d+$`; the tightening is additive — every canonical version stays valid.)
- Spec text Apache-2.0; contributions under DCO. Breaking changes bump the version.
- **v0.3 (breaking):** the §9.2 signature now binds `payload_sha256` (a digest of the response payload), so
  a v0.2 verifier and a v0.3 signer compute different canonical strings — a v0.2 agent rejects v0.3
  signatures and vice-versa. There is no signature interop across this break within major `0`; upgrade and
  re-vendor both ends together.
- **Push version parity (v0.3).** Because the break is in the *signature*, a v0.3 Hub's **pushed** Response
  can be verified only by a v0.3+ agent. A Hub MUST therefore reject (or negotiate) a message that requests
  a **push** callback at a pre-0.3 `ma2h_version` with `version_not_supported` — emitting a callback the
  agent provably cannot verify is worse than a clear rejection. **Pull is unaffected**: a pulled Response is
  not signature-verified (§8.2, trusted via the authenticated GET), so a pre-0.3 *pull* message stays
  compatible. (§9.2.) The parity threshold is the **signature-break minor (3)**, independent of the highest
  minor a Hub implements: a v0.4 Hub still accepts a 0.3 push (0.3 and 0.4 share the payload-bound §9.2
  signature) and still rejects a pre-0.3 push.
- **v0.4 (additive, non-breaking).** v0.4 adds the human→agent **directive** leg (§13) — a new inbound
  message type, the `/v1/inbox` transport (§8.7), the §9.7 directive signature, and the `inbound` capability
  field (§8.0). It changes **no** v0.3 wire format: every v0.3 leg (notify/ask/task + responses) is
  byte-for-byte unchanged, and a 0.4 Hub accepts 0.3 agent→human envelopes and signs their responses at the
  version carried (the §9.2 algorithm is unchanged). New `spec/v0.4.md` + `schema/v0.4/` (a full snapshot;
  the agent→human schemas are re-`$id`'d to the v0.4 path unchanged, `capability` gains `inbound`, and
  `inbound-message.schema.json` is added). Because the directive signature is a **new** context (§9.7), it
  interoperates only between a 0.4 Hub and a 0.4-aware agent — but a pre-0.4 agent simply does not consume
  the inbound leg, so nothing it already does breaks. v0.4 additionally adds the cross-cutting
  **acknowledgment/receipt** (§14) and **presence** (§15) primitives — both additive, capability-advertised
  (`ack`, `presence`, §8.0), and orthogonal to every existing leg (the ack track never touches the §7
  `resolution`; presence is a read-only derived signal).

**Roadmap (out of v0.4):** human SSO mechanics; assignment/escalation/SLA routing; **human→agent `ask`/`task`**
(a routed agent-response leg — the mirror of §6); multi-turn threads; streaming (AG-UI interop); channel
fan-out.

## 11. Provenance (what MA2H reuses)

| From | Reused in MA2H |
|------|---------------|
| **A2A** (Linux Foundation) | `Part` union; `callback` ← `PushNotificationConfig` auth schemes; `project` ≈ `contextId`; terminal-state irreversibility; `input-required` → the `ask` lifecycle; capability discovery ≈ Agent Card (§8.0). |
| **HITL Protocol** | `202` + `poll_url`/`review_url` submit handshake (§8.1). |
| **MCP elicitation** | `select` options as enum+label; flat input schema; accept/decline/cancel-shaped resolutions. |
| **HumanLayer** | opaque `state` round-trip; typed contact channels (roadmap). |
| **LangGraph** | `request.permissions` ← `HumanInterruptConfig`. |
| **CHEQ** (IETF draft) | keep human-entered secrets out of the agent's LLM context (§9.6). |
| **RFC 8785 (JCS)** | canonical serialization for the detached Response (§9.2) and directive (§9.7) signatures. |
| **Durable message queues** (SQS / AMQP) | the inbound mailbox: at-least-once delivery, visibility-timeout redelivery, explicit consume/ack, FIFO-per-consumer (§8.7, §13). |
| **Messaging read-receipts / delivery status** | the acknowledgment/receipt (§14): a terminal, per-message "delivered → acknowledged" track, orthogonal to the decision. |
| **Presence / last-seen heartbeats** (XMPP, chat) | derived presence (§15): a `last_seen` from existing activity, freshness-windowed, owner-scoped. |

## 12. Validation and Proof Obligations

Conformance vectors **cannot** prove the security/concurrency requirements — a JSON Schema checks shape,
not cryptographic soundness or race-freedom. Vectors are labeled by class (see `conformance/README.md`):

1. **Schema-validation vectors** (executable now, no Hub) — each example validates, or is an intentional
   negative vector. Covers the envelope, return leg, resolution mapping, per-scheme auth conditionals,
   cross-type rejection.
2. **Prose audits** (human sign-off during spec review, not executable) — the "spec contains a MUST"
   checks. Verifies text presence, not implementation conformance.
3. **Downstream proof obligations** (validatable only against a conformant Hub) — the security/concurrency
   controls. This spec **specifies candidate controls** for resume-hijack (§9.3), SSRF (§9.4), Response
   integrity (§9.2), and concurrency (§7); **closure is proven against the Hub**, which MUST discharge:
   the §9.2 signature test vector (known input → known signature), a replay-cache bypass test, an
   SSRF-allowlist + DNS-rebinding bypass test, and a CAS-race test (two terminal transitions in a sub-ms
   window → first wins, one `resolution_id`; answer-at-`expires_at` beats default).

The inbound leg (§13) adds vectors of the same three classes: schema-validation for the directive envelope
(valid; missing `to`; a non-`human`/`system` `from`; cross-type `request`/`action`/`state` rejection); the
§9.7 directive-signature obligation (deterministic fixture → known signature; recompute `payload_sha256`
from the directive; reject a tampered `from`/`to`/`body`; reject a replayed `jti`); and the mailbox
at-least-once obligation (a redelivered directive with the same `id` is acted on **once**; drain/ack are
submitter-bound so one agent cannot drain or ack another's mailbox).

The **ack** (§14) and **presence** (§15) primitives add: schema-validation for the `ack` and `presence`
envelopes; the §14.4 pushed-ack signature obligation (deterministic fixture → known signature; reject a
replayed `jti`/out-of-window `t`); the ack terminal-once obligation (first ack for an `in_reply_to` wins,
submitter-bound); and the presence obligations (`last_seen` derived from poll/long-poll/SSE activity;
`online`/`offline`/`unknown` by the advertised window; owner-only read — a non-owner sees `unknown`).

## 13. The Inbound Leg — Human → Agent Directives (v0.4)

The mirror of the agent→human legs: a Hub-attested human sends one agent a **directive** — an instruction
or FYI — and the agent receives it by draining its mailbox (§8.7). It is the inbound counterpart of `notify`
(one-way, no routed agent response — see §2 Non-Goals for why `ask`/`task` inbound are deferred), and it
reuses the v0.3 machinery throughout: the `Part` union (§4.2), Hub attestation of identity (§9.1), RFC 8785
JCS + detached signature (§9.7 ← §9.2), `jti` replay dedup (§9.2), SSRF controls (§9.4), and durability
(§3.1).

```
   Human (attested)            Hub                         Agent (drains when it polls)
   ────────────────            ───                         ───────────────────────────
   author directive ─────────▶ attest `from`, assign id
   to agent:X                  enqueue in X's mailbox
                               (durable, FIFO)
                                          ┌── GET /v1/inbox ──────────  1. drain (bearer = agent:X)
                               sign each  │                             2. verify §9.7 signature
                               delivery ──┤── {directive, signature} ─▶ 3. dedup on id; act once
                               (fresh t,  │                             4. POST /v1/inbox/ack {ids}
                                jti)      └── (redeliver if unacked ──  (at-least-once)
                                              past visibility window)
```

### 13.1 The directive envelope

A JSON object; schema `schema/v0.4/inbound-message.schema.json`. This is the **delivered** form (the agent
validates it): `id` and `from` are Hub-assigned/attested, never agent- or request-supplied.

| Field | Type | Req | Notes |
|-------|------|-----|-------|
| `ma2h_version` | string | MUST | `"0.4"`. |
| `type` | const | MUST | `"directive"` — the single inbound type in v0.4. |
| `id` | string | MUST | Hub-assigned canonical directive id. The agent's at-most-once dedup key (§13.4). |
| `from` | string | MUST | **Hub-attested** author, `^(human\|system):.+$`; derived from the authenticated operator session, never the request body (as §9.1 attests `actor`). `human:<id>` in v0.4. |
| `to` | string | MUST | `^agent:.+$` — the addressed agent; the mailbox routing key. |
| `created_at` | string | MUST | RFC 3339 (Hub clock; informational). |
| `title` | string | MUST | One line. ≤ 200 chars. |
| `body` | string | SHOULD | Markdown detail. **Untrusted agent input** — the inbound mirror of §9.6 (§13.5). |
| `priority` | enum | MAY | `low` \| `normal` \| `high` \| `urgent`. Default `normal`. |
| `tags` | string[] | MAY | Free-form labels. |
| `context` | Part[] | MAY | Attachments (§4.2). |
| `expires_at` | string | MAY | RFC 3339. After it, the Hub stops delivering and drops the directive (§13.3). MUST be in the future at author time. |
| `sensitive` | bool | MAY | Excludes value(s) from export/telemetry (§9.6). |

A directive MUST NOT carry `request`, `action`, or `state` (schema-enforced): inbound `ask`/`task` are
deferred, and a directive is not an agent-owned resume blob.

### 13.2 Delivery

Pull-first, exactly as §8.7 specifies: the agent authenticates with its existing bearer token / API key
(§9.1) and drains `GET /v1/inbox` (FIFO, long-poll-capable), then consumes with `POST /v1/inbox/ack`.
Delivery is **at-least-once**: a drained-but-unacked directive is redelivered after the visibility window.
A Hub MAY additionally **push** each signed directive to a pre-registered, §9.4-verified agent webhook
(§8.7); push is best-effort and the mailbox remains the source of truth.

### 13.3 Retention and expiry

An acked directive is consumed and removed. An un-acked directive is retained and kept deliverable until it
is acked or until its `expires_at` (or, absent that, the Hub's `inbound.retention_days`, §8.0) passes, at
which point the Hub drops it. A directive emits **no §6 Response** — there is no routed agent-response leg
(it is the `notify` mirror). To confirm *receipt/understanding* to the human, the agent posts an
**acknowledgment** (§14): the `POST /v1/inbox/ack` consume doubles as the receipt (optionally with a note),
advancing the directive's delivery track to `acknowledged` (§14.2) — this is the first-class mechanism, not
a separate out-of-band `notify`.

### 13.4 Agent duties (normative)

A conformant consuming agent MUST, for each drained or pushed directive, in this order:

1. **Verify** the §9.7 detached signature against the `inbound_signed_context` it reconstructs — recomputing
   `payload_sha256` from the directive it received, rejecting a `t` outside the replay window, and rejecting
   a replayed `jti` (from a `jti` cache with TTL ≥ the window, §9.7). It MUST treat the directive as
   untrusted until verified, and MUST also **validate the directive's shape** against
   `inbound-message.schema.json` — because `payload_sha256` binds only the content fields, an on-path
   injector (or a buggy Hub) can append a forbidden `request`/`action`/`state` field without breaking the
   signature, so the agent MUST reject such cross-type data rather than acting on it. Per the §10 robustness
   rule the agent ignores *unknown* fields, but — since they are likewise unsigned — MUST NOT surface them to
   its execution/LLM context (strip to the known fields before acting).
2. **Confirm the addressee.** After verifying, the agent MUST confirm `to` is its own `agent:<id>` and
   reject the directive otherwise. The signature *binds* `to`, so a valid signature proves the Hub intended
   a specific addressee — but only the recipient checking `to` against its own identity stops a directive
   validly signed for `agent:X` from being acted on by `agent:Y`. The pull mailbox enforces this Hub-side
   (an agent drains only its own mailbox), but the **webhook** channel has no such gate, so the check is a
   MUST on the agent for both channels.
3. **Deduplicate** on the directive `id` and act **at most once** — because at-least-once delivery (and any
   webhook/drain overlap) can present the same `id` more than once, possibly *concurrently* (a webhook and a
   poll racing) or after the visibility timeout while still processing. At-most-once for a non-idempotent
   side effect therefore requires **reserving** the `id` before the work begins (so an overlapping delivery
   is deduped in-flight), not only recording it after. This id-dedup gives at-most-once *within the agent's
   dedup horizon*; an agent that must remain at-most-once **across a restart** MUST either make its side
   effect idempotent or persist its dedup state, since a redelivered-but-unacked directive is re-presented
   after a restart clears an in-memory cache.
4. **Ack** (`POST /v1/inbox/ack`) once it has **durably processed** the directive, so the Hub stops
   redelivering it. The safe ordering is **verify → confirm addressee → act (durably/idempotently) → record
   the `id` in the dedup cache → ack**: recording the dedup id or acking *before* durable processing risks
   silently dropping a directive on a crash. Acking is the explicit consume; id-dedup guards overlapping
   deliveries within the window, ack + redelivery guards a crash before processing.

### 13.5 Security considerations (inbound)

- **Authenticity / injection.** A directive enters the agent's execution/LLM context, so a forged or
  tampered one is a command-injection vector. Defenses: Hub-attested `from`, the §9.7 signature the agent
  MUST verify, `payload_sha256` binding the instruction bytes, `to`-binding against cross-agent replay, and
  `jti`+window against replay. An agent SHOULD apply its own authorization policy to a verified directive
  (which `from` principals may instruct it, and to do what) — verification proves origin, not intent-safety.
- **Isolation.** The bearer credential (scoped to `agent.id`) selects the mailbox; a principal MUST NOT
  drain or ack another agent's mailbox, and a foreign/unknown directive id MUST be indistinguishable from a
  non-existent one (the §9.1 submitter-binding, applied to the inbox).
- **Content safety.** The agent MUST treat `body`/`context` as untrusted input; a Hub that echoes a
  directive into any HTML surface applies the §9.6 no-raw-HTML sanitization.
- **SSRF.** The optional webhook reuses §9.4 wholesale (host-ownership verification, delivery-time
  private-range refusal / DNS-rebinding defense, no redirects, credential-host binding).

## 14. Acknowledgment / Receipt (cross-cutting, v0.4)

A **cross-cutting** primitive — modeled once, applied to both directions. An **acknowledgment** (receipt) is
a one-shot, **terminal** confirmation that the receiving party got the other party's message or answer,
optionally with a short human-readable note. It is distinct from a later `notify`: it closes the loop on a
specific message rather than starting a new one. It is **additive and non-breaking** — a separate
delivery/receipt track layered on top of the unchanged legs, advertised via `ack` in §8.0.

Applied in v0.4 to:
- **(a) a human→agent directive (§13):** the agent acks the directive it drained — *"got it, on it."*
- **(b) an agent→human `ask`/`task` response (§6):** when the human resolves, the agent — on receiving the
  Response — acks that it got the answer and is proceeding — *"got your Ship it — resuming the deploy."*
  This enriches the shipped v0.3 response leg without changing it.

### 14.1 The ack envelope

A JSON object; schema `schema/v0.4/ack.schema.json`. The Hub produces it when an ack action (§14.3) fires.

| Field | Type | Req | Notes |
|-------|------|-----|-------|
| `ma2h_version` | string | MUST | `"0.4"`. |
| `type` | const | MUST | `"ack"`. |
| `in_reply_to` | string | MUST | The acknowledged id — a directive `id` (§13.1) or an `ask`/`task` message `id` (§6). |
| `by` | string | MUST | **Hub-attested** acking party, `<type>:<id>` (§9.1). `agent:<id>` in v0.4's two applications; the field is direction-agnostic. Never request-supplied. |
| `acked_at` | string | MUST | RFC 3339 (Hub clock). |
| `note` | string | MAY | A short, human-readable note. **Untrusted** agent→human text — the Hub sanitizes it to the §9.6 no-raw-HTML profile before rendering it to the human. |
| `resolution_id` | string | MAY | For an ask/task ack, the `resolution_id` of the Response being acknowledged (disambiguates which resolution). |

An ack is **terminal** for the receipt track: the **first** ack for a given `(in_reply_to)` wins; a repeat
is a no-op returning the existing ack (first-terminal-wins, mirroring §7).

### 14.2 Lifecycles (additive; orthogonal to the §7 `resolution` track)

The §7 `resolution` (open → answered/declined/…) is **unchanged**. Ack adds a separate **delivery/receipt**
track surfaced on the message GET body (§8.2) as an additive `delivery` object:

```
directive:  queued ─▶ delivered ─▶ acknowledged
ask/task:   answered ─▶ delivered-to-agent ─▶ acknowledged
```

- **`delivered` / `delivered-to-agent`** — the Hub confirmed the receiving party *got* the message: a
  directive is `delivered` when first drained (§8.7); an ask/task Response is `delivered-to-agent` on a
  successful push (2xx) **or** when the agent authenticated-GETs the resolved message (§8.2).
- **`acknowledged`** — the receiving party posted an ack (§14.3). Terminal for this track.

`delivery` is advisory to the flow but authoritative for the human's "did they get it / act on it?" view. It
never gates or alters the §7 resolution.

### 14.3 Transport — resource-scoped, per leg

Following the repo's resource-scoped sub-action convention (`/…/resolve`, `/…/cancel`):

- **Response leg (ask/task):** `POST /v1/messages/{id}/ack` with `{ "note"?: "…", "resolution_id"?: "…" }`.
  **Only the submitting principal** may ack (§9.1). Marks the message `acknowledged`; returns `200` with the
  ack envelope. Requires the message to be terminal (a resolution exists) — acking an `open` message is
  `409`.
- **Directive leg:** the receipt **folds into** the existing mailbox consume (§8.7). `POST /v1/inbox/ack`
  gains an optional `note`: `{ "ids": [...], "note"?: "…" }` — consuming a directive **is** acknowledging it
  (they are the same moment), so no separate call. The Hub marks each listed directive `acknowledged` and
  records the receipt (with `note` when present). A note-less `{ "ids": [...] }` still acks (no human note).

Either action makes the ack readable by the human over the authenticated Hub transport (§14.4). A Hub with
an ack **webhook** for the human's client MAY also push the signed ack.

### 14.4 Trust

An ack is **informational to the human** — it updates a receipt/status view and triggers no automated
action (unlike a §13 directive, which drives agent behavior). Its trust model therefore mirrors the **§6
response leg**, not the §9.7 directive rule:

- **Pull:** the human reads the ack over an **owner-authenticated Hub GET** — the `delivery` object on the
  human's own inbox/status surface for that message (or an ack feed) — trusted via transport exactly as a
  pulled v0.3 Response (§8.2) and the inbox itself. No signature required. (This is the human's owner-scoped
  read; it is distinct from `GET /v1/messages/{id}`, which is submitter-bound to the originating **agent**,
  §9.1 — the same `delivery` also appears there so the acking agent sees its own receipt.)
- **Push:** a Hub that pushes acks to a human callback MUST sign each with a **detached** signature
  (§9.2-style) over an `ack_signed_context = { ack_sha256, by, in_reply_to, jti, ma2h_version, t }`, using
  an advertised `ack.signature_algs` alg. `ack_sha256` is the lowercase-hex SHA-256 of the JCS of a
  **fixed-key** object with exactly these five keys — `{ acked_at, by, in_reply_to, note, resolution_id }` —
  where each **absent optional** field (`note`, `resolution_id`) is serialized as JSON **`null`** (exactly
  as §9.2's `{ response, state }` wrapper nulls absent members, so the digest is unambiguous regardless of
  which optionals the ack carries). The client MUST **recompute** `ack_sha256` from the ack it received
  using this same fixed-key/`null` wrapper (never omitting the absent keys), verify the signature, and
  reject a replayed `jti` / out-of-window `t`, mirroring §9.2. Signing on push is REQUIRED **when a push
  channel is used**; verification on pull is not (transport-trusted).

`by` is Hub-attested; the free-text `note` is untrusted agent→human content sanitized per §9.6.

## 15. Presence / "Listening" (cross-cutting, v0.4)

A **derived**, best-effort signal of whether an `agent:<id>` is currently reachable/listening — so a human's
client can set expectations before or during a message exchange. It is a **heuristic, not a guarantee**
(there is no always-on connection; an agent may poll then exit a second later). Advertised via `presence`
(§8.0). Read-only; it changes no other leg.

### 15.1 Derivation (no new agent obligation)

The Hub derives a per-`agent.id` **`last_seen`** timestamp from the agent's **existing** authenticated
activity — the traffic it already handles: a mailbox drain (`GET /v1/inbox`, §8.7), a response long-poll
(`GET /v1/messages/{id}?wait=`, §8.2), or an SSE subscription (`GET /v1/stream`). No heartbeat endpoint and
no persistent connection are required. A Hub MAY additionally accept an explicit refresh ping, but that is
non-normative — the signal is the agent's real reachability traffic.

### 15.2 States

From an advertised freshness window `W` = `presence.freshness_seconds` (§8.0), evaluated against the Hub
clock (§9.5):

| State | Meaning |
|-------|---------|
| `online` | `now − last_seen ≤ W` — the agent polled/subscribed recently; likely reachable now. |
| `offline` | `now − last_seen > W` — the agent was seen but not within the window; it will receive on its next run. |
| `unknown` | no `last_seen` on record — the agent has never connected (distinct from `offline`). |

### 15.3 Read endpoint (owner-only)

`GET /v1/agents/{agent_id}/presence` → `{ "agent_id", "state", "last_seen"?, "freshness_seconds" }` (schema
`schema/v0.4/presence.schema.json`). **Owner-only, per-account:** only the authenticated **human** whose
account the agent belongs to MAY read it; the Hub MUST NOT expose an agent's presence to other agents or
publicly, and an agent the caller does not own MUST be indistinguishable from `unknown`. Presence leaks
*when* an operator's automation runs, so it is privacy-scoped to the owner.

### 15.4 Applications

- **Before a human→agent send (§13):** `online` → *"listening now"* (the directive will be picked up
  promptly); `offline`/`unknown` → *"will receive on its next run"* (the mailbox is durable, §13.3).
- **On a pending `ask`/`task` (§6):** while the agent is blocked waiting on the human, the **same** signal
  shows whether it is **actively waiting** (`online` → answering resumes it near-instantly via its open
  long-poll/callback) or **already exited** (`offline` → the answer is picked up later via callback or the
  next run). No separate mechanism — presence serves both.

## Appendix A — Worked Examples

See [`/examples`](../examples): `notify-daily-digest`, `ask-dev-team-decision`, `ask-mode-input`,
`ask-mode-confirm`, `ask-sensitive-field`, `task-manual-action`, and the Responses
`response` (answered), `response-expired-default`, `response-cancelled`, `response-declined`,
`task-completion-response`, the worked `response-signature-v0.3` (the §9.2 payload-bound signature), plus
the safe `callback-agent-resume` and the `callback-anti-pattern` note. For the inbound leg (§13):
`directive-inbound` (a human→agent directive) and the worked `directive-signature-v0.4` (the §9.7
directive signature).
