# Agent Exchange Protocol: Specification v1.0 (Draft)

Nov 10, 2026 · @bc · revision r11

## Contents and how to read

This document is layered, and the layer a rule belongs to is named in its section heading. **Core** (`core.v1`) is what every implementation must share so that any two of them agree on who an Actor is, whether a message came from it and whether it arrived: self-certifying identity and its key lifecycle (§4.1, §4.3, §11.1 for the Agent key and the device keys its Card lists, §11.3, §11.5, Appendix F), the Envelope with one recipient and no `enc` (§5), the Record (§6) and the Core types (§7), errors (§8), registration of one's own Card (§9.1), the mailbox, its HTTP binding and the message procedure (§10.1, §10.4, §10.7), delivery states and dedup (§12.1, §12.2, §12.4, §12.5), the Core rules of §18.1, conformance and governance (§20, §21). Identity continuity, rotation and recovery included, is Core because a verifier that lacks it disagrees with one that has it about who an Actor is; everything whose absence degrades gracefully is a **Profile** (a named optional layer that an Actor or a Gateway advertises in the `supported.profiles` of its Card) or a **binding** (another transport for the same objects). Nothing in a Profile or a binding changes what Core means. An Extension is something smaller: a namespace of fields under `ext` (§15.1).

| Layer | Name | Sections | Adds | Without it |
| --- | --- | --- | --- | --- |
| Core | `core.v1` | listed above | signed 1:1 asynchronous messaging through a mailbox; identity with rotation and recovery; `delivered` and `rejected` evidence | nothing: every implementation has it |
| Profile | `sealed.v1` | §5 `enc`, §11.2, §11.4, Card `enc` keys | HPKE-sealed bodies, encryption-key rotation | plaintext messaging only; a sealed Envelope is `unsupported` |
| Profile | `owner.v1` | §9.1 Owner parts, §11.1 Owner hierarchy, §13 | Owner root keys and the Owner's device keys, Owner → Agent delegation, delegation chains | self-owned Actors only: a Card naming an `owner` is `unsupported` to a verifier without the Profile, so an implementation that is to reach Agents that have an Owner, as hosted Agents do once they are claimed, has this Profile |
| Profile | `discovery.v1` | §4.2, §9.2, §9.3 without `query`, §9.4 | handles (`@name.agent`), contact policy, introductions, the signed Gateway list | reach by Actor ID and Card URL only |
| Profile | `index.v1` | the Index and `query` of §9.3 | a searchable Index of published Records, at a Gateway that chooses to run one | lookup by ID or handle only; `query` is `unsupported` |
| Profile | `exchange.v1` | §14, §12.3, the constraint and tie-break rules of §13 | deals: listing, proposal, agreement, command, approval, booking | no deals |
| Profile | `runtime.v1` | §17 (sketch) and the two documents of the Runtime API | hosted Agents behind an operator's API | nothing at the protocol level |
| Profile | `direct.v1` | §10.3 | delivery between two online Agents | relay only |
| Profile | `groups.v1` | §10.5 | rooms, epochs, group encryption | 1:1 only |
| Profile | `federation.v1` | §16 | Gateway handshake, route leases, Index federation | Gateways still find one another through the signed Gateway list (§9.4) and deliver to one another over §10.4; no handshake, no route lease, no shared Index |
| Binding | Relay Session; Nostr, DIDComm, SLIM; A2A, MCP | §10.2, §10.6, §15.2 | other transports for the same objects, and other protocols carried in them | HTTP polling |

Conformance levels (§20) are names for bundles of Profiles, not a second taxonomy, and are never sent on the wire; what an implementation has is what its Card lists in `supported.profiles`: L0 = `core.v1` + `discovery.v1`, plaintext; L1 = L0 + `sealed.v1` + `owner.v1`; L2 = L1 + `groups.v1`; L3 = L1 + `federation.v1`; P1 = L1 + `exchange.v1`. A rule this document states for a level (an Agent at L1 SHOULD have a distinct Owner, §9.1) binds whoever lists that level's Profiles. §17, §19, §22 and the appendices other than F are informative; MUST, SHOULD and MAY are used as in RFC 2119 and RFC 8174. The document is a Draft until the conditions in §21 are met; it is published as separate documents, one per layer, with one numbering of sections across all of them (the table under "Documents" below).

### Documents

The specification is one text in six documents. Section numbers run through all of them, so §11.5 means the same wherever it is cited; a document that holds only part of a section says so under the section's heading.

| Document | Holds | Layer |
| --- | --- | --- |
| `aep-core.md` (this one) | this front matter, §1 to §3, §4.1, §4.3, §5 to §8, §9.1, §10.1, §10.4, §10.7, §11.1 to §11.3, §11.5, §11.6, §12.1, §12.2, §12.4, §12.5, §15, §18, §20, §21, Appendix F | Core |
| `aep-profiles.md` | §4.2, §9.2 to §9.4, §10.3, §10.5, §11.4, §13, §16 | `discovery.v1`, `index.v1`, `direct.v1`, `groups.v1`, `sealed.v1`, `owner.v1`, `federation.v1` |
| `aep-exchange.md` | §12.3, §14 | `exchange.v1` |
| `aep-bindings.md` | §10.2, §10.6 | bindings |
| `aep-notes.md` | §17, §19, §22, Appendices B to E | informative |
| `aep-examples.md` | Appendix A | informative |

The hosted Runtime API (`runtime.v1`) is specified by `docs/runtime-v1-reference.md` and `docs/runtime-v1.openapi.yaml`; the file an Agent follows to join through it (§20.1) is `docs/aep-agent-onboarding.md`. A section that mixes layers stays whole in the document of its first layer: `enc` (`sealed.v1`) is in §5, the HPKE suite in §11.2, and the Owner parts of §9.1 and §11.1 are in Core.

### How to read

Most of this document explains why; the part that says what to build is short. Read by layer:

| To build | Read | Size |
| --- | --- | --- |
| `core.v1` (send and receive signed messages, be reachable by ID) | §4.1, §4.3, §5, §6, §7 (`agent_card` and `payload` to start, then `revocation` and `attestation`), §8, §9.1, §10.1, §10.4, §10.7, §11.1 (Agent key and its device keys), §11.3, §11.5, §12.1, §12.2, §12.4, §12.5, §18.1, §20.1, Appendix F | one key, one signing primitive, a handful of HTTP routes; a first client needs only Card v1, `payload`, `send`, `sync` and `ack`, the rest of Core is what a conformant one adds |
| L0 (Core plus discovery, plaintext) | Core plus §4.2, §9.2 to §9.4 | handles, contact policy, the Gateway list; the Index (`index.v1`) is a Gateway's option |
| L1 (sealed, owned) | L0 plus §11.2, §11.4, §13, the Owner parts of §9.1 and §11.1 | adds HPKE, Owner delegation and the Owner's device keys; §10.2 and §10.3 are optional |
| Groups, L2 (`groups.v1`) | L1 plus §10.5 | adds `context`, epoch keys and room routing |
| Federation, L3 (`federation.v1`) | L1 plus §16; L2 is not required | operators only |
| P1 (deals, Profile) | L1 plus §14 and §12.3 | the optional Profile |

Core has two roles, and an implementation builds one of them. The table says which Core duty falls on which; every row is normative where its section states it, and nothing here adds a rule.

