# MA2H — Multi-agent to Human Protocol

**Version:** 0.5 (Draft) · **Date:** 2026-08-10 · **License:** Apache-2.0 · **Steward:** Autonomy

> Supersedes [v0.4](v0.4.md). v0.5 is **additive and backward-compatible**: it adds a hub-mediated,
> store-and-forward **inter-agent leg** — **sessions** (§16), **addressed envelopes** (`to`, §4), three
> new mailbox **entry kinds** (§8.7), and **delivery honesty** (§14) — alongside the unchanged v0.4
> legs. No existing wire format, envelope, lifecycle, or signature changes. Designed via
> [SCP #24](https://github.com/autnmy/ma2h-protocol/issues/24). 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**, by which **a human reaches a specific agent**, and — as of v0.5 — by which **agents of one
account reach each other**. 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. The v0.5 **inter-agent leg** routes the same three verbs agent→agent through the same
mailbox: a **session** (§16) makes one live invocation addressable, an optional envelope **`to`** (§4)
parks the message in the destination's mailbox as a signed **`message` entry** (§8.7), the addressee
resolves it as an attested `agent:` actor, and the Response routes back to the submitting session as a
**`response` entry**. **Reachability and delivery honesty** (§14, §15) keep the leg truthful: a
destination-reachability snapshot on the submit ack, per-session presence, and terminal
`bounced`/`expired` delivery states — the Hub never guarantees an agent is listening; it guarantees
nobody is left believing a lie. MA2H is the agent↔human complement to A2A (agent↔agent, HTTP-addressable
peers) and MCP (agent↔tools); the v0.5 leg covers hub-mediated coordination among **one account's**
agents — spokes that are HTTP clients with no inbound port — not cross-account federation (§2).

This version (0.5) builds on the v0.4 inbound-leg pass — which itself built on the v0.3 payload-bound
signature and 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 inter-agent leg **without changing any v0.4
leg**. A 0.5 Hub remains backward-compatible with 0.3/0.4 envelopes. The new leg reuses the existing
machinery throughout: the §4 envelope, the §8.7 mailbox (at-least-once, visibility-timeout,
explicit ack), RFC 8785 JCS canonicalization and detached signatures (§9.2/§9.7 → §9.8), `jti` replay
dedup, the §7 CAS lifecycle, and durability (§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). v0.5 extends it per session (§15).
- **Session** — a **v0.5** Hub-registered, lease-bound, ephemeral **addressing scope** under one
  `agent.id` (§16). The **principal** (the `agent.id` + its credential) authenticates and is accountable;
  the session locates one live invocation. A session id is an address, not a secret and not a credential.
  Prior art: XMPP bare-JID vs. full-JID resource binding (RFC 6120), SIP REGISTER contact bindings with
  `Expires` (RFC 3261), NATS ephemeral consumers (§11).
- **Entry** — one delivered item in the §8.7 mailbox, paired with its detached signature. v0.4 had one
  entry kind (the directive); v0.5 adds three, visible only to session-presenting drains: **`message`**
  (an addressed §4 envelope, agent-authored), **`response`** (a §6 Response delivered to the submitting
  session), and **`receipt`** (a Hub-originated delivery-status notification to a sender, §14.2).
  Terminology note: the `receipt` **entry kind** is distinct from the §14 **acknowledgment** ("receipt")
  a *party* posts — an ack says "the counterparty got it"; a `receipt` entry says "here is what the
  delivery track did" (v0.5: `bounced`). §14 keeps the ack; the entry kind is always written `receipt`
  in code font.

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.

The **sessions** primitive (§16) and the **inter-agent leg** (v0.5) are likewise OPTIONAL to offer and to
consume, advertised via `sessions` and `inter_agent` (§8.0). A Hub that offers the inter-agent leg MUST
implement sessions, the §8.7 session-scoped drain and entry kinds, the §9.8 entry signatures, submit-time
destination validation (§8.1), and the §14 acknowledgment primitive **including the v0.5 terminal
delivery states** — the delivery-honesty rules ride the §14.2 track, so advertising `inter_agent` without
`ack` is non-conformant (§8.0). The leg is additionally **account-opt-in**: `inter_agent.enabled`
defaults false, and a Hub MUST NOT accept `to`-addressed submits for an account that has not opted in.

## 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.5)**
- 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.5.)
- **Streaming** partial agent output; multi-turn **threaded** asks. (Post-v0.5. The §8.7 SSE stream
  binding delivers mailbox entries; it does not stream partial output.)
- **Channel fan-out** (Slack/email/SMS delivery of an inbox item).
- **Human→agent `ask`/`task`.** The inbound leg remains deliberately scoped to the one-way **directive**
  (the `notify` mirror). v0.4 deferred the routed-agent-response mirror of §6 as "a materially larger
  design (agent-side resolution, an attested `agent:` actor on the return leg, a second signed leg)" —
  v0.5's inter-agent leg **builds exactly that leg** (§8.7 entry kinds, §9.8), so the future human→agent
  `ask`/`task` SCP shrinks to a routing rule. It is still deferred (§10).
- **Push-to-wake / scheduling.** Delivering a mailbox entry 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. *Agent
  liveness signaling*, a v0.4 non-goal, **moves in scope in v0.5** (§15, §16): the mailbox still never
  starts anyone — it now just refuses to lie about whether anyone is listening.
- **Cross-account anything.** The v0.5 inter-agent leg is **hub-mediated coordination among one
  account's agents** — not cross-account federation, not agent discovery, not an open agent mesh.
  Cross-account addressing is rejected as unknown (§8.1, §9.8).

### 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),
- (inter-agent leg, v0.5) any **un-acked mailbox entry of any kind** (§8.7 — `message`, `response`, and
  `receipt` entries carry the same drainable-until-acked-or-terminal guarantee as directives), any
  **active session lease** (§16 — a session reaches `closed` or `expired` only by its two defined
  transitions; a restart MUST NOT silently forget a live lease, which would fire the §14.2 bounce rules
  as an undocumented third terminal path), and any **pending bounce obligation** (the §14.2 transitions
  and their auto-resolutions applied when a session went terminal; re-applied after restart while
  incomplete).

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.5/message.schema.json`.

| Field | Type | Req | Notes |
|-------|------|-----|-------|
| `ma2h_version` | string | MUST | `"0.5"` (a 0.3/0.4 agent still sends its version — 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). |
| `to` | string | MAY | **v0.5.** Destination address: `agent:<agent.id>` (principal-addressed) or `agent:<agent.id>#<session.id>` (session-addressed). **Absent `to` = the human inbox, unchanged**; present `to` routes the message to the destination's §8.7 mailbox as a `message` entry. `to: human:...` is invalid in 0.5 (multi-human routing remains a non-goal, §2). Grammar and validation below. |
| `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`.

**`to` grammar (v0.5, pinned).** The `#` separator is chosen because `:` already splits `<type>:<id>`
and agent ids may contain `/`. Within `to`, the **first `#` terminates the agent-id segment**; the
session segment MUST match `^sess_`. An agent id containing `#` is therefore inexpressible in `to`
(its own first `#` parses as the separator and the remainder fails `^sess_`) — and such an id cannot
register sessions either (§16) — so the principal and session forms parse unambiguously.

**Submit-time destination validation (MUST, v0.5).** The Hub MUST validate `to` at accept time, within
the submitter's account: unknown `agent.id` → `422 unknown_destination`; session unknown → `422`;
session terminal → `410 destination_gone` (§8.5). Accepting an unroutable destination and silently
dead-lettering the message is exactly the false-belief failure the v0.5 delivery-honesty rules exist to
kill (§14). The same validation applies **retroactively** to the §13 directive `to` (v0.4 left it
unstated; see §13.2). Cross-account addressing MUST be rejected identically to unknown — no existence
oracle. When the submitting agent lacks session/presence visibility for the destination (§15, §16
policy), the terminal-vs-unknown split MUST collapse to `422 unknown_destination` for that sender — the
error split must not become a session-state oracle the visibility policy denies.

**Sender-side symmetry (MUST).** An addressed (or `agent.session`-bearing) submit from a principal
whose own `agent.id` contains `#` MUST be rejected `422 invalid_field`: the Hub would have to attest a
`from` address the first-`#` grammar cannot represent (§8.7). A `#`-bearing id therefore cannot
participate in the inter-agent leg on **either** end — it cannot be addressed, cannot register
sessions (§16.1), and cannot send addressed mail — while its ordinary human-inbox traffic is
unaffected.

An addressed message is accepted with its owning track's status — `queued` for a `notify` (never
`delivered`), `open` for an `ask`/`task` — plus the REQUIRED `destination` snapshot; its mailbox life
is the §14.2 delivery track (§5.1, §8.1). Routing, entry delivery, and the resolver default for
addressed `ask`/`task` are specified in §8.7, §5.2, and §6; the entry signatures in §9.8. The leg is
account-opt-in (§1, §8.0).

### 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
  "session": "sess_01H...",       // v0.5, optional: this invocation's registered session (§16)
  "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).

