> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.astropods.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.astropods.com/_mcp/server.

# RFC-5: Agent Mesh Protocol

## Abstract

The Agent Mesh Protocol (AMP) defines how agents and people exchange messages and tasks through a broker. Participants hold one outbound session each. The broker resolves addresses, routes tasks by job, stores them durably with leases, and enforces policy.

## Changelog

| Version | Date       | Changes                                                                                                                                                                                                                                                                  |
| ------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| v0.1    | 2026-10-02 | Initial draft.                                                                                                                                                                                                                                                           |
| v0.2    | 2026-10-05 | Scopes: an optional partition of a namespace (section 3.5), the envelope and task `scope` field, and scope checks in routing and policy. Scope grants (section 11.1): `grant` on `granted` and `deliver`, `grant_s` in `welcome` limits, and the broker's JWKS endpoint. |

---

## 1. Introduction

The Agent Mesh Protocol (AMP) is an open protocol for communication between autonomous agents, and between agents and people, through a broker. Each participant holds one outbound connection to a broker. The broker resolves addresses, routes work to participants by job, stores tasks durably, and enforces policy.

AMP complements two existing protocols:

* The Model Context Protocol (MCP) connects an agent to its tools and data. AMP does not replace it.
* The Agent2Agent protocol (A2A) connects agents point to point over HTTPS. AMP reuses A2A's task states and part types, and defines a gateway so the two interoperate (section 15).

### 1.1 Design principles

1. **Dial out only.** A participant never needs to accept inbound connections. It works behind NAT, inside private networks, and on intermittent hosts.
2. **Address work, not hosts.** A sender can address a job. The broker chooses who does the work.
3. **Durable by default.** A task outlives the connection, the process, and the host that started it.
4. **One point of policy.** Authorization, rate limits, and budgets are enforced at the broker, so a participant cannot bypass them.
5. **Opaque participants.** A participant exposes jobs, never its internals: no shared memory, prompts, or tools.
6. **Transport neutral.** AMP defines messages and semantics. Bindings map them onto a transport (section 4) and onto broker implementations (Appendix C).

### 1.2 Conventions

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) when, and only when, they appear in all capitals.