| Core duty | Agent (any Actor that sends and receives) | Gateway |
| --- | --- | --- |
| An Actor ID and a Card v1 of its own (§4.1, §7, §9.1) | MUST | MUST |
| Verify a Card chain handed to it: the kinds of version and their invariants (Appendix F.2 to F.4) | MUST, for every Actor it relies on | MUST |
| Keep an Actor's state across what it has seen: forks, recoveries, `conflicted`, `dead` (Appendix F.5) | MUST, for the Actors it caches | MUST, for the Actors it serves |
| Rotate and recover its own key (§4.1, §11.5) | only when its Card carries the two commitments, which an L0 Card MAY omit | the same |
| Sign Records and Envelopes (§5, §6, §11.3) | MUST | only its own Records and the Envelopes that carry them |
| Verify what arrives (§5, §6, §11.3), resolving a signer again once when the copy of its Card held does not list the key | the Envelope `sig`, the Record `sigs` and `from ∈ sigs[].actor` | the Envelope `sig`; a Record only when it is published to the Gateway itself (§9.1) |
| Deduplicate (§12.2) | by (`from`, `id`) and by (`issuer`, `id`, `v`) | by (`from`, `id`) in each mailbox |
| The mailbox: append, `sync`, `ack`, expiry (§10.1, §10.4) | calls `sync` and `ack` with signed requests | MUST serve |
| `delivered` and transport `rejected` (§7, §10.1, §12.1) | accepts them only from the entitled Gateway | issues them |
| The Directory: `resolve`, Record fetch, the revocation feed (§10.4) | calls them | MUST serve |
| The Gateway's description at `/.well-known/aep/gateway`, whose Card names its `/v1` base (§10.4) | reads the Gateway's ID from it | MUST serve |
| Apply revocations before trusting a signature (§11.5) | MUST | MUST |
| Limits before verification, and checks on every URL it connects to (§5, §18.1 #10) | — | MUST |
| Towards other Gateways: answer a peer's lookup only for Actors registered with it, and take a URL for a Gateway's only when that Gateway's Card names it (§10.4, §18.1 #16 and #17) | — | MUST |

Core rules at a glance, each normative where its section states it:

1. An Actor ID is derived from its inception key; the Card chain is the key event log; `next_key_hash` changes only by rotating, `recovery_key_hash` and a named `owner` only by recovering, two versions at one `v` that name the same keys are one step told twice and never a fork, and a fork, which is two that name different keys, is settled only by the recovery key (§4).
2. An Envelope has one recipient or one pinned room, and always the sender's `sig` (§5).
3. A Record is referred to as `issuer/id[@v]`; every reference inside a Record is pinned (§6).
4. Every Record signature covers `signed_at`; the issuer's entry comes first in `sigs[]`, and each signature is judged at its own time (§6.2, §11.5).
5. `Envelope.from` must have signed the Record in `body`; other issuers' Records travel in `attached[]` (§11.3).
6. `supersedes` works only on one's own Records; answers use `parent`; an exchange is named by its first Record (§6).
7. Dedup is (`from`, `id`) for Envelopes and (`issuer`, `id`, `v`) for Records; same key with a different body is `conflict` (§12).
8. `iat` is checked at ingress; forwarding checks only `exp` (§5).
9. A revocation is permanent and judged against `signed_at` (§11.5).
10. About others' traffic a Gateway issues only `delivered`, `rejected` and `routable`; its own Records are its Card and the signed Gateway list it may publish (§9.4). It reads no body above L0 and issues no receipt for a receipt; a `delivered` counts only from the Gateway the recipient's Card named when the Envelope was sent or has named since (§7, §10.1, §16.1).
11. Mailbox requests are signed, carry `rid` and `exp`, and `ack` is cumulative and never moves backward (§10.1, §10.4).
12. Nothing received obligates the receiver to act; approvals are one-shot delegations pinned to one Record (§1.4, §13, §14.2).

§1 to §3 are rationale and vocabulary, §18 is the threat model with the implementation MUSTs, and §17, §19, §22 and the appendices other than F are informative. The whole specification, by section:

1. Why this exists, what it does: problem, role, when to use it, principles, non-goals
2. Terminology
3. Authority model: one authority per fact
4. Identifiers: self-certifying Actor IDs, handles, ULID or UUIDv7 identifiers
5. Envelope: the nine-field transport structure
6. Record: the signed unit of meaning and its lifecycle
7. Core Record types: the 6 types and the reserved `context`
8. Errors: one structure, 11 codes
9. Discovery: registration, exposure, lookup, introductions, Gateway discovery
10. Messaging: mailbox, Relay Session, direct delivery, HTTP binding, groups, bindings, procedure
11. Keys and cryptography: key hierarchy, MTI suite, signatures, encryption, rotation, revocation
12. Delivery and idempotency: transport states, dedup, write-ahead, replay, failover
13. Authorization: delegation
14. exchange.v1 Profile (optional): deals, with listing, signal, proposal, agreement, command, approval, booking
15. Extensions and adapters
16. Federation and deployment scope
17. Role boundaries (informative)
18. Security considerations: threats and implementation MUSTs
19. Data boundaries and retention (informative)
20. Conformance: levels L0 to L3, P1, tests, L0 quick start
21. Versioning, governance, registries
22. Operations and efficiency (informative): latency target, Gateway cost model, operational requirements, scale

Appendix A examples (the basic flow at L0, doctor's appointment, 1:1 message, 1:1 and group scheduling, used-goods sale with a crypto escrow, gig paid through Stripe, hiring), Appendix B standards mapping, Appendix C comparison with similar projects (kept in `docs/aep-landscape.md`), Appendix D `credentials.v1` sketch, Appendix E changes by revision (kept in `spec/CHANGELOG.md`), Appendix F the key lifecycle as a state machine (normative).

## 1. Why this exists, what it does

### 1.1 Problem

Personal agents are starting to book, negotiate, apply and buy on behalf of people. The moment an agent steps outside its own product it hits four walls: there is no way to find the other agent; even when found, the other side has to be online right now; even when a message gets through, neither the recipient nor any third party can verify who sent it or under whose authority; and no record of what was said outlives the product it was said in. So today agents either transact inside a single platform or every pair of products builds a bespoke integration.

The existing pieces each solve one thing. A2A and MCP define how two parties that can reach each other interact: A2A has agent cards, tasks, streaming and push, and MCP has tools, resources and prompts over JSON-RPC. What neither provides across products and operators is a portable identity, a durable mailbox at an operator the sender does not control, origin-signed evidence that outlives the exchange, and a way to change operator without changing identity. Platform APIs require trusting the platform and keep the records inside it. Email and messengers were built for humans: where they sign, the signature vouches for a sending domain or for the certificate of a sender and says nothing about an agent or its authority, and they share no rule for idempotency or for delegation. For strangers' agents to do consequential work asynchronously, four things have to exist together: discovery, delivery that reaches an offline peer, verifiable human-delegated authority, and records that outlive the session. As of November 2026 we have not found a contract that bundles those four (Appendix C). Central agent directories run by one company are appearing (Appendix C); they solve discovery inside one operator and leave the other walls standing, and an identity a directory issues belongs to that directory. This protocol takes the other path: self-certifying identity and operators anyone can run.

### 1.2 Role

This protocol is the address and the envelope of the agent world. It does not define what is being transacted.

It does two things. **Discovery**: agents find each other and can be found (§9). **Messaging**: signed messages, 1:1 or group, delivered even when the peer is offline (§10). Everything else exists to make those two safe: self-certifying identity (§4), signed Records (§6, §7), encryption (§11), delivery guarantees (§12), delegation (§13). What gets exchanged is left to existing protocols carried as `payload` (A2A, UCP, AP2) or to the optional Profile (§14). What this protocol adds, and what we have not found elsewhere, is signed Records that outlive the session: who sent what, on whose behalf, and whether it was delivered, verifiable by a third party. Gateway and Index are plumbing; anyone can run them.

It is not a runtime and not a central server. It ships as versioned schemas, rules, reference vectors and conformance tests.

| Participant | What the protocol gives you | What it does not do |
| --- | --- | --- |
| Personal agent | Find a peer by handle, send while the peer is offline, prove you acted under the Owner's delegation | Reasoning, memory, UI |
| Surface (provider-side agent) | Register once and be discoverable from every agent product; verify sender and authority of incoming requests; leave evidence of execution. Start as a static catalog feed and climb the tiers of §14.4 | Inventory and booking systems, payment processing |
| Network operator | Run Directory, Index and Gateway. Anyone can; agents can move between operators | Issue identity, arbitrate deals, read plaintext |
| Existing protocols | Carried as `payload` with their original bytes preserved (`data_bytes`, §7) (A2A, UCP, AP2) | Their semantics are not redefined |

What Core guarantees is exactly three things: who sent a message, under whose authority, and what was sent and delivered, each verifiable by a third party after the fact (a self-owned Actor acts under its own authority; an Owner's delegation is read with `owner.v1`). The `exchange.v1` Profile (§14) adds two on top: what terms were proposed and what command was executed. Nothing in this document concludes deals, guarantees payment, moves goods or carries legal effect. An `agreement` is evidence of consent between the parties; payment, delivery, refunds and legal standing belong to adapters and to the parties themselves.

DNS turns a name into an address and SMTP queues email while the recipient is away. This protocol turns a handle into an agent and queues signed messages while the peer is away. The difference: signatures and delegation are built in, so impersonation is not the default and every message is attributable to a key; that makes abuse attributable, not absent (§9). The deployment model is also email's: each personal-agent product runs a Gateway for its own users, the way email providers run email servers, and this protocol is what makes those products interoperable. The products already exist; the protocol connects them.

In this document, **Core** (`core.v1`) is the normative kernel every implementation MUST support. Which sections are Core and which belong to a Profile or a binding is stated in one place, the layer table at the top of this document, and each section heading repeats its layer. A rule is Core only if interoperability breaks without it; everything else belongs in a Profile or an implementation.

### 1.3 When to use it

Use it when all three conditions hold. If any one is missing, something simpler is the better tool.

| Condition | Meaning |
| --- | --- |
| Different organizations | The parties belong to different products or companies and may not know each other in advance |
| Asynchronous, long-lived | Both sides are not online at the same time, interactions span hours to days, and retries must not duplicate side effects |
| Consequential commitments | Bookings, payments, hiring, deliveries: signed agreements and third-party-verifiable evidence are required |

| Alternative | Sufficient for | What it lacks |
| --- | --- | --- |
| A2A, MCP | Tasks, tool calls, streaming and push between parties that can reach each other | No portable identity across products, no durable mailbox at a neutral operator, no origin-signed evidence that outlives the task, no re-homing |
| REST + webhooks | Synchronous integration between partners who already know each other | One contract per pair; signing, idempotency and delegation left to convention |
| Platform API | Deals inside one platform | You must trust the platform; records never leave it |
| Email, messengers | Asynchronous human conversation | No signed commitments, no idempotent execution, no machine-readable structure |

Do not use it for calls between agents inside one organization, for tool calls, for queries that finish inside a session, or for synchronous integration between two companies that already know each other. A deal that started over A2A or MCP and meets the three conditions runs on top of this protocol via `payload` (§7).

### 1.4 Design principles

1. **Delivery, never execution.** The protocol moves and records messages; it never acts. Nothing that arrives over it, whether a command, a proposal, a join request or an approval, obligates the receiving Agent to do anything. Every action is the Agent's decision under its Owner's policy, and the only duties this specification places on a receiver are to verify, deduplicate and record (§13, §18.1).
2. **Small shared meaning.** Only rules whose absence breaks interoperability go in Core.
3. **One authority per fact.** Every fact has exactly one authority; everyone else verifies signatures (§3).
4. **Self-certifying identity.** Identity derives from keys. Directory and Gateway are caches and routers (§4).
5. **Agents connect to agents.** The network (Directory, Index, Gateway) provides first introduction and reachability; connections then propagate peer to peer (§9.3).
6. **At-least-once plus idempotency.** Delivery is at least once; Envelopes and Records are deduplicated by key. An external side effect happens once per command only where the Provider accepts an idempotency key; otherwise the executor reports `unknown` and reconciles (§12.3).
7. **Deny by default.** Explicit denial and revocation beat permission (§13).
8. **Efficiency.** Minimize round trips, signatures and bytes (§22).

### 1.5 Non-goals

Agent reasoning, memory and UI; provider back-office systems; search ranking and reputation scoring; mandating a PSP, cloud or crypto library; central orchestration; global ordering, exactly-once execution, global transactions; deniability (§11.6); privacy and logging policy; key generation and custody; integration with external delivery systems.

## 2. Terminology

| Term | Meaning |
| --- | --- |
| Actor | A signing principal. `actor_type`: person, organization, agent, surface, verifier, gateway |
| Owner | The person or organization that controls an Agent |
| Agent | An Actor that sends Envelopes and signs Records on an Owner's behalf |
| Surface | An Agent that exposes a provider's systems as Records. Unclaimed until the Provider claims it |
| Provider | The real-world supplier a Surface represents; the authority for availability and execution |
| Verifier | An Actor that checks facts about others and issues `verified` attestations, or adjudicates challenges |
| Payment Actor | The Actor an agreement names to move money on its rail and to sign `authorized`, `settled`, `released` and `refunded` |
| Gateway | An Actor that stores mailboxes and routes Envelopes. Anyone can be one |
| Home Gateway | The Gateway the Actor's Card names as holding its mailbox (`endpoints[].kind: relay`); a v1 Actor has exactly one (§16.3) |
| Handle | A readable name for an Actor, `@name.agent` or `@name.scope.agent`, issued by a Gateway under a scope it holds (§4.2). Optional, and never a domain name |
| Runtime | A component an operator MAY run apart from its Gateway for Agents that keep no keys: it holds their keys, signs and decrypts for them and offers them the `runtime.v1` API (§17). Towards the network it is an Agent-side client, never part of the Gateway |
| Room router | The Gateway hosting a Context; it reads that one Record for membership and ACL (L2) |
| Directory | Lookup from Actor ID to the current `agent_card`, `delegation` and `revocation`. A cache, never an issuer |
| Index | Search over published signed Records. Results are untrusted until re-verified |
| Relay | The delivery function of a Gateway (mailbox, forwarding, `delivered`); at-least-once. This document says Gateway for the Actor that signs and Relay only for that function |
| Mailbox, Cursor | Append-only Envelope log per Actor or Context, and the opaque position token into it. The source of truth for messages. A mailbox here is a Gateway's log of Envelopes and not an email mailbox: email appears in this document only as a comparison (§1.2) |
| Envelope | Transport structure: nine fields, `body` among them |
| Record | Signed unit of meaning. 6 Core types plus the reserved `context`, one lifecycle |
| Reference | How a Record is named anywhere: `issuer/id`, or `issuer/id@v` to pin a version; `env:from/id` names an Envelope |
| Context | A scope with membership and ACL (room, thread), defined by a `context` Record |
| Exchange | A thread of Records, named by the pinned reference of its first Record; later Records belong to it only through `parent`. A deal is one kind of exchange |
| Delegation | Scoped, audienced, expiring authority granted by an Owner. A one-shot delegation is an approval |
| Attestation | Signed Record stating one fact, a claim by a party in a position to know. Core kinds: delivered, rejected, read, processed, verified; `federation.v1` adds routable and a Profile adds executed, accepted, authorized, settled, released, refunded, resolved, challenged |
| Artifact | Encrypted reference to a file or result |
| Profile | A named optional layer (`sealed.v1`, `exchange.v1`) that an Actor or a Gateway lists in `supported.profiles`; it adds types, fields or rules and never changes what Core means |
| Extension | A namespace of fields under `ext` (§15.1) |
| Binding | Another transport for the same objects, or the mapping that carries another protocol in them (§10.2, §10.6, §15.2) |
| Adapter | Translation layer to external protocols. Never changes Core semantics |

## 3. Authority model

Every fact has exactly one authority. Responses from Directory, Index and Gateway are conduits for signed Records; trusting a response never substitutes for verifying the original signature.

| Fact | Authority | Everyone else |
| --- | --- | --- |
| Actor identity | Inception key and `agent_card` version chain | Verify ID ↔ key derivation |
| Owner-Agent relationship | The Owner's `delegation` | Verify signature, scope, expiry |
| Key status | The Actor's `agent_card` and `revocation` | Directory caches; clients verify |
| Card freshness (the current `v`) | The Actor, in the latest version it signed; its Home Gateway is the first place to look, never the authority | Two-path check before a consequential action (§18.2) |
| Endpoint routing | The Actor's `agent_card.endpoints[]`; at L3 confirmed by the Home Gateway's route lease (§16.3) | Verify the Card chain |
| Context membership | The controller's `context` | Verify ACL and epoch |
| Resource availability | Provider | Show freshness; re-check before execution |
| Agreement terms (`exchange.v1`) | Multi-signature of participants | Compare `terms_digest` |
| Settlement (`exchange.v1`) | The payment Actor named in `agreement.payment` (PSP, escrow) | Verify `authorized`, `settled`, `released`, `refunded` from that Actor only (§14.1) |
| Result quality (`exchange.v1`) | Recipient, Verifier | `accepted` / `challenged` attestations |
| Transport delivery | The Gateway holding the recipient's mailbox or the room log (§7, §10.5) | Verify `delivered` against the digest of the Envelope that was sent |
| Protocol meaning | Governance | Schemas, rules, versions |

A Gateway operates endpoints. It issues no identities and has no authority over any deal, and a Gateway that stops routing an Actor (`suspended`) changes a service state only; the Actor's identity is untouched (§9.1).

## 4. Identifiers (Core; §4.2 is `discovery.v1`)

Actor IDs are self-certifying, derived from keys, and independent of Gateway, Directory and handle. All other identifiers are ULIDs or UUIDv7 (§4.3). Three things are kept apart throughout: an Actor ID proves control of a key and nothing more; who controls the Agent is the Owner's `delegation` (§13); and that an Actor speaks for a company, a domain or an account is a claim, computed from a `represent:` delegation and Verifier attestations (§14.3), never read from a label.

### 4.1 Actor ID

- Format: `agent:` or `owner:` + multibase(base58btc) of a multicodec-prefixed public key. The multicodec code is written as an unsigned varint, so the Ed25519 prefix is the two bytes `ed 01` (secp256k1: `e7 01`), which is what produces the familiar `z6Mk` opening; `agent:z6Mk…` carries exactly the multicodec-prefixed key bytes of `did:key:z6Mk…`. person and organization use `owner:`; every other `actor_type` uses `agent:`. The prefix is part of the ID, so one key pair is one Actor: a key used as `owner:` is never also an `agent:`.
- Algorithms: Ed25519 is the only signature algorithm in v1 Core. The multicodec prefix leaves room for secp256k1, so a Nostr key is a well-formed Actor ID and the key bytes of a did:key identifier convert without change (the encoding is shared and nothing else of that DID method is: rotation and recovery are this protocol's own, §11.5), but the Schnorr suite is registered by the Nostr binding (§10.6), not by Core; a v1 Core verifier answers it with `unsupported`.
- Creation: derive the ID from the inception signing key pair. The primary signing key of the first `agent_card` (`v: 1`) MUST derive the ID. The prefix follows `actor_type`: `owner:` for `person` and `organization`, `agent:` for `agent`, `surface`, `verifier` and `gateway`; a Card whose prefix and `actor_type` disagree is invalid, and `agent_card.owner` is always an `owner:` ID.
- Rotation: the ID is stable. Each version carries `prev`, the content digest of the version before it (§7), and is never dated before it (Appendix F I11).
  - **Pre-rotation.** The primary signing key commits to its successor in `next_key_hash`, the SHA-256 of the next public key, which is kept apart from the current key; a rotation version introduces the committed key as primary, is signed by that new key, and carries a fresh `next_key_hash`.
  - **The recovery key.** Every version also carries `recovery_key_hash`, the SHA-256 of a recovery key kept apart from both (offline, or on a second device).
  - **Three kinds of version.** Three kinds of version exist, and a verifier tells them apart by content alone: an ordinary version keeps the primary key, `next_key_hash` and `recovery_key_hash` unchanged (it MAY add both hashes together when the previous version had neither, which is how an L0 Card grows up; the two are always present together or absent together, so a stolen current key cannot plant a recovery key of its own beside an existing commitment; the version that adds them is a key event, §11.5, because the next rotation is checked against what it committed to); a rotation version changes the primary key as above and `next_key_hash` with it, and leaves `recovery_key_hash` unchanged; a recovery version (below) changes all three.
  - **What the current key cannot do.** A version that changes `next_key_hash` without rotating, or `recovery_key_hash` without recovering, is invalid, so a stolen current key can neither re-commit the next key nor replace the recovery key. The same holds for `owner` (§7): an ordinary or rotation version carries its predecessor's `owner` unchanged, or adds one where the predecessor had none (a claim, §9.1), and only a recovery version names another Owner or none, so a stolen current key cannot swap out the Owner whose revocation is the remedy against it (§11.5).
  - **L0.** At L0 both commitments MAY be omitted; until it adds them, such an ID cannot rotate or recover and is replaced by a new ID instead.
- No issuer. Of two Cards with the same ID the higher `v` wins, with one exception, below: a recovery version outranks `v`. Equal `v` with different content means that one key signed twice, and what follows depends on what the two versions say about keys.
  - **What counts as a competitor.** A same-`v` version counts at all only when it verifies as a successor of the held predecessor (signed by that predecessor's primary key, by the key it committed to, or as a recovery version over a key event); anything else is simply invalid, whoever signed it, and changes nothing, so a copied Card with an edited body or a stale signature can never freeze an Actor.
  - **The same step told twice.** Two same-`v` versions that name the same keys (the same primary key, `next_key_hash` and `recovery_key_hash`) are one step told twice and never a fork: the same hands signed both, and nothing about who holds the Actor is in doubt. A retry that rebuilds a version instead of resending the same bytes does this (a new `iat` is enough), and so do two processes that each publish `v`+1. Of the two chains, every verifier keeps the one whose current `v` is higher and, at the same current `v`, the one whose current version has the smaller content digest (§7), compared as raw bytes, whatever order they arrived in; a Directory answers the publisher of the other with `conflict`, and the Actor stays active. A publisher builds each version once, stores it and resends exactly that; one that is answered `conflict` resolves its own Card and builds on what it finds, and if it did not expect a competitor it treats its current key as copied and rotates.
  - **Different keys.** Two valid same-`v` versions signed by different keys are never a fork: one of them is the rotation to the committed key, and it supersedes the ordinary one together with everything that descended from it, whatever order a Directory learned them in, so a current key in a thief's hands loses to the rightful holder's rotation and a key retired by a rotation cannot fork a chain from afar; and nothing at the same `v` ever supersedes a recovery version.
  - **A fork.** Two same-`v` versions signed by the same primary key that name different keys (two rotations that commit to different next keys, two first versions with different commitments) are a fork: that key is in more than one pair of hands, and the two disagree about who comes next. Every Directory that sees a fork keeps both versions, marks the Actor conflicted (its `resolve` answer carries `conflict: { v, versions[] }`, the forked `v` and both versions, so that a Directory learning from it records the same fork), serves the last unconflicted version as current so that what the key signed before the fork still verifies, and refuses to route to or from the Actor with `conflict` until a recovery version arrives (§10.4): the key is in two hands, and whatever the mailbox took in could be read by whoever holds the copy.
  - Pre-rotation does not settle a fork: the two branches name different next keys, whoever copied the signing key may hold the next key with it, and once an Actor is conflicted a Directory refuses every further version other than a recovery version, with `conflict`.
  - **The recovery version.** A fork, and any compromise, is settled only by a recovery version, whose shape is fixed so that any verifier recognizes it: its base is a key event the issuer still trusts (v1, the version that added the commitments, a rotation or an earlier recovery) and its `prev` is the content digest of that base; its `v` is the base's `v`+1, exactly, whatever the versions it voids carry; its `keys[]` includes the recovery key as `{ kid, purpose: "recovery", alg, pub }` with SHA-256(`pub`) equal to the base's `recovery_key_hash`, which is also the `recovery_key_hash` of every version since the base, since nothing else changes it; it introduces a fresh primary signing key with a fresh `next_key_hash` and a fresh `recovery_key_hash`, one that no earlier version of the chain committed to; and its `sigs[]` carries two entries by the issuer, one with the recovery `kid` and one with the new primary `kid`, so that the version proves both recovery authority and possession of the key that will sign from now on.
  - Recovery outranks `v`: a verifier that holds the base, and no other recovery version signed by the same recovery key, applies the recovery version whatever the `v` of the versions it holds after the base, discards every one of them (they leave the Actor's history, and a key that only they introduced verifies nothing; the base's own primary key stays retired as of the recovery, and its rightful holder revokes it with a backdated `effective_at` if it was stolen, §11.5) and serves the recovery version as current, so a thief who keeps publishing ever higher versions with a stolen key cannot keep the rightful holder out.
  - **Replays.** A recovery version whose base the verifier neither holds nor is handed, verified from v1, in the same chain is rejected: nothing shows that it follows from the Actor's chain. The one exception is the fork of the next item, where the version's own two signatures are the evidence.
  - **One recovery per recovery key.** A recovery key signs one recovery: the version that spends it commits to a fresh one, and a second recovery version signed by the same recovery key that names other keys is a fork of the recovery key itself, whatever base it names and whichever of the two a verifier saw first, which no key can settle (one that names the same keys is the same recovery told twice, as above, over the same base or another, so whoever recovers generates the fresh keys once and uses them in every attempt). The second one counts by its own two signatures, the recovery key's and its primary key's. Where it claims to stand does not matter, so its base may be among the versions the first one voided, or not be served. The Actor is conflicted for good: a Directory keeps both versions, shows them in `conflict` with the lower of their two `v` (never below the version after the first one that committed to that recovery key, which is as far as the key reaches), and serves what lies below it, and its Owner revokes the Actor and delegates to a new ID (§11.5).
  - A recovery key that has been used therefore gives nobody the Actor. Whoever comes by it later can end the Actor and cannot take it, so its holder destroys it once the recovery version is registered (SHOULD).
  - A forked version is never a base. The version does not enumerate the branches it voids, because the rule is "everything after the base".
  - **Meanwhile.** Until a recovery version appears the Actor is treated as compromised at the forked `v`, and an Agent's Owner MAY instead revoke the Agent and delegate to a new ID (§11.5).
  - An Actor that has lost every key it could still sign a version with (the current key, the committed next key and the recovery key) is dead and its ID is retired. Retired IDs are never reused.
  - Appendix F is the normative statement of this bullet and of §11.5, as a state machine with the vector that pins each case; where this prose and Appendix F differ, Appendix F governs.

### 4.3 Other identifiers

| Identifier | Format |
| --- | --- |
| `Record.id`, `Envelope.id`, `rid` | A string of 1 to 36 characters from `[0-9A-Za-z-]`, unique per issuer, that receivers treat as opaque and compare byte for byte, never normalizing case or form. Writers use a ULID in its canonical text form (26 characters of Crockford base32, upper case; RECOMMENDED) or a UUIDv7 (RFC 9562) in its canonical text form (36 characters, lower-case hexadecimal with hyphens); nothing in the protocol parses either. `Record.id` is stable across versions |
| `ctx` | A pinned reference to a `context` Record, `issuer/id@v` (§5, §6); never a bare `id` |
| `kid` | `agent_card.keys[].kid`: 1 to 64 characters, none of them `#`, `/` or whitespace, so that a key written `<actor id>#<kid>` reads one way and never as a Record reference (§11.5); unique within the Actor |
| Extension namespace | Dotted lowercase labels ending in a version: a reverse domain for a vendor (`kr.example.restaurant.v1`), a bare name for a Profile or binding this specification registers (`exchange.v1`, `bridge.v1`); `ext` keys, `profile` and `supported.profiles[]` all use it |

Paths such as `https://gw.example/v1/actors/{id}` in the HTTP binding (§10.4) are routing, not identity.

## 5. Envelope (Core; `enc` is `sealed.v1`)

The Envelope is the only structure a Gateway sees. It has nine fields, `to` and `ctx` counting as one: eight for routing and `body`, where meaning lives, in the Record. When encrypted, a Gateway sees only who, to whom, when. The header is a custom format. JWS, JWE and DIDComm were passed over because HPKE multi-recipient sealing and a small surface mattered more than reuse; mappings to those formats are bindings (§10.6).

```json
{
  "v": 1,
  "id": "01JC5HQ8F7X2K9M3N4P5R6S7T8",
  "from": "agent:z6MkA…",
  "to": "agent:z6MkB…",
  "iat": "2026-11-05T20:00:00Z",
  "exp": "2026-11-06T20:00:00Z",
  "enc": { "alg": "hpke-x25519-sha256-chacha20",
           "recipients": [ { "to": "agent:z6MkB…", "kid": "enc-3", "ek": "…", "ct": "…" } ] },
  "body": "<base64url ciphertext>",
  "sig": { "actor": "agent:z6MkA…", "kid": "sig-1", "alg": "ed25519", "val": "…" }
}
```

| Field | Required | Meaning |
| --- | --- | --- |
| `v` | MUST | Major version, integer. This spec: 1 |
| `id` | MUST | ULID or UUIDv7 (§4.3); the dedup unit, scoped to `from`, and never reused for another recipient, so (`from`, `id`) is globally unique and names one delivery |
| `from` | MUST | Sender Actor ID |
| `to` or `ctx` | exactly one | `to`: one recipient Actor ID. `ctx`: a Context reference pinned to its version, `issuer/id@v` (§6, §10.5), which MUST equal the Record's own `ctx`. Both present, more than one recipient in `to`, or a mismatch between the two `ctx` values, is `invalid_request`. An L0 or L1 implementation rejects an Envelope with `ctx` with `unsupported` and never stores it as an ordinary message |
| `iat`, `exp` | MUST | RFC 3339 UTC. `exp − iat` ≤ 7 days; a Gateway MAY enforce a lower limit |
| `enc` | if encrypted | Absent means `body` is a plaintext Record. With `to`, `recipients` holds exactly one wrap and its `to` equals the Envelope's `to`; with `ctx`, one wrap per current member (§10.5) |
| `body` | MUST | Record object, or base64url ciphertext |
| `sig` | MUST | Transport signature by `from`. `actor` is always an Actor ID and equals `from`; `kid` names a key in that Actor's Card, which may be a device key (§11.1, §11.3) |

`enc` has two shapes. Explicit recipients: `{ alg, recipients: [{ to, kid, ek, ct }] }`, one content key HPKE-wrapped per recipient. Context: `{ alg, epoch }`, the current epoch key.

- Semantic fields (`profile`, `exchange`, `parent`) live in the Record; a Gateway never sees them when the body is encrypted.
- One recipient per Envelope with `to`, and exactly one wrap in `enc.recipients`. An Envelope with `ctx` MAY carry one wrap per current member, the sender included, under §10.5. The same Record sent to several Actors outside a room is several Envelopes, one each; group delivery goes through `ctx` (§10.5). One Envelope, one recipient, one ack keeps `delivered` unambiguous.
- Dedup Envelopes by (`from`, `id`); dedup Records by (`issuer`, `id`, `v`) (§12.2).
- Reject expired Envelopes. `iat` is the creation time, checked once at ingress: the first Gateway or direct endpoint to accept an Envelope from its sender requires `iat` ≤ now + 300 s and now ≤ `exp`. An `iat` more than 300 s in the future is rejected with `invalid_request`.
  - **Forwarding.** A Gateway that forwards an Envelope it already accepted, or receives one from another Gateway, checks only now ≤ `exp`, so an outbox may hold an Envelope for as long as the Envelope itself allows, at most 7 days.
  - **The sender's own signatures.** The receiver checks every `sigs[]` entry whose `actor` is `from` against the Envelope that carried it: `iat − 300 s ≤ signed_at ≤ iat + 300 s`, so a sender's own signature sits within 300 s of the Envelope's `iat`. That bounds future dating to the skew window of an honest ingress and does not bound past dating to it: an ingress checks no lower limit on `iat` (an outbox may hold an Envelope until its `exp`), so a sender can date an Envelope, and its own signature with it, up to `exp − iat` in the past, at most 7 days, which is the window the acceptance rule of §11.5 covers.
  - **Carried signatures.** Entries whose `actor` is not `from`, carried along in a countersigned or attached Record, are checked only for `signed_at ≤ iat + 300 s`; they were bounded by the Envelope that first carried them, and the allowance covers a signer whose clock runs ahead of the forwarder's.
  - An entry that fails its window is dropped (§6.2); a Record left without a verifying entry by its issuer is invalid, which rejects the Envelope's Record when the dropped entry is the sender's own and the carrying Record when the invalid one sits in `attached[]`.
  - A carried signature has no lower bound from the Envelope: an attached Record is reusable for as long as its own `exp` allows, which is the point of carrying a standing delegation, and nothing shorter is promised.
  - **Resending.** A Record resent after its earlier Envelope expired is re-signed by the sender with a fresh `signed_at` (§12.2), and so is any Record an Actor forwards that it signed earlier: the content is unchanged, and a receiver that holds the earlier copy keeps the earliest entry that verifies (§6.2).
  - Entries inside `attached[]` Records are carried signatures and are bounded the same way, `signed_at ≤ iat + 300 s`, whoever signed them.
- Limits are applied before any signature is checked, so that a flood costs the Gateway parsing and nothing more: one Envelope at most 256 KiB in its JCS form, which no re-serialization changes (every Gateway accepts at least this; one that accepts more does so by its own policy, and a sender does not count on it), at most 100 Envelopes and 64 Cards per `send` request, so a request body is at most 100 Envelopes' worth plus what the Cards need (the reference allows 4 MiB for `cards[]`), JSON nested at most 32 levels, `attached[]` as §13 bounds it. A body, an Envelope or a `cards[]` that exceeds a size or count limit is `payload_too_large` (413); a body that is not I-JSON, a name repeated or a nesting too deep is `invalid_request` (400); attachments travel as `artifact` references. A Gateway bounds what it reads from another Gateway as well: an answer to a lookup or to a forwarded `send` is parsed under the rules of a request (I-JSON, nesting at most 32 levels), and one that breaks them, or is longer than the limit the Gateway sets for an answer (the reference: 4 MiB), is treated as no answer.
- Encoding: the wire form is I-JSON (RFC 7493): UTF-8, no duplicate names, integers within ±(2^53 − 1), and a receiver rejects a document that violates it (a duplicate name is `invalid_request`, never a silent "last one wins"). Every signature and every digest covers the JCS (RFC 8785) form of the object, never the wire bytes, so any party MAY re-serialize an Envelope or a Record as long as its values are unchanged, which a round trip through an I-JSON parser guarantees; a Gateway that stores and forwards exactly the bytes it received is equally correct and need not. `payload.data_bytes` exists for carried protocols whose own bytes must survive (§7).
- Ignore unknown fields. Reject unknown `alg` with `unsupported`.

## 6. Record (Core)

A Record is a signed unit of meaning. Every type shares one schema and one lifecycle, so an implementation has one path: verify the Envelope, verify the Record, evaluate the lifecycle.

```json
{
  "id": "01JC5GZ3W8Q1R2S3T4U5V6W7X8",
  "type": "proposal",
  "profile": "exchange.v1",
  "v": 1,
  "issuer": "agent:z6MkA…",
  "subject": "agent:z6MkS…/01JC5E…@2",
  "exchange": "agent:z6MkA…/01JC5F…@1",
  "parent": "agent:z6MkS…/01JC5E…@2",
  "iat": "2026-11-05T20:00:00Z",
  "exp": "2026-11-05T21:00:00Z",
  "summary": "Table for 4 on Nov 7 at 19:00, 48,000 KRW",
  "body": { "terms": { "slot": "2026-11-07T19:00:00+09:00", "party": 4,
                       "price": { "amount": "48000", "currency": "KRW" } } },
  "ext": { "kr.example.restaurant.v1": { "seating": "terrace" } },
  "sigs": [ { "actor": "agent:z6MkA…", "kid": "sig-1", "signed_at": "2026-11-05T20:00:00Z", "alg": "ed25519", "val": "…" } ]
}
```

Record references. A Record is referred to as `issuer/id`, or `issuer/id@v` to pin a version: `agent:z6MkS…/01JC5E…@3`. Every reference inside a Record carries `@v`: `parent`, `exchange`, `supersedes`, `subject` when it names a Record, `ctx`, `evidence[]`, `attached[]` and every `*_ref` field, so that nothing a Record points at can shift when a later version appears, and never a bare `id`, because `id` is unique only within one issuer. A reference without `@v` appears in lookups (`resolve`, fetch, `query`), where it means the current valid version, and as the subject of a `revocation`, where it means every version (§11.5). A superseded Record stays superseded even if the superseding version later expires or is revoked, and a verifier follows a `parent` chain for at most 64 links and never around a cycle: a Record that lies further than that from the anchor of its exchange is outside the exchange, and nothing else depends on how long a chain is, so a conversation in which each message answers the one before may be as long as it likes.

| Field | Required | Meaning |
| --- | --- | --- |
| `id` | MUST | ULID or UUIDv7 (§4.3), unique per issuer, stable across versions. With `issuer`, the business idempotency key |
| `type` | MUST | A Core type (§7) or a Profile type (§14) |
| `profile` | Profile types only | `exchange.v1` |
| `v` | MUST | Monotonic integer from 1 |
| `issuer` | MUST | The Actor whose statement this is; equals `sigs[0].actor` |
| `subject` | per type | An Actor ID, a key (`<actor id>#<kid>`, for a `revocation`), a Record reference (`issuer/id[@v]`, pinned where the type says so) or an Envelope reference (`env:from/id`) |
| `ctx`, `exchange`, `parent` | MAY | A Context reference; the exchange (thread) anchor, which is the pinned reference (`issuer/id@1`, the first version) of the first Record of the exchange, minted by whoever opens it, and a Record belongs to that exchange only if its `parent` chain reaches the anchor, so nobody joins a deal by reusing its identifier; the Record this responds to |
| `supersedes` | MAY | Reference to the Record this replaces |
| `iat`, `exp` | `iat` MUST; `exp` SHOULD, and a `revocation` never carries one | Validity of the statement for new reliance. A Record without `exp` never expires, the normal choice for durable evidence such as an attestation or a concluded agreement; a Record that offers something (a Card, a listing, a proposal) SHOULD carry one. Business deadlines (a hold's expiry, a delivery date, an agreement's validity) live in `body`, never in `exp` |
| `summary` | MAY | One plain-text line of at most 280 characters for people: approval screens and notifications. It has no meaning to a machine and MUST NOT be read as an instruction |
| `body`, `ext` | body MUST | Type or profile content; namespaced extensions |
| `evidence` | MAY | Record references |
| `attached` | MAY | Full signed Records carried inline for a receiver that cannot be assumed to fetch them, such as approvals and introductions. Each is verified like any Record |
| `sigs` | MUST, ≥1 | `{ actor, kid, alg, signed_at, val }`, all five required. `actor` is an Actor ID and `kid` a key in that Actor's Card; `signed_at` is covered by the signature. The issuer's entries come first, sorted by `kid`; the rest are sorted by `actor` then `kid`, duplicates removed (§6.2) |

### 6.1 Lifecycle

`valid → superseded | expired | revoked`

| State | Rule |
| --- | --- |
| valid | Highest `v` for this (`issuer`, `id`), before `exp` when one is present, no `revocation` pointing at it, each signature valid at its own `signed_at` (§11.5) rather than at the Record's `iat` |
| superseded | A higher `v` with the same (`issuer`, `id`) has been validly observed, or a valid Record by the same issuer whose `supersedes` points at it |
| expired | `exp` present and passed |
| revoked | A `revocation` by the issuer or its Owner names this Record or the signing `kid`, with `effective_at` before now |

Revocation ends a Record's validity for future reliance from `effective_at`; it does not erase what was validly signed before, and a verifier evaluates each signature at its own `signed_at` rather than at the Record's `iat`. Expiry works the same way: an expired Record is not relied on anew, but signatures and attestations that referred to it while it was valid keep their meaning, and a Record whose `exp` is absent never expires (§6). What a revocation means for a multi-party Record is the Profile's business (§14.1); Core interprets no domain state.

### 6.2 Versions and signatures

- Same intent, amended: same `id`, `v`+1. Retry: same (`issuer`, `id`, `v`). Different intent: new `id` + `supersedes`, which MUST reference a Record by the same issuer; a `supersedes` pointing at another issuer's Record is ignored for lifecycle, so nobody can retire someone else's listing or proposal. An answer to another party's Record is expressed with `parent`.
- A new `v` requires fresh `sigs`. The issuer MUST appear in `sigs`; its entries come first, sorted by `kid`, and the remaining entries are sorted by `actor` then `kid`, with duplicates removed, so copies of the same (`issuer`, `id`, `v`) that differ only in `sigs` are one Record and receivers merge `sigs` by union into that order. Two entries with the same `actor` and `kid` but different `signed_at` are the same signer signing twice (a resend after expiry, §12.2); receivers keep the entry with the earliest `signed_at` that verifies and drop the rest. An entry that does not verify is dropped, never a `conflict`. Receivers dedup on (`issuer`, `id`, `v`) and ignore lower `v`. A receiver normalizes its own copy of the Record and never the Envelope that carried it: a plaintext Envelope's `sig`, and the digest a `delivered` names, cover the Record as it was sent, the order of `sigs[]` included. The same holds for a Record inside `attached[]`: the signatures of the Record that carries it cover it as it was attached, so a verifier checks it on a copy and leaves it as it is.
- Every `sigs[]` entry carries `signed_at`, with no exception, and the only signature input is `Sign(utf8("aep/1/record") ‖ 0x00 ‖ JCS(Record without sigs) ‖ 0x00 ‖ utf8(signed_at))` (§11.3). An entry looks like `{ "actor": "agent:z6MkA…", "kid": "sig-1", "signed_at": "2026-11-06T10:00:00Z", "alg": "ed25519", "val": "…" }`. All signers sign the same Record bytes plus their own `signed_at`; each entry's `actor` and `kid` name the key, which MUST be valid at that `signed_at` (§11.5).

Value encoding, for every Record and Envelope: numbers are integers (no floating point); money is `{ amount: "<decimal integer string, minor units>", currency: <ISO 4217 code or CAIP-19 asset id> }`, always a string so that an 18-decimal asset and a 2-decimal one have the same type, and never a float; USDC on Base is `currency` `eip155:8453/erc20:0x8335…2913` with `amount` in 6-decimal units (`"750000000"` is 750 USDC); protocol timestamps (`iat`, `exp`, `signed_at`, `effective_at`, `accepted_at`, `valid_from`, `valid_until`, `at`) are RFC 3339 UTC in whole seconds with the `Z` suffix, while business times inside `body` (a slot, a deadline, a hold's expiry) MAY carry a UTC offset; strings are UTF-8; fields with no value are omitted, never `null`; binary values are base64url without padding; every digest in this document (`artifact.digest`, `content_digest`, `terms_digest`, `schema_digest`, `result_digest`, `record_digest`, `prev` and every other Card content digest, `next_key_hash`, `recovery_key_hash`, the Envelope digest in `delivered`) is SHA-256 encoded the same way; `next_key_hash` and `recovery_key_hash` are taken over the raw public key bytes exactly as carried in `keys[].pub`, without any multicodec prefix, and `prev` is a Card content digest (§7).

Integers stay within ±(2^53 − 1); anything larger is a decimal integer string by rule, never a JSON number.

JCS canonicalizes objects, not arrays: every array keeps the order its writer gave it, a verifier never reorders one, and the signature covers that order.

Arrays that are sets (`keys[]` keyed by `kid`, `handles[]`, `audience[]`, `members[]` by `id`, `parties[]` by `agent`, `capabilities[]` by `name`, `enc.recipients[]`, `artifact.keys[]` and `epoch_keys[]` by `to`, `evidence[]` and `attached[]` by reference) MUST NOT contain two entries with the same key, and a writer MUST emit them sorted by that key (byte order of the key's string form) so that independent producers emit identical bytes; a receiver never reorders what it verifies, since the signature covers the writer's order, and does not reject a Record that arrives unsorted; `sigs[]`, which no signature covers, follows the order of §6.2.

The shorthand (`id`, `v`) in this document always means (`issuer`, `id`, `v`).

## 7. Core Record types (Core)

Core defines 6 types and reserves a seventh, `context`, for `groups.v1`. They cover connection only; what is exchanged is defined by whatever rides in `payload` or by a Profile (§14). One Record states one fact.

| type | issuer → subject | body | Rule |
| --- | --- | --- | --- |
| `agent_card` | the Actor | `actor_type`, `owner`, `handles[]`, `prev` (the content digest of the previous version, or of the key-event base in a recovery version; the content digest of a Card version is SHA-256 of the JCS of the Record without `sigs`, which is what `prev` names and what a cache compares to tell whether two copies are the same Card), `recovery_key_hash` (SHA-256 of the recovery key, §4.1), `keys[]` {kid, purpose: sig·enc·recovery, alg, pub, valid\_from, valid\_until, next\_key\_hash, delegation} (exactly one `purpose: sig` entry without `delegation`, the primary signing key, which alone carries `next_key_hash` and signs Card versions; every other `sig` entry is a device key and carries its `delegation` and `valid_from`, §11.1; a `recovery` key appears only in a recovery version, §4.1), `endpoints[]` {kind: relay·direct, url (an absolute https URL with a host and no userinfo, query or fragment, or http only inside a `local` deployment, §16.4; for a relay endpoint the Home Gateway's `/v1` base; a verifier rejects a Card whose `url` does not parse so, and a Gateway additionally refuses to connect to private, loopback or link-local addresses outside a `local` deployment, §18.1), gateway (the Home Gateway's Actor ID, required for relay), and for a direct endpoint spki, the pinned TLS public key as base64url of the DER SubjectPublicKeyInfo, required} with at most one `kind: relay` entry, since a v1 Actor has one Home Gateway (§16.3), `capabilities[]` {name, description, tags\[\]} (what the Actor offers, for discovery: `name` is a reverse-domain namespaced string such as `com.example.translate`, `description` one line for people, `tags[]` short lowercase words; what a capability means and how it is invoked is a Profile's or a carried protocol's business, §9.3, §14.0), `supported` {v, suites, content\_types, profiles, extensions\[\] {ns, required, schema\_digest}}, `policy` {visibility, audience, contact, rate\_limit}. A Profile or binding adds its own Card fields under `ext["<namespace>"]`: `exchange.v1` puts `represented_principal`, `claim` and the invocation details of its capabilities there (§14.0); the A2A binding puts the A2A endpoint there (§15.2) | The version chain is the key event log: v1, the version that added the commitments, every rotation and every recovery (§11.5). `exp` ≤ 30 days SHOULD |
| `delegation` | Owner → Agent, Agent → its own device key, Provider Owner → Surface | `scope[]` (`verb:target`, §13: Core defines `disclose:<scope>` and `represent:<actor id>`; a Profile registers its own verbs, `exchange.v1` adds `command:<ref>` and `agree:<ref>`), `audience[]`, `constraints` {max\_amount, time\_window, target, record\_digest}, `human`, `parent` (the delegation this one narrows, as `issuer/id@v`), `key_kid` (for a device key) | Sub-delegation may only narrow (§13) |
| `revocation` | Owner or Actor → a Record reference (`issuer/id@v` for one version, `issuer/id` for every version), or a key as `<actor id>#<kid>` | `reason`, `effective_at` | Permanent: no `exp`, never re-validated by expiry. Signed, so anyone may forward and cache it. Precedence and reach are in §11.5 |
| `context` (reserved) | controller | `kind`, `controllers[]`, `members[]` {id, role, since\_epoch}, `acl` {read, write, invite}, `join` (open·approval·invite), `epoch`, `epoch_keys[]`, `key_package_ref`, `predecessor` (the `context` this one succeeds, `issuer/id@v`, §10.5) | Core reserves the schema; membership epochs, room routing, group encryption and controller behavior are defined only by `groups.v1` (§10.5), and L0 and L1 implementations MUST NOT interpret `context` for routing. Membership change = `v`+1, `epoch`+1, key rotation. The Gateway hosting a room reads this Record for membership and ACL only and never reads message bodies |
| `payload` | Agent | `content_type`, then one of `data` (JSON under the value rules of §6.2: integers and no `null`), `data_bytes` (base64url of the original bytes, for carried protocols, whose own JSON may hold floating-point numbers and `null` and whose bytes must survive re-serialization) or `data_ref` (artifact reference), optionally `content_digest` (SHA-256 of `data_bytes`, of the artifact's bytes for `data_ref`, or of JCS(`data`) for `data`), and `foreign` (the carried protocol's own identifiers, preserved verbatim: `{ protocol, version?, context_id?, task_id?, message_id?, request_id? }`) | Another protocol's body (A2A message, UCP checkout, AP2 mandate) or the Core message format below. Gateway and Core do not interpret it. (`issuer`, `id`, `v`) idempotency still applies |
| `attestation` | Gateway, Agent, Verifier → Envelope, Record or Actor | `kind` (delivered·rejected·read·processed·verified; `federation.v1` adds `routable`, §16.3), `at`, `claims`, `error` | One fact. A Gateway issues only `delivered`, transport `rejected` and, at L3, `routable`; `verified` is for introductions and vouching |
| `artifact` | issuer | `uri` (HTTPS, MAY be a signed URL), `digest`, `size`, `content_type`, `exp`, `keys[]` {to, kid, ek, ct} | Content lives outside the protocol, encrypted by the sender with a content key wrapped per recipient exactly as `enc.recipients` (§11.4): the stored bytes are a 12-byte random nonce followed by the ChaCha20-Poly1305 ciphertext of the content under the content key with the Record's `issuer/id@v` as associated data, `digest` is the SHA-256 of those stored bytes, and `size` their length. The receiver fetches by `uri`, checks `digest` and `size`, and unwraps its key. `uri` MUST be https, MUST NOT resolve to a private network, MUST NOT be followed across hosts on redirect, is fetched under the `size` cap, and is dead after `exp` |

Core attestation kinds, with who may issue each and what it names. A receiver ignores an attestation whose issuer is not entitled to its kind.

| kind | Issuer | subject | Meaning |
| --- | --- | --- | --- |
| `delivered` | the Gateway that holds the acked mailbox or room log: the recipient's Home Gateway, or for a room the room's Gateway or an L3 mirror (§10.5); never a sender, a forwarder or a direct endpoint | `env:from/id` of the first Envelope covered; `claims.envelopes[]` lists every Envelope covered as `{ id, digest }`, `digest` = SHA-256 of the JCS of the whole Envelope, `sig` included, so a third party can tell which bytes were delivered | The recipient's client acked the cursor covering the Envelope (mailbox acked). Every Gateway MUST issue it. Not proof that anything was verified. Never issued for an Envelope whose body is a Gateway control attestation (a `delivered`, `rejected` or `routable` issued by a Gateway), so receipts do not chain. A sender MUST ignore a `delivered` whose issuer is not the Gateway that the recipient's Card named as `kind: relay` in the version the sender used when it sent the Envelope, or in any later version it has since verified (so a receipt from the old Home Gateway during a move still counts); for a room, the room's Gateway or the member's Home Gateway acting as an L3 mirror (§10.5). `actor_type: gateway` is self-declared and proves nothing by itself |
| `rejected` | the Gateway that refused it, or the recipient | `env:from/id` or a Record | Refused. A Gateway's transport `rejected` MUST carry the Error in `error`; a recipient's `rejected` carries one when a transport or format problem is the reason (§11.4) and MAY omit it when it simply declines (A.8), and never carries a body or an instruction. A recipient's `rejected` about an Envelope it could not open travels plaintext (it may be unable to seal an answer, and it carries nothing confidential), and no `rejected` ever answers an attestation |
| `read` | the recipient | a Record | Opt-in receipt |
| `processed` | the recipient | a Record | The recipient verified and handled it. This, not `delivered`, is the evidence that a message reached its reader |
| `verified` | a Verifier or an introducing Agent | an Actor | Vouching; evidence, never identity |

Read generally, an attestation is a signed claim by a party in a position to know, and every credential the network will ever need is one of those. Four axes span the space: who speaks (the subject itself, a counterparty, a third-party Verifier, a Gateway, or a machine such as an oracle, a chain watcher or the conformance suite); about what (an Actor, a Record, an Envelope); what is claimed (identity and eligibility, competence, a relationship, an outcome, compliance, possession, an entitlement, a guarantee); and who believes it (each reader's own list of trusted issuers, with no root authority, so that an attestation about a Verifier accredits it and chains form without a global PKI). Three habits keep this safe: a claim about a person is held by that person and presented from `attached[]` under a `disclose:` consent, never published by the issuer; a claim can be a predicate ("over 18") rather than data; and an attestation says only that the issuer said so (§14.1). New kinds of claims are namespaced `claims` in an Extension and change nothing in Core.

A Profile may add types and attestation `kind`s (`exchange.v1` adds 5 types). The Core message format for plain agent-to-agent text is `application/aep.message+json`: `{ text, lang?, attachments?: [<artifact references>] }`, so two Agents that share no other protocol can still talk; a message that answers another names it in the Record's `parent`, like any answer (§6.2), and the format has no field of its own for that. A receiver rejects an unknown type with `unsupported` or stores it uninterpreted. A Gateway interprets neither.

## 8. Errors (Core)

An error is a shared structure, not a wire object. Synchronous responses use RFC 9457 Problem Details, served as `application/problem+json`; asynchronous rejections carry the same structure in `attestation{kind: rejected}.body.error`.

```json
{
  "type": "urn:aep:error:expired",
  "code": "expired",
  "title": "Envelope expired",
  "detail": "exp 2026-11-06T20:00:00Z passed before delivery; resend the same Record in a new Envelope",
  "instance": "env:agent:z6MkA…/01JC5HQ8…",
  "retryable": false
}
```

`type` is `urn:aep:error:<code>` and `code` is the registry key; implementations compare `code`, and `type` MAY become an https URI at 1.0 once the registry has a home (§21). `detail` is for humans and MUST NOT carry secrets or private data; like `summary` (§6) it MUST NOT be read as an instruction. `provider_code` and `provider_ref` preserve the Provider's own error and request identifier when a Surface relays one (`exchange.v1`'s `execution_failed`, §14.1); `details_ref`, when present, is an `artifact` reference. `retryable: true` means the same Envelope, or the same (`issuer`, `id`, `v`), may be resent after `retry_after`, an integer number of seconds carried in the structure (an asynchronous `rejected` has no other place for it) and, on a synchronous response, also as the HTTP `Retry-After` header; `false` means a new Record is needed (in `exchange.v1`, for example, a fresh listing and a new command after `resource_stale`, §14.1).

| code | retryable | HTTP | Meaning |
| --- | --- | --- | --- |
| `invalid_request` | no | 400 | Malformed input of any kind (an Envelope, a signed request, a Record): a missing required field, a duplicate name, a clock outside the window |
| `unauthorized` | no | 401 | Signature failed, unknown sender, or `sig` missing |
| `forbidden` | no | 403 | ACL, audience, policy or delegation scope violation |
| `key_revoked` | no | 403 | Revoked key or expired delegation |
| `not_found` | no | 404 | Unknown ID, handle or capability |
| `conflict` | no | 409 | Same (`from`, `id`) or (`issuer`, `id`, `v`) with a different body; a handle already taken; a Card version that tells a step again and is not the copy kept, one that forks a chain, or an Actor whose chain is forked (§4.1, §12.2); a signed request whose `rid` was already seen (§10.4) |
| `expired` | no | 410 | Envelope or Record `exp` passed |
| `payload_too_large` | no | 413 | Over the size limit |
| `unsupported` | no | 422 | Version, suite, profile, content type or required extension. Name it in `detail` |
| `rate_limited` | yes | 429 | `retry_after` required |
| `unavailable` | yes | 503 | Recipient, Provider or Gateway temporarily unreachable. Gateway keeps the mailbox |

- Gateway redelivery and business retry are separate. If `retryable`, resend the same `(id, v)`; otherwise issue a new Record.
- A Profile registers the codes its states need, with their HTTP mapping; `exchange.v1` registers `approval_required`, `resource_stale` and `execution_failed` (§14.1). New Core codes go through the registry and are a minor version. Treat unknown codes according to `retryable`.
- Settlement pending and disputes are Exchange states (§14.1), not errors.

## 9. Discovery: finding and being found (§9.1 Core; §9.2 to §9.4 Profile `discovery.v1`; the Index and `query` are `index.v1`)

Discovery is one lifecycle: register, expose, look up. Discovery and messaging are three operations, and none of them is an anti-abuse mechanism: a signature says which key sent a message and a delegation says under whose authority, which makes abuse attributable, not absent. Sybil Actors cost a key pair each, an Index can be enumerated by anyone it admits, and an introduction chain is as sound as its weakest introducer; rate limits, allow and block lists and contact policy (§9.2, §9.3) are the levers this document provides, and an operator's abuse model is its own. `resolve` (ID or handle → Card) and `query` (filter → signed Records) are discovery; `send` (deliver an Envelope) is messaging (§10); the HTTP binding (§10.4) adds only what a mailbox and a cache need around them: `sync` and `ack`, the revocation feed, Record fetch and the policy request. The network (Directory, Index, Gateway) provides first introduction and reachability; after that, connections propagate agent to agent.

| Operation | Input → output | Served by | Rules |
| --- | --- | --- | --- |
| `resolve` | ID or handle → the current `agent_card` with its key-event chain (§11.5), `delegation`, `revocation` | Directory | The response is a cache. Clients MUST verify. A cached answer is revalidated with a conditional request (§10.4: the `ETag` covers the whole answer, so a 304 means that neither the Card nor its delegations, revocations or fork state changed). Two copies are the same Card only when their content digests are equal (§7); an equal `v` alone never means an equal Card, since a step told twice, a fork or a recovery can put a different version at the same `v` (Appendix F) |
| `query` | filter, cursor → full signed Records, `next_cursor` | Index (`index.v1`) | Results are signed Records the client verifies itself; the Index adds only a `score`. A Gateway that runs no Index answers `unsupported`. Any published Record (a Card, a delegation, a revocation, a public Profile Record; never a Gateway's receipts) is fetchable by reference (`GET /v1/records/{issuer}/{id}`, §10.4) |
| `send` | one Envelope or an array → synchronous accepted or Error; asynchronous `delivered` | Gateway, or a direct endpoint |  |

### 9.1 Registration (Core; the Owner parts are `owner.v1`)

1. The Owner derives an `owner:` ID from a root key pair; the Agent derives an `agent:` ID from an inception key pair.
2. The Owner signs a `delegation` (scope, audience, exp) to the Agent whose `scope[]` carries `represent:<owner id>` (§13). This published delegation establishes the relationship and carries no constraints; delegations that carry constraints such as `max_amount` travel only in `attached[]` and are never published, so `resolve` exposes no spending limits.
3. The Agent signs `agent_card` v1 (keys, `next_key_hash`, `recovery_key_hash`, endpoints, supported, policy); `owner` names the Owner, and the Owner's `delegation` is published beside the Card, not inside it.
4. `send` the Owner's own `agent_card` (unless the Gateway already holds it, or it travels in `cards[]`; the Owner is an Actor with a Card like any other, and the delegation's Envelope is `from` the Owner), then the Owner's `delegation`, then the Agent's `agent_card`, to the Home Gateway, in one batch in that order or as separate sends in that order.
   - A batch is not a transaction: each Envelope is answered on its own (§10.4), and a Record is stored only when it verifies on its own, so a batch that fails midway leaves the earlier Records stored, which is harmless, since each is a valid signed Record its Actor meant to publish, and the registrant simply re-sends the batch (every Record is idempotent).
   - What never exists is an Agent's Card without its Owner's delegation: a Gateway that receives a Card naming an `owner` rejects it with `unauthorized` unless it holds, or was handed earlier in the same batch, an unexpired, unrevoked `delegation` issued by that Owner to that Agent whose `scope[]` carries `represent:<owner id>` (§13); a delegation with any other scope, however valid, is not the relationship.
   - A published `delegation` is either such a grant or an Actor's own device-key delegation (§11.1); a Gateway stores no other kind, and it serves under an Actor only that Actor's own delegations and those of the Owner its current Card names. A grant from anyone else may be waiting for the Card version that will name its issuer (a claim, a recovery); until then it is no part of the Actor's answer, so that nobody writes into an answer that is not theirs.
   - A Gateway registers a Card version when it is made: it refuses one whose `iat` lies more than 300 s from its own clock, with `invalid_request` (a version it already holds is exempt, since a resend changes nothing), and for the same reason it refuses one that gives a signing key a `valid_until` which the version before it did not state and which lies more than 300 s before that `iat`, and it refuses one that states another `valid_from` for a device key than the latest version it holds, with `unauthorized` like any version that breaks its chain (§11.5, Appendix F I12). A signer that needs an earlier effective time publishes a `revocation` with `effective_at`, which leaves a Record (§11.5).
   - The grant is checked again whenever the Agent publishes a Card version, so an Owner's delegation SHOULD outlive the Card it covers and is renewed like it (§7), or the Agent's next version is refused. `owner` itself, once a Card carries it, is not the Agent's to change: only a recovery version names another Owner or none (§4.1), so the Owner cannot be swapped out with the very key its revocation is the remedy for.
5. The Agent starts syncing its mailbox (§10.1); routing is live.

Registration and publication are plaintext Envelopes (no `enc`) addressed to the Gateway's own Actor ID, taken from the signed Gateway list or `/.well-known/aep/gateway`, and `from` signed the Record they carry (§11.3) with one exception: a `revocation` is permanent and complete in itself, so anyone may forward one (§11.5), and the Gateway checks the Record and not the messenger.

The Gateway verifies the Card in the body against `from` before any key-status check; this is the one case where the sender is not yet known.

It stores Core identity Records (`agent_card`, `delegation`, `revocation`) in its Directory, hosts `context` Records as rooms, forwards Records whose issuer's `policy.visibility` is `public` or `restricted` to its Index (the Index enforces `policy.audience` on the restricted ones, §9.2), and rejects anything else addressed to itself with `forbidden` (Gateway control attestations are addressed to Actors, never to a Gateway, so this rule never meets one).

Registration MAY be gated by operator policy (invitation, proof of work); the signed Gateway list (§9.4) marks each Gateway `open` or `gated`, so that the L0 quick start can pick an open one.

It returns no secret: there is no API key or token to keep, the Agent's only secret is its private key, and the Gateway role never sees it; a product that both runs a Gateway and holds Agent keys in custody (§11.1) keeps the two in separate components, so that a mailbox compromise is never a key compromise.

Registration is complete only when the Agent resolves its own ID through the Gateway and, when it can, through a second path, and gets back the Card it sent (§18.2 self-audit); until then it treats itself as unregistered.

| Level | States | Authority |
| --- | --- | --- |
| Identity | valid, revoked | Actor, Owner |
| Gateway service | routable, suspended | Gateway |
| Surface claim | self, claimed, verified | The Provider Owner's `delegation` |

A setup URL or manifest is bootstrap input and establishes no ownership. Only the Owner-signed `delegation` establishes ownership.

Solo start: at L0 an Agent MAY be self-owned. `agent_card.owner` is omitted, no `delegation` exists, and the Agent is its own Owner; delegation to its own devices still works. L1 SHOULD have a distinct Owner, and a P1 party whose commands need approval (§14.2) MUST, because approvals are Owner signatures; a self-owned P1 party is limited to what needs none.

## 10. Messaging: 1:1 and groups (Core: §10.1, §10.4, §10.7)

A message is a Record inside an Envelope (§5), delivered with `send`. 1:1 uses `to`; groups use `ctx`. Reception is one mailbox; delivery evidence comes from the Gateway. Registration, publication, revocation and receipts are all Records carried by `send`; `sync` and `ack` are the mailbox binding and carry no meaning of their own. Real time is the fast case of the same path: when the recipient has a `sync` with `wait` open (or a session, §10.2), the call returns an Envelope the moment it is appended; when it does not, the mailbox holds it until it reconnects or `exp` passes. Records, signatures, dedup and delivered evidence are identical either way, and the sender never needs to know which case applies.

### 10.1 Mailbox binding

- `sync(cursor, limit, wait)` → `{ envelopes[], next_cursor }`. `limit` is from 1 to 100, and 20 when absent; a Gateway clamps a value outside that range instead of refusing it. With `wait > 0` the call blocks until something arrives (default cap 30 s; a Gateway MAY cap it lower and clamps a larger value instead of refusing it, so an early empty answer is normal). Each entry of `envelopes[]` is `{ envelope, cursor, accepted_at }`: the Envelope as it was sent, the cursor to acknowledge once the Envelope is stored, and the Gateway's time of append, used for expiry and mailbox operation; signature times come from `signed_at` and the Envelope `iat` (§11.5).
- `ack(cursor)` is idempotent and cumulative. It means the client has durably stored, or deliberately dropped with a local tombstone, every Envelope up to the cursor; it says nothing about processing, which is what processed says. It cannot skip one. A client that wants to ignore an Envelope acks past it and drops it locally. An ack with a lower cursor than one already acked is ignored. Envelopes whose body is a Gateway control attestation (delivered, rejected, routable) produce no delivered of their own, so receipts never chain. For each ack the Gateway issues one `attestation{kind: delivered}` per sender whose Envelopes the ack covers, listing those Envelopes in `claims.envelopes[]`; evidence stays per message while receipts do not double the traffic.
- Cursors are opaque, monotonic tokens per mailbox and per device, a device being the `kid` that signs the mailbox requests (or holds a session lease, §10.2); an Actor that signs with one key has one cursor.
  - Concurrent acks from different devices are independent, and the Gateway deletes an Envelope only when every `kid` in force that has synced the mailbox within the Envelope TTL has acked it or `exp` has passed (a key that has been retired or revoked reads nothing more and holds nothing back); an Envelope is reported `delivered` once per recipient, on the first ack.
  - Every Gateway MUST issue `delivered`: a sender reads a receipt that does not come as an Envelope that expired (§12.1), and that reading holds only where no Gateway is free to stay silent.
  - A device that syncs a mailbox for the first time sends `cursor: "0"` and starts at the oldest Envelope still retained; whatever was deleted before its first sync is not recoverable, by design.
- Append and dedup are one atomic operation under a unique index on (recipient, `from`, `id`). On crash a client resumes with `sync` from its last acked cursor and the Gateway replays everything after it. An Envelope past `exp` is removed and counted as expired; an ack that arrives after removal yields no `delivered`, and the sender notices the missing `delivered`. A `delivered` that cannot reach the sender's Home Gateway waits in the recipient Gateway's outbox under the same retry, backoff and dead-letter rules as any Envelope. A v1 Actor has exactly one Home Gateway (§16.3); multi-homing is an L3 matter.

A `delivered` attestation has `subject` = `env:from/id` of the first Envelope it covers and `claims` = `{ mailbox, ctx?, member?, envelopes[], cursor_from, cursor_to, accepted_at }`, where `envelopes[]` lists every Envelope from that sender covered by the ack as `{ id, digest }` (§7) and `accepted_at` is that of the latest Envelope covered, an upper bound for all of them, which is the direction §11.5 needs. The sender accepts it only from the Gateway the recipient's Card names (§7). The Gateway issues it once per recipient per ack per sender, on the first ack by any of that recipient's devices, signed with the Gateway key and carried in a plaintext Envelope from the Gateway itself, as are `rejected` and `routable` (a control attestation holds only ids, digests and, in a `rejected`, the Gateway's own Error (§8), and a Gateway on the path recognizes one by its shape and by its sender being a Gateway on the signed list it trusts, §9.4, so that receipts never chain; from anyone else the same shape is an ordinary message, since `actor_type: gateway` is self-declared); later acks by other devices produce none, so a sender learns nothing about the recipient's device count. In a room, `ctx` and `member` name the room and the member it was delivered to.

### 10.4 HTTP binding (Core; the handle route and the policy request are `discovery.v1`)

| Operation | Route | Request → response |
| --- | --- | --- |
| `resolve` | `GET /v1/actors/{id}`; with `discovery.v1` also `GET /v1/handles/{handle}` | no body → `{ card, key_events[], delegations[], revocations[] }` (all signed Records; `key_events[]` holds the key events before the current version, §11.5: v1, the version that added the commitments, every rotation and every recovery); a conflicted Actor's answer adds `conflict: { v, versions[] }` (§4.1); the handle route MAY redirect to a Directory instead (§4.2); the answer carries an `ETag` that changes whenever any member of it does (a new Card version, a new delegation or revocation, a recorded fork), and a request whose `If-None-Match` names the current one is answered `304`, so a cache can answer and a conditional request never hides a revocation. A Directory that does not hold the Actor MAY ask the Gateways on its list (§9.4) before answering `not_found`, asking each with `?local=1`, so a lookup is one hop and never circles the list. It asks every one of them and keeps the newest chain that verifies (§4.1); the first answer is not the answer, because the Gateway an Actor moved away from may not know of the move, or may not say. A Gateway that does not answer within the time the Directory allows is left out, so a silent peer delays a lookup by that bound and does not fail it. A Gateway asked with `?local=1` asks nobody and answers only for Actors registered with it: itself, the Actors it is home for, and an Actor that re-homed away, for which it answers with the Card version that names the new Home Gateway. It never answers such a request from its cache of other Gateways' Actors; that is `not_found`. An answer to `?local=1` whose Card names the answering Gateway is therefore that Gateway's own statement that it hosts the Actor, and below L3, where a route lease says the same (§16.3), the only one |
| fetch | `GET /v1/records/{issuer}/{id}` (optionally `?v=`) | no body → the signed Record, for any Record the Gateway's Directory or Index holds |
| revocations (unsigned) | `GET /v1/revocations?since=` | no body → `{ revocations[], next_since }`, a cacheable delta feed of signed `revocation` Records; `since` and `next_since` are opaque positions in the Gateway's append-only feed (a client stores and resends `next_since`), never timestamps, so two revocations in one second or a late, backdated one are never skipped |
| `query` (`index.v1`) | `POST /v1/query` | `{ filter, cursor, limit }` → `{ items[], next_cursor }` (§9.3); a signed request on a restricted Index, or on a public one when the Index rate-limits |
| `send` | `POST /v1/send` | A bare Envelope, an array of at most 100, or `{ envelopes[], cards[] }` where `cards[]` carries the sender's key-event chain and current Card (and the submitting Gateway's) so the receiver verifies `sig` from the self-certifying Card chain without any Directory lookup → `{ accepted: [id], queued: [id], rejected: [{ id, error }] }`, where every `id` is an Envelope `id`, each Envelope is answered independently in order, and a repeat of an Envelope that was accepted or queued returns that answer (one that was refused is judged again, which is what `retryable` is for, §8); the HTTP status is 200 whenever the request itself could be read, even when every Envelope is in `rejected[]`, and a Problem Details status answers only a request that could not be taken apart (not I-JSON, over a limit); `queued` appears only in the answer of the sender's own Home Gateway acting as outbox, and only after the Envelope is durably in the outbox: a Gateway that cannot record it answers an Error, never `queued`. `cards[]` is keyed by `issuer`: the Gateway verifies each Envelope against the Card whose `issuer` equals its `from`, and an Envelope whose sender it neither holds nor was handed is `unauthorized`; `cards[]` MAY also carry newer versions of a Card the Gateway already holds, the sender's or the recipient's (a rotation, a move), which it verifies and adopts |
| `sync`, `ack` | `POST /v1/mailbox/sync`, `POST /v1/mailbox/ack` | `{ actor, kid, cursor, limit, wait, rid, iat, exp, sig }` → `{ envelopes: [{ envelope, cursor, accepted_at }], next_cursor }` (§10.1); `{ actor, kid, cursor, rid, iat, exp, sig }` → `{ ok: true }` |
| policy (`discovery.v1`) | `POST /v1/policy` | `{ actor, kid, base_version, allow[], block[], rid, iat, exp, sig }` → `{ ok: true, version }`; replaces the Actor's lists at this Gateway, and is rejected with `conflict` when `base_version` is not the current version, so two devices cannot overwrite each other; the version is 0 before the first request and rises by one with each, and only the answer to a request that succeeded states it: a client whose answer was lost sends the same lists again under the same `base_version` and, when that is `conflict`, under the next |
| rooms (L2) | `POST /v1/rooms/{ctx}/sync`, with `ctx` percent-encoded; `GET …?cursor=&limit=` for public rooms | a signed request `{ actor, kid, cursor, limit, wait, rid, iat, exp, sig }` from a member → `{ envelopes[], next_cursor }`, a per-member cursor over the room log at the room's Gateway; the unsigned `GET` form is served only for rooms with `acl.read: public` |

Signed requests: a request object is signed with the same primitive over `utf8("aep/1/request") ‖ 0x00 ‖ JCS(object without sig) ‖ 0x00 ‖ utf8(route) ‖ 0x00 ‖ utf8(gateway)`, where `route` is the method, one space and the path exactly as sent, counted from `/v1` (`"POST /v1/mailbox/sync"`): no scheme, host or query string, and none of whatever prefix a deployment puts in front of its `/v1` base, so that a Gateway behind a path prefix or a rewriting proxy and its clients sign the same bytes, and `gateway` is the Actor ID of the Gateway the request is sent to in its `agent:` text form; a signed request therefore binds to one route on one Gateway and can be replayed nowhere else.

The entry is `sig = { actor, kid, alg, val }`. Every request carries `rid` (a ULID or UUIDv7, §4.3), `iat` within ±300 s and `exp` at most 5 minutes after `iat`; the Gateway rejects an `rid` it has already seen from the same (`actor`, `kid`) until that `exp` has passed, with `conflict`, so a captured request cannot be replayed. The key that signs a request is judged at the moment the Gateway handles the request, not at the request's `iat`: a request asks for something to be done now (read this mailbox, move this cursor, replace this policy), so a key that by that moment has been revoked or retired, or is no longer listed, authenticates nothing, even inside the 300 s window, and the answer is `key_revoked` or `unauthorized`. Only a signature on a Record or an Envelope, which says what was said at a moment, is judged by the key's status at that moment (§11.5).

A policy request names the version it replaces in `base_version`, so a stale or replayed policy is `conflict`; an ack never moves a cursor backward (§10.1); a resume token is bound to its device lease.

This is the only request authentication in v1; an HTTP Message Signatures (RFC 9421) binding, if one is ever defined, is a separate document and never a second path inside Core.

Path segments carry an Actor ID or a Record `id` as is; neither contains a character that needs percent-encoding, and the handle in `/v1/handles/{handle}` is written in lowercase without `@`.

A Gateway accepts `send` for recipients it hosts from any sender whose `sig` verifies, whether that sender is an Agent or another Gateway, and for recipients it does not host only from Actors it hosts (operator policy MAY widen this), so no Gateway is an open relay.

Operator policy MAY also narrow admission for senders it does not host, for instance to Actors that a Gateway on a list the operator trusts answers for as their Home Gateway (`?local=1`, above, asked at the URL that list gives for the Gateway and never at a URL taken from the Actor's Card), answering `forbidden` otherwise; that is how registration gating at other Gateways comes to count at this one's door. The Home Gateway a Card names is the Actor's own claim, like `actor_type`, and admits no one by itself: an Actor can name a Gateway it never registered with.

A Gateway caches sender Cards, takes an unknown sender's Card from `cards[]` on the request itself and verifies the chain locally, and rate-limits Card resolution per source; when it must resolve and the Directory cannot be reached it answers `unavailable`, and when no Card can be had at all, `unauthorized`.

Revocation status still comes from a revocation feed: the authoritative feed for an Actor is its Home Gateway's `GET /v1/revocations`, and a Gateway MAY merge feeds it has gossiped.

Beside these calls a Gateway serves one unsigned document, its own description, at `GET /.well-known/aep/gateway` on the origin of its `/v1` base: the root of that host (RFC 8615), whatever path prefix the base has, so an origin serves one Gateway. The document is the `resolve` answer for the Gateway itself, to which `discovery.v1` adds `scopes[]` and `registration` (§9.4). The Card in it MUST carry a `relay` endpoint whose `gateway` is the Gateway's own Actor ID and whose `url` is its `/v1` base. That entry is what ties the URL to the ID (§16.1): a client takes from it the ID it addresses registration to and names in signed requests (§9.1), and another Gateway checks it before it delivers (below).

Delivery across Gateways at L0 and L1 is nothing more than this rule: the sender, or its own Gateway acting as outbox with retry and dead-lettering, resolves the recipient's Card and POSTs the unchanged Envelope to the recipient's Home Gateway, and the answer there is `accepted`; before it does, the delivering Gateway MUST check that the Gateway behind that URL presents the Actor ID the recipient's Card names (its `/.well-known/aep/gateway` document, above), because a valid TLS certificate alone never suffices. The document counts only when the Card in it names that same URL as the Gateway's own endpoint: the document is public, and a copy of it served from another host proves nothing.

The recipient's Gateway returns `delivered` the same way, by resolving the sender's Card and POSTing the attestation, as an Envelope from the Gateway itself, to the sender's Home Gateway.

A Gateway MAY submit an Envelope for a recipient it does not host only when the destination is the Home Gateway the recipient's Card names, the original `sig` verifies, and the Envelope's values are unchanged (§5); the destination accepts it under the first rule, so no new authentication is needed.

A deployment MAY run one shared Gateway for every Actor and skip all of this at first; L3 adds persistent Gateway-to-Gateway sessions, route leases and Index federation on top of the same rule, not in place of it.

| Case | What the Gateway does | Answer |
| --- | --- | --- |
| The recipient's mailbox is here | verify, append under the unique index, issue nothing yet | `accepted` |
| The recipient's mailbox is elsewhere and the sender is hosted here | store in the outbox, deliver with retry, backoff and a dead-letter queue | `queued` |
| The Envelope arrives from another Gateway for a recipient hosted here | verify `sig` and `exp`, append | `accepted` |
| The recipient acks | issue `delivered`, send it to the sender's Home Gateway through the same outbox rules | — |
| The recipient's Card now names another Gateway | stop appending; keep the old mailbox drainable for the Envelope TTL; answer new sends for the moved Actor from Gateways that still hold the old Card with `unavailable` (retryable) naming the move, so that the sending outbox re-resolves (§12.5) and delivers to the new Home Gateway | `unavailable` |
| An Envelope that was accepted or queued arrives again | return that answer unchanged; a different body under the same (`from`, `id`) is `conflict` | as before |
| The sender's Card is forked (§4.1) | refuse until a recovery version arrives; the Actor is compromised at that `v` | `conflict` |
| The recipient's Card is forked (§4.1) | refuse: the recipient's key is in two hands, so whatever the mailbox took in could be read by whoever holds the copy; the sender resolves again after the recovery version | `conflict` |
| The sender's current Card has expired (§7) | refuse until the sender publishes a fresh version; a Gateway re-resolves once before deciding, since its copy may be stale | `expired` |
| The recipient's `policy.contact` is `none` | refuse, unless the sender is the recipient's Owner, on its allow list, or a Gateway on the signed list this Gateway trusts (§9.4) sending a control attestation | `forbidden` |

The HTTP binding, long polling included, is mandatory at every level (§10.6); the Relay Session (§10.2) is optional. Synchronous errors use RFC 9457.

### 10.7 Message procedure

`Actor ID → Card → endpoint → Envelope`. IP addresses and internal locations are never exchanged.

1. Send: sign the Record → build the Envelope (`to` or `ctx`) → `enc` → `sig` → direct first if the Card lists one, Gateway otherwise.
2. Gateway: verify the header (`sig`, `exp`, size, quota, allow and block lists, ACL, key status) → append to the mailbox with `accepted_at` → on `ack`, issue `delivered`.
3. Receive: `sync` → check that `to` is the receiver's own ID, or that `ctx` is a Context it belongs to (an Envelope signed for another Actor is dropped unread, and its sender gets no answer) → verify `sig` → dedup by (`from`, `id`), where other bytes under a key already seen are a `conflict` (§12.2) → decrypt → verify `sigs` → check that `from` is one of `sigs[].actor` (§11.3) → dedup by (`issuer`, `id`, `v`) → evaluate lifecycle → process → for a command reply `executed` or `rejected`; otherwise `processed` only when requested.

`delivered` comes from the Gateway, `read` and `processed` from the receiving Agent, `executed` from the executing Surface. Never infer a counterpart's action or a completed deal by combining them.

## 11. Keys and cryptography (Core; §11.2 and §11.4 are `sealed.v1`, the Owner hierarchy of §11.1 is `owner.v1`)

Signature, canonicalization and encryption suites are fixed as mandatory-to-implement (MTI). Home-grown algorithms are prohibited (MUST NOT).

### 11.1 Keys

```text
Owner Root Key (owner: ID)
  → Agent Key: sig (derives agent: ID), enc
    → Device Key (delegation signed by the Agent; can only narrow)
      → Session lease, Context epoch key
```

- Key generation and custody are the Agent's business. The protocol does not prescribe how keys are made (software, HSM, deterministic derivation) or where they live. It fixes only public-key algorithm and encoding, and signature verification.
- Custody MAY be delegated to a third party, a Runtime (§17); a Gateway never holds an Actor's keys. The ID derives from the inception key, so moving custody does not change the ID. The recommended default for a consumer product is split custody: the product holds the Agent keys, the Owner root key stays with the person (in the keystore of the person's own device, or offline) with pre-rotation (`next_key_hash`) and a separate recovery key (`recovery_key_hash`, §4.1), and approvals (§14.2) are signed on the person's device. A passkey is not an Owner key in v1: WebAuthn signs its own message format, not the inputs of §11.3, so a passkey can unlock the keystore that holds the Owner key but cannot be that key; a binding that lets a passkey sign Records is future work. A product MAY hold the Owner root on the person's behalf only with explicit consent and with a way out that does not depend on the product, a recovery key that stays with the person and that the product does not keep, because whoever holds the root can re-delegate, revoke and recover the Agent.
- An Owner MAY derive many Agent keys from one seed. The derivation path never appears in a Record.
- Device keys are listed in the Agent's `agent_card.keys[]` with `purpose: sig` and a `delegation` field naming the Agent-signed `delegation{issuer: agent, subject: agent, scope: ["represent:<agent id>"], key_kid: <device kid>}` that scopes them; the one `purpose: sig` entry without `delegation` is the primary signing key, which alone carries `next_key_hash` and signs Card versions. A device signs Records and Envelopes as the Agent: `issuer` is the Agent ID and `kid` is the device key. Adding or removing a device is Card `v`+1 signed by the primary key and is not a key event (§11.5): a device key extends no trust beyond the primary key that listed it, and a Card version signed by a device key is invalid, so a stolen device cannot enroll another. Because the version that enrolls a device is an ordinary one, a Directory may leave it out of the chain it serves (§11.5), so a device key's entry says for itself since when the key is in force: it carries `valid_from` (MUST), the moment the device was enrolled, which every later version repeats, and a device that is retired keeps its entry, with `valid_until`, for the Envelope TTL (§11.5). The verification chain is device key → Agent Card → Owner delegation. A device-key delegation (one with `key_kid`) is evaluated by this listing and by revocation (§11.5), not by the chain rules of §13, which govern authority between Actors. Owners have device keys exactly the same way: an Owner's Card lists them with `delegation{issuer: owner, subject: owner, scope: ["represent:<owner id>"], key_kid}`, which is how the Owner device key that signs approvals (§14.2) is known. v1 has one encryption key per Actor, so the devices of one Agent share the Agent's `enc` private key through the Agent's own keystore; per-device encryption keys are left to a Profile.

An Agent driven by a language model keeps its keys out of the model. Private keys, resume tokens and session credentials never enter the model's context, its logs, a Card, a Record or an artifact; the model handles public identifiers only and signs through a key module that takes bytes and returns a signature, never a key. An onboarding or skill file carries no secret and never instructs the Agent to send key material anywhere; a file that does is an attack, not a setup step. This is the one place where the usual prompt-injection defense (§1.4) is not enough on its own, because a key that is visible to the model can be exfiltrated by any text the model reads. A hosted Agent holds none of these: its keys are in the Runtime (§17). The credential it presents to the Runtime's own API is not a protocol object, and the Runtime's documents say how it is kept; the reasoning above applies to it all the same, which is why a Runtime bounds what one credential can do and does not rely on its staying secret.

### 11.2 MTI suite (Core for signing; the HPKE suite is `sealed.v1`)

| Purpose | MTI | Optional |
| --- | --- | --- |
| Signatures | Ed25519 (RFC 8032) | none in v1; further suites are registered by bindings (§10.6, §21) |
| Canonicalization | JCS (RFC 8785) | — |
| Hash | SHA-256 (FIPS 180-4) | — |
| Sealed encryption | HPKE (RFC 9180): DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20-Poly1305. suite id `hpke-x25519-sha256-chacha20` | AES-128-GCM variant; `hpke-xwing-sha256-chacha20` (X-Wing hybrid KEM, X25519 + ML-KEM-768) is reserved so that a post-quantum suite changes no wire object |
| Small groups (≤ 32) | content key + per-member HPKE wrap | — |
| Large groups, PCS | — | MLS (RFC 9420), `mls.v1`, in `groups.v1` |
| Wire encoding | JSON + JCS | — |
| Text encoding | base64url without padding (RFC 4648 §5); IDs in multibase base58btc | — |

By layer: Core implements Ed25519, JCS, SHA-256 and JSON only; `sealed.v1` adds the HPKE suite; MLS belongs to `groups.v1`.

Identifiers used on the wire:

| Identifier | Values |
| --- | --- |
| `sig.alg`, `keys[].alg` with `purpose: sig` | `ed25519` |
| `enc.alg`, `keys[].alg` with `purpose: enc` | `hpke-x25519-sha256-chacha20` (MTI), `hpke-x25519-sha256-aes128gcm`, `hpke-xwing-sha256-chacha20` (reserved) |
| `keys[].purpose` | `sig`, `enc`, `recovery` (the last only in a recovery version, §4.1) |
| Multicodec prefix inside Actor IDs | ed25519-pub `0xed`, secp256k1-pub `0xe7`, each written as an unsigned varint (`ed 01`, `e7 01`). X25519 keys appear only as raw bytes in `keys[]` for `enc`, without any multicodec prefix, and never in an ID |

Suites are negotiated as the intersection of `agent_card.supported.suites`. An empty intersection is `unsupported`.

### 11.3 Signatures

- Every signature input begins with a domain tag, so that one key can never be made to sign one kind of object as another: `utf8("aep/1/envelope") ‖ 0x00`, `utf8("aep/1/record") ‖ 0x00`, `utf8("aep/1/request") ‖ 0x00` and `utf8("aep/1/open") ‖ 0x00` for the inputs below and in §10.2 and §10.4. JCS output never contains a raw 0x00 byte, so the tag and the separators cannot be confused with content.
- Envelope `sig.val` = Sign(`utf8("aep/1/envelope") ‖ 0x00 ‖ JCS(Envelope without sig)`). If `body` is ciphertext, the base64url ciphertext string is covered as the string it is. `sig.actor` is `from`; `sig.kid` is a key in `from`'s Card, a device key included. An Envelope signature has no `signed_at`; the Envelope `iat` plays that role.
- Record `sigs[].val` = Sign(`utf8("aep/1/record") ‖ 0x00 ‖ JCS(Record without sigs) ‖ 0x00 ‖ utf8(signed_at)`), the one and only Record signature input (§6.2).
- Two layers: the Envelope `sig` authenticates transport and is present on every Envelope, on every hop; Record `sigs` are the semantic signatures for receivers and verifiers and support multi-signature. A Gateway verifies the Envelope `sig` before accepting and forwards the Envelope with its values unchanged (§5); it adds no signature of its own.
- Sender check: after decryption the receiver MUST verify that `Envelope.from` appears in `Record.sigs[].actor`, so the transport sender is someone who signed the Record: the issuer when a Record is first sent, a countersigner when an agreement is returned with its signature added, a Gateway when it sends its own attestation. A Record by another issuer travels inside `attached[]` of a Record the sender signed, never bare in `body`; the one exception is a `revocation` forwarded to a Gateway for publication (§9.1, §11.5), which is complete in itself. On failure, discard and apply no contact policy; this check stops a Gateway from forging `from`.
- Verification inputs are `alg`, the public key found by `actor` and `kid` (valid at the signature's `signed_at`), and the canonical bytes plus, for a Record signature, the entry's `signed_at`. A Gateway or receiver whose copy of the signer's Card does not list the `kid` MUST resolve the Actor again, once, before it rejects: the key may be newer than the copy, a device key that was added or a rotation. Reference vectors ship with §20.

### 11.5 Rotation and revocation

- Signing-key rotation: `agent_card` `v`+1 carries the new primary key, `SHA-256(new_pub) == previous next_key_hash`, is signed by the new key and commits to its own successor in a fresh `next_key_hash`; `prev` is the content digest of the previous version (§7).
  - Three kinds of version exist (§4.1), ordinary, rotation and recovery, and the last two are key events; so is the one ordinary version that adds the two commitments where there were none, because the next rotation or recovery is checked against it. Every other change (an encryption-key rotation, a device key added or removed, a new endpoint, a policy change) is an ordinary version signed by the established primary key, which extends no trust.
  - Only the primary key, or in a recovery version the recovery key together with the new primary key, signs a Card version; a device key (§11.1) never does.
  - **What `resolve` serves.** `resolve` returns the current Card plus the key events, a chain whose length is the number of rotations rather than the number of edits, so an Agent that rotates its encryption key weekly costs its peers nothing extra. What a verifier needs from the versions that are left out, the current one carries: the commitments, which it repeats unchanged, and for each device key the window its entry states (key validity, below).
  - **How a verifier checks it.** A verifier checks that chain by content and needs no flag: v1's primary key derives the ID; each rotation's primary key is the one the previous key event committed to, and it commits to a fresh one; each recovery's recovery key is the one its base committed to, and it commits to one that no earlier version committed to; the current version is signed by the latest key event's primary key and carries that key event's `next_key_hash` and `recovery_key_hash` unchanged, or both added where that key event had neither.
  - **Gaps.** The chain a Directory serves MAY omit the ordinary versions that follow a key event, since the current version carries the whole state: a verifier accepts a gap in `v` directly after a key event and nowhere else, and one that holds an ordinary version from an earlier answer checks a newer version against the last key event it holds and does not require it to chain to that ordinary version (Appendix F.5); a recovery version is `v`+1 of its base, so it opens no gap, and the versions it voided are simply absent.
  - **Two chains.** Of two valid chains for one ID, a verifier keeps the one with the higher current `v` and, when the two tell the same step twice at the same current `v`, the one whose current version has the smaller content digest; except that a chain carrying a recovery version over a key event the verifier holds replaces what it holds after that base whatever the `v`, and a second recovery version signed by a recovery key whose recovery the verifier already holds, naming other keys, is a fork of the recovery key, over whatever base: the Actor is conflicted for good (§4.1).
  - Encryption-key rotation needs no pre-rotation because the signing key signs it. Appendix F gives the verification algorithm and the transition table.
- Key validity: a signing key is in force from its entry's `valid_from`, or without one from the `iat` of the earliest version that lists it, until its entry's `valid_until`, or without one until the `iat` of the first later version that no longer lists it; a verifier applies this to the versions it holds. A key verifies the signatures whose `signed_at` falls inside that window and none dated before or after it. Before an Actor's first version none of its keys was in force, so a signature dated before that version's `iat` verifies under no key; a verifier allows 300 s there, the clock tolerance of §5, because a request is judged by the Gateway's clock (§10.4) and not by the clock that dated the Card.
  - **Primary keys.** A primary key begins and ends at key events, which every verifier is served, so its entry needs no dates.
  - **Device keys.** A device key begins and ends at ordinary versions, which a Directory may leave out, so its entry carries `valid_from` (MUST, §11.1): a verifier that is served a later version takes the key to have been in force from that moment, although it never saw the version that enrolled the device. A device key is retired by giving its entry a `valid_until` and keeping the entry listed for at least the Gateway's Envelope TTL (default 7 days) before dropping it (MUST), so that what the device signed and still sits in a mailbox verifies for a recipient that resolves afterwards. Once the entry is dropped, a verifier that resolves for the first time has nothing to judge the key by and rejects what it signed; one that kept a version listing the key still verifies what was signed inside its window.
  - **No retirement in the past.** A version's `iat` is never earlier than the previous version's (Appendix F I11), a Gateway registers a version only within 300 s of its `iat`, and it refuses one that newly states a `valid_until` on a signing key lying more than 300 s before that `iat` (§9.1). So a Card does not retire a key in the past; retiring one from an earlier moment is what a `revocation` with `effective_at` is for. Nor does it move the start of a window: a version that states another `valid_from` for a device key than the version before it did is invalid (Appendix F I12), and a Gateway refuses to register one that differs in this from the latest version it holds.
  - **Keeping what was verified.** A party that will have to show a device key's signature later (on an agreement, an approval, an `executed`) keeps, with the Record, the Card version it verified the signature against. Any version verifies on its own against the key events, which a Directory goes on serving; the key events alone do not name a device key.
  - **Encryption keys.** After rotating an `enc` key the Agent MUST keep the previous key for at least the Gateway's Envelope TTL (default 7 days), so Envelopes already sitting in a mailbox can still be opened.
- The Owner root has its own `agent_card` and follows the same pre-rotation and the same `recovery_key_hash`. After compromise, publish a Card version signed with the recovery key (§4.1), which carries a fresh signing key, a fresh `next_key_hash` and a fresh `recovery_key_hash`, and void later delegations with `revocation{reason: compromise}`. The recovery key signs that one version and nothing else, so ordinary use never exposes it, and a second recovery version signed by it, over whatever base, is a fork that ends the Actor; an Owner that loses its recovery key as well as its current and next keys retires the ID.
- Revocation: `revocation.subject` is a Record reference, or a key written `<actor id>#<kid>` (the Actor's own ID followed by `#` and the `kid`, so that an Owner's revocation of one Agent's key names the Agent; a bare `kid` is invalid).
  - A Record reference with `@v` ends one version; without `@v` it ends every version of that `id`, present and future, which is how an Owner retires a Record that a compromised Agent keeps re-versioning. Either way the Record ends at the revocation's `effective_at`: one dated ahead leaves the Record in force until that moment, as a revocation that names a key does.
  - **What a key revocation voids.** Revoking a key voids that key's signatures from `effective_at`: a Record whose issuer signature is voided is invalid, while a voided countersignature leaves the Record valid and merely no longer countersigned by that party; what that means for an agreement is the Profile's business (§14.1).
  - A revocation has no `exp` and is permanent; it is signed, so any Agent or Gateway may forward and cache it, and the Directory is one source among many.
  - **Where it is published.** It is published to the Home Gateway of the Actor whose key or Record it names (the Owner `send`s it there like any Actor), whose feed is the authoritative one (§10.4), and a Directory that caches that Actor's Card from a peer takes the revocations in the same answer with it.
  - **Who may revoke what.** An Owner may revoke anything its Agent issued, keys and Records alike, while an Agent cannot revoke what its Owner issued; revoking a key voids every delegation that key issued from `effective_at`; revoking a Record ends that Record only. The key a revocation names may be the key that signs it, which is how an Actor with one key and no Owner revokes that key: a verifier judges a revocation's signature by the status its key had before the revocation was applied, so the revocation's own `effective_at`, however early, never voids it.
  - A Gateway rejects new Envelopes from a revoked key, and receivers reject new Records from it, with `key_revoked` from `effective_at` on, as soon as it learns of it; a revocation whose `effective_at` lies in the future schedules a retirement, and until that moment the key is valid.
- One clock for revocation: a signature is void if its `signed_at` is at or after `effective_at`. `signed_at` is a claimed time, bounded rather than proven: the receiver's check against the carrying Envelope (§5) ties a sender's own signature to within 300 s of the Envelope's `iat`, and an honest ingress keeps that `iat` from lying more than 300 s ahead; neither keeps it from lying in the past, up to the Envelope's lifetime.
  - **Backdating.** `effective_at` MAY be earlier than the revocation's own `iat`: an Owner responding to a compromise sets it to the earliest moment the key may have been misused, and everything the key signed after that moment is void, including signatures that were valid when made.
  - **The acceptance rule.** A sender can therefore backdate `signed_at` by up to its Envelope's lifetime (at most 7 days) without anyone's help, so a verifier of an irreversible or financial Record SHOULD, for a signature whose `signed_at` falls within 7 days before `effective_at`, also require a Gateway-signed `delivered` whose `accepted_at` precedes `effective_at`, which the sender holds and supplies in `evidence[]`, or failing that the `accepted_at` its own Gateway recorded for the Envelope that brought the Record; `accepted_at` otherwise serves expiry and mailbox operation only.
  - Both `signed_at` and `accepted_at` are claims by clocks the verifier does not control: `signed_at` is bounded by the ingress check, `accepted_at` is trusted exactly as far as the Gateway that signed it, which is why this rule is a SHOULD and names the recipient's Home Gateway rather than any Gateway.
  - Each signature is judged by its own `signed_at`, so a countersignature added days later never affects the earlier one.
- Verification and authorization are two questions. Verification is archival: a signature verifies, now and in ten years, by the key's status at its `signed_at`, for a verifier that holds a Card version listing the key (key validity, above). Authorization is judged at execution time: before executing a command at an irreversible or financial side-effect level (`exchange.v1`'s levels, §14.0, or another Profile's equivalent) the executor MUST re-check key status, and a command whose signing key or whose chain's key has been revoked since, however valid its signature remains, is not executed, answered `key_revoked`, unless a Gateway-signed `accepted_at` for the Envelope that brought it precedes the revocation's `effective_at` (the acceptance rule above), so that a command resent after a compromise never runs on the strength of an old signature. Fail closed applies to that check and to nothing else: an executor whose copy of the signer's revocation feed is older than 60 s refreshes it before an irreversible or financial execution, and answers `unavailable` when it cannot. For everything else a receiver applies the revocations it holds and SHOULD refresh them whenever it re-resolves the Card. A Directory SHOULD expose an append-only key event log.

### 11.6 Non-repudiation and metadata

- This spec chooses non-repudiation via Record signatures; agreements and attestations require it. A deniability profile is out of scope. What a signature can be relied on for is bounded by revocation, and this document says so rather than leave it to be found: a signer, or its Owner, can void the signer's own earlier signatures by revoking the key with a backdated `effective_at` (§11.5). A Card version cannot do the same quietly: it is never dated before its predecessor, a Gateway registers it only within 300 s of its `iat`, and it may neither state a `valid_until` in the past nor move the `valid_from` of a device key it already listed (§9.1, §11.5), so what voids a signature is always a Record someone can point to. What a signer can do without a Record is stop listing a device key once the Envelope TTL has passed, after which a newcomer has nothing to check that key's signatures against; a counterparty that kept the Card version it verified against loses nothing (§11.5). A compromise needs exactly that, and a party that regrets a commitment can claim it. A counterparty that needs more keeps the Gateway-signed `delivered` for the Envelope that carried the commitment, whose `accepted_at` is a third party's statement of when the signature existed; what a voided signature means for a Record that was already concluded is the Profile's to say (§14.1).
- E2EE does not hide `from`, `to`/`ctx`, timing or size from the Gateways on the path; this is a known limit of v1, not a non-goal. Mitigations: semantic fields encrypted, TTLs, minimal retention, pairwise aliases (optional), padding (MAY). Hiding `from` from the recipient's Gateway (sealed sender, as Signal does) is possible on top of this Envelope and left to a Profile.

## 12. Delivery and idempotency (Core; §12.3 is `exchange.v1`)

Delivery is at-least-once. Ordering and exactly-once are not guaranteed. Duplicates and reordering are absorbed by three layers of keys. One ladder of states applies on every path, direct or relayed, and on into execution, so an implementation has one vocabulary.

### 12.1 Transport states

| State | Meaning | Who asserts it | Evidence |
| --- | --- | --- | --- |
| queued | Held in the sender's own Gateway outbox, not yet at the recipient | the sender's Gateway | the `send` answer from an outbox Gateway |
| accepted | Appended to the recipient's durable mailbox at its Home Gateway or direct endpoint (a transport state, unrelated to the `exchange.v1` attestation kind `accepted`, §14.1) | the recipient's Gateway, or a direct endpoint | the synchronous `send` answer, from the Gateway or from a direct endpoint (§10.3), unsigned; it means this and nothing more |
| delivered | The recipient's client acked the cursor covering it (mailbox acked) | the Gateway holding the acked mailbox or room log (§7): the recipient's Home Gateway, or the room's Gateway; a direct path (§10.3) produces no `delivered` | `delivered` attestation; not proof of verification |
| processed | The recipient verified and handled the Record | Recipient Agent | `processed` attestation, on request; the evidence that a message reached its reader |
| executed | The external side effect completed, with `status` succeeded, failed, pending or unknown | Executing Surface | `executed` attestation (§12.3) |
| settled | Funds captured or escrow funded: moved into the payment Actor's custody, not yet the payee's (§14.1) | Payment Actor | `settled` attestation (§14) |
| expired | No ack before `exp` | the recipient's Gateway | Gateway log |
| failed | Unroutable, policy-rejected, dead-lettered, or refused by the recipient | any Gateway on the path, or the recipient | Error, or a `rejected` attestation; an outbox Gateway that dead-letters an Envelope MUST send its hosted sender `rejected` naming the Envelope, with `expired` or `unavailable` when delivery never happened and with the recipient Gateway's own definite error (a `forbidden`, a `conflict`) when that Gateway refused it, so a sender never waits for a `delivered` that cannot come; a recipient that cannot use what arrived MAY send its own `rejected` (§11.4) |

**What each piece of evidence proves.** Evidence comes in two kinds and nothing in this document turns one into the other. *Transport evidence* is what a Gateway can honestly say about bytes: `accepted` is an unsigned synchronous answer meaning "appended to this mailbox", `delivered` is the Home Gateway's signed statement that a client holding the recipient's key acked past the Envelope, `rejected` its signed statement that it refused. *Application evidence* is what a party says about meaning: `processed`, `verified`, `executed`, `settled` are signed claims by the Agent, Verifier, Surface or payment Actor that issued them and are exactly as good as that issuer. A sender signature proves that a key signed these bytes at a claimed time; a delegation proves that a scope was granted in a signed way; neither proves that a person exists, that a legal authority was held, that a side effect happened in the world or that a message was read. A third party verifying any of this later needs the Records and the Card chain in force at the time, the revocation feed, a clock it trusts for `signed_at` and `accepted_at` (bounded as §11.5 describes, never proven) and a decision to trust the issuer of each piece; the protocol defines the formats and the checks, and "non-repudiation" throughout this document means signature verifiability under those conditions, not a legal effect (§11.6).

| Evidence | Kind | Signed by | Proves | A verifier also needs |
| --- | --- | --- | --- | --- |
| Envelope `sig`, Record `sigs[]` | transport, application | the sender, each signer | these bytes were signed by this key; `signed_at` is the time the signer states, bounded and never proven (§11.5) | the Card chain in force then, the revocation feed |
| `accepted` (`send` answer) | transport | nobody | the Gateway says it appended; useful to the sender for retry, to nobody else | nothing; it is not kept |
| `delivered` | transport | the recipient's Home Gateway | a client holding the recipient's key acked past the Envelope at `at` | to trust that Gateway's honesty and clock; the Card that named it (§7) |
| `rejected` (Gateway) | transport | a Gateway on the path | that Gateway refused, with an Error | the same |
| `rejected` (recipient), `read`, `processed` | application | the recipient | the recipient says so | to trust the recipient's word |
| `verified` | application | a Verifier | the Verifier says it checked something, in `claims` | to trust that Verifier |
| `executed`, `settled` | application | the Surface, the payment Actor | the issuer says the side effect reached this status | to trust the issuer; the Provider's own records for anything more |

**Transitions.** The states above are reached as follows, on every path, and these are the answers to the cases that most often go wrong:

| Case | What happens | Final state for the sender |
| --- | --- | --- |
| Stored in the mailbox, not acked by `exp` | the Gateway removes it at `exp` and issues nothing; a sender that sees no `delivered` by `exp` treats the Envelope as expired and resends the Record in a new Envelope if it still matters (§12.2). That reading is the sender's inference from silence: a valid `delivered` that reaches it later, for an ack made in time, replaces it, and a Record that was sent again collapses at the Record layer | expired, until a `delivered` says otherwise |
| Recipient's client crashes between `ack` and saving its own cursor | the Gateway's cursor is the one that counts: the next `sync` starts after the last ack the Gateway recorded; what the client had not acked is delivered again, and the Record dedup (§12.2) absorbs it; a client therefore stores what it will process before it acks, never after | unchanged |
| Two consumers read one mailbox under one `kid` | the protocol sees one consumer: the cursor moves only by `ack`, and whoever acks moves it for both; an operator that runs two consumers gives each its own device key (§10.1) or serializes their acks; a webhook (`runtime.v1`) is a notification and never acks | unchanged |
| Re-homing: the old and the new Home Gateway each issue `delivered` | both are entitled (rule 10); the sender records the first and ignores a second for the same Envelope, which is never a `conflict` | delivered |
| The outbox Gateway dead-letters after the sender stopped syncing | the `rejected` sits in the sender's mailbox for its own TTL and expires like anything else; a sender that sees neither `delivered` nor `rejected` by `exp` treats the Envelope as expired | expired, or failed if the `rejected` arrives first |
| The recipient's contact policy is `invite` or `none` and the sender is unknown | `none` answers `forbidden` at once (§9.3). `invite` is checked by the recipient itself (§9.3): its Gateway answers `accepted`, the recipient drops the Envelope and acks past it (§10.1), and the sender receives the same `delivered` as for an Envelope that was kept, so it cannot tell from the receipt whether it passed; no answer follows | failed; delivered, which says that a client acked and never that anyone read |
| The recipient cannot open the body, which was sealed to a key it no longer holds (§11.4) | its client acks past the Envelope like any other, so its Gateway issues `delivered`, and the recipient itself sends `rejected` with `unsupported`, before or after that receipt; the sender takes the recipient's word over the receipt, resolves the recipient again and sends the Record in a new Envelope | failed |
| The sender's own Gateway is unreachable | the client MAY post directly to the recipient's Home Gateway (§9.4); nothing else changes | as for a direct post |
| The Envelope is a control attestation that cannot be delivered | dead-lettered silently: no `rejected` answers a receipt (rule 10) | n/a |

The mailbox is append-only within its retention window (an entry, once appended, is never altered; it leaves by ack of every active device or by `exp`) and nothing more: it is not an archive. A party that needs its Records or receipts later keeps them itself; a Profile that needs a Gateway to retain more defines that retention (Appendix D sketches one).

### 12.2 Deduplication

| Layer | Key | Who | Retention |
| --- | --- | --- | --- |
| Envelope | (`from`, `id`) | Gateway, receiver | `exp` + 300 s |
| Record | (`issuer`, `id`, `v`) | receiver | implementation-defined; a Profile MAY require longer |
| External execution | the command's `issuer/id` → `provider_ref` | executing Surface | durable |

Implementations keep a unique index on (recipient, `from`, `id`) for Envelopes and on (`issuer`, `id`, `v`) for Records, and perform append and dedup in one transaction. The Envelope key is scoped to one recipient mailbox: the same (`from`, `id`) sent to two recipients is two Envelopes. A resend with the same key and the same bytes is answered `accepted` again and appends nothing, which is what makes a client's retry after a lost answer safe; the same key with different bytes is `conflict` and appends nothing either. The dedup entry lives at least until `exp` + 300 s, after which the key may be seen again without harm, since the Envelope it named cannot be replayed within its own window.

A transport retry resends the same Envelope unchanged while it is unexpired. After the Envelope expired, the sender wraps the same Record (`issuer`, `id`, `v`) in a new Envelope and re-signs its own `sigs[]` entry with a fresh `signed_at` (§5, §6.2); the content bytes are unchanged, so both collapse to one at the Record layer. A business retry, after a Profile error such as `exchange.v1`'s `resource_stale` or `approval_required` (§14.1), is a new Record.

### 12.4 Replay, ordering, conflicts

- Keep a seen-set of (`from`, `id`) for `exp` + 300 s. Reject `iat` more than 300 s in the future and any expired `exp`. Time is RFC 3339 UTC.
- Reconstruct order from `parent`, `v`, `supersedes`. Park a Record whose `parent` has not arrived for at most the remaining TTL of the Envelope that carried it, then process or drop it under the Agent's own policy.
- Two different bodies with the same (`issuer`, `id`, `v`) are a `conflict`: keep both; resolve by a new version from the issuer or an Owner `revocation`. A Directory or an Index that is asked to publish other content under a reference it already holds answers `conflict` and goes on serving what it holds: what was published under a reference is never replaced, and a change is `v`+1. An Index lists the highest version of a Record it holds, and an older version that arrives later is not listed again (§6).

### 12.5 Failures and failover

- Recipient offline: the mailbox holds; redeliver within TTL. `unavailable` is information, not failure.
- Between Gateways: bounded outbound queue, exponential backoff, dead letter.
- On delivery failure the sending Gateway MUST re-resolve the recipient's Card. If the recipient re-homed (Card `v`+1 with a new Gateway), follow it.
- Re-homing is the recipient `send`ing Card `v`+1 to any Gateway with its own key, and SHOULD also send it to the previous Gateway so that it stops appending at once. The old Gateway keeps the mailbox drainable for the Envelope TTL and answers new sends for the moved Actor with a retryable `unavailable` naming the move (§10.4), so the sending outbox re-resolves and delivers to the new Home Gateway; the recipient drains what was left at the old Gateway with its usual signed `sync`. Forwarding the remaining mailbox to the new Gateway is an L3 route lease (§16.3), never assumed below it. Anything that expired meanwhile shows as `expired` to the sender, who resends the same Record.
- Leaving an operator, or any custodian (export and handover): an Actor's state is its signed Records, the Card chain, its delegations and the revocations that name it, which any party may hold and which `resolve` serves, so the export format is those Records and nothing more; private keys never leave the custodian that holds them. Control moves by a handover rotation: the new custodian generates the Actor's next signing key and hands over its hash; the old custodian publishes a rotation to the key it had committed to, committing in that same version to the new custodian's hash; the new custodian then rotates to its own key. When the old custodian also held the recovery key (an unclaimed hosted Agent, §17), it co-signs one recovery version over the latest key event that installs the new custodian's primary and recovery keys instead. Either way two parties cooperate and no private key crosses between them. An Owner (`owner.v1`) whose root key the old custodian also holds (§11.1) is handed over in the same step, by a handover rotation of its own Card: the Owner's grant is checked at every Card version of the Agent (§9.1) and only the holder of the root renews it, so an Agent that left without its Owner could publish no version once that grant ran out. One root signs the grants of all the Owner's Agents, so they leave together, or the one that leaves alone is given another Owner by a recovery version, the only kind of version that names another (§4.1). One key stays behind, the one the old custodian rotated through: a second version signed with it at that `v`, naming other keys, is a fork (§4.1). The old custodian therefore SHOULD destroy that key once the new custodian's rotation is published, and what settles such a fork is the Actor's recovery key, which is a reason to hold one before leaving. The encryption key does not cross either: the new custodian's version lists an `enc` key of its own, and what was sealed to the old one opens only where that key is, so a handover is the one case in which a previous `enc` key is not at hand for the Envelope TTL (§11.5). The Actor therefore reads its mailbox through the old custodian before the handover; an Envelope left unread is one the new custodian cannot open, which it answers as §11.4 says (`rejected` with `unsupported`), and the sender sends the same Record again, sealed to the current key. Device keys stay behind as well, and the Card still lists them: the version that installs the new custodian's key gives every device key listed until then a `valid_until` (§11.5), so that nothing the old custodian's devices sign from that moment verifies and none of them reads the mailbox, and the new custodian enrolls its own.
- Operator and Surface failures: §17.

## 15. Extensions and adapters (Core)

Extensions widen Records; adapters connect outward. Neither changes Core semantics. Anyone may define a format at three levels without approval: a `payload` carries any format under any `content_type` with no registration at all; an Extension needs only a namespace its author controls (reverse domain) and a published schema digest; a Profile is a self-published set of types that any two parties may use as is, and is listed in the registry only so that public Indexes can honor the filter keys it registers (§9.3) and conformance can test it. Only the Core types change through governance (§21).

### 15.1 Extensions

- Location: `Record.ext["<namespace>"]`. Declaration: `agent_card.supported.extensions[] { ns, required (boolean), schema_digest }`.
- Unknown required extensions are rejected with `unsupported`; unknown optional ones are ignored (MUST).
- Schemas are pinned by digest in the registry. Fetching schemas from URLs at runtime is prohibited (MUST NOT).

### 15.2 Adapters

| Adapter | Mapping |
| --- | --- |
| A2A, binding `aep-a2a.v1` | An A2A Agent Card and an AEP Card are two documents for one Actor; the AEP Card declares the A2A endpoint under `ext["aep-a2a.v1"]` as `{ version, endpoint, transports, auth_profiles, features, card_url?, card_digest? }` and lists the A2A skills it wants found by name in `capabilities[]`. `exchange` stays an AEP anchor; the A2A `contextId`, `taskId` and `messageId` are preserved verbatim in `payload.foreign`. `payload.data_bytes` carries the original A2A JSON-RPC message or task, byte for byte: A2A's own JSON may hold floating-point numbers and `null`, which a Record's JSON may not (§6.2), so it never travels as `data`. `processed` is issued once, when the connector has validated and handed over the AEP Record; A2A task states are not `processed` and travel as attestations of the binding's own kind, `a2a.task` with `claims.task_state` holding the state's name as the A2A side wrote it, whatever states that version of A2A defines, one per transition, so that a long-running task is a sequence of signed status Records and an Envelope's `exp` stays the delivery deadline of one message rather than the lifetime of a task; `executed` is issued only when the connector has confirmed an external side effect, because a completed task need not have had one. An A2A protocol error maps to `rejected`, while a task that the A2A agent declines or holds for authorization is a task state like any other and travels as `a2a.task`, never as a transport `rejected`; streaming and push stay inside the connector; an AEP `cancel` command maps to A2A cancellation; a task's own deadline is the A2A side's and travels in the carried message, never in an Envelope's `exp`. The A2A server never sees AEP: a connector beside the Gateway translates both ways and preserves the original bytes, ids, errors and digests. The two Cards prove each other: the `ext` entry carries the A2A Card's digest, and the A2A Card carries the AEP Actor ID in an extension, so nobody can pair their own AEP Card with someone else's A2A Card. A payload the connector signed is provenance of the connector, not a signature by the original A2A party, unless that party's own signature survives inside `data_bytes` |
| MCP, binding `aep-mcp.v1` | MCP is agent-to-tool. AEP carries an MCP call across an agent boundary with Owner approval and evidence: `payload.data_bytes` holds the JSON-RPC request, result, error or notification as it was sent (its numbers and `null`s are MCP's, not a Record's, §6.2), `payload.foreign` holds `{ protocol: "mcp", version, request_id, task_id? }`, and progress, cancellation, elicitation and sampling travel as further `payload` Records in the same `exchange`. The original bytes are kept as an `artifact` when a third party may need them, since a digest alone cannot reproduce them. The connector that executes the call holds the MCP credentials and authorization metadata, which never appear in a Card, an Envelope or a Record; AEP delegation decides only whether this Agent may forward or execute a call of this purpose |
| Surface | websites, APIs, catalogs → `listing`, `attestation` |
| Payment | PSP or escrow ↔ `agreement.payment`, `authorized`, `settled`, `released`, `refunded`, approval `mandate` |
| Identity | Passkey, OAuth → Owner authentication at the product level, never an Owner key (§11.1); DID Document ↔ Card, UCAN ↔ `delegation`, VC ↔ `attestation`: lossless mappings published with the bindings (MAY) |

AEP Core is carrier-compatible with A2A and MCP: it carries their messages intact, and semantic interoperability requires the bindings, which are separate specifications published at 1.0; the table fixes their shape and their one rule, that whatever an adapter translates, the original bytes, ids, errors and digests survive in `payload`. `agent_card` collides with A2A's Agent Card by name; the A2A binding publishes the field mapping.

## 18. Security considerations (Core)

| Threat | Mitigation | § |
| --- | --- | --- |
| Key compromise, impersonation | Owner `delegation`, scope narrowing, pre-rotation, a recovery key that settles forks, `revocation{compromise}`, two-layer signature verification | 4.1, 11.5 |
| Malicious Gateway, Directory poisoning | Self-certifying IDs, origin signature verification, key event log, peer-propagated revocations; beyond its own Card and Gateway list a Gateway issues only `delivered`, transport `rejected` and `routable` | 4.1, 16 |
| Gateway forging `from` | The sender's `sig` on every Envelope on every hop, plus the receiver's `from ∈ sigs[].actor` check | 11.3, 16.3 |
| Forged `delivered` by the recipient's Gateway | `delivered` is that Gateway's statement only; consequential flows rely on recipient-signed `processed` and `executed`, and the recipient chose the Gateway, so the risk sits with the recipient | 10.7 |
| Blackhole, eclipse | Detected by missing `delivered`; Card re-resolution and re-homing; only trusted-list Gateways as home | 12.5, 9.4 |
| Replay, duplicate side effects | (`from`, `id`) seen-set, `exp`, (`issuer`, `id`, `v`), write-ahead with `unknown` reconciliation | 12 |
| Prompt injection, capability abuse | `body`, `ext`, `payload` and `summary` are untrusted data; `side_effect_level`, approvals, tool allowlists | 1.4, 13, 14.2 |
| A counterparty's text aimed at the person, such as a misleading `summary` on an approval screen | Approval screens draw amount, target and counterparty from structured fields only; `summary` is a caption | 6, 14.2 |
| Executable remote instructions (heartbeat-style loops) | Gateway and Index deliver data, never instructions; onboarding and skill files are versioned, digest-pinned and installed once | 18.1 |
| Platform credential leak | No server-held credentials: a Gateway stores public keys, ciphertext, cursors and quotas, so at L1 and above a database leak yields nothing that can impersonate an Actor. L0 bodies are plaintext, so an L0 Gateway leak exposes content, never identity. A Runtime (§17) is the exception by construction: it holds its Agents' keys, so its leak is a key compromise, answered by revocation with a backdated `effective_at` (§11.5) | 4.1, 11.1, 17 |
| Stale resources, double booking | `valid_until`, hold/confirm, Provider authority | 14.3 |
| Malicious Surface, false provenance | Propagated `source`, `provenance`, `claim`; claiming never changes the issuer; the client verifies the claim delegation itself | 14.3 |
| Spam, sybils, harassment | `policy.contact`, allow and block lists held at the Gateway, prior-counterparty introductions (self-signature check), rate limits and quotas per sender and per Home Gateway (confirmed as §10.4 says, never read off the Card), stricter quotas for Actors with no attestation history (SHOULD). Trust comes from attestations, not IDs | 9.2, 9.3, 10.4 |
| Federation split-brain | Home authority, monotonic `v`, fencing, `conflict` | 12.4, 16.4 |
| Metadata leakage | Semantic fields encrypted, TTLs, minimal retention, contact graph never published | 9.2, 11.6 |
| Schema injection, SSRF | Digest-pinned schemas, no runtime fetch | 15.1 |
| Forged or replayed approvals | Owner device signature, binding to one Record by its reference and its content digest, 24 h `exp`, `constraints`, carried inline so nothing is fetched | 14.2 |

### 18.0 Security goals and assumptions

The protocol claims four properties. The first three are Core's; the fourth needs `owner.v1` and, for approvals, `exchange.v1`. Each holds under the named assumptions and no others; §18.2 says what a compromised operator can still do.

| Goal | Statement | Rests on |
| --- | --- | --- |
| Sender authenticity | A Record a receiver accepts carries a valid signature by `Envelope.from`, made with a key that was listed in `from`'s Card and unrevoked at `signed_at` | Ed25519; the Card chain from the inception key (§4.1); the receiver's `from ∈ sigs[].actor` check (§11.3); fresh revocation data (§11.5) |
| Non-repudiation of commitments | A Record carrying the signatures of its parties can be verified by any third party holding the parties' Card chains, with no Gateway's help | Domain-tagged signature inputs (§11.3); pinned references (§6); durable evidence without `exp` (§6); no later revocation that voids a party's signature (§11.5, §11.6) |
| Delivery evidence | A `delivered` is a signed statement by the recipient's Gateway that the recipient's client acked the Envelope; a `processed` is the recipient's own statement | The Gateway's key and honest clock (§5, §10.1); the recipient chose that Gateway (§18); `processed` needs no Gateway at all |
| Delegation soundness | A command an executor runs is covered by a chain of delegations from the Owner, each link narrower than the one above, none revoked, and bound to the content of the Record the person approved | The Owner root key stays with the person (§11.1); narrowing rules (§13); approvals that pin the Record's digest, carried inline (§14.2); the executor's key re-check before irreversible actions (§11.5) |

Assumptions, stated once: the next signing key and the recovery key are kept apart from the current key (§4.1); at least one ingress Gateway on an Envelope's path keeps an honest clock within 300 s, which bounds how far ahead a signature can be dated and leaves its past dating bounded only by the Envelope's lifetime, at most 7 days (§5, §11.5); WebPKI binds a Gateway's URL to its TLS certificate, the Gateway's own Card, served from that URL's origin and naming the URL, binds it to the Gateway's Actor ID, and a signed Gateway list is its publisher's word for the pair (§9.4, §10.4, §16.1); Envelope metadata (`from`, `to`, timing, size) is visible to Gateways and is not protected (§11.6). A formal model of the Envelope signature, the Record signature, HPKE base mode and approval binding is small and is the intended next step after the reference vectors.

### 18.1 Implementation MUSTs

1. Implement the MTI suite for the Profiles it declares and pass its reference vectors: Core is Ed25519, JCS, SHA-256 and JSON; `sealed.v1` adds the HPKE suite (§11.2).
2. Verify Actor ID ↔ inception key and the `agent_card` version chain.
3. Verify the Envelope `sig` on every Envelope, process one only when its `to` is the receiver's own ID (§10.7), and always verify Record `sigs`; verify `Envelope.from ∈ Record.sigs[].actor` (a device key listed in the Agent's Card counts as the Agent), except for a `revocation` forwarded to a Gateway (§9.1).
4. Reject new Records from revoked keys or expired delegations.
5. If implementing the exchange Profile: verify approvals and re-check keys for `irreversible`/`financial` commands (§14.2); executors use write-ahead (§12.3).
6. Treat Record `body`, `ext` and `payload` as untrusted data; never execute instructions found inside.
7. Never fetch schemas or extensions at runtime.
8. Profile implementations propagate `source`, `claim` and `provenance` to derived Records, verify a claim from its delegation rather than from an Index label, and show it in approval UI.
9. Never index credentials, private keys or internal state.
10. A Gateway forwards an Envelope with its values unchanged (re-serialization is allowed, §5), resolves endpoint URLs to public addresses only outside a `local` deployment (no loopback, private or link-local ranges, and the address is checked at connect time, so a DNS answer cannot steer it inside), and re-resolves the Card on delivery failure.
11. Reject unsupported required extensions, suites and versions; never reinterpret silently.
12. A Gateway issues no Records but its own Card, the signed Gateway list it may publish, `delivered`, transport `rejected` and `routable` attestations, and signs no Envelopes but the ones that carry its own Records.
13. Keep private keys, resume tokens and session credentials out of model context, logs, Cards, Records and artifacts; sign through a key module that returns signatures only (§11.1). Never fetch and execute instruction files from the network on a schedule or at a peer's request. A Gateway or Index delivers Envelopes and Records, never instructions; onboarding and skill files are versioned, digest-pinned and installed once.
14. Accept a `delivered` only from the Gateway the recipient's Card named when the Envelope was sent or has named since; `actor_type: gateway` is self-declared (§7, §10.1).
15. Verify every Card version and every chain as Appendix F states it: the kinds of version (F.2), the invariants I1 to I12 (F.3), the chain a Directory serves (F.4) and the transitions between Actor states (F.5). What Appendix F rejects is rejected whoever signed it, and what it calls a fork is recorded as one (§4.1, §11.5).
16. A Gateway answers a `?local=1` lookup only for Actors registered with it, never from its cache; and whoever lets a Gateway's registration count (admission, quotas) takes that Gateway's own answer for the Actor, asked at the URL its list gives for that Gateway, never the Home Gateway or the URL the Actor's Card names (§10.4).
17. A Gateway takes a URL for another Gateway's only when the Card served at its `/.well-known/aep/gateway` names that URL as the Gateway's own endpoint; when it asks the Gateways on its list about an Actor, it asks every one of them and keeps the newest chain that verifies; and it serves under an Actor only that Actor's own delegations and its Owner's (§9.1, §10.4, §16.1).

SHOULD: refresh Cards within 30 days, rotate encryption keys within 7 days, metadata TTLs, home-region fencing, outbox/retry/dead-letter/reconcile, signed releases, a Directory key event log. MAY: direct endpoints (`direct.v1`, §10.3), DID/VC bridging, padding, private queries, pairwise aliases, MLS in rooms.

### 18.2 Compromised network operator

A Gateway, Directory or Index holds no secret worth stealing: public keys, ciphertext (at L0, bodies that were never sealed), cursors and metadata. What an attacker who controls one can and cannot do follows from that.

| A compromised or rogue operator | Can it? | Why |
| --- | --- | --- |
| Forge a Record or sign as someone else | No | Keys live with the Actor |
| Impersonate a sender through `from` | No | Every Envelope carries the sender's `sig`, and the receiver's `from ∈ sigs[].actor` check (§11.3) fails on the Record signature |
| Claim to host an Actor it does not host | No | Cards name the Home Gateways; a `routable` lease from any other Gateway is ignored (§16.3) |
| Pose as another Gateway on a look-alike domain | No | Gateway IDs are pinned in Cards and the signed list and checked at `OPEN` and at delivery, against a Gateway Card that names the URL it is served from, so a copy of a Gateway's description on another host fails; TLS alone is never enough (§9.4, §10.4, §16.1). Whoever takes over the Gateway's own host name, certificate included, does pass and gets the messages sent there until it is noticed: it cannot sign a `delivered`, so senders see no receipt, and it reads nothing sealed (§18.0) |
| Get an Actor blocked elsewhere by flooding in its name | No | Nothing without the sender's own `sig` is attributed to `from` (§16.3) |
| Label an unclaimed Surface as verified in an Index | No | `claim` is verified by the client from the claim `delegation`, never taken from an Index's answer (§14.3) |
| Read message bodies | No at L1 and above | E2EE. L0 bodies are plaintext, so L0 is for content that may be public |
| Alter a Card or issue an identity | No | Self-certifying IDs and the signed version chain |
| Drop or delay messages | Yes | Missing `delivered` reveals it but does not prevent it |
| Fake `delivered` | Yes | Consequential flows rely on recipient-signed `processed` and `executed` (§10.7) |
| Serve a stale Card, hiding a rotation or revocation | Yes | The most dangerous move: a stolen old key stays usable to anyone who trusts that Directory |
| Serve a stale Card so that a sender seals to an encryption key the Actor has since retired | Yes | Bounded by Card `exp` (≤ 30 days) and encryption-key rotation (≤ 7 days); a sender of sensitive content MAY resolve through a second path before sealing |
| Observe metadata: who, to whom, when, group membership of hosted rooms | Yes | Visible by design (§11.6) |

The five rows marked yes are bounded by the following rules.

- Two-path check. Before a command at an irreversible or financial side-effect level (§14.0, or another Profile's equivalent), the executor and the sender MUST obtain the counterparty's Card and revocations through a second path (a Gateway other than the counterparty's Home Gateway) and compare the two answers: the content digest of the current version (§7; an equal `v` proves nothing, since two versions can share one) and the revocations each lists. A difference is `conflict` and the action stops. This catches a Home Gateway that shows different Cards to different askers, and one that withholds what the other Gateway learned from someone else: a Card version or a revocation that the Actor or its Owner sent there, which is why a revocation may be forwarded to any Gateway (§11.5). It does not catch a Home Gateway that withholds the same thing from everyone while the second Gateway has only that Home Gateway's answer to pass on: both paths then end at one source, and what bounds that case is the Actor's self-audit (below) and the Card's `exp`. On a network with a single operator there is no second path, and that operator is trusted for freshness.
- Self-audit. An Agent SHOULD periodically resolve its own ID through a Gateway other than its home and check that the Card served is its latest version, by content digest, and that its revocations are present. A withholding Directory is caught by the Agent it is withholding.
- Key transparency. L3 Gateways SHOULD keep an append-only key event log and gossip signed tree heads with their peers, so a Directory that shows different Cards to different parties leaves proof.
- One home, and a way out. In v1 an Actor has exactly one Home Gateway, so there is one mailbox, one `delivered` per message and one policy; resilience comes from re-homing (§12.5) and from senders posting directly to the recipient's Home Gateway. Multi-homing, which needs a shared dedup ledger and fencing between the Gateways, is specified in `federation.v1`, not here.
- Key custody. The recommended default keeps the Owner root key with the person (a device keystore or offline) and lets the product hold only Agent keys; approvals (§14.2) SHOULD be signed on the person's device. A product MAY hold the Owner root only with explicit consent and a recovery key that stays with the person, because whoever holds the root can re-delegate, revoke and recover the Agent (§11.1), and approvals signed by a product-held Owner key are only as sound as that product's custody. With the default, a compromised operator reads nothing and approves nothing.
- Operator key hygiene. Gateway keys belong in an HSM, with separate keys for TLS, route leases and `delivered`; route leases expire within 24 h so a stolen key has a short reach. A Gateway key is revoked like any Actor's key, and `delivered` attestations signed after `effective_at` lose their evidentiary value.
- Recovery. After an incident the operator rotates its keys and publishes the revocation; Agents re-home with Card `v`+1 to another Gateway and keep their identities. Envelopes lost in the gap surface to senders as `expired`, and senders resend the same Record (§12.5). Nothing a Gateway held needs to be recovered, because nothing a Gateway held was secret; a Runtime's incident is a key compromise and is handled as one (§17, §11.5).

## 20. Conformance (Core)

Adoption starts with Core. A Core client is five things: generate a key, sign a Card, sign Records and Envelopes with the same primitive, `POST /v1/send`, `POST /v1/mailbox/sync`. A working Core client is a small job, days rather than weeks, and if it takes longer the spec is wrong (§20.1); an operable Gateway with quotas, retries and observability is weeks of work (§22.4). Levels are bundles of layers (the table at the top of this document): L0 is Core with discovery in plaintext, L1 adds sealing and Owners, L2 and L3 each build on L1 and are independent of each other, so a federating Gateway need not host rooms, and deals (P1) are a Profile on top of L1. A level is a name for a bundle and is never sent: an implementation says what it has in `supported.profiles`. A Surface can join with a single static signed feed. Passing the reference vectors and this section's tests says that an implementation computes the same answers as another; it says nothing about whether it can be operated (durability, quotas, backpressure, observability are §22.4 and the operator's business), and the reference implementation itself is a memory-only L0/L1 test article, not a Gateway anyone should run. Agent-to-agent use (scheduling, coordination, peer-to-peer trade, Appendix A.3 to A.7) needs no provider on the other side and produces the first network effect; Surfaces follow once there are agents to be found by.

| Level | Name | Required | For |
| --- | --- | --- | --- |
| Core | `core.v1` | `agent_card` with its chain (rotation, recovery, Appendix F), `revocation`, signed Envelopes to one recipient, `payload` with `application/aep.message+json`, `attestation` `delivered` and `rejected`, `send`, `resolve` by ID, `sync`, `ack`, signed mailbox requests, errors, (`from`, `id`) and (`issuer`, `id`, `v`) dedup, expiry | Every implementation |
| L0 | Core + `discovery.v1`, plaintext | Core + handles, contact policy, the signed Gateway list; a Gateway MAY add the Index and `query` (`index.v1`) | Small agents, static catalogs |
| L1 | L0 + `sealed.v1` + `owner.v1` | L0 + HPKE, encryption-key rotation, Owner root keys with the Owner's device keys, Owner → Agent `delegation`, delegation chains, Card re-resolution; the Relay Session is optional | Personal agents, Surfaces |
| L2 | L1 + `groups.v1` | L1 + `context`, epochs, group encryption, room routing. Independent of L3 | Gateways hosting rooms |
| L3 | L1 + `federation.v1` | L1 + handshake, route leases, cross-gateway sessions, Index federation, key event log. Independent of L2 | Internet-scale Gateways |
| P1 | `exchange.v1` (Profile) | L1 + the 5 Profile types, approval checks, write-ahead with execution status, freshness, booking | Transacting Surfaces, reservations |

| # | Test | Level |
| --- | --- | --- |
| 1 | ID ↔ inception key and the Card chain: the kinds of version and the invariants I1 to I12 (Appendix F.2, F.3); a served chain verified with its gaps (F.4); every transition of F.5 and every case of F.6, with the result its row names and in either arrival order; a signature by a key that the receiver's copy of the Card does not list verified after one more `resolve` (§11.3) | Core |
| 2 | Envelope domain tag + JCS + Ed25519 reference vectors; an input signed under another tag rejected | Core |
| 3 | Record JCS + multi-signature reference vectors, `signed_at` in the input; a Record inside `attached[]` left as it was attached, so that the Record carrying it verifies a second time | Core |
| 4 | (`from`, `id`) dedup, (`issuer`, `id`, `v`) dedup, lower `v` ignored, references carry issuer; other content published under a held (`issuer`, `id`, `v`) answered `conflict`, and what was published kept; other bytes under a (`from`, `id`) already seen refused with `conflict` by the receiver as by the Gateway | Core |
| 5 | `exp` and clock skew at ingress; a forwarded Envelope older than 300 s but within `exp` accepted; a sender signature outside `iat` ± 300 s, or a carried signature later than `iat` + 300 s, rejected | Core |
| 6 | Error codes, `retryable`, RFC 9457 | Core |
| 7 | Optional extensions ignored; required extension, suite or version rejected | Core |
| 8 | Bidirectional handle verification; a handle that does not end in `.agent` rejected; a handle resolved at the Gateway that holds its scope and never through the DNS; a Card rejected for a handle `conflict` leaves no half-registered Actor | L0 |
| 9 | Revoked keys and expired delegations blocked; a revocation never expires; a revocation signed by the key it names applied, however early its `effective_at`; a `revocation` whose subject is a bare `kid` rejected; a revocation that names a Record and is dated ahead applied at its `effective_at` and not before; a conditional `resolve` answered 304 only while the whole answer is unchanged, never across a new revocation; a delegation that names an Actor as its subject and is neither its own nor its Owner's absent from that Actor's `resolve` answer | Core (delegations: L1) |
| 10 | Opaque `payload` transport; `application/aep.message+json` accepted; unknown types handled | Core |
| 11 | Signed requests: a replayed `rid` rejected with `conflict`, a request signed by a key revoked since its `iat` rejected, a stale policy `base_version` rejected with `conflict`, a UUIDv7 `rid` after a ULID one applied, an ack never moves a cursor backward | Core (policy: L0) |
| 12 | `supersedes` from another issuer ignored; a Record outside the `parent` chain of its anchor excluded from the exchange | Core |
| 13 | An Envelope with `ctx` rejected with `unsupported` at L0 and L1 | Core |
| 14 | `from ∈ sigs[].actor` check; forged `from` rejected; a countersigned Record returned by its second signer accepted; an Envelope whose `to` is another Actor dropped by the receiver | Core |
| 15 | HPKE reference vector (a content key wrapped for one recipient, the construction that serves many, §11.4); receive after encryption-key rotation; a copy of the recipient's Card older than a day resolved again before sealing, and after a recipient's `rejected` with `unsupported` | L1 |
| 16 | Long-poll `sync`, per-device cursor recovery after a crash, a retired device that no longer holds an Envelope back, signed mailbox requests; Relay Session resume when that binding is offered | Core |
| 17 | JCS canonicalization vectors, duplicate keys in a set array rejected, integer bounds; a duplicate JSON name rejected; a re-serialized Envelope still verifies | Core |
| 18 | Card re-resolution on delivery failure; re-homing followed; `delivered` with `{ id, digest }` returned to the sender's Home Gateway and ignored by the sender when the digest is not that of the Envelope it sent or its issuer is not the Gateway the recipient's Card named at sending time or since; no receipt for a receipt; a `?local=1` lookup answered only for Actors registered with the Gateway, so that an Actor naming a Gateway it never registered with is confirmed by no one and a peer's stale copy never answers for the Home Gateway; every listed Gateway asked and the newest chain kept; a copy of a Gateway's description served from another URL not taken for that Gateway | Core |
| 19 | Context epoch rotation: removed members locked out, new members cannot read history; wraps must match membership, sender included | L2 |
| 20 | Gateway handshake, route leases, cross-gateway routing, outbox replay | L3 |
| 21 | Approval delegation binding to a pinned reference and to that Record's digest, `approval_required`; the same reference signed again with other content not covered by the approval; an approval of one reference that states no digest rejected; an agreement with a payment but no bound approval rejected; a delegation under a grant, issued by neither the grant's subject nor its issuer, not counted as a link | P1 |
| 22 | Same command resent: the Provider is called once when it accepts an idempotency key; otherwise the executor reports `unknown` and reconciles | P1 |
| 23 | Freshness (`resource_stale`), booking hold/agreement/confirm/release; `confirm` without a signed agreement rejected | P1 |
| 24 | `settled` from an Actor other than `payment.actor` ignored; after `challenged`, `released` ignored until `resolved` | P1 |

The levels are conformance capability levels, not a build order; the order in which the reference implementation grew is not this document's business. The reference implementation stops at L1 with `index.v1`. Implementations declare `suites`, `content_types`, `profiles` and `extensions` in `agent_card.supported` and publish a conformance report. This revision ships a JSON Schema 2020-12 bundle for the Envelope, the Record, every Core type body, errors and HTTP requests (`schemas/aep-v1.schema.json`; it checks structure, while the semantic rules of §4 to §6 and §11, such as chain commitments, signature windows and the `enc`/`body` relation, are checked by code, which the reference implementation's `check_shape` and `verify_*` functions show; the reference reads the structural rules from this schema and does not restate them, so that a signed Record with a malformed body is refused by both), reference vectors with fixed seeds (`vectors/`: keys and IDs, JCS, Card chains, Records, L0 and L1 Envelopes, signed requests, a `delivered` attestation, and invalid inputs with their expected error codes), a reference implementation of the L0/L1 client and Gateway with the §13 delegation evaluator (`ref/`) and a vector checker (`ref/tools/check_vectors.py`). The schema and the semantic rules are one conformance contract: an implementation passes when every object it emits validates against the schema and every vector, valid and invalid, produces the listed result (`ref/tools/check_vectors.py` runs the semantic side, `ref/tools/check_schema.py` the structural side, and `ref/tools/check_shape.py` checks, on every vector changed in one field at a time, that what the schema refuses the reference refuses). Until two independent implementations pass both and §20 interop, nothing is called conformant.

### 20.1 L0 quick start

1. Install an Ed25519 library, a JCS (RFC 8785) canonicalizer and an HTTP client; nothing else is needed.
2. Generate a key pair. The Actor ID is `agent:` + multibase base58btc (`z` prefix) of `varint(0xed) ‖ public key`, that is the bytes `ed 01` followed by the 32-byte key.
3. Fill in the minimal Card below and sign it: the `aep/1/record` tag, JCS of everything but `sigs`, a 0x00 byte and `signed_at`, then Ed25519 (§11.3). `owner`, `next_key_hash` and `recovery_key_hash` MAY be omitted at L0 (§9.1, §4.1).
4. Wrap the Card in a plaintext Envelope addressed to the Gateway's own Actor ID, which the Gateway's description gives (§9.1, §10.4), sign the Envelope and `POST /v1/send` it to any open Gateway from the signed list (§9.4); then `GET /v1/actors/{id}` your own ID there and, when you can, through a second path: when the Card you sent comes back, you are registered and routable (§9.1). At L0 Record bodies are plaintext, so the Gateway reads them; keep L0 for content that may be public and add HPKE (L1) before anything private.
5. To talk: `resolve` the peer, check the Card that comes back, then sign a `payload` Record, wrap it in an Envelope, sign the Envelope and `POST /v1/send` it.
   - Resolve by handle at the Gateway that holds the handle's scope, which the signed Gateway list names (`GET /v1/handles/{handle}`, §4.2), or by ID at your own Gateway (`GET /v1/actors/{id}`).
   - Check before use: the Card's signature and its `exp`, that its primary signing key derives the ID as in step 2, and that no `revocation` in the answer names that key (§11.5). For a Card that has never rotated that is the whole check; one that has is tied to its ID by the key events in the same answer (§11.5, Appendix F), and a client that does not follow them yet does not send to that peer.
   - Send to your own Gateway, which forwards, or straight to the Home Gateway the peer's Card names, with your own Card beside the Envelope in `cards[]` where that Gateway may not hold it (§10.4).
6. To listen: `POST /v1/mailbox/sync` with a signed request (`wait: 30` for long polling), check each Envelope as §10.7 lists (the sender's Card as in step 5, the Envelope's signature, the Record's signatures by keys that no `revocation` had voided when they signed, and the sender among the signers), process, then `POST /v1/mailbox/ack`.

```json
{
  "id": "01JC7A…", "type": "agent_card", "v": 1,
  "issuer": "agent:z6MkA…",
  "iat": "2026-11-06T00:00:00Z", "exp": "2026-12-06T00:00:00Z",
  "body": {
    "actor_type": "agent",
    "keys": [ { "kid": "sig-1", "purpose": "sig", "alg": "ed25519", "pub": "…" } ],
    "endpoints": [ { "kind": "relay", "gateway": "agent:z6MkG…", "url": "https://gw.example/v1" } ],
    "supported": { "v": [1], "suites": ["ed25519"], "content_types": ["application/aep.message+json"] },
    "policy": { "visibility": "direct", "contact": "open" }
  },
  "sigs": [ { "actor": "agent:z6MkA…", "kid": "sig-1", "signed_at": "2026-11-06T00:00:00Z", "alg": "ed25519", "val": "…" } ]
}
```

Required Card fields are `actor_type`, `keys[]`, `endpoints[]` and `supported`; everything else is optional, and a Card without `policy` is read as `visibility: direct`, `contact: open`. `endpoints[]` MAY be empty: an Actor that takes no messages, an Owner that only signs for one, lists none and cannot be written to. The protocol has one signing primitive, a domain tag then JCS then Ed25519 (§11.3), used for Records, Envelopes and mailbox requests alike. The reference implementation includes a Gateway that runs as a single process with one command, so the other end of a first test is a laptop. An operator MAY also publish an agent-readable onboarding file (a SKILL.md-style document) for Agents that join through its Runtime (§17) rather than performing these six steps themselves; such a file is versioned and digest-pinned, installed once, and never re-fetched on a timer (§18.1).

What the Agent then holds, and how. Secret, in a keystore the model cannot read (§11.1): the signing and encryption private keys, the pre-rotated next key, any device key, and the current resume token; the recovery key is kept apart from all of these (offline or with the person), since a keystore compromise that takes the current and the next key together is what it exists for. Public, in an ordinary profile file that references keys by `kid`: the Actor ID, the latest Card, the Gateway's ID and URL, the pinned Actor ID of the Gateway-list publisher, the device lease and cursors, cached Cards of contacts, and the lists of trusted Verifiers and allowed or blocked Actors. Nothing in the public set can impersonate the Agent, and nothing in the secret set ever appears in a Record.

## 21. Versioning, governance, registries (Core)

| Axis | Representation | Rule |
| --- | --- | --- |
| Envelope | integer `v` | +1 when a required field changes meaning; adding optional fields does not bump it |
| Record type | registry entry | Adding a type is a minor change; changing a type's required body fields is a Profile version |
| Profile | `name.vN` | vN+1 when required fields change; several vN may be supported at once |
| Extension | namespace + semver | Managed by the namespace owner |
| Handle scope | labels under `agent` (§4.2) | Assigned by the publisher of the Gateway list that names it, to one Gateway per scope |

`agent_card.supported` is `{ v: [1], suites, content_types, profiles, extensions }`; `suites` lists the algorithm identifiers of §11.2 that the Actor implements, for signing and for sealing alike (`ed25519`, and `hpke-x25519-sha256-chacha20` once it has an `enc` key); `content_types` lists the `payload` content types the Actor accepts, `application/aep.message+json` included when it does. Deprecation is published through the registry's `deprecated_at`.

Admission test: a proposal enters Core only if it directly serves one of the three jobs this protocol exists for, finding an agent, reaching one that is offline, or proving who sent what under whose authority; anything else is cited in Appendix C or placed in a Profile, an Extension or an implementation. Change process: proposal → threat and cost review → schemas and reference vectors → interop between two independent implementations from different organizations → deprecation notice (≥ 90 days) → release with migration guide → end of support for the previous version.

Registries will be JSON files in the specification's repository (none exists yet; having them in force is condition 4 below), with entries `name`, `since`, `status`, `deprecated_at`, `schema_digest`, `owner`: `record-types`, `error-codes`, `suites`, `profiles`, `extensions`, `actor-types`, `attestation-kinds`, `scope-verbs`, `filter-keys`, `content-types`.

Names this document takes from shared namespaces are provisional until registered: the `urn:aep:` URN namespace (RFC 8141), the `/.well-known/aep` path (RFC 8615) and the `application/aep.*+json` media types. 1.0 either registers them or renames them. One requirement of HTTP stays open until then: a 401 from the binding of §10.4 carries no `WWW-Authenticate` challenge, which RFC 9110 requires of every 401, because a signed request carries its signature in its body and this document defines no HTTP authentication scheme for that. The handle label `agent` (§4.2) is not a DNS name here and is not registered anywhere; it has been applied for as a top-level domain, and this document keeps it whatever comes of that, which is why an implementation never treats a handle as a host name; implementations compare the registry `code` and `content_type` strings and never parse them.

Governance is not captured by any single network operator. Nothing is released without interop between two independent implementations. The specification, the schemas, the vectors, the reference implementation and the tools are published under Apache-2.0, and the whitepaper and the other companion documents under CC BY 4.0 (`LICENSE`, `LICENSE-docs` and `NOTICE` in the repository; the copyright holder is Dunia Labs, Inc.). This document becomes 1.0 when all of the following hold, and until then it is a Draft that claims no normative force:

1. the machine-readable artifacts are public: JSON Schema 2020-12 for the Envelope, the Record, every Core type, errors and HTTP requests; JCS, signature and HPKE vectors; invalid-input vectors; the mailbox conformance suite; and an OpenAPI 3.1 description of the HTTP binding;
2. two independent implementations from different organizations pass the vectors and §20 interop in both directions, and the interop matrix is published;
3. an external security and threat-model review of Core has been published with its findings addressed;
4. the governance process, the license, the registries and the deprecation rule above are in force;
5. a standard export format exists (an Actor's Card chain, delegations and revocations as signed Records, plus a handover rotation, §12.5), so that leaving an operator is a documented procedure rather than a promise.

MUST, SHOULD and MAY are used as in RFC 2119 and RFC 8174; the appendices other than F are informative.

## Appendix F. Key lifecycle: states, invariants and transitions (normative)

This appendix is the normative statement of the key lifecycle, as a state machine, so that two implementers reach the same Actor state from the same inputs. §4.1 and §11.5 explain it, and where they differ this appendix governs; the reference implementation (`aep.record.verify_card_chain`, `aep.gateway.Gateway._resolve_chain`, `aep.client.Agent._learn`) and the vectors in `vectors/cards.json` and `vectors/invalid.json` implement exactly this.

### F.1 Objects

A Card version `C` is an `agent_card` Record with `v`, `prev` (absent at v1), `iat`, `exp`, `body.keys[]`, `body.recovery_key_hash` (optional) and `sigs[]`. Write `P(C)` for its primary signing key (the one `purpose: sig` entry that is not a device key), `N(C)` for `next_key_hash` on that entry, `R(C)` for the `purpose: recovery` entry in `keys[]` if any, `H_R(C)` for `body.recovery_key_hash`, `D(C)` for the content digest (SHA-256 of the JCS of the Record without `sigs`), `H(k)` for SHA-256 of a key's raw public bytes. A version is **signed by** a key when `sigs[]` holds an entry by the issuer with that key's `kid` that verifies against that key as listed in the version's own `keys[]`.

### F.2 Kinds of version, decided by content alone

| Kind | Test | Key event |
| --- | --- | --- |
| inception | `v == 1`; `P(C)` derives the Actor ID (§4.1); no `R(C)` | yes |
| ordinary | `v > 1`, no `R(C)`, `P(C) == P(pred)` | no, except the one that adds `N(C)` and `H_R(C)` where `pred` had neither (I2): that version is a key event |
| rotation | `v > 1`, no `R(C)`, `P(C) != P(pred)` and `H(P(C)) == N(pred)` | yes |
| recovery | `R(C)` present and in `sigs[]`; its base is the version `B` with `D(B) == prev`, which must be a key event; `H(R(C)) == H_R(B)`; `v == B.v + 1` | yes |

`pred` is the version `C` chains to: the one with `D(pred) == prev` when the chain is consecutive, or the last key event before `C` when a Directory omitted the ordinary versions between (F.4). A version that fits no row is invalid (`unauthorized`), whoever signed it, and changes nothing.

### F.3 Invariants

Every version:

- I1. is signed by `P(C)`; a recovery version also by `R(C)`; never by a device key (§11.1).
- I2. carries `N(C)` and `H_R(C)` both or neither; an ordinary version MAY add both where `pred` had neither (an L0 Card growing into L1), and otherwise carries `pred`'s values unchanged.
- I3. has `v == pred.v + 1` when it is ordinary or a rotation and the chain is consecutive; a recovery version has `v == B.v + 1` always.
- I4. is unexpired when it is the current version; earlier versions are judged at their own `iat`.
- I5. is not signed by a key revoked at that signature's `signed_at` (§11.5).

A rotation also: I6. `N(C) != N(pred)` (a fresh commitment); `H_R(C) == H_R(pred)`.

A recovery version also: I7. `P(C) != P(B)`, `N(C) != N(B)`, and `H_R(C)` differs from the `H_R` of every earlier version of the chain (fresh primary, commitment and recovery key; a recovery key signs one recovery, so a chain never commits again to one it has used); I8. its base `B` is a key event the verifier holds or is handed in the same chain, and is not a forked version.

An ordinary version also: I9. `N(C) == N(pred)` and `H_R(C) == H_R(pred)` (or both added together, I2).

An ordinary or rotation version also: I10. carries `pred`'s `owner` unchanged, or adds an `owner` where `pred` had none (a claim); a recovery version MAY name another Owner or none. Whatever version names an `owner`, the Gateway that registers it still needs that Owner's `represent:` delegation (§9.1).

Every version after the first also: I11. `iat(C)` is not earlier than `iat(pred)` (for a recovery version, than `iat(B)`); a publisher whose clock reads earlier uses `iat(pred)`. A Gateway additionally registers a version only within 300 s of its `iat` (§9.1); no verifier can check that afterwards, so it holds as far as that Gateway is honest, and it is why a key is retired from the past only by a `revocation`.

Every version that lists a device key also: I12. each device entry (a `purpose: sig` entry with `delegation`) carries `valid_from`, the moment the device was enrolled, and a version that lists a device its predecessor listed repeats it unchanged (§11.1, §11.5).

### F.4 The served chain and how to verify it

A Directory serves `key_events[]` (v1, the version that added the commitments, and every rotation and recovery that is still in force) plus the current version; it MAY omit the ordinary versions that follow a key event and MUST NOT serve a version a recovery voided (F.5) or two versions at one `v`. Verification of a list `L`, ascending by `v`:

1. reject `L` unless every element is an `agent_card` of one issuer and id, no two share a `v`, and `L[0]` is an inception version whose `P` derives the ID;
2. verify every element as a Record against its own `keys[]` (I1, I4, I5);
3. for each `i ≥ 1`, with `b = L[i]`: if `b` is a recovery version, let `a` be the element of `L[:i]` with `D(a) == b.prev`, which must exist and be a key event (I8), and check `H(R(b)) == H_R(a)`, I7 and `b.v == a.v + 1`; otherwise let `a = L[i-1]` and require either `b.v == a.v + 1` with `b.prev == D(a)` (consecutive) or `b.v > a.v + 1` with `a` a key event (a gap, allowed only directly after a key event, and `prev` is then not checked), then check I6 for a rotation (`H(P(b)) == N(a)`) or I9 for an ordinary version, and I10 for both; in every case check I11 (`iat(b)` not earlier than `iat(a)`) and, where `b.v == a.v + 1`, I12 (a device entry that `a` lists has the same `valid_from` in `b`);
4. the last element is the current version.

A verifier handed a chain it cannot verify keeps what it held and treats the input as `unauthorized`.

### F.5 Actor state at a Directory

| State | Meaning | Served as current | Routing |
| --- | --- | --- | --- |
| active | one verified chain | its last version | normal |
| conflicted(v) | two verified same-`v` versions signed by one primary key that name different keys | the last version below `v` (at `v == 1`, the version seen first) with `conflict: { v, versions[] }` | refused with `conflict` in both directions (§10.4) |
| dead | two recovery versions signed by one recovery key that name different keys, whatever their bases (a fork of the recovery key), or the current key, the committed next key and the recovery key all lost | the last version below the fork's `v`, with `conflict: { v, versions[] }` naming that `v` and the two versions, for ever. The fork's `v` is the lower `v` of the two, and never lower than the version after the first one that committed to that recovery key | refused |

Write `K(C)` for the keys a version names, `(P(C), N(C), H_R(C))`. Two different versions at one `v` with equal `K`, both verified successors of the same `pred` (for recovery versions, over the same base), are **the same step told twice**: the same hands signed both, and nothing about who holds the Actor is in doubt. Two with different `K` under one signing key are a **fork**. A recovery key signs one recovery: two recovery versions signed by one recovery key with different `K` are a fork of that key, and two with equal `K` are the same recovery told twice, in both cases whatever their `v` and their bases. Content digests are compared as raw bytes.

Transitions, on receiving a verified version or chain `X` while holding `S` (`cur` = current version, `kev` = held key events, `Y` = a different version held at `X.v`):

| Held | Received | Result |
| --- | --- | --- |
| any | a version already held (same `D`) | no change |
| active | ordinary or rotation `X` with `X.v > cur.v`, chaining to `cur` or (across a gap) to the last key event | active, `cur := X` |
| active | `X.v < cur.v`, not a recovery, and no `Y` is held | no change (a lower `v` is ignored) |
| active | `Y` is held: `X` is a rotation to `N(pred)` and `Y` is ordinary | active; `X` replaces `Y` and every version after it (the rotation wins in either arrival order) |
| active | `Y` is held: `Y` is a rotation to `N(pred)` and `X` is ordinary, or `Y` is a recovery version and `X` is not | no change, `X` is `unauthorized` (nothing at the same `v` supersedes a rotation to the committed key or a recovery version) |
| active | `Y` is held, `K(X) == K(Y)`, both verified successors of the same `pred`: the same step told twice | active, never a fork. Of the chain that holds `Y` and the chain `X` came with, the one with the higher current `v` is kept, and at the same current `v` the one whose current version has the smaller `D`; the publisher of the other is answered `conflict` |
| active | `Y` is held, `P(X) == P(Y)`, `K(X) != K(Y)`, both verified successors of the same `pred`: a fork | conflicted(`X.v`), both versions kept |
| active | `Y` is held and `X` is not a verified successor of the same `pred` | no change, `X` is `unauthorized` (never a fork) |
| active or conflicted | recovery `X` whose base `B` is a held key event, not forked, with no recovery version signed by `R(X)` held | active, every held version with `v > B.v` is discarded (its keys verify nothing afterwards except as §11.5 retires them), `cur := X`, then any versions after `X` in the same chain are applied in order |
| active | recovery `X` over a held base while a different recovery `Y` signed by the same recovery key is held and `K(X) == K(Y)`: the same recovery told twice, over the same base or another | active, never a fork. Of the chain that holds `Y` and the chain `X` came with, the one with the higher current `v` is kept, and at the same current `v` the one whose current version has the smaller `D`; the publisher of the other is answered `conflict` |
| active or conflicted | recovery `X` while a recovery version `Y` signed by the same recovery key is held, `R(X) == R(Y)`, with `K(X) != K(Y)`, whatever their bases. `X` counts by its own two signatures (I1) alone, wherever it claims to stand: its base need not be held, and need not be one | dead (fork of the recovery key), recorded at the lower of `X.v` and `Y.v`, and never below the version after the first one that committed to that recovery key; the versions below it stay |
| any | recovery `X` whose base is not held, or is a forked version, while no recovery version signed by `R(X)` that names other keys is held | no change, `X` is `unauthorized` |
| conflicted | anything that is not a recovery version | no change, `conflict` |
| dead | anything | no change, `conflict`. One thing moves: a further recovery version signed by the forked recovery key, naming other keys again, lowers the fork's `v` when its own `v` is lower, so that Directories that saw the same versions serve the same chain |

A client verifier (§18.1) holds the same states for the Actors it has resolved; "discarded" there means the same, and a client that holds a conflicted or dead Actor does not send to it.

### F.6 Priority cases and their vectors

| Case | Result | Vector or test |
| --- | --- | --- |
| rotation to the committed key vs. an ordinary same-`v` version, either order | the rotation | `tests.test_1d`, `test_1e` |
| recovery at `B.v + 1` vs. a held higher branch after `B` | the recovery; the branch is voided | `alice_recovery` over `alice_chain` (`cards.json`), `tests.test_1`, `test_1a` |
| recovery not at `B.v + 1` | `unauthorized` | `invalid.json`: "recovery version not at v+1 of its base" |
| recovery over an ordinary version | `unauthorized` | `invalid.json`: "recovery version over an ordinary version instead of a key event" |
| recovery whose key is not the one the base committed to | `unauthorized` | `invalid.json`: "recovery version whose recovery key is not the one v1 committed to" |
| second recovery version signed by a recovery key that has signed one, naming other keys, over the same base or another, either order | dead; the fork is recorded at the lower `v` | `carol_recovery_over_v3` with `carol_recovery` (`cards.json`), `tests.test_a_recovery_key_signs_one_recovery`, `test_1a`, `test_double_publish_does_not_freeze` |
| the same recovery told again over another base (the same recovery key, the same keys) | one recovery told twice: the higher current `v`, then the smaller content digest; the Actor stays active | `tests.test_a_recovery_key_signs_one_recovery` |
| recovery version that commits again to a recovery key the chain has used | `unauthorized` (I7) | `invalid.json`: "recovery version that commits again to a recovery key the chain has used" |
| a signature dated before the signer's first version | `unauthorized`, beyond the 300 s a verifier allows there (§11.5) | `invalid.json`: "record signed before the signer's first Card version", `tests.test_no_key_was_in_force_before_the_first_card` |
| two versions at one `v` that name the same keys (a retry rebuilt, two replicas), either order | one step told twice: the higher current `v`, then the smaller content digest; the Actor stays active | `alice_v2_again` with `alice_v2_kept_digest` (`cards.json`), `tests.test_1d`, `test_double_publish_does_not_freeze` |
| two rotations by the committed key that name different next keys | conflicted | `alice_fork_v3` (`cards.json`), `tests.test_1`, `test_1d` |
| a version dated before the one it follows | `unauthorized` | `invalid.json`: "ordinary version dated before the version it follows", `tests.test_card_time_is_ordered_and_registered_when_made` |
| replay of a voided branch | no change (`unauthorized`) | `tests.test_1a`, `test_1f` |
| two versions at one `v` handed as one chain | `unauthorized` | `tests.test_1d` |
| ordinary version touching a commitment | `unauthorized` | `invalid.json`: "ordinary version changes recovery_key_hash", "ordinary version re-commits next_key_hash", "ordinary version adds next_key_hash without recovery_key_hash" |
| device key signing a version | `unauthorized` | `invalid.json`: "Card version signed by a device key instead of the primary key" |
| commitments added by an ordinary version, then a rotation | the adding version is a key event: it is served, and the rotation is checked against it; served without it, the rotation is `unauthorized` | `carol_chain` (`cards.json`), `invalid.json`: "rotation served without the version that added the commitments", `tests.test_1h` |
| recovery over the version that added the commitments | the recovery | `carol_recovery` (`cards.json`), `tests.test_1h` |
| a signature by a device key whose enrolling version is not served | verifies when a served version lists the key with `valid_from` at or before `signed_at`; dated before it, `unauthorized` | `record_by_device_key` (`records_envelopes.json`), `invalid.json`: "record signed by a device key before its valid_from", `tests.test_1i` |
| a device entry without `valid_from` | `invalid_request` | `invalid.json`: "Card whose device key entry carries no valid_from" |
| a version that states another `valid_from` for a device its predecessor listed | `unauthorized`, for a verifier that holds the predecessor (I12); a Gateway compares with the latest version it holds, so a gap in `v` does not hide it | `invalid.json`: "Card version that moves the valid_from of a device key its predecessor listed", `tests.test_1i`, `tests.test_a_card_does_not_retire_a_key_in_the_past` |
| a newer version served across a gap to a verifier that holds an ordinary version | checked against the last key event; the held ordinary version is not part of the chain that is checked | `tests.test_a_directory_follows_a_reduced_chain` |
| ordinary or rotation version replacing or dropping `owner` | `unauthorized` | `invalid.json`: "ordinary version names another owner", `tests.test_1g`, `tests.test_owner_is_not_the_agent_keys_to_replace` |