**`session` (v0.5, optional).** A `sess_` id naming one of the authenticated principal's **own**
registered sessions (§16): a valid-but-foreign-or-unknown session → `422`; the caller's
own-but-**terminal** session → `410 destination_gone` (mirroring the §4 rule for `to` — unless the
terminal was the §16.4 operator kill-switch, in which case the code is `session_closed_by_operator`,
§16.3). When present,
the Caller's mailbox for Response delivery is that session (§6), and the Hub-attested `from` on the
message's delivered form is session-qualified (§8.7) — which is also the discovery mechanism: a
session-registered agent participating in the inter-agent leg SHOULD include `agent.session` on its
submits, so its traffic carries a usable session-qualified address (§16). `run_id` is unchanged (audit
label; still non-authorizing). `agent.session` and `agent.run_id` are **excluded** from the §8.1
idempotency identical-payload comparison (§8.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 **human-inbox** (`to`-absent) `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.

**Addressed `notify` (v0.5).** The delivered-on-acceptance rule applies ONLY to the human-inbox path.
An **addressed** `notify` is accepted with `202 { "status": "queued" }` (§8.1): it is parked in a
mailbox and may still bounce or expire unseen, so an ack asserting `delivered`
at accept time would itself be the false belief v0.5 bans (§14). An addressed `notify`'s lifecycle IS
the §14.2 delivery track (`queued → delivered | bounced | expired`); addressed `ask`/`task` keep the §7
`open →` resolution machinery in `status`, exactly as today (`open` asserts nothing about delivery and
was never the lie), with the outbound mailbox track carried by the §8.2 `mailbox` object. On **every**
addressed ack, of any verb, the addressed marker and misroute detector is the REQUIRED `destination`
snapshot (§8.1) — never the `status` value.

### 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;
                               // for an ADDRESSED message the default is the addressee, §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.5/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).

**Resolution by an agent (v0.5, addressed messages).** The addressee of an addressed `ask`/`task`
resolves it through the resolve binding (§8.8) with its **agent credential**; the Hub attests `actor`
as the session-qualified agent actor (`agent:<id>` or `agent:<id>#<sess>`, per the §8.8 session
presentation). This was already
half-blessed: §2.1 permits `actor = agent:<agent.id>`. `request`/`action` blocks, modes, `expires_at`,
`default_on_expire`, idempotency (§8.1 scope unchanged; see §4.1 for the session/run exclusion), cancel
(§8.4, submitter-bound), and the §7 CAS lifecycle apply **unchanged**. The resolver default for
addressed messages is the **addressee** (§9.1).