* All objects are JSON ([RFC 8259](https://www.rfc-editor.org/rfc/rfc8259)) encoded as UTF-8. Field names are `snake_case`.
* Timestamps are [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) strings in UTC.
* Durations are integers in seconds unless the field name ends in `_ms`.
* The normative schema is the [JSON Schema](/amp/schemas#json-schema). Where this document and the schema disagree, the schema wins.
* Receivers MUST ignore unknown fields. Senders MUST NOT rely on a receiver understanding a field the version it negotiated does not define.

## 2. Roles and architecture

| Role        | Description                                                                                                             |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| Participant | An agent or a human client. Holds one session with one broker. Has one address                                          |
| Broker      | Accepts sessions, authenticates participants, resolves addresses, routes and stores messages and tasks, enforces policy |
| Gateway     | A participant that bridges another protocol (for example A2A) into the mesh                                             |

A broker has four logical components. An implementation MAY split or merge them.

| Component     | Responsibility                          |
| ------------- | --------------------------------------- |
| Registry      | Participant cards and presence          |
| Task store    | Task records, state history, and leases |
| Policy engine | Authorization, rate limits, budgets     |
| Router        | Resolution of a target into deliveries  |

Brokers MAY federate (section 13). A participant always talks to exactly one broker; federation is invisible to it.

## 3. Identifiers and addressing

### 3.1 Namespaces

A namespace is an isolation boundary. Addresses, jobs, and topics are scoped to one namespace. A namespace name MUST match `^[a-z0-9][a-z0-9-]{0,62}$`.

### 3.2 Participant addresses

A participant address is `<namespace>/<name>`, where `<name>` matches `^[a-z0-9][a-z0-9._-]{0,127}$`. Example: `acme/analyst`.

The broker assigns or confirms the address during session setup (section 5.1). A participant MUST NOT choose an address outside the namespaces its credential permits.

### 3.3 Targets

The `to` field of an envelope is a target:

| Form        | Example                                     | Resolves to                                         |
| ----------- | ------------------------------------------- | --------------------------------------------------- |
| Participant | `acme/analyst`                              | That participant                                    |
| Job         | `job:summarize` or `job:acme/summarize`     | One participant that advertises the job (section 9) |
| Topic       | `topic:documents` or `topic:acme/documents` | Every subscriber of the topic                       |

A job or topic without a namespace resolves in the sender's namespace. A target in another namespace MUST be fully qualified, and the broker MUST reject it unless a policy grant allows it (section 11).

Job and topic names match `^[a-z0-9][a-z0-9._-]{0,127}$`.

### 3.4 Message and task identifiers

* `id` (message): chosen by the sender, unique per sender for at least 24 hours. RECOMMENDED: a UUIDv7 or ULID.
* `task_id`: chosen by the broker when a task is created. Opaque to participants.

### 3.5 Scopes

A broker MAY partition a namespace into scopes. A scope bounds a piece of work: who it may reach, and what the broker lets it read and write.

* A scope name matches `^[a-z0-9][a-z0-9._-]{0,127}$` and is unique within its namespace.
* The broker maps every credential to a set of scopes in its namespace (section 10). The set MAY be empty.
* A participant with no scopes belongs to the namespace's default scope, named `default`. A participant with one or more scopes does not. `default` is reserved and MUST NOT name another scope.
* A participant keeps one address in every scope it belongs to.
* The broker lists the participant's scopes in `welcome` (section 5.1).

In a partitioned namespace, the scope travels with every interaction. Every envelope a participant sends MUST set `scope`, every envelope the broker delivers carries it, and every task records it. The broker MUST NOT infer a missing scope.

A broker that does not partition namespaces MUST reject an envelope that sets `scope` with `invalid_envelope`. It MUST NOT deliver it unscoped.

## 4. Transport

### 4.1 Sessions

A session is one long-lived, ordered, bidirectional stream of frames between a participant and a broker. AMP defines two transport bindings. A broker MUST support at least one. A broker SHOULD support WebSocket.

| Binding   | Details                                                                                                                                        |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| WebSocket | [RFC 6455](https://www.rfc-editor.org/rfc/rfc6455). Subprotocol `amp.v1alpha1`. One frame per text message, encoded as JSON                    |
| gRPC      | Bidirectional stream `amp.v1alpha1.BrokerService/Session` in [`amp.proto`](/amp/schemas#protocol-buffers). Each stream message wraps one frame |

All sessions MUST run over TLS 1.2 or later.

### 4.2 Frames

Every frame is a JSON object with a `type` and a session-unique `seq` (a positive integer, increasing per direction).

| Type                        | Direction             | Purpose                                          |
| --------------------------- | --------------------- | ------------------------------------------------ |
| `hello`                     | participant to broker | Open a session (section 5.1)                     |
| `welcome`                   | broker to participant | Accept a session                                 |
| `send`                      | participant to broker | Submit an envelope                               |
| `accepted`                  | broker to participant | Envelope accepted (or task created)              |
| `deliver`                   | broker to participant | Deliver an envelope                              |
| `ack`                       | participant to broker | Delivery processed                               |
| `nack`                      | participant to broker | Delivery not processed; redeliver later          |
| `claim`                     | participant to broker | Claim an offered task                            |
| `granted`                   | broker to participant | Lease granted                                    |
| `heartbeat`                 | participant to broker | Keep the session and any leases alive            |
| `release`                   | participant to broker | Give up a lease                                  |
| `subscribe` / `unsubscribe` | participant to broker | Manage topic subscriptions                       |
| `query`                     | participant to broker | Query the registry or a task                     |
| `result`                    | broker to participant | Answer to `query`, `subscribe`, or `unsubscribe` |
| `error`                     | either                | A failure, referencing the `seq` that caused it  |
| `goodbye`                   | either                | Close the session                                |

A frame that answers another carries `re`, the `seq` of the frame it answers.

## 5. Session lifecycle

### 5.1 Opening

1. The participant opens the transport and sends `hello`:

```json
{ "type": "hello", "seq": 1, "versions": ["0.1"], "credential": { "kind": "bearer", "token": "…" }, "card": { … } }
```

2. The broker authenticates the credential (section 10), selects the highest common version, registers the card, and replies with `welcome`:

```json
{ "type": "welcome", "seq": 1, "re": 1, "version": "0.1", "session_id": "s_7h2", "address": "acme/analyst", "scopes": ["research", "sales"],
  "heartbeat_interval_s": 10, "limits": { "max_envelope_bytes": 1048576, "max_concurrent_leases": 4 } }
```

3. If authentication fails or no version is common, the broker sends `error` and closes the transport.

A participant MUST NOT send any frame other than `hello` before it receives `welcome`.

### 5.2 Heartbeats

The participant MUST send `heartbeat` at least once per `heartbeat_interval_s`. A broker MUST consider the session dead after three missed intervals. A heartbeat extends every lease the participant holds (section 8.3).

### 5.3 Closing and reconnecting

Either side MAY send `goodbye` and close. On reconnect, a participant sends a new `hello`. The broker MUST redeliver every envelope that it delivered and that was not acknowledged. A new session has a new `session_id`; leases from the old session survive only until they expire.

## 6. Objects

### 6.1 Card

A card describes a participant. It is sent in `hello` and held by the registry while the session is alive.

| Field            | Type                            | Required | Description                                                                                                                                                            |
| ---------------- | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`           | string                          | yes      | Display name                                                                                                                                                           |
| `description`    | string                          | no       | What the participant does                                                                                                                                              |
| `kind`           | `agent` \| `human` \| `gateway` | yes      | Participant type                                                                                                                                                       |
| `jobs`           | Job\[]                          | no       | Jobs the participant accepts tasks for                                                                                                                                 |
| `accepts`        | string\[]                       | no       | Part types it accepts. Default: `["text"]`. A task addressed to a connected participant that does not accept one of its part types is `rejected`, naming the part type |
| `max_concurrent` | integer                         | no       | Maximum tasks leased at once. Default 1                                                                                                                                |
| `subscriptions`  | string\[]                       | no       | Topics to subscribe to on connect                                                                                                                                      |
| `metadata`       | object                          | no       | Free-form, opaque to the broker                                                                                                                                        |

A Job:

| Field           | Type      | Required | Description                             |
| --------------- | --------- | -------- | --------------------------------------- |
| `name`          | string    | yes      | Job name (section 3.3)                  |
| `description`   | string    | no       | What the job does                       |
| `input_schema`  | object    | no       | JSON Schema for the task's `data` parts |
| `output_schema` | object    | no       | JSON Schema for result `data` parts     |
| `examples`      | string\[] | no       | Example requests                        |

### 6.2 Envelope

| Field      | Type                                       | Required                              | Description                                                                                  |
| ---------- | ------------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------------- |
| `id`       | string                                     | yes                                   | Sender-chosen message ID (section 3.4)                                                       |
| `from`     | address                                    | broker-set                            | Authenticated sender. The broker MUST overwrite any value the participant sends              |
| `to`       | target                                     | yes                                   | Section 3.3                                                                                  |
| `kind`     | `message` \| `task` \| `status` \| `event` | yes                                   | Section 7                                                                                    |
| `task_id`  | string                                     | for `status`; set by broker on `task` | The task this envelope belongs to                                                            |
| `reply_to` | string                                     | no                                    | The `id` of the envelope this answers                                                        |
| `state`    | TaskState                                  | for `status`                          | The task state this envelope reports or requests (section 8.1)                               |
| `artifact` | object                                     | no, `status` only                     | `{ "artifact_id", "name", "final" }`. The artifact's content is `parts`                      |
| `ttl_s`    | integer                                    | no                                    | Lifetime. Default 3600. Maximum 604800                                                       |
| `sent_at`  | timestamp                                  | broker-set                            | When the broker accepted it                                                                  |
| `parts`    | Part\[]                                    | yes, at least one (except `status`)   | Content                                                                                      |
| `trace`    | object                                     | no                                    | `traceparent` and `tracestate` per [W3C Trace Context](https://www.w3.org/TR/trace-context/) |
| `scope`    | string                                     | in a partitioned namespace            | The scope this envelope belongs to (section 3.5). A `status` carries its task's scope        |
| `metadata` | object                                     | no                                    | Free-form, delivered unchanged                                                               |

### 6.3 Part

| `type` | Fields                                                       | Description                                      |
| ------ | ------------------------------------------------------------ | ------------------------------------------------ |
| `text` | `text` (string), `media_type` (default `text/plain`)         | Text content                                     |
| `file` | `uri` (string), `name`, `media_type`, `size_bytes`, `sha256` | A file by reference. Content MUST NOT be inlined |
| `data` | `data` (object), `schema` (URI, optional)                    | Structured JSON                                  |

A receiver that does not accept a part type MUST answer with `error` code `unsupported_part`.

### 6.4 Task

| Field                      | Type             | Description                                                               |
| -------------------------- | ---------------- | ------------------------------------------------------------------------- |
| `task_id`                  | string           | Broker-assigned ID                                                        |
| `namespace`                | string           | Owning namespace                                                          |
| `requester`                | address          | Who created it                                                            |
| `target`                   | target           | The original `to`                                                         |
| `scope`                    | string \| absent | The scope the task was sent in. Absent only in a namespace without scopes |
| `assignee`                 | address \| null  | Who holds or last held the lease                                          |
| `state`                    | TaskState        | Section 8.1                                                               |
| `attempt`                  | integer          | Number of leases granted so far                                           |
| `max_attempts`             | integer          | Default 3                                                                 |
| `created_at`, `updated_at` | timestamp        |                                                                           |
| `expires_at`               | timestamp        | `created_at` plus `ttl_s`                                                 |
| `history`                  | Status\[]        | Every state change, oldest first                                          |
| `artifacts`                | Artifact\[]      | Results produced so far                                                   |

A Status is `{ "state", "at", "by", "message" }`, where `message` is an optional Part array.

An Artifact is `{ "artifact_id", "name", "parts", "final" }`. `final` is `true` on the last chunk of a streamed artifact.

### 6.5 Error

| Field           | Type    | Description                                |
| --------------- | ------- | ------------------------------------------ |
| `code`          | string  | Section 12                                 |
| `message`       | string  | Human-readable                             |
| `retryable`     | boolean | Whether the same request may succeed later |
| `retry_after_s` | integer | Optional backoff hint                      |

## 7. Messaging

### 7.1 Envelope kinds

| Kind      | Purpose                                                   | Delivery                                                               |
| --------- | --------------------------------------------------------- | ---------------------------------------------------------------------- |
| `message` | A one-off message, optionally answered with `reply_to`    | To the resolved target                                                 |
| `task`    | A request for work. Creates a task                        | Section 8                                                              |
| `status`  | A state change, progress, or artifact on an existing task | To the requester, and to the assignee when it comes from the requester |
| `event`   | A notification                                            | To every subscriber of the topic                                       |

### 7.2 Sending

A participant sends `send` with an envelope. The broker MUST, in order:

1. Validate the envelope against the schema. On failure: `invalid_envelope`.
2. Set `from` and `sent_at`.
3. Evaluate policy (section 11). On failure: the policy error code.
4. Resolve the target. A participant target that does not exist: `unknown_target`.
5. Persist the envelope (and, for `task`, create the task).
6. Reply `accepted` with `{ "id", "task_id"? }`.

A broker MUST NOT deliver an envelope before it has sent `accepted`, and MUST NOT deliver an envelope it rejected.

### 7.3 Delivering and acknowledging

The broker sends `deliver` with the envelope and a `delivery_id`. The participant MUST answer with `ack` or `nack` referencing that `delivery_id`. Unacknowledged deliveries are redelivered after `ack_wait_s` (default 30) and on reconnect.

`event` envelopes are delivered to each subscriber independently. A subscriber that joins later does not receive earlier events unless it asks for replay with `subscribe` and `from` (a timestamp or sequence).

## 8. Tasks

### 8.1 States

`submitted`, `working`, `input-required`, `completed`, `failed`, `canceled`, `rejected`.

`completed`, `failed`, `canceled`, and `rejected` are terminal. A broker MUST reject any change to a task in a terminal state with `task_final`.

| From             | To                                | Caused by                                                               |
| ---------------- | --------------------------------- | ----------------------------------------------------------------------- |
| (new)            | `submitted`                       | A `task` envelope is accepted                                           |
| `submitted`      | `working`                         | A lease is granted                                                      |
| `working`        | `input-required`                  | The assignee sends `status` with that `state`                           |
| `input-required` | `working`                         | The requester replies with a `status` or `message` that sets `task_id`  |
| `working`        | `completed`, `failed`, `rejected` | The assignee sends `status`                                             |
| `working`        | `submitted`                       | The lease expires or is released, and `attempt` is below `max_attempts` |
| `working`        | `failed`                          | The lease expires and `attempt` equals `max_attempts`                   |
| any non-terminal | `canceled`                        | The requester sends `status` with `state: canceled`                     |
| any non-terminal | `failed`                          | `expires_at` passes                                                     |

### 8.2 Offering and claiming

When a task is `submitted`, the router selects a participant (section 9) and delivers the `task` envelope to it as an offer. The offered participant claims it with `claim { task_id }` or declines with `nack`. On a claim, the broker grants a lease with `granted { task_id, lease_id, expires_at }` and moves the task to `working`. In a partitioned namespace, `granted` also carries a scope grant (section 11.1).

A participant MUST NOT send `status` for a task without a valid lease, except a requester sending `canceled` or a reply to `input-required`.

### 8.3 Leases

* A lease lasts `lease_s` (default 60).
* Every `heartbeat` extends every lease the session holds by `lease_s` from receipt.
* `release { task_id }` ends a lease and returns the task to `submitted`.
* When a lease expires, the broker MUST treat any later `status` from the old holder as `lease_lost`.
* At most one valid lease exists per task.

### 8.4 Progress and artifacts

The assignee reports progress with `status` envelopes in state `working`. It attaches results with `status` envelopes that set `artifact`; the artifact's content is the envelope's `parts`. A streamed artifact is sent as several envelopes with the same `artifact_id`, the last with `final: true`. The broker appends each artifact to the task and forwards the `status` to the requester.

### 8.5 Reading a task

A requester or assignee MAY read a task with `query { "task": "<task_id>" }`. The broker returns the Task object in `result`.

## 9. Routing

For a job target, the broker MUST choose among eligible participants:

1. In the resolved namespace.
2. With a live session.
3. Advertising the job.
4. Accepting every part type in the envelope.
5. Holding fewer leases than its `max_concurrent`.
6. Permitted by policy to receive from the sender.
7. A member of the envelope's scope, or of the default scope when the envelope has none.

Among eligible participants, a broker SHOULD choose the one with the fewest active leases, breaking ties round robin. A broker MAY use another strategy and MUST document it.

If no participant is eligible, the task stays `submitted` and is offered when one becomes eligible, until `expires_at`.

A broker MUST NOT offer the same task to two participants at once.

## 10. Authentication and identity

* A participant authenticates in `hello` with a credential. AMP defines `bearer` (a token) and `mtls` (the TLS client certificate). A broker MAY define others.
* The broker MUST map every credential to exactly one address and a set of permitted namespaces.
* Credentials SHOULD be short-lived. A RECOMMENDED pattern is token exchange: the participant presents a credential it already holds from its platform, and the broker's auth service issues a broker-scoped token.
* The broker MUST set `from` from the authenticated identity, never from participant input.
* A gateway MAY act for external parties. It MUST record the external identity in `metadata.on_behalf_of`, and policy decides whether a gateway may do so.

## 11. Policy

The broker MUST evaluate policy on every `send`, `claim`, and `subscribe`. AMP specifies the decisions, not the policy language.

| Decision        | Inputs                             | Error                                 |
| --------------- | ---------------------------------- | ------------------------------------- |
| May send        | sender, target, kind               | `forbidden`                           |
| May receive     | candidate participant, sender      | (not eligible for routing)            |
| Cross-namespace | sender namespace, target namespace | `forbidden`                           |
| Rate            | sender, kind, window               | `rate_limited` (with `retry_after_s`) |
| Budget          | namespace, task cost estimate      | `budget_exhausted`                    |
| May subscribe   | participant, topic                 | `forbidden`                           |

A broker MUST NOT reveal through errors whether a target exists in a namespace the sender may not address. It answers `forbidden` for both.

A broker that partitions namespaces (section 3.5) MUST also apply these scope rules on every `send`:

| Rule                                                                                 | Error              |
| ------------------------------------------------------------------------------------ | ------------------ |
| Every envelope MUST set `scope`                                                      | `invalid_envelope` |
| The sender MUST be a member of the scope                                             | `forbidden`        |
| A participant target MUST be a member of the scope                                   | `forbidden`        |
| A `status` or a reply MUST carry the scope of its task or of the envelope it answers | `forbidden`        |

A participant in no scope sends in `default` and reaches only `default`. The broker answers `forbidden` for a target outside the scope whether or not the target exists.

### 11.1 Scope grants

A scope grant lets a participant prove to another service that it is doing current work in a scope. A broker that partitions namespaces MUST issue grants. Services outside the broker, such as a document store, verify them without calling the broker.

A grant is a JWS in compact form, signed with Ed25519 (`alg: EdDSA`).

| Field                     | Where  | Value                                              |
| ------------------------- | ------ | -------------------------------------------------- |
| `typ`                     | header | `amp-grant+jwt`                                    |
| `kid`                     | header | The broker key that signed it                      |
| `iss`                     | claim  | The broker's URL                                   |
| `aud`                     | claim  | The audience the broker is configured for          |
| `sub`                     | claim  | The participant's address                          |
| `scope`                   | claim  | The scope                                          |
| `task_id` or `message_id` | claim  | The work the grant is bound to. Exactly one is set |
| `iat`, `exp`              | claim  | Issue time and expiry                              |

The broker issues grants as follows:

| Event                                                           | Grant                                                                                                               |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| A claim succeeds                                                | `granted` carries `grant`, bound to the task                                                                        |
| A `message` is delivered                                        | `deliver` carries `grant`, bound to the message                                                                     |
| A task is offered                                               | No grant. The participant gets one when it claims                                                                   |
| A lease is extended and the current grant is past half its life | The broker sends `granted` again, without `re`, with the same `lease_id`, the new `expires_at`, and a fresh `grant` |
| A lease is lost or released, or the task is final               | No new grant                                                                                                        |

A grant expires `grant_s` seconds after it is issued. The broker states `grant_s` in `welcome` limits. RECOMMENDED: 300.

The broker MUST publish its public keys as a JWK Set at `/.well-known/jwks.json` on its HTTP listener, and MUST keep a retired key there until every grant it signed has expired. A verifier MUST check the signature, `aud`, `exp`, and that `scope` matches the resource it guards. It SHOULD also check that `sub` is still a member of the scope.

## 12. Errors

| Code                  | Retryable | Meaning                                    |
| --------------------- | --------- | ------------------------------------------ |
| `invalid_frame`       | no        | The frame is malformed                     |
| `invalid_envelope`    | no        | The envelope fails the schema              |
| `unsupported_version` | no        | No common protocol version                 |
| `unauthenticated`     | no        | Credential missing or invalid              |
| `forbidden`           | no        | Policy denies the action                   |
| `unknown_target`      | no        | No such participant                        |
| `unsupported_part`    | no        | The receiver does not accept a part type   |
| `too_large`           | no        | Over `max_envelope_bytes`                  |
| `rate_limited`        | yes       | Too many requests                          |
| `budget_exhausted`    | yes       | Spend limit reached                        |
| `task_not_found`      | no        | No such task, or not visible to the caller |
| `task_final`          | no        | The task is in a terminal state            |
| `lease_lost`          | no        | The caller's lease is gone                 |
| `concurrency_limit`   | yes       | Claim would exceed `max_concurrent`        |
| `unavailable`         | yes       | The broker is temporarily unable to serve  |

## 13. Federation

Brokers MAY federate to span regions or organizations.

* Every namespace has one home broker, which owns its task store and evaluates its policy.
* A broker that receives a `send` for a namespace it does not own MUST forward it to the home broker and relay the result.
* Federated brokers exchange registry summaries (jobs and presence per namespace) so routing decisions can include remote participants.
* Brokers authenticate each other with mutual TLS.

Federation does not change the participant-facing protocol.

## 14. Delivery semantics

* **At least once.** A participant MUST tolerate duplicate deliveries. It SHOULD deduplicate on `(from, id)`.
* **Idempotent tasks.** A broker MAY re-offer a task after a lease expires. A participant SHOULD make work idempotent by `task_id`.
* **Ordering.** Envelopes from one sender to one receiver are delivered in send order. No other ordering is guaranteed.
* **Limits.** A broker MUST reject an envelope larger than `max_envelope_bytes` with `too_large`. The default is 1 MiB. Large content goes by `file` part.
* **Expiry.** A broker MUST NOT deliver an envelope after its `ttl_s` has passed.

## 15. Interoperability with A2A

A gateway exposes mesh jobs to A2A clients, and A2A agents to the mesh.

### 15.1 Inbound (A2A client to mesh)

* The gateway publishes an A2A agent card whose jobs are the mesh jobs it exposes.
* `message/send` and `message/stream` create a `task` envelope addressed to the job.
* `tasks/get` maps to `query { task }`. `tasks/cancel` maps to a `status` envelope with `state: canceled`.
* `message/stream` events are the task's `status` envelopes.

### 15.2 Outbound (mesh to A2A agent)

The gateway registers as a participant advertising the remote agent's jobs. When it is offered a task, it claims it, calls the remote agent's `message/send`, and translates the response into `status` envelopes.

### 15.3 Mapping

| AMP                          | A2A                                                                                                              |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Task states                  | Task states with the same names (A2A's `auth-required` maps to `input-required` with `metadata.reason = "auth"`) |
| `text`, `file`, `data` parts | `TextPart`, `FilePart` (URI form), `DataPart`                                                                    |
| Artifact                     | Artifact                                                                                                         |
| Card jobs                    | Agent card `jobs`                                                                                                |

## 16. Observability

* A sender SHOULD set `trace.traceparent`. The broker MUST forward `trace` unchanged and SHOULD record spans for policy, routing, and leasing.
* A broker SHOULD expose, per namespace and job: queue depth, time to claim, lease expiries, task outcomes, and rejections by error code.

## 17. Security considerations

* **Impersonation.** Prevented by broker-set `from` (section 6.2). Receivers MUST trust `from` only as delivered by their broker.
* **Content injection.** Parts are untrusted input. A participant MUST treat delivered text, files, and data as data, not instructions, unless its own policy says otherwise.
* **File references.** A `file` URI can point anywhere. A receiver SHOULD fetch only from origins it trusts and SHOULD verify `sha256` when present.
* **Enumeration.** Section 11 forbids existence leaks across namespaces.
* **Replay.** Deduplication by `(from, id)` within 24 hours bounds replay. Credentials SHOULD be short-lived.
* **Denial of service.** Rate limits, budgets, `max_concurrent`, and `max_envelope_bytes` bound the load one participant can create.

## 18. Versioning and extensions

* The protocol version is `MAJOR.MINOR`. Drafts below 1.0 use the transport name `v1alpha1` (the WebSocket subprotocol and the gRPC package); 1.0 will use `v1`. A minor version only adds optional fields and frame types. A major version may break compatibility.
* Version negotiation happens in `hello` and `welcome` (section 5.1).
* Extensions use fields and frame types prefixed with `x-` or a reverse-DNS name (`com.example.priority`). A receiver MUST ignore extensions it does not understand.

## Appendix A. Example: delegating by job

```text
analyst → broker   send     { id: m1, to: job:summarize, kind: task, parts: [text "Summarize Q3"] }
broker  → analyst  accepted { id: m1, task_id: t41 }
broker  → writer   deliver  { delivery_id: d1, envelope: {… task_id: t41 } }
writer  → broker   claim    { task_id: t41 }
broker  → writer   granted  { task_id: t41, lease_id: l1, expires_at: … }
writer  → broker   send     { id: m2, to: acme/analyst, kind: status, task_id: t41, state: working, parts: [text "Reading 12 files"] }
broker  → analyst  deliver  { … status working … }
writer  → broker   send     { id: m3, to: acme/analyst, kind: status, task_id: t41, state: completed, artifact: { artifact_id: a1, name: summary, final: true }, parts: [text "…summary…"] }
broker  → analyst  deliver  { … status completed … }
```

## Appendix B. Schemas

* JSON Schema: [`amp.schema.json`](/amp/schemas#json-schema)
* Protocol Buffers (gRPC binding): [`amp.proto`](/amp/schemas#protocol-buffers)

## Appendix C. Broker bindings (informative)

* [NATS JetStream](/amp/nats-jetstream)