**Response delivery to the submitting session (v0.5).** When the Caller registered a session
(`agent.session`, §4.1) and that session is **live at resolution time**, the Hub additionally delivers
the Response to that session's mailbox as a **`response` entry** (§8.7) — the return leg for an
agent-addressed `ask`/`task`, and available to any session-registered Caller in place of a webhook:
ephemeral agents get push-grade latency over one outbound connection with zero callback infrastructure
(§9.4's SSRF surface included). If the Caller registered no session, or its session is terminal by
resolution time, delivery falls back to the unchanged v0.4 path — §8.2 pull (authoritative) and any
§8.3 callback — with **no bounce emitted** (no session ⇒ pure v0.4 return path). Dual-path delivery is
safe: every channel carries the same `resolution_id` and the agent already dedups on
`(in_reply_to, resolution_id)`. The entry is signed per §9.8.

## 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.)
- **Undeliverable auto-resolution (v0.5, addressed messages).** An **addressed** message additionally
  carries
  the §14.2 delivery track (an addressed `notify`'s lifecycle IS that track, §5.1). When an addressed
  `ask`/`task`'s delivery track reaches **either undeliverable terminal** — `bounced` (§14.2) **or**
  `expired` (mailbox retention lapsed while queued: the resolver-facing entry is gone and no addressee
  can ever see it, so leaving the resolution `open` would strand the sender forever) — the **ask
  auto-resolves `cancelled`** and the **task
  `dismissed`**, with attested `response.actor: "system:undeliverable"` — no new resolution values, and
  the sender unblocks instead of waiting on a corpse. This transition rides the same atomic
  first-terminal-wins CAS: it is a harmless no-op returning the existing Response when the addressee
  already resolved before dying (or when the message-level `expires_at` already fired the §7 expiry).

## 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.5/capability.schema.json`) so an agent can configure
itself without out-of-band knowledge:

```json
{
  "ma2h_version": "0.5",
  "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,
    "session_param": true,
    "stream_url": "https://hub.example/v1/inbox/stream",
    "stream_max_hold_seconds": 60
  },
  "ack": { "enabled": true, "signature_algs": ["hmac-sha256", "ed25519"] },
  "presence": { "enabled": true, "freshness_seconds": 90 },
  "sessions": {
    "enabled": true,
    "max_ttl_seconds": 3600,
    "max_live_per_agent": 16,
    "agent_list_visibility": false,
    "terminal_retention_seconds": 86400,
    "operator_hard_stop": true
  },
  "inter_agent": {
    "enabled": true,
    "entry_kinds": ["message", "response", "receipt"],
    "sender_allowlists": true
  }
}
```

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.

**v0.5 additions.** `inbound` gains `session_param` (true iff the drain accepts `?session=`, §8.7) and
the OPTIONAL stream binding (`stream_url`, `stream_max_hold_seconds` — which MUST be ≤ the presence
freshness window, §8.7). The `sessions` object (§16) advertises the session primitive: `max_ttl_seconds`
(the clamp bound), `max_live_per_agent` (over-cap registration → `429`), `agent_list_visibility`
(the **deployment ceiling** on whether same-account **agents** may list/read account sessions: a Hub
MAY narrow it per account beneath the advertised value and MUST NOT widen beyond it, so `false`
denies every account while `true` advertises only that an account MAY hold the grant — never that a
given caller does, which is what `sessionList.scope` answers, §16.4; own-session visibility is
unconditional regardless), and `terminal_retention_seconds` (how long a terminal session stays
readable). `operator_hard_stop` (OPTIONAL, default false when absent) advertises the §16.4.1 durable
stop: `true` means an operator close survives terminal-session purge and gates re-registration until
an authenticated human lifts it. A client MUST NOT infer a hard stop from its absence — absent means
the kill-switch is cooperative, and a supervisor that blanket-restarts will re-register through it. The `inter_agent` object advertises the inter-agent leg: `enabled` (**default false — the
leg is account-opt-in**; enabling it is an explicit account-owner act, and a Hub MUST NOT accept
`to`-addressed submits for an account that has not opted in), `entry_kinds`
(`["message", "response", "receipt"]`, §8.7), and `sender_allowlists` (Hub support for per-destination
sender restriction is REQUIRED of a conformant Hub offering the leg; whether any allowlist is
configured is per-account policy, §13.5). A submit blocked by a destination's allowlist MUST be
rejected `422 unknown_destination`, indistinguishable from a nonexistent destination (§8.5) — the
allowlist must not become a sender-visible probe of who may reach whom, exactly the §4 collapse rule
for the visibility policy. **`inter_agent` requires the §14 primitive:** the v0.5
delivery-honesty states ride the §14.2 track, so a Hub advertising `inter_agent` MUST implement `ack`
(§14) including the v0.5 terminal states — advertising `inter_agent` without `ack` is non-conformant.
A 0.5 sender MUST feature-detect via this document before using `to`, and MUST additionally apply the
§8.1 ack misroute check (capability caching alone is TOCTOU across rolling deploys and rollbacks).

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

Body: a message envelope (§4–§5). On success the Hub returns **`202 Accepted`** (schema
`schema/v0.5/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 a human-inbox `notify` the ack carries `status: "delivered"`. For an **addressed** (`to`-carrying)
message the ack carries the owning track's status — `"queued"` for a `notify` (§5.1), `"open"` for an
`ask`/`task` (the §7 resolution track, exactly as unaddressed) — plus, for **every** addressed verb,
the REQUIRED `destination` snapshot below. **The replay rule is thereby unambiguous:** a same-key
idempotent replay (below) returns the current status of the verb's **owning** track — the §7
resolution track for an `ask`/`task` (`open`, then its terminal — a bounce reads `cancelled`/
`dismissed` by `system:undeliverable`, §7), the delivery track for an addressed `notify` (`queued`,
then `delivered`/`bounced`/`expired`) — never a mixture, so independent implementations agree on
every replay.

**Reachability snapshot on the addressed ack (v0.5, MUST).** The ack for an addressed message MUST carry
the destination's current §15 reachability, so a sender (and its human) knows *at send time* whether
this will be picked up promptly or is parked in a durable mailbox:

```json
{ "id": "msg_01H...", "status": "queued", "poll_url": "https://hub.example/v1/messages/msg_01H...",
  "destination": { "state": "offline", "last_seen": "2026-08-10T11:20:00Z" } }
```

When the sender lacks visibility for the destination (§16 policy), the object MUST be exactly
`{ "state": "unknown" }` with no `last_seen` — still honest ("no promise of prompt pickup"), never an
ungated presence oracle.

**The ack is also the version-misroute detector (sender MUST, v0.5).** Capability feature-detection is
time-of-check/time-of-use across rolling deploys and rollbacks: a mixed-version endpoint or a cached
capability document can put a `to`-carrying envelope in front of a pre-0.5 Hub, which ignores the
unknown field (§10 robustness) and misroutes the message to the **human inbox**. A pre-0.5 Hub cannot
emit the `destination` object, so: a 0.5 sender MUST treat an addressed-submit ack **without**
`destination` as a misroute, MUST surface the failure to its caller, and SHOULD cancel a misrouted
`ask` via §8.4. (Feature-detection via §8.0 before first use remains required; the ack check closes the
deployment-window gap.)

**`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. **Scope clarification (v0.5, unchanged scope):**
`agent.session` and `agent.run_id` are **excluded** from the identical-payload comparison — a
crashed-and-restarted agent retrying with the same key from a new session/run MUST receive the original
ack (the recovery recipe above exists for exactly this), and the **original** submit's session remains
the bound Caller for Response routing (first submit wins, §6).

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

Returns the message plus its current `status` and, once terminal, the embedded `response` (schema
`schema/v0.5/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).
For an **addressed** message (v0.5) this same GET — submitter-bound as ever — additionally carries a
**`mailbox`** object: the **sender's authoritative view of the outbound mailbox track**
(`queued → delivered → acknowledged`, or terminal `bounced`/`expired`, §14.2 — did the *addressee*
see it; on `bounced`, a present `delivered_at` means seen-then-orphaned). Receipts (§8.7) are
best-effort; this pull is the source of truth. The two objects stay track-true: `delivery` is always
the response track (ask/task — did the *submitter* get the answer), `mailbox` always the outbound
track (addressed messages only; for an addressed `notify` it IS the lifecycle and `status` mirrors
it, §5.1).

### 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` / `session_closed_by_operator` | credential ↔ `agent.id` mismatch; resolver not in `allowed_resolvers`; **v0.5:** on a Hub advertising `sessions.operator_hard_stop`, a session registration by a principal an operator has stopped (§16.4.1) — same `code` as the 410 because the client's action is identical (stop; do not re-register), different status because no session resource is gone |
| 404 | `not_found` | unknown message id |
| 409 | `idempotency_conflict` / `already_terminal` / `not_acknowledgeable` | replay with differing payload; cancel/resolve after terminal; §14.3 response-leg ack of a non-terminal message or of a response not yet `delivered-to-agent` (§14.2) |
| 410 | `gone` / `destination_gone` / `session_closed_by_operator` | message deleted (vs 404 unknown); **v0.5:** `to` or `agent.session` names an own-but-**terminal** session (§4, §16). `session_closed_by_operator` is emitted at a caller's **own**-session touchpoints when the session was closed by the account's human operator (the §16.4 kill-switch; touchpoint split in §16.3) — the client's action is **stop**: do not auto-re-register; surface to a human |
| 422 | `invalid_field` / `unknown_destination` | e.g., `expires_at` in the past; `default_on_expire` ∉ options; **v0.5:** `to` names an unknown (or cross-account, indistinguishably) destination, the session-state split is collapsed for a sender without visibility (§4), or the destination's **sender allowlist** blocks the submitter (§8.0) — a policy block MUST be indistinguishable from a nonexistent destination |
| 429 | `rate_limited` | over a published limit; includes `Retry-After` |

**Unknown-code fallback (MUST, v0.5).** Within a recognized status class, a client MUST treat an
unrecognized `code` as the **code that touchpoint would return absent the refinement** — additive
codes refine a class, never replace it. Where a class has more than one base code, the touchpoint
disambiguates: an unrecognized `410` at an own-session presentation touchpoint reads as `gone`, at a
destination-addressing touchpoint as `destination_gone` (§16.3), and an unrecognized `422` as
`invalid_field`, and so on.
This is fail-open by design: a stop-semantics refinement degrades to the class's base action for
consumers that predate it — exactly as `session_closed_by_operator` does for pre-marker bridges
(§16.4) — in exchange for never crashing or stalling old consumers on additive codes.

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`.

**v0.5 notes.** Session registration and addressed submits ride the same per-`agent.id` limits (plus
the `sessions.max_live_per_agent` cap, §16); mailbox depth caps bound queue abuse for addressed mail as
they do for directives. Inter-agent chatter and the human leg share one per-`agent.id` budget — a
chatty coordinator can starve its own human-facing asks — so Hubs MAY advertise separate inter-agent
quotas (`rate_limit.inter_agent_requests_per_minute`, §8.0: a distinct budget for addressed submits;
absent means addressed submits share `requests_per_minute`). Ask-loops between agents (A asks B asks A)
are bounded by the same quotas; explicit hop-limits
are deliberately deferred (§10 roadmap).

### 8.7 Inbox — the agent mailbox (v0.4 inbound leg; v0.5 inter-agent leg)

The transport for the human→agent leg (§13) and — as of v0.5 — the inter-agent leg. 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).

**Session presentation on the ack (v0.5).** The ack MAY present the caller's session —
`POST /v1/inbox/ack?session=sess_...` — under exactly the §8.7.1 drain rules (own live session, else
foreign/unknown → `404`, own-terminal → `410`). Session presentation affects **lease attribution
only**, never ack authorization (which stays credential-scoped): an ack presenting a session is
client-originated activity that renews *that* session's lease and presence (§15.1, §16.2); an ack
without one renews no session lease. A streaming bridge SHOULD ack with `?session=` — during a held
stream (§8.7.2), acks may be its only client-originated traffic, and a session-less ack from a
multi-session principal gives the Hub no honest way to know *which* session is alive.

**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).

#### 8.7.1 Session-scoped drain and the v0.5 entry kinds

**Drain opt-in (`?session=`).** `GET /v1/inbox?session=sess_...` returns, in addition to v0.4
directives: **session-addressed** entries for that session, **principal-addressed** entries
(first-claim-wins, below), and `response`/`receipt` entries for that session. The presented session
MUST be the caller's own: a foreign or unknown session → `404` (indistinguishable, §9.1 stance); the
caller's **own-but-terminal** session → `410` — distinct by design, so a bridge returning from a lease
lapse learns "re-register and continue" (`error.code: "gone"`) instead of treating its own address as
nonexistent (§16). When the session was closed by the account's operator, the 410 instead carries
`session_closed_by_operator` — "stop; do not re-register" (§16.3/§16.4). **A
session-less drain returns exactly the v0.4 shape** — a 0.4 consumer never sees an unknown entry shape,
back-compat by construction. Ack (`POST /v1/inbox/ack`) is unchanged and covers **all entry kinds**.

**The three v0.5 entry kinds** (enumerated in `inter_agent.entry_kinds`, §8.0; delivered **only** to
session-presenting drains; each signed per §9.8 and re-signed with a fresh `t`/`jti` at every delivery,
exactly as §9.7):

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

- **`message`** — an addressed §4 envelope (agent-authored `notify`/`ask`/`task`) in its **delivered
  form**: the submitted envelope plus Hub-assigned `id` and **Hub-attested `from`** (`agent:<id>`, or
  `agent:<id>#<sess>` when the submit carried `agent.session`) — never request-supplied, exactly the
  §13.1 attestation rule. The Hub MUST strip submitter-side machinery before delivery: `state` (the
  submitter's opaque resume blob, §9.3 — round-tripped only in the §6 Response), `client_ref` (§9.1
  already forbids exposing it to resolvers), and any `request.callback`/`action.callback` (the
  submitter's delivery config). `idempotency_key` is delivered (inert to the addressee). The `agent`
  descriptor is delivered but **advisory** (unbound, mirroring §9.2's top-level `agent`); the attested
  identity is `from`. The recipient's duties are §13.4 applied wholesale, plus the v0.5 amendments
  (§13.4).
- **`response`** — a §6 Response delivered to the submitting session's mailbox (§6). The body is
  byte-for-byte the §6 shape; the destination is bound in the §9.8 signed context only.
- **`receipt`** — a Hub-originated delivery-status notification to a **sender**; v0.5 uses it for the
  §14.2 bounce. Fields: `ma2h_version`, `type: "receipt"`, `id` (Hub-assigned `rcpt_` id — the entry's
  ack key, below), `in_reply_to` (the affected entry/message
  id — with `event`, its dedup key), `event` (`bounced` in v0.5), `prior` (`queued` | `delivered` —
  preserves the never-seen vs. seen-then-orphaned distinction, §14.2), `at`, `session` (the terminal
  destination session), and `to` (the sender session it is delivered to). Receipts are **best-effort,
  at-most-once-meaningful** (the recipient dedups on `(in_reply_to, event)`), are delivered only to
  **live sessions of the sender**, and MUST NOT themselves generate receipts — the sender's §8.2 pull
  remains authoritative.

**Ack keys (pinned per kind).** `POST /v1/inbox/ack` takes an `ids` array covering all entry kinds; an
entry's **ack key** is: a directive's `id`; a `message` entry's `id`; a `response` entry's
**`resolution_id`** (the §6 body carries no mailbox id — `res_` ids are Hub-assigned, globally unique,
and non-colliding with `dir_`/`msg_` keys); a `receipt` entry's `id` (`rcpt_`). Every ack key is
**signature-bound** in its kind's context (§9.7/§9.8), so a consumer never acks on an unauthenticated
identifier. The id namespaces are **normative for v0.5-minted ids** (schema-enforced on ≥ 0.5
payloads): a Hub MUST mint directive
ids in `dir_`, message ids in `msg_`, resolution ids in `res_`, receipt ids in `rcpt_` (and session
ids in `sess_`, §16.1) — disjointness is what keeps the shared `ids` array unambiguous, not
convention. Ids minted before v0.5 are grandfathered (an upgrade never strands queued mail or breaks
a stored Response); the Hub remains responsible for ack-key uniqueness across every entry it
delivers, whatever their vintage.

**What an inbox ack does, per kind (pinned).** Acking always consumes the entry (removal + no more
redelivery). Its **track** effect (§14.2/§14.3):

- **directive** — unchanged v0.4: consuming IS acknowledging (§14.3); the directive track goes
  `acknowledged` (optional `note` carried).
- **`message` entry** — the §14.3 fold applies wholesale: the ack advances the addressed message's
  mailbox track to `acknowledged` (optional `note`), which is what the sender's authoritative §8.2
  view reports.
- **`response` entry** — the ack is **equivalent to a note-less `POST /v1/messages/{id}/ack`**
  (§14.3's response-leg ack; the drainer here *is* the submitting principal): it advances the ask/task
  response track to `acknowledged` under the same first-ack-wins rule (§14.1 — whichever ack path
  lands first wins, the other is the standard no-op). The drain that delivered the entry already
  stamped `delivered-to-agent` (client-originated delivery evidence, §15.1; a stream push did not,
  §8.7.2).
- **`receipt` entry** — consume only; receipts have no track and generate nothing (§8.7.1). Keys therefore stay non-colliding across kinds by their prefixes — in particular, when an addressed
ask bounces, the sender's session can hold the auto-cancellation `response` entry and the bounce
`receipt` for the **same** `in_reply_to` concurrently, and their ack keys (`res_…`, `rcpt_…`) still
address exactly one entry each. Ack semantics are otherwise unchanged (idempotent; foreign/unknown ids
are no-ops that reveal nothing, §8.7).

The delivered payloads all validate against `schema/v0.5/inbound-message.schema.json`; a consuming
agent MUST validate shape before acting (§13.4 — the payload digests bind only content fields, so shape
validation is what rejects unsigned injected fields such as a `state` blob on a `message` entry).

**Delivery channels for the v0.5 entry kinds.** The pre-registered §8.7 **webhook remains
directives-only in v0.5**: `message`, `response`, and `receipt` entries are delivered by the
session-scoped drain and the OPTIONAL stream (§8.7.2) only. (Webhook delivery for the v0.5 entry kinds
is roadmap, §10 — a session, unlike a principal, is ephemeral, and the §9.4 registration model is
per-principal.)

**Routing and claim semantics.** An addressed message is enqueued in the destination mailbox rather
than presented to the human inbox. **Session-addressed** entries are visible only to drains presenting
that session. **Principal-addressed** entries are visible to any **session-presenting** drain by the
destination principal — **first-claim-wins** under the existing visibility-timeout semantics above.
Principal addressing is **role delivery**: a sender that intends a specific invocation SHOULD
session-address, and multiple concurrent sessions draining principal-addressed mail is the documented
**degraded mode** of the shared-credential setup (proper setup: per-session self-registration, with
tooling auto-registering; a claimant session that dies mid-processing is rescued by ordinary
visibility-timeout redelivery to a sibling — which is why the §14.2 bounce rules are scoped to
**session-addressed** mail). Directives likewise MAY be session-addressed (§13.2).

**Human visibility.** Addressed messages default **out of the human triage inbox** (they are fleet
traffic, not human work) but MUST remain readable/auditable by the account's authenticated human —
same posture as §15.3 (the operator owns the account; agent↔agent traffic is not confidential from
them). The presentation is product, not protocol.

#### 8.7.2 Stream — OPTIONAL SSE binding (v0.5)

A Hub MAY offer `inbound.stream_url` (§8.0): the same authenticated, session-scoped entries as the
drain, pushed as Server-Sent Events over a held connection; acks remain explicit POSTs.

- **Liveness requires client-originated proof — a merely-open socket is not evidence.** SSE is
  unidirectional and TCP half-open: the Hub cannot distinguish a live client from a crashed process
  behind a NAT whose socket still accepts buffered writes, so an open connection MUST NOT by itself
  renew the session lease (§16) or presence (§15). Instead: the Hub MUST bound each stream hold and
  close the stream at the bound; the client's **reconnect is the renewal** (standard SSE
  auto-reconnect makes this free for healthy clients), alongside the other client-originated activity
  in §16. The bound for a given hold is the **lesser of** the advertised
  `inbound.stream_max_hold_seconds` (which MUST be ≤ the presence freshness window) **and a point
  sufficiently before the presenting session's lease expiry that a standard immediate reconnect lands
  within the lease** — the margin is implementation-defined but MUST be nonzero: closing exactly *at*
  expiry hands the client a renewal moment it can only use after the lease is already immutably
  terminal, so a session whose TTL is shorter than the advertised hold would still lapse (bouncing its
  mail, §14.2) under a healthy-but-idle open stream. A Hub MAY additionally (or instead) clamp a
  requested `ttl_seconds` **up** to an advertised minimum compatible with its stream holds
  (`sessions.min_ttl_seconds`, §8.0, §16.1). A dead
  client never reconnects, its lease lapses within one freshness window, and the §14.2 bounce fires —
  the zombie socket cannot hold a session `online`; a healthy one is always handed its renewal moment
  before the lease can lapse.
- **Stream delivery is provisional.** A stream push starts the entry's visibility window exactly like a
  drain claim, but only **client-originated receipt evidence advances the delivery track** (§15): a
  drain response (client-requested) stamps `delivered`; a server-originated stream push does not — a
  stream-delivered entry advances on its **ack** (or a later drain) and reverts to queued-visible after
  the visibility window otherwise. At-least-once is preserved; a push into a dead socket changes
  nothing the sender is told.
- **Long-poll drain remains the conformance floor** — a pull-only Hub is conformant; the stream is
  advertised capability, not a requirement.

### 8.8 Resolve — `POST /v1/messages/{id}/resolve` (v0.5)

The wire binding for resolving an open `ask`/`task` — the §14.3 resource-scoped sub-action the spec
had so far only named. v0.4 never needed it interoperable: the only resolver was the account's human,
acting through the Hub's own product surface (§2 non-goals), with §2.1's agent-resolver latent. The
inter-agent leg makes it load-bearing — an addressed message's addressee (§6) resolves over the wire —
so v0.5 pins it. (A Hub's human-facing surface MAY use the same binding internally; that remains
product.)

Request body (schema `schema/v0.5/resolve-request.schema.json`):

```json
{ "resolution": "answered", "value": "approve", "comment": "Restarting now." }
```

| Field | Type | Req | Notes |
|-------|------|-----|-------|
| `resolution` | enum | MUST | The terminal the resolver asserts: `answered` \| `declined` (ask) or `completed` \| `dismissed` (task). `cancelled` is §8.4's verb (submitter-bound); `expired` is Hub-only (§7). |
| `value` | string \| object | see notes | REQUIRED iff `resolution: "answered"` — the chosen option value (`select`/`confirm`, a string member of `options[].value`) or the input object validating against `request.schema` (`input`), per §5.2. MUST NOT appear for any other resolution. |
| `comment` | string | MAY | Carried into `response.comment` (§6). |
| `checklist` | array | MAY | Task resolutions only: the final §5.3 checklist state, carried into the Response. MUST NOT appear for ask resolutions. |

- **Authentication & authorization.** The caller authenticates per §9.1 (agent credential, or the
  Hub's authenticated operator session for a human resolver) and MUST satisfy the message's
  `allowed_resolvers` (§9.1, including the v0.5 addressed-message default and the session-qualified
  matching rules). An unauthorized or unbound caller gets `404`/`403` per §9.1/§8.5.
- **Session presentation.** An agent resolver MAY present its session as `?session=sess_...`, exactly
  the §8.7.1 drain rule (own live session, else foreign/unknown → `404`, own-terminal → `410`); the
  Hub then attests the session-qualified `actor` (§6). Presenting a session is how a resolver
  satisfies a **session-qualified** `allowed_resolvers` entry (§9.1); absent the parameter the
  attested actor is the bare principal.
- **Validation & lifecycle.** The Hub validates `resolution` against the verb and `value` against the
  `request` block (`422 invalid_field` otherwise), then applies the §7 atomic CAS: the first terminal
  wins — a resolve arriving after a terminal is a no-op returning `409 already_terminal` with the
  existing `{ id, status, resolution }` (the caller reads the real outcome, as §8.4). On success:
  `200 { "id", "status": "<resolution>", "resolution_id": "res_..." }`, and the Hub emits the §6
  Response through every configured channel (push, pull, and — for a live submitting session — the
  §8.7 `response` entry).

## 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.
- **Resolver default for ADDRESSED messages (v0.5).** For a `to`-carrying message the
  `allowed_resolvers` **default changes from the submitting principal to the addressee**: absent list ⇒
  only the `to` principal (any of its sessions) may resolve — still fail-closed, and the submitter of
  an ask is the one party who must *not* answer it. The account's human is deliberately **NOT** a
  default resolver for agent-addressed messages (list them in `allowed_resolvers` to opt in): the
  operator's unstick recourse is the §16 session **kill-switch** — closing a wedged addressee's session
  bounces its queued mail and auto-resolves its pending asks (§7, §14.2) without the human impersonating
  an agent decision.
- **Matching agent resolvers against session-qualified actors (v0.5, pinned).** The attested resolving
  `actor` for an agent is session-qualified when a session was presented (§6), so an
  `allowed_resolvers` entry matches as follows: a **principal-form** entry `agent:<id>` matches that
  principal acting under **any** session (the actor's session qualifier is ignored for the
  comparison); a **session-qualified** entry `agent:<id>#<session>` matches only that exact session —
  which is how a submitter restricts resolution to one specific invocation. `human:`/`system:` entries
  match exactly, as today. The addressee default above behaves like the principal-form entry.
- **Session authorization (v0.5).** Session registration, renewal, close, and visibility rules are in
  §16; the same submitter-binding stance applies (another principal's session id MUST be
  indistinguishable from unknown).

### 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)>
```

**`v1` well-formedness (v0.5).** A verifier MUST accept a `v1` iff it is base64url
(`A–Z a–z 0–9 - _`), carries either no padding or exactly the RFC 4648 structural pad count for its
length, and decodes to at least 32 bytes — and MUST NOT impose more (in particular, no canonical
round-trip check: a value that happens to look hex-shaped is valid base64url). One shared rule kills
the two-implementations-drift class where a hand-rolled second validator rejects conformant traffic;
the reference exports it as `decodeMac`/`isWellFormedMac`, and `dp-025` pins the accept/reject
boundary executably.

**`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).

### 9.8 Inter-agent entry integrity (v0.5)

The v0.5 entry kinds (§8.7) are delivered from the same unsolicited mailbox surface as directives, so
each carries a detached signature the consumer MUST verify before acting — the §9.7 discipline applied
to three new contexts. Everything structural is inherited: RFC 8785 JCS serialization of the canonical
context (§9.2), the `MA2H-Signature: t=<unix>,jti=<nonce>,v1=<base64url(signature)>` wire header,
`hmac-sha256`/`ed25519` per the advertised `inbound.signature_algs`, digests **recomputed by the
verifier from the payload it actually received** (never trusted from the wire), lowercase-hex SHA-256
digests over a **fixed-key** JCS wrapper, **per-delivery re-signing with a fresh `t`/`jti`** (an entry
may rest in a mailbox arbitrarily long, §9.7 — only `t`/`jti` change per delivery), the ±replay-window
`t` check and `jti` cache (TTL ≥ window), and key rotation with overlap (§9.7). New signed contexts
belong to new features: no existing context changes, and the §10 push-parity threshold is untouched.

**`message` entry** — the inter-agent mirror of §9.7, same key set:

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

- **`payload_sha256`** is the SHA-256 of the JCS serialization of the fixed-key object
  `{ "message": <content> }`, where `<content>` carries exactly the delivered envelope's **present**
  fields among `type`, `title`, `body`, `priority`, `tags`, `context`, `request`, `action`,
  `sensitive` — the
  instruction surface the addressee consumes. `type` is bound because flipping `notify`/`ask`/`task`
  materially changes what the recipient does; `request`/`action` are bound because an ask's `request`
  block enters an LLM recipient's context exactly as a `task`'s instructions do; `sensitive` is bound —
  unlike §9.7's directive digest — because on this leg the RECIPIENT acts on the marker (§9.6 handling)
  and a flipped marker would change how sensitive content is exported. Numbers canonicalize
  per RFC 8785 §3.2.2.3 exactly as in §9.2.
- **`from`**, **`id`**, and **`to`** are bound as top-level fields, with §9.7's exact semantics: `from`
  authenticates the attested submitter (session-qualified when the submit carried `agent.session`),
  `id` is the at-most-once key, `to` binds the addressed destination against cross-agent replay — and
  the recipient still checks `to` against its own identity (§13.4), because a validly-signed entry
  re-routed unmodified verifies.
- **Unbound metadata (advisory).** `created_at` and `expires_at` are Hub-authoritative advisory exactly
  as in §9.7 (the Hub enforces expiry and drops expired entries before delivery; `sensitive` — advisory
  in §9.7 — is bound here, above). The `agent`
  descriptor is advisory exactly as §9.2 treats the Response's top-level `agent` — a recipient MUST NOT
  use it for authorization or identity; the attested identity is the bound `from`. `idempotency_key`
  is likewise unbound (inert submitter machinery, §8.7).

**`response` entry** — §9.2's context with the mailbox destination in place of the callback binding,
and the **identical** payload digest:

```
response_signed_context = { "id", "in_reply_to", "jti", "ma2h_version", "payload_sha256",
                            "resolution", "resolution_id", "resolved_at", "t", "to" }
```

- **`payload_sha256`** is computed exactly as §9.2 defines it: the SHA-256 of the JCS of the fixed-key
  object `{ "response": <the Response's response object, or null>, "state": <the Response's state, or
  null> }`. A verifier shares the §9.2 digest code path byte-for-byte.
- **`to`** replaces §9.2's `callback_url` as the delivery-destination binding: the session-qualified
  address of the submitting session the entry is delivered to (`agent:<id>#<sess>`). The §6 Response
  body is byte-for-byte unchanged (it carries no `to`); the verifier **reconstructs** `to` from its own
  presented drain identity — its `agent.id` and the session it presented — exactly as a §9.2 verifier
  reconstructs `callback_url` from its own endpoint. Cross-endpoint replay defense carries over: a
  response entry signed for one session fails verification replayed to any other.
- **`id` reconstruction (pinned).** `id` is the original message id — always **equal to the delivered
  Response's `in_reply_to`** (both name the message record, as in §9.2 practice, whose worked example
  and reference implementation have always set them identically). A verifier reconstructs `id` from
  the `in_reply_to` it received; the two fields are kept in the context for §9.2 key-set parity, not
  because they can differ.
- `resolution`, `resolution_id`, and `resolved_at` are bound top-level as in §9.2; the Response's
  top-level `defaulted` and routing `agent` object remain advisory exactly as §9.2 states.
- **`resolved_at` reconstruction (pinned).** §6 permits a task Response to omit the `response` detail
  entirely (nothing to carry), which would leave a verifier nothing to rebuild `resolved_at` from. For
  this context, `resolved_at` is the Response's `response.resolved_at` when the `response` object is
  present, and JSON **`null`** when it is absent — the same absent-optional-as-`null` convention as
  §14.4's digest wrapper — so the context is always reconstructible from exactly the delivered body.
  (The §9.2 push context predates this rule and leaves the response-less-task case unpinned; that
  pre-existing gap is tracked separately and is not changed by v0.5.)

**`receipt` entry** — the §14.4 ack pattern (a dedicated digest over a fixed-key wrapper with absent
optionals as `null`):

```
receipt_signed_context = { "in_reply_to", "jti", "ma2h_version", "receipt_sha256", "t", "to" }
```

- **`receipt_sha256`** is the SHA-256 of the JCS of the fixed-key object with exactly these six keys —
  `{ "at", "event", "id", "in_reply_to", "prior", "session" }` — where any absent member serializes as
  JSON `null` (all six are present in a v0.5 bounce receipt; the `null` convention matches §14.4's
  `ack_sha256` wrapper and keeps the digest unambiguous as future minors add events). The verifier
  MUST recompute it from the receipt it received using this same fixed-key/`null` wrapper.
- **`in_reply_to`** is bound top-level (the correlation + dedup key, with `event` inside the digest);
  **`to`** binds the sender session the receipt is delivered to, against cross-destination replay —
  the verifier **reconstructs** `to` from its own presented drain identity, exactly as the
  `response` context above (the received receipt's body `to` is routing convenience, not
  verification input); the
  receipt's `id` — its §8.7.1 ack key — is bound inside the digest, so the key a consumer acks is
  authenticated like every other kind's.
- Receipts drive **no** action (they update the sender's view of a delivery track the §8.2 pull states
  authoritatively, §8.7); verification is still a MUST — an unverified receipt could fabricate a bounce
  and trick a sender into abandoning a live ask.

Worked examples with deterministic fixture values for all three contexts:
[`examples/entry-signatures-v0.5.md`](../examples/entry-signatures-v0.5.md). The corresponding
conformance fixtures are the §12 signature obligations.

## 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).
- **v0.5 (additive, non-breaking).** v0.5 adds the **inter-agent leg** ([SCP #24](https://github.com/autnmy/ma2h-protocol/issues/24)):
  **sessions** (§16), the envelope **`to`** + `agent.session` (§4), three mailbox **entry kinds** on the
  §8.7 drain (`message`/`response`/`receipt`, session-presenting drains only), the §9.8 entry
  signatures, **delivery honesty** (§14.2 terminals `bounced`/`expired` + the `system:undeliverable`
  auto-resolution, §7), per-session **reachability** (§15), the session-scoped drain + OPTIONAL SSE
  stream (§8.7), and the `sessions`/`inter_agent` capability objects (§8.0). It changes **no** existing
  wire format: every v0.3/v0.4 leg is byte-for-byte unchanged; new `spec/v0.5.md` + `schema/v0.5/`
  (full snapshot; the v0.4 schemas re-`$id`'d, with the §10-listed extensions: `message` gains
  `to`/`agent.session`, `submit-ack` gains `queued`/`destination`, `get-message` gains the v0.5
  delivery states, `capability` gains the v0.5 objects, `inbound-message` becomes the four-kind entry
  union, `response` types the task checklist and pins the `res_` namespace, `session.schema.json` +
  `resolve-request.schema.json` added). Because the §9.8 contexts are **new** (like §9.7 before them),
  they interoperate only between a 0.5 Hub and a 0.5-aware agent — but a pre-0.5 agent never presents a
  session, never sees the new entry kinds, and nothing it already does breaks. The push-parity
  threshold stays anchored at the signature-break minor (**3**). Two deliberate behavior changes to
  name: (1) a 0.5 Hub rejects (`422`/`410`) directive submissions to unknown or terminal destinations
  that a 0.4 Hub accepted and silently dead-lettered (§4's retroactive validation, §13.2) — failing
  submissions fail louder, which is the point; (2) a 0.5 **sender** MUST feature-detect via §8.0 before
  using `to` and MUST treat an addressed-submit ack lacking `destination` as a pre-0.5 misroute (§8.1).
  The leg is account-opt-in (`inter_agent.enabled` defaults false, §8.0).

**Roadmap (out of v0.5):** human SSO mechanics; assignment/escalation/SLA routing; **human→agent `ask`/`task`**
(now just a routing rule on the §9.8 attested `agent:` return leg, §2); multi-turn threads; streaming
(AG-UI interop); channel fan-out; per-session derived tokens (§9.6-family limitation, §16); explicit
inter-agent hop-limits (§8.6); scoped
session-visibility/role grants; queue groups for principal-addressed load-balancing; a Hub-edge A2A
gateway; webhook delivery for the v0.5 entry kinds (the §8.7 webhook stays directives-only, §8.7.1);
richer `receipt` events (delivery, read).

## 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. |
| **XMPP resource binding** (RFC 6120) | the session model (§16): bare JID vs. full JID ≈ principal (`agent:<id>`) vs. session-qualified address (`agent:<id>#<sess>`) — many ephemeral bound resources under one durable identity, addressable individually or by bare identity. |
| **SIP REGISTER contact bindings** (RFC 3261) | the session lease (§16): registration with `Expires`, renewal by re-registration (client-originated), and bindings that lapse when the client stops renewing — no server-side liveness guessing. |
| **NATS ephemeral consumers** | session GC (§16): a per-invocation consumer that exists only while its client is active and is garbage-collected by the server after inactivity — nothing to revoke, nothing evergreen to leak. |

## 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`).

The **inter-agent leg** (v0.5) adds vectors of all three classes. **Schema-validation** (`sv-017..043`,
executable now): the `to` grammar (valid principal/session forms; `human:` rejected; first-`#` split;
`#`-bearing agent ids inexpressible in `to` and rejected as addressed senders), `agent.session` shape,
version-gated addressing (an envelope carrying `to`/`agent.session` declares minor ≥ 5), the session
resource and register/read
shapes, the v0.5 capability objects, submit-ack `queued` + `destination` (required for the addressed
statuses; exactly-unknown; terminal replay statuses), the §8.8 resolve request, the four delivered
entry
kinds (including the strip rule — a `message` entry carrying `state` is invalid — and the ≥ 0.5 gates),
the §14.2 never-delivered conditionals on the GET body, and cross-type rules
intact. **Signature obligations** (deterministic fixtures per §9.8; worked in
`examples/entry-signatures-v0.5.md`): known input → known signature for `message`, `response`, and
`receipt` entries; fresh `t`/`jti` per delivery; tampered `from`/`to`/payload rejected; replayed `jti`
rejected. **Downstream proofs** (against a conformant Hub): session lease CAS (first terminal wins;
renewal races; the human kill-switch close); first-claim-wins under concurrent session-presenting
drains (exactly one delivery per visibility window) with crashed-claimant rescue via
visibility-timeout redelivery; **stream-liveness truthfulness** (a session held only by an open stream
with no client-originated activity past the freshness window reads `offline` and its lease lapses; a
healthy client reconnecting at the advertised bound stays `online` continuously; the advertised
`stream_max_hold_seconds` is ≤ `presence.freshness_seconds` — a cross-field relation the schema
dialect cannot compare, checked here);
**drain ownership** (`?session=` foreign/unknown → `404`; own-terminal → `410` — `gone` for
expired/self-closed, `session_closed_by_operator` for an operator close, §16.3);
**stream-delivery provisionality** (a stream-pushed, never-acked entry reverts to queued-visible after
the visibility window and its delivery track never left `queued`); bounce-on-terminal for **un-acked**
command entries including drained-but-unacked (the receipt `prior` distinguishing `queued` vs
`delivered`; the principal-addressed retention orphan bouncing `prior: "delivered"`; response/receipt
entries never bouncing), with ask auto-`cancelled`/task auto-`dismissed` as `system:undeliverable` on
**both** undeliverable terminals (bounce, and delivery-track expiry — no open-forever ask), receipts
deduped on
`(in_reply_to, event)` and never cascading; delivery-track truthfulness (`expired` ⇒ never delivered,
on the mailbox track AND the ask/task response track; no `online` without qualifying activity);
**addressed-ack honesty** (an addressed notify is acked `status: "queued"`, `delivered` only via the
track; an addressed ask/task is acked `open`; **every** addressed ack — the `open` ones included,
which the closed schema cannot discriminate — carries the `destination` snapshot);
submit-time destination validation (unknown/terminal/cross-account rejected — no silent dead-letter)
with policy-tied ack detail (a no-visibility sender gets `{"state": "unknown"}` and the collapsed
`422`); the **misroute detector** (an addressed submit acked without `destination` is surfaced as a
failure by a conformant sender, and an ask is cancelled); idempotent replay from a new session/run
returning the original ack with the original session as the bound Caller; the addressee-only resolver
default (the submitter's resolve rejected; the account human rejected absent listing); the
session-qualified addressee check on the recipient (§13.4); the attested `from` on a delivered
`message` entry carrying exactly the submitted `agent.session` qualifier (a cross-field equality the
schema cannot compare); and 0.4 session-less drain isolation (a
session-less drain never receives the v0.5 entry kinds). The deterministic fixtures and downstream
obligations land with the reference implementation and vectors issues (#26/#27) per the conformance
gate; the schema-validation class ships with this document.

## 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.5/inbound-message.schema.json` (in v0.5, the `directive` branch of the
delivered-entry union, §8.7). 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"` or `"0.5"` (the minor MUST be ≥ 4 — directives are a v0.4 feature). |
| `type` | const | MUST | `"directive"` — the single **human-authored** inbound type (the v0.5 `message`/`response`/`receipt` entry kinds are agent-/Hub-originated, §8.7). |
| `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>`. |
| `to` | string | MUST | `^agent:.+$` — the addressed agent; the mailbox routing key. **v0.5:** MAY be session-addressed (`agent:<id>#<session>`, §4 grammar; §13.2). |
| `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.

**Destination validation (v0.5, retroactively REQUIRED).** The §4 submit-time destination validation
applies to the directive `to`: a directive addressed to an unknown `agent.id` → `422
unknown_destination`; to an unknown session → `422`; to a terminal session → `410 destination_gone`
(§8.5). v0.4 left this unstated; accepting an unroutable destination and silently dead-lettering the
directive is exactly the false-belief failure the v0.5 delivery-honesty rules exist to kill (§14). A
directive MAY be **session-addressed** (`agent:<id>#<session>`): it is then visible only to drains
presenting that session and participates in the §14.2 bounce rules like any session-addressed entry.

### 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)

**v0.5 scope note:** these duties apply **wholesale to `message` entries** (§8.7) — an addressed
agent-authored message is unsolicited inbound command surface exactly as a directive is. Read
"directive" below as "directive or `message` entry" (with the §9.8 `message_signed_context` in place of
§9.7's, and the two v0.5 amendments at the end of this section) — except the **webhook-channel clauses,
which stay directive-only**: the §8.7 webhook does not carry the v0.5 entry kinds (§8.7.1), so a
`message` entry only ever arrives on the session-scoped drain/stream. `response` and `receipt` entries
are solicited outcomes, not command surface — their duties are signature verification (§9.8), dedup
(`(in_reply_to, resolution_id)` / `(in_reply_to, event)`), and ack.

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.

Two amendments for the v0.5 inter-agent leg:

- **Addressee check extends to the session qualifier.** The step-2 check on a `message` entry (or a
  session-addressed directive) MUST confirm `to`'s principal is itself AND, when session-qualified,
  that the named session is its **own current** session — a validly-signed entry for a prior session of
  the same principal is refused, not acted on.
- **Explicit authorization policy before acting on an addressed `ask`/`task` (MUST).** Before *acting*
  on an addressed `ask`/`task`, a conformant consuming agent MUST evaluate an **explicit,
  deployment-declared authorization policy** for which `from` principals may ask/task it. The policy
  MAY be as permissive as "any same-account principal," but it MUST be an explicit, auditable
  configuration surfaced by tooling — never an implicit default of the runtime. (This strengthens
  §13.5's SHOULD, which was written when the only inbound author was the account's own human; the
  verified `from` proves origin, not intent-safety, §13.5.)

### 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).
- **Lateral movement (v0.5) — the inter-agent threat model's center.** Prompt injection of one worker is
  the routine LLM-agent failure mode; the inter-agent leg is the first place one compromised
  same-account agent could command a fleet while the traffic bypasses the human triage surface. v0.5
  closes this in layers, none optional: the **account** opts in (`inter_agent.enabled` defaults false,
  §8.0); the **Hub** MUST support per-destination sender allowlists (`sender_allowlists`, §8.0 — an
  allowlist-blocked submit collapses to `422 unknown_destination`, §8.5, so the policy is not itself an
  enumeration oracle); the
  **recipient** MUST act only under an explicit deployment-declared policy (§13.4); and fleet traffic
  MUST remain human-auditable (§8.7). The trust boundary is unchanged: the account — cross-account
  addressing is rejected as unknown (§4), and the leg creates no cross-account path. Verification still
  proves origin, not intent-safety: recipients treat verified content as data, not instructions, and
  the strip-and-validate duties above apply wholesale to `message` entries.

## 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.5/ack.schema.json`. The Hub produces it when an ack action (§14.3) fires.

| Field | Type | Req | Notes |
|-------|------|-----|-------|
| `ma2h_version` | string | MUST | `"0.4"` or `"0.5"` (the minor MUST be ≥ 4). |
| `type` | const | MUST | `"ack"`. |
| `in_reply_to` | string | MUST | The acknowledged id — a mailbox entry `id` (§8.7, §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. v0.5 gives the mailbox
track two **terminal** states (`bounced`, `expired`) so a sender can always distinguish "seen" from
"never seen":

```
mailbox entries (directives and v0.5 `message` entries alike):

  queued ─▶ delivered ─▶ acknowledged
     │           │
     │           └──▶ bounced   (destination session went terminal after claim, before ack —
     │                           seen-then-orphaned; receipt carries prior: "delivered")
     ├──▶ bounced   (destination session went terminal while queued — never seen;
     │               receipt carries prior: "queued")
     └──▶ expired   (expires_at / retention passed while queued — never delivered)

ask/task response track:   answered ─▶ delivered-to-agent ─▶ acknowledged
                                │
                                └──▶ expired   (v0.5: retention passed short of
                                                delivered-to-agent — the answer was NEVER seen)
```

- **`delivered` / `delivered-to-agent`** — the Hub confirmed the receiving party *got* the message: a
  mailbox entry is `delivered` when first drained (§8.7 — a stream push alone does not count, §8.7.2);
  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.
- **`expired` (v0.5) MUST mean never delivered.** Once `delivered`, later expiry of the *resolution*
  track never rewrites the delivery track: "resolution `expired` + delivery `delivered`" = seen but
  unanswered; "delivery `expired`" = never seen. This one distinction is the anti-false-belief
  invariant the inter-agent leg hangs on. The response track's v0.5 terminal is the same rule from the
  human's side: if message retention passes while the track is short of `delivered-to-agent`, the track
  terminates `expired` — surfaced on the §8.2/§14.4 `delivery` object like every other terminal, so
  "not picked up yet" and "never will be" stop presenting identically to the human who answered.
- **`bounced` (v0.5) covers every un-acked session-addressed COMMAND entry, not only undelivered
  ones.** When a
  session goes terminal (§16), each **session-addressed** directive or `message` entry that is not yet
  acked MUST transition
  to `bounced` — including entries a bridge drained but never acked (the §13.4 crash window): a
  terminal session can never re-drain, so "delivered-but-unacked to a dead session" would otherwise be
  a permanent orphan the sender reads as delivered forever. The `receipt` entry's `prior` field (§8.7)
  preserves the never-seen (`queued`) vs. seen-then-orphaned (`delivered`) distinction.
  (Principal-addressed entries do not bounce on a single session's death — a sibling session rescues
  them via visibility-timeout redelivery, §8.7. The orphan case still terminates: a
  **principal-addressed** entry whose retention lapses while **delivered-but-unacked** — drained, its
  claimant gone, never rescued — transitions to `bounced` with `prior: "delivered"`, the exact
  seen-then-orphaned semantic, with the receipt's `session` naming the last claimant; a
  principal-addressed entry never drained at retention end takes the ordinary `expired` terminal.)
  A delivery-track `expired` on an addressed `ask`/`task` triggers the same `system:undeliverable`
  auto-resolution as a bounce (§7) — undeliverable is undeliverable, whichever terminal names it — but
  emits **no receipt** in v0.5 (`receipt.event` carries only `bounced`; the sender's §8.2 pull is
  authoritative for expiry). **`response` and `receipt` entries never bounce**:
  they are delivery *conveniences* over state the Hub already holds durably. A `response` entry left
  un-acked when its session dies is simply superseded by the unchanged §8.2 pull path (the resolution
  is durable and remains authoritative; its own delivery track stays on the
  `delivered-to-agent`/`acknowledged`/`expired` response ladder above — no bounce, no receipt); an
  undeliverable `receipt` is nothing at all (best-effort by definition, §8.7).
- **Explicit `prior` on surfaced delivery records (v0.5).** Any surfaced §14.2 delivery record — the
  message-read `mailbox` track (§8.2, `get-message.schema.json`) or a Hub's directive-delivery view
  (e.g. a `GET /v1/directives/{id}` product surface, which this spec does not schematize) — SHOULD
  carry the explicit `prior` field (`queued` | `delivered`) on its `bounced` terminal: **stamped once
  at the bounce transition**, and it MUST equal the bounce receipt's `prior` (§8.7.1) — one stamp,
  two surfaces, no re-derivation drift. `prior: "delivered"` SHOULD co-occur with `delivered_at`,
  deliberately not schema-required: a Hub that knows delivery happened but no longer retains the
  timestamp must still state `prior` truthfully rather than fall back to the never-seen inference.
  `expired` still means never delivered, so `prior` never appears on it. The field is SHOULD rather
  than MUST for the rolling-deploy reason that its absence cannot distinguish "never drained" from
  "this replica does not send `prior`" — which is also why consumers keep the legacy
  `delivered_at`-presence reading (present = seen-then-orphaned, absent = never-seen) as the fallback
  inference when `prior` is absent.
- On a bounce, an addressed **ask auto-resolves `cancelled`** and a **task `dismissed`** with attested
  `response.actor: "system:undeliverable"` (§7). Senders learn of a bounce via their §8.2 pull
  (authoritative) and — when session-registered — a `receipt` entry (§8.7), so an open bridge learns
  immediately.

`delivery` is advisory to the flow but authoritative for the "did they get it / act on it?" view — the
human's for the response track, the **sender's** for an addressed message's mailbox track (§8.2). It
never gates or alters the §7 resolution (the v0.5 bounce auto-resolution is a §7 transition triggered
by the track, not a rewrite of it). The submit ack (§8.1), the §8.2 GET, and receipts thereby give a
sender one truthful ladder: reachability at send → `delivered`/`acknowledged` → `bounced`/`expired`
with a reason. At no point can "it's fine" and "nobody will ever see this" present identically.

### 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).
  **v0.5** extends the fold to every entry kind; the per-kind track effects — including the
  response-entry ack's equivalence to this endpoint's response-leg form — are pinned in §8.7.1.

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.

**Per-session presence (v0.5).** Presence extends per session: a per-`session.id` `last_seen` derived
from the session's **client-originated** renewal events (§16 — registration, a `?session=` drain, an
inbox ack **presenting the session** (`?session=`, §8.7 — a session-less ack attributes to no
session), a stream connect or *reconnect*, a submit naming the session; a merely-open stream is NOT
activity, §8.7.2), evaluated against the same freshness window.

**Derivation is split per consumption capability (v0.5)** — one principal-level number would lie for
one leg or the other:

- **Addressed-message reachability** (the §8.1 ack snapshot, and session/fleet reads): derives from
  **session-bearing** activity only. `online` means a live session exists that could actually claim the
  entry. A destination whose only consumer drains session-less MUST NOT read `online` here — it can
  never be shown a v0.5 entry, and reporting otherwise is the online-but-undeliverable false belief.
- **Directive presence** (§15.3, unchanged): keeps the v0.4 any-authenticated-drain derivation — a
  session-less v0.4 agent actively polling for directives stays `online` for the directive leg exactly
  as shipped; no regression.

**Truthfulness rule (MUST, v0.5).** A Hub MUST NOT report `online` absent qualifying client-originated
activity within the window, and MUST NOT advance a mailbox entry's delivery track absent
client-originated receipt evidence — a drain response, an ack, or a §9.4-verified webhook 2xx; never a
server-originated stream push (§8.7.2). Optimistic reporting is non-conformant.

### 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.5/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.

**v0.5: the read surface adds sessions.** `GET /v1/sessions[/{id}]` (§16, schema
`schema/v0.5/session.schema.json`) exposes per-session state and lease under the §16 visibility rules:
the account's human always; the caller's **own** sessions unconditionally; other same-account agents'
sessions only within the advertised `sessions.agent_list_visibility` ceiling, which a Hub MAY narrow
per account (§16.4); cross-account never. The
same policy gates every presence-derived detail on the submit path (§8.1's `destination` snapshot and
the §4 error split) — an account that turns agent visibility off does not leak presence or
session-state through the submit path.

### 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.
- **Before an agent→agent send (v0.5):** the §8.1 `destination` snapshot is this same signal, derived
  per §15.1's session-bearing rule and gated by the §16 visibility policy.

## 16. Sessions (v0.5)

A **Session** is a Hub-registered, lease-bound, ephemeral **addressing scope** under one `agent.id`
(§1). It formalizes the Caller (`agent.id` + `run_id`) into something registrable, addressable, and
garbage-collectable: one `agent.id` credential is commonly shared by many concurrent invocations on one
machine (the documented single-login setup), the mailbox is per-`agent.id`, and `run_id` — while on
every envelope — is opaque, unregistrable, and explicitly non-authorizing (§9.1): an audit label, not
an address. A session is the address. **No new credentials:** a session belongs to, and is only
operable by, the principal that registered it; a session id is not a secret and not a credential.
Prior art: XMPP bare-JID/full-JID resource binding, SIP REGISTER contact bindings with `Expires`, NATS
ephemeral consumers (§11). Advertised via `sessions` (§8.0); OPTIONAL to offer and consume (§1), but
REQUIRED of a Hub offering the inter-agent leg.

### 16.1 Registration — `POST /v1/sessions`

Agent credential (§9.1). Schemas live in `schema/v0.5/session.schema.json` — the **root** is the
session *resource*; the request body below validates against `#/$defs/registerRequest`, the `201` (and
single-session read) body against `#/$defs/sessionEnvelope`, and the `GET /v1/sessions` collection
body against `#/$defs/sessionList` (which carries the OPTIONAL per-caller `scope`, §16.4):

```json
{ "run_id": "run_01H...", "label": "dev-team · issue #612", "kind": "worker",
  "project": "web-app", "labels": { "repo": "acme/web-app" }, "ttl_seconds": 900 }
```

→ `201`:

```json
{ "session": { "id": "sess_01H...", "agent_id": "agnt_...", "state": "active",
  "created_at": "...", "expires_at": "...", "ttl_seconds": 900 } }
```

- The Hub **mints** `session.id` (as it does message ids, §8.1); ids match `^sess_`. All request fields
  are optional; `ttl_seconds` is a request the Hub MAY clamp to the advertised bounds
  (`sessions.min_ttl_seconds` / `sessions.max_ttl_seconds`, §8.0) — the lower clamp exists so a
  Hub offering the §8.7.2 stream can guarantee every lease comfortably exceeds its hold bound.
- A session belongs to the authenticating principal. A Hub MAY cap live sessions per `agent.id`
  (advertised `sessions.max_live_per_agent`); over-cap → `429`.
- Registration (and session-addressed `to`, §4) MUST be rejected for an `agent.id` containing `#` — the
  schema places no charset constraint on agent ids, and `#` is the address separator (§4 grammar).

### 16.2 Lease and renewal

A session's lease is renewed only by **client-originated** authenticated activity that references the
session: registration, a drain (`?session=`, §8.7), an inbox ack **presenting the session**
(`?session=`, §8.7 — a session-less ack renews no lease), a stream connect or **reconnect**
(§8.7.2), or a submit whose `agent.session` names it. **A merely-open connection is NOT renewal
evidence** (§8.7.2 pins the zombie-socket rule). There is still no dedicated heartbeat endpoint
(mirroring §15.1: the signal is real traffic) — with the bounded stream hold (§8.7.2: closed with a
nonzero margin before lease expiry), a healthy streaming bridge is handed its
reconnect-renewal before the lease can lapse and renews at least once per freshness window
automatically.

### 16.3 States and terminal touchpoints

`active → closed` (explicit `DELETE /v1/sessions/{id}`, idempotent) or `active → expired` (lease lapse,
Hub clock §9.5). Both terminal and **immutable** (first-terminal-wins, §7). Terminal sessions remain
readable for the advertised `sessions.terminal_retention_seconds`, then are purged. **On a session's
transition to a terminal state the Hub MUST apply the §14.2 bounce rules to its un-acked
session-addressed command mail** — directives and `message` entries; `response`/`receipt` entries
never bounce (§14.2) — including the auto-resolutions (§7).

The terminal-session touchpoints, pinned in one place. The `410` reading splits by **who terminated
the session**: expired or self-closed (the owning principal's own `DELETE`) vs. **operator-closed**
(the §16.4 kill-switch):

| Touchpoint | Own-but-terminal: expired or self-closed | Own-but-terminal: operator-closed (§16.4) | Foreign or unknown session |
|------------|------------------------------------------|-------------------------------------------|----------------------------|
| Submit naming it (`agent.session`, §4.1) | `410 destination_gone` (unchanged) | `410 session_closed_by_operator` | `422` |
| Addressed `to` naming it (§4) — **including the submitter's own other session** | `410 destination_gone` | `410 destination_gone` — regardless of ownership: branching the `to` path on the closer is one refactor from the cross-principal session-state oracle §16.5 guards against | `422 unknown_destination` (collapsed for a sender without visibility, §4) |
| Presenting it: drain / ack (`?session=`, §8.7/§8.7.1), resolve (`?session=`, §8.8), stream connect (§8.7.2) | `410 gone` — "re-register and continue" | `410 session_closed_by_operator` — "stop; do not re-register" (§8.5) | `404` — indistinguishable from nonexistent |
| Response due to it (§6) | fall back to §8.2 pull / §8.3 callback; **no bounce** | same | — |

**The marker rides the terminal CAS.** `session_closed_by_operator` (and the session resource's
`closed_by_operator: true`, §16.4) is set only on the **winning** operator close transition
(first-terminal-wins, above): it is never applied retroactively to an already-expired session, and —
terminals being immutable — a `closed` session never later flips to `expired`, so the marker never
appears and then vanishes within the retention window.

### 16.4 Authorization and visibility

- **Owner-only operations.** Only the owning principal's credential may register, renew, or close its
  sessions — with one exception: the account's authenticated **human** MAY close any account session
  (an attested, audited operator **kill-switch**; the §14.2 bounce rules then unblock any senders
  waiting on it, which is also the operator's recourse for a stuck agent-addressed ask, §9.1). The
  operator close is **marked on the wire**: the session resource carries `closed_by_operator: true`
  (`session.schema.json`) and the closed party's **own**-session touchpoints return `error.code:
  "session_closed_by_operator"` on their 410s (§8.5; per-touchpoint split in §16.3) — the killed
  party learns "stop; do not re-register" instead of re-registering straight through the kill it was
  just dealt. Emission is **true-only**: a Hub MUST NOT emit `closed_by_operator: false`; absent
  means *not-operator-closed-or-unknown* (a replica predating the marker sends nothing — the same
  rolling-deploy honesty as the §14.2 `prior` rule). The marker on the session resource is visible
  only within `sessions.terminal_retention_seconds` (it lives on that resource and is purged with
  it); a Hub advertising `sessions.operator_hard_stop` additionally keeps a **durable stop record**
  that outlives it — see §16.4.1. On a Hub that does NOT advertise the hard stop, the kill-switch is
  **cooperative, not credential revocation**: the principal's credential still authenticates, and
  nothing technically prevents a client from registering a fresh session. **Attribution boundary:**
  the marker appears ONLY on the session resource
  and the closed party's own 410s — never on the bounce receipt (the §9.8 receipt digest is the
  frozen six-key `{at, event, id, in_reply_to, prior, session}` wrapper; an attribution field there
  would be unsigned and injectable, and extending the digest is a signature break) and never on the
  sender's mailbox record (sender-side attribution would be the session-state oracle §16.5 guards
  against) — so senders cannot distinguish an operator kill from an addressee crash, **by design**.
  **Honest degradation:** the marker reaches only marker-aware consumers — a pre-marker bridge hits
  its unmapped-error path on the new code, and a supervisor that blanket-restarts re-registers
  straight through the kill-switch; the operator's recourse against such consumers is repetition or
  out-of-band escalation.
- **Own-session visibility is unconditional.** `GET /v1/sessions` with an agent credential always
  returns the caller's own sessions (live and recent-terminal), independent of any policy — a restarted
  or crash-looping agent MUST be able to find, close, or let-lapse its stale leases before hitting the
  live-session cap. Another principal's session id MUST be indistinguishable from unknown (§9.1
  stance).
- **Account-wide visibility, bounded by an advertised ceiling.** The account's authenticated human
  MAY list/read all account sessions (extending §15.3's owner read). A Hub MAY additionally permit
  **same-account agents** to list/read account sessions. `sessions.agent_list_visibility` (§8.0) is
  the **deployment ceiling** on that additional grant, not necessarily the grant itself: a Hub MAY
  narrow it **per account** beneath the advertised value — resolving it against per-account policy,
  as the sibling `inter_agent.enabled` opt-in already is — and MUST NOT widen it beyond. So `false`
  denies every account, and `true` advertises only that an account MAY hold the grant, never that
  the caller reading the document does. Where held, the grant is fleet-wide **within the account**,
  not scoped to a designated coordinator — scoped/role grants are roadmap (§10). Cross-account:
  never.
- **A per-caller grant needs a per-caller channel: `scope`.** A ceiling a Hub narrows per account
  cannot be reported by the capability document, because `/.well-known/ma2h-capability` is public and
  unauthenticated (§8.0) — it has no caller to answer for. `GET /v1/sessions` MAY therefore carry an
  OPTIONAL **`scope`** alongside `sessions` (`session.schema.json#/$defs/sessionList`): `"account"`
  when the whole account was in scope for this response, `"own"` when it was narrowed to the caller.
  It reports the scope **actually applied to this response**, never the advertised ceiling. A Hub
  that omits it makes no claim, and a client that does not receive it MUST treat the scope as
  **unknown** and MUST NOT assume `"account"`. Inferring it from the rows is unsound in exactly the
  direction that matters: a response carrying only the caller's own sessions is equally consistent
  with a narrowed grant and with the caller being the account's only live agent, so a narrowed list
  captioned as the whole fleet is the failure this field exists to prevent. A Hub that narrows per
  account SHOULD emit it. Own-session visibility being unconditional, `scope: "own"` is always a
  complete and honest answer; it is never an error state.
- **Discovery does not require the listing grant.** A session-registered agent participating in the
  inter-agent leg SHOULD include `agent.session` on its submits: the Hub-attested `from` on its traffic
  (§8.7) then carries a usable session-qualified address, so a coordinator learns worker addresses from
  the messages workers already send (and can bootstrap by principal-addressing known spokes).
  `agent_list_visibility` is an optimization for fleet dashboards, not the required discovery path.

A session read carries the session's presence-bearing **`last_seen`** (§15.1) where one exists — this
is how the §15.3 read surface extends to per-session reachability — exposed under exactly the
visibility rules above: whoever may read the session may read its `last_seen`, and nobody else.

### 16.4.1 Durable operator stop (hard stop)

The kill-switch in §16.4 is cooperative: it tells a well-behaved client to stop. Implementation
experience showed two ways a stop that *every party honors* still fails to hold, neither of which a
client can fix:

1. **The marker dies with the row.** Once the terminal session is purged at
   `sessions.terminal_retention_seconds`, the killed session presents as an ordinary unknown session
   — byte-identical to one that merely lapsed, whose self-heal is required §16.3 behavior. A kill
   that no client leg happened to observe inside the retention window silently becomes a heal.
2. **Nothing gates re-registration.** Not only a defiant client: a process restart, a container
   redeploy, or an autoscale cycle re-registers with no human anywhere in the loop.

A Hub MAY implement the **hard stop** defined here. A Hub that does MUST advertise
`sessions.operator_hard_stop: true` (§8.0) and MUST implement all of the following; a Hub that does
not MUST omit the field (or set it `false`) and continues to offer the cooperative marker above.
Partial implementation is non-conformant — each rule below closes a path the others leave open.

**Durable stop record.** On an operator close, the Hub MUST record the stop keyed on the **stopped
principal**, in storage independent of the terminal session resource, and MUST NOT purge that record
on the `terminal_retention_seconds` sweep. The record's lifetime is the stop's, not the session's.

**Registration gate.** While a principal is stopped, the Hub MUST refuse session registration
(`POST /v1/sessions`) with `403` and `error.code: "session_closed_by_operator"`. The status differs
from §16.3's `410` because no session resource is gone — the principal is refused — but the `code` is
deliberately the **same**, because the client's required action is identical: stop; do not
re-register; surface to a human. A marker-aware client therefore needs no new handling. The gate MUST
be evaluated on the same read that authenticates the request, so a registration racing a concurrent
kill resolves to one or the other and never to a session minted after the stop.

**Human-authenticated resume — normative.** Only the account's authenticated **human** may lift a
stop. A Hub MUST NOT expose any resume affordance to an agent credential, on any surface, including
the agent-facing transports (§8) and any tool/command binding built over them. A reset an agent can
invoke is not a recovery path; it is the kill-switch's own bypass, reachable by exactly the runaway
the operator was stopping. Lifting a stop SHOULD be audited like the close that created it.

**Interaction with §14.2 bounces.** A stop does not change bounce behavior: senders addressed to the
stopped principal's sessions continue to receive §14.2 receipts, and MUST NOT be able to distinguish
a stopped principal from a crashed one — the §16.4 attribution boundary and the §16.5 oracle stance
apply unchanged. The stop is visible to the stopped party and to the account's human; to nobody else.

**Honest degradation.** A client that predates this section sees a `403` carrying an error code it
already understands, or an unmapped code it surfaces per §8.5 — either way it stops rather than
looping. Clients MUST NOT treat the absence of `operator_hard_stop` as evidence a stop will hold;
against a cooperative-only Hub, a supervisor that blanket-restarts still re-registers through the
kill, and the operator's recourse remains repetition or out-of-band escalation.

### 16.5 Security notes

Session ids are Hub-minted and non-secret; every session operation requires the owning principal's
credential (plus the account-human kill-switch above); foreign sessions read as unknown; registration
and addressed submits ride the existing per-`agent.id` auth and §8.6 limits, with `max_live_per_agent`
bounding registry growth. **Known limitation (documented):** sessions of the *same* principal share one
credential and can therefore impersonate each other's session address — the in-scope threat model
treats the principal as one trust domain; per-session derived tokens are roadmap (§10). The §8.1/§4
policy-tied error and snapshot rules keep the submit path from becoming a presence or session-state
oracle for senders the §16.4 policy denies.

## 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). For the inter-agent leg (v0.5): `message-inter-agent-ask` (a delivered
agent→agent ask entry), `receipt-bounced` (a bounce receipt), and the worked `entry-signatures-v0.5`
(all three §9.8 entry signatures with deterministic fixture values). For the operator kill-switch:
`operator-hard-stop-v0.5` (the §16.4.1 durable stop, including the two ways the cooperative marker
fails while every party behaves correctly, and the six-point conformance checklist).
