# AIRC Protocol Specification

**Draft 0.3 — 2026-09-26.** Wire protocol version `airc/0.1`.

AIRC (AI Internet Relay Chat) is a routed text-message protocol for software
agents. This document is complete: an implementer who has only this text can
build a conforming client, fleet relay, or federated server, and interoperate
with the reference implementation. The companion *AIRC Addressing Scheme*
draft gives the rationale behind the naming rules restated in §2; nothing
normative lives only there.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are as in RFC 2119.
Where this document gives an algorithm, an implementation MUST produce the
same externally visible behaviour; the internal structure is free.

### Document map

| § | what you need it for |
|---|---|
| 1 | the model and the words |
| 2 | addresses: grammar, canonical form, patterns, reserved names |
| 3 | connections and framing: the stream binding and the HTTPS binding |
| 4 | every frame, field by field |
| 5 | who a client may speak as |
| 6 | routing a message |
| 7 | the spool: queueing, TTL, dedupe |
| 8 | finding and linking to another realm |
| 9 | signing: keys, records, canonical strings |
| 10 | policy rules |
| 11 | links |
| 12 | passports |
| 13 | the audit stream |
| 14 | codes and reasons |
| 15 | constants |
| 16 | security requirements |
| 17 | conformance profiles and checklist |
| 18 | test vectors |
| 19 | end-to-end encryption (optional) |
| A | complete exchanges |

---

## 1. Model and terminology

```
                 realm example.com                          realm partner.example
   ┌──────────────────────────────────────┐        ┌──────────────────────────────┐
   │  client        client       client   │        │   client        client       │
   │ (fleet alice)  (fleet bob)  (a bot)  │        │ (support desk)  (billing)    │
   │     │             │           │      │        │       │            │         │
   │     └──────┬──────┴─────┬─────┘      │        │       └─────┬──────┘         │
   │        client face (unix socket)     │        │        client face           │
   │            ┌───────────┐             │  TLS   │        ┌───────────┐         │
   │            │  server   │─────────────┼────────┼────────│  server   │         │
   │            └───────────┘  peer link  │        │        └───────────┘         │
   └──────────────────────────────────────┘        └──────────────────────────────┘
```

- **Realm.** A DNS name (or IP literal) that owns an address space and runs
  one logical AIRC server. Also called the *authority* of an address.
- **Server.** Routes for exactly one realm. Has a **client face** for its own
  endpoints and a **peer face** for other realms' servers.
- **Endpoint.** Anything addressable under a realm: an agent, a bot, a queue.
  Named by a **path** of one or more segments.
- **Namespace.** The segments of a path before its last one; also the set of
  endpoints beneath a prefix. On a multi-user host the first segment is
  typically the user (`alice/net-ops`).
- **Client.** A process connected to its realm's client face. It may **bind**
  names to receive for them, send messages, and manage links, passports and
  subscriptions for endpoints it may act for.
- **Fleet relay.** A client that binds a whole namespace and delivers each
  inbound message to the right local agent by whatever means that site uses.
  The relay is the boundary between this protocol and a site's agent runtime.
- **Peer link.** A connection between two servers. Either may open it; once
  open it carries traffic both ways.
- **Message.** A text body with an id, an origin, a destination and a
  timestamp. Relayed origin client → origin server → destination server →
  destination client; an **ack** flows back the same way.
- **Link.** A standing grant between two endpoints (§11).
- **Passport.** A bearer invitation that creates a link when redeemed (§12).

**No transit.** A server accepts from a peer only messages *originating in
that peer's realm* and *addressed to its own realm*. A message crosses at most
one server-to-server hop. Trust decisions are therefore always about the
server that claims to have originated a message.

---

## 2. Addresses

### 2.1 Grammar

```
airc-uri      = "airc:" network-form
network-form  = "//" authority "/" path [ "#" fragment ]
local-form    = path [ "#" fragment ]

authority     = host [ ":" port ]
host          = dns-name / IPv4address / "[" IPv6address "]"
dns-name      = label *( "." label )            ; labels per RFC 1123
port          = 1*5DIGIT                         ; 1..65535

path          = segment *( "/" segment ) [ "/" ]  ; trailing "/" = namespace reference
segment       = 1*63( ALPHA / DIGIT / "-" / "_" ) ; MUST NOT begin or end with "-"
fragment      = *pchar                           ; RFC 3986
```

- The whole address MUST NOT exceed 1024 octets.
- `.` is not permitted in a segment; the authority is the only dotted
  component.
- No percent-encoding. A name that cannot be written in `segment` is not a
  valid AIRC name.
- `airc:` MUST be followed by `//`. `airc:foo` is invalid.
- A single leading `/` is invalid.

### 2.2 Canonical form and equivalence

The canonical form is the URI form with:

- authority lower-cased; a trailing `.` removed; the default port (2472)
  omitted; an IPv6 literal in brackets, lower-cased;
- every segment lower-cased;
- the trailing `/` preserved if present (it is significant);
- the fragment preserved if present.

Two addresses are **equivalent** when their canonical forms without the
fragment are byte-identical. `//h/a` and `//h/a/` are not equivalent. The
canonical form without fragment is the address's **key**, used for every
comparison in this document (binds, links, policy, dedupe).

### 2.3 Forms and resolution context

| written | means | resolved by |
|---|---|---|
| `airc://R/p` | exactly that | anyone |
| `//R/p` | `airc://R/p` | anyone |
| `ns/name` (any `/`, no leading `//`) | `airc://<local realm>/ns/name` | the local server |
| `name` (no `/`) | `airc://<local realm>/<sender's namespace>/name` | the local server |

The local server resolves local forms against its own realm and the
sender's first namespace (§5) when a client sends them. **Only the URI form
appears in `from` and `to` on the wire**; a local form in a peer-face frame is
invalid.

### 2.4 Endpoint, namespace, fragment

- A path without a trailing `/` names an endpoint: the thing at its last
  segment.
- A path with a trailing `/` names a namespace: the set of endpoints beneath
  it. It is a valid destination syntactically; delivery to it is fan-out,
  which a server MAY refuse (§6.4).
- The fragment identifies an incarnation of an endpoint. Servers MUST ignore
  it for comparison and routing and MUST pass it through unchanged in
  `from` and `to`.

### 2.5 Delegation

The endpoint at path P is the resolver for every path beneath P/. A server
forwards a message addressed beneath a bound endpoint to that endpoint with
the remaining segments intact (§6.4, §4.3) and never interprets them.

### 2.6 Reserved names

A segment beginning with `_` is reserved. Defined:

| name | at | meaning |
|---|---|---|
| `_resolver` | `//R/_resolver` | the server itself; answers a health check (§6.4) |
| `_passport` | `//R/_passport` | where redemptions are sent (§12.5) |
| `_postmaster` | `//R/_postmaster` | human contact for the realm; a server that does not provide it answers `no_such_path` |
| `_dir` | `//R/_dir`, `//R/ns/_dir` | directory agent, if a realm offers listing |
| `_links` | `//R/_links` | the sender of the server's own notices about links and passports (§11.4, §12.5); not an endpoint, messages to it are `failed no_such_path` |

Clients MUST NOT be allowed to bind a reserved name unless the operator
configures it.

### 2.7 Patterns (not addresses)

Binds (§4.3) and policy rules (§10) use **patterns**. A pattern is a
network-form address in which:

- the authority may be `*` (any authority);
- a segment may be `*` (exactly one segment);
- the last segment may be `**` (one or more segments);
- a trailing `/` matches everything strictly beneath the prefix;
- the whole pattern may be `*` (matches every address).

Matching is against the address's key. A pattern never travels inside a
message; `*` is not a valid address character.

Pattern matching algorithm, given pattern `P` and address `A`:

```
if P == "*": match
strip an "airc:" prefix; require "//"
host, path = split at first "/" after "//"
if host != "*" and host != A.authority: no match
segs = path segments (ignoring empty); ns_form = path ends with "/"
if segs is empty: no match
if ns_form: match iff len(A.path) > len(segs) and A.path[:len(segs)] == segs
for i, s in enumerate(segs):
    if s == "**": match iff i < len(A.path)
    if i >= len(A.path): no match
    if s != "*" and s != A.path[i]: no match
match iff len(segs) == len(A.path)
```

---

## 3. Connections and framing

### 3.1 Streams

An AIRC connection is an ordered, reliable byte stream: a Unix domain socket,
TCP, TLS over TCP, or a WebSocket carrying text frames. A server exposes two
**faces**, each on its own listener:

| face | who connects | typical transport |
|---|---|---|
| client | the realm's own endpoints and relays | Unix socket (identity from peer credentials, §5); loopback TCP |
| peer | other realms' servers | TCP with TLS (§8.3) |

A server MUST refuse a `hello` whose `role` does not match the face.

### 3.2 Frames

A connection carries **frames**. A frame is one JSON object (RFC 8259),
UTF-8, terminated by `\n`. Newlines inside strings are JSON-escaped, so a
frame never contains a raw newline. Every frame has a string field `t`
naming its type.

- A frame MUST NOT exceed 1 048 576 bytes including the newline.
- Blank lines MUST be ignored.
- Unknown fields MUST be ignored.
- An unknown `t` on an established connection is a protocol error: send
  `error` (§4.8) and close.
- Writers MUST serialize concurrent writes to one connection so frames do not
  interleave. Readers MUST NOT assume one frame per read.

### 3.3 Connection lifecycle (stream binding)

1. The connecting side sends `hello` (§4.1) first. The accepting side answers
   `welcome` or `error` and closes.
2. Either side may send `ping`; the other MUST answer `pong`.
3. Either side may close at any time. A client that was bound SHOULD
   reconnect with backoff and re-bind; the server spools for it meanwhile
   (§7).

### 3.4 The HTTPS binding

The same frames, one per HTTP request or response, over HTTPS. This binding
exists so that a realm can run where no long-lived socket is possible (edge
compute, serverless) and so that every realm can be reached by every other.
**A federated server MUST implement the HTTPS peer face**; it MAY also
implement the stream peer face. A client face may be either or both.

Requests and responses carry `Content-Type: application/json`; the body is
one frame (or a list where stated). HTTP status is 200 for any frame the
server produced, including a `failed` ack; 401 for a request whose
authentication failed; 404 for an unknown path; 413 for a body over 1 MiB;
429 when rate-limited; 503 when the server cannot take the request now.

#### 3.4.1 Peer face

| method and path | body | response |
|---|---|---|
| `POST /airc/v0/msg` | one `msg` frame in wire form (URI addresses, `via`, `hdr.sig`) | one `ack` frame |
| `GET /.well-known/airc?nonce=<n>` | — | the capabilities document (below) |

There is no `hello`: each message is authenticated by its signature (§9.3),
so a peer request needs nothing else. The role the `welcome` signature plays
in the stream binding, proving that the server DNS led you to holds the
realm's key, is played by the **capabilities document**:

```json
{"t":"capabilities","realm":"airc.dev","proto":"airc/0.1","bindings":["https"],
 "kids":["65b60673"],"nonce":"<echoed>","ts":1790380000,
 "sig":"ed25519:65b60673:…"}
```

`sig` is over these lines joined by `\n` (UTF-8, no trailing newline):

```
airc-capabilities
<realm>
<nonce>
<ts>
```

A dialler
MUST fetch it with a fresh nonce before its first message to a realm over
this binding, verify it against the realm's published keys, and MAY cache
the result for one hour. Discovery: `_airc-https._tcp.<realm>` SRV (target
host and port, default 443), else `https://airc.<realm>/`, else
`https://<realm>/`. A realm publishing both bindings lists both SRV
records; a dialler that implements both MAY prefer either.

#### 3.4.2 Client face

Clients authenticate every request with a key registered with the realm
(§5, remote clients; a hosted realm registers keys with §3.4.4):

```
Authorization: AIRC-Key <kid>:<ts>:<nonce>:<sig>
```

`sig` is ed25519 over these lines joined by `\n` (UTF-8, no trailing newline):

```
airc-client
<realm>
<METHOD>
<path>
<ts>
<nonce>
<body hash>
```

- `<realm>` is the realm's name as its capabilities document states it
  (`realm`), which need not be the host you connect to: `https://www.airc.dev`
  serves the realm `airc.dev`. Fetch `/.well-known/airc` first and sign for
  its `realm`.
- `<METHOD>` is upper case. `<path>` is the request path **without** the query
  string: `/airc/v0/inbox`, not `/airc/v0/inbox?max=50`.
- `<body hash>` is the lower-case hex SHA-256 of the exact body bytes sent;
  for a request without a body (every `GET`) it is the hash of the empty
  string, `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`.
- `ts` is integer Unix seconds and must be within 300 s of the server's clock.
  `nonce` is at least 12 random bytes as lower-case hex (the reference uses 12
  bytes, 24 characters) and must be unique per key for 300 s; the server keeps
  it and refuses a repeat.
- `kid` is the first 8 hex characters of SHA-256 over the raw 32-byte public
  key (§9.1). `sig` is the bare base64 of the 64-byte signature: unlike the
  signature strings in `hdr.sig` (§9.3), it has no `ed25519:<kid>:` prefix,
  because the header already names the key.

Example (the key of §18.2, realm `airc.dev`):

```
GET /airc/v0/inbox?max=50 HTTP/1.1
Host: www.airc.dev
Authorization: AIRC-Key 65b60673:1790380000:9f86d081884c7d659a2feaa0:<base64 of the 64-byte signature>

signed string:
airc-client\nairc.dev\nGET\n/airc/v0/inbox\n1790380000\n9f86d081884c7d659a2feaa0\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```

The server maps `kid` to the client's namespaces. A request that fails any of
these checks gets HTTP 401 with a reason naming the check.

| method and path | body | response |
|---|---|---|
| `POST /airc/v0/send` | one `msg` frame; local-form addresses allowed | one `ack` |
| `POST /airc/v0/bind` | `{"names":[…],"mode":"push"\|"webhook"\|"pull","webhook":"https://…"}` | `bound` plus `{"mode":…}` |
| `GET /airc/v0/inbox?max=50` | — | `{"t":"inbox","msgs":[msg…]}`: queued messages for the client's binds, oldest first, not removed until acked; each is the `msg` frame exactly as received, so its signature still verifies |
| `POST /airc/v0/ack` | a list of `ack` frames | `{"t":"acked","n":…}` |
| `GET /airc/v0/stream` | — | `text/event-stream`; each event's `data:` is one frame (`msg`, `event`, `ping`); the push channel |
| `POST /airc/v0/link` | one `link` frame | `linked` |
| `POST /airc/v0/passport` | one `passport` frame | `passported` |
| `GET /airc/v0/events?since=<ts>&kinds=msg,link` | — | `{"t":"events","events":[…]}` |

An inbox response:

```json
{"t":"inbox","msgs":[{"t":"msg","id":"cf0a7537-cecc-45aa-a7a8-0362cfdca73c","from":"airc://oroboro.com/rafael/airc",
 "to":"airc://airc.dev/alice/agent","ts":1790380000,"body":"hello","type":"text/plain","ttl":86400,
 "via":["oroboro.com"],"hdr":{"sig":"ed25519:9235d863:…"}}]}
```

**Delivery modes.** `bind` records how the server reaches this client:

- `push`: the server writes each message to the client's queue and pushes it
  over the client's open `/stream` (or WebSocket, if the server offers one).
  The client acks with `POST /ack`. A server that holds the stream itself
  MAY wait for that ack (§6.6) and answer the sender `delivered`; a server
  that cannot (edge compute pushing through a fan-out service) answers
  `queued` with code `pushed` at once, and the audit stream records the
  later `delivered`.
- `webhook`: the server `POST`s each `msg` to the client's URL (signed as a
  peer message would be, with the realm key) and relays the ack it gets.
- `pull`: the client polls `/inbox`; the sender gets `queued endpoint_offline`.

In every mode the queue is the truth: a client that connects, or polls,
drains its backlog with `/inbox` and acks. Messages are removed from the
queue only by an ack; `/inbox` may return the same message twice. A server
that does not offer a mode refuses the `bind` for it; which modes a hosted
realm offers is stated on its own documentation.

#### 3.4.4 Workspace registration (hosted realms)

A hosted realm gives a stranger an address with one unauthenticated request:

| method and path | body | response |
|---|---|---|
| `POST /airc/v0/workspaces` | `{"name":"<ns>","pub":"<base64 of the raw 32-byte ed25519 public key>"}` | `{"t":"workspace","realm":…,"ns":…,"kid":…,"uri":"airc://<realm>/<ns>/"}` |

- `name` becomes the namespace: one address segment (§2.1), lower-cased,
  letters, digits, `-` and `_`, not starting with `_`.
- On success the key acts for the namespace `<ns>/`: every endpoint beneath
  it (`<ns>/agent`, `<ns>/laptop/build`) may bind, send and manage links and
  passports. New workspaces start in `pull` mode; `bind` changes that.
- Errors: 400 `bad_name` or `bad_key`, 403 `closed` (the realm takes no new
  workspaces), 409 `taken`, 429 when rate-limited. A realm that offers no
  registration answers 404.

A server that is not a hosted realm need not offer this route; its operator
registers client keys by configuration.

What registration means to others: a hosted realm's signature on an outbound
message (§9.3) vouches that one of its workspaces sent it, and nothing about
who registered that workspace. A realm SHOULD refuse names that impersonate
infrastructure or the operator (`admin`, `abuse`, `postmaster`, its own
brands; `409 taken` or `400 bad_name`), and receivers SHOULD treat workspace
names as labels, not identities (§10.1).

#### 3.4.3 Server to server, mixed bindings

A server that receives a message over one binding and forwards it over the
other changes nothing in the frame. `via`, signatures and acks are binding-
independent.

---

## 4. Frame catalogue

Fields marked **R** are required. Types: `str`, `int`, `num` (JSON number),
`bool`, `list`, `obj`.

### 4.1 `hello` → `welcome`

`hello`, from the connecting side:

| field | type | R | meaning |
|---|---|---|---|
| `proto` | str | R | `airc/<major>.<minor>`. Refuse a different major with `error bad_proto`. |
| `role` | str | R | `client` or `peer` |
| `agent` | str |  | software name/version, informational |
| `realm` | str | peer: R | the connecting server's realm |
| `ts` | int | peer, signed | Unix seconds when the hello was made |
| `nonce` | str | peer | random, ≥ 12 bytes of entropy, hex or base64 |
| `sig` | str | peer, signed | signature over the hello (§9.4) |
| `ns` | list of str |  | client: namespace claim. Servers MUST ignore it unless explicitly configured to trust unauthenticated claims (tests only). |

`welcome`, from the accepting side:

| field | type | R | meaning |
|---|---|---|---|
| `realm` | str | R | the accepting server's realm |
| `proto` | str | R | the protocol version it speaks |
| `server` | str |  | software name/version |
| `ns` | list of str | client face: R | namespaces this connection may act for (§5). Empty means anonymous: it may neither send nor bind. |
| `sig` | str | peer face | signature over the welcome (§9.4), present when the server has a key and the hello carried a nonce |
| `kid` | str | peer face | key id of `sig` |

A dialling server MUST check `welcome.realm` equals the realm it resolved
and drop the link otherwise.

### 4.2 `msg`

| field | type | R | meaning |
|---|---|---|---|
| `id` | str | R | unique id chosen by the origin, ≤ 128 chars. UUIDv4 recommended. Duplicates are suppressed by id (§7.3). |
| `from` | str | R | origin endpoint. A client MAY write a local form; the server rewrites it to the URI form and verifies it (§5). URI form on the peer face. May carry a fragment. |
| `to` | str | R | destination. Same canonicalization; fragment passed through. |
| `ts` | num | R | origin time, Unix seconds (fractional allowed) |
| `body` | str | R | the text |
| `type` | str |  | media type of `body`; default `text/plain`. Control traffic uses `application/airc-link+json` (§11.4) and `application/airc-passport+json` (§12.4). |
| `re` | str |  | the full `id` of the message this answers, exactly as received (never shortened). Opaque to servers; correlated by readers. |
| `ttl` | int |  | seconds after `ts` the message stays deliverable; default 86400. Expired messages are failed, never delivered. |
| `via` | list of str |  | realms traversed; set by servers (§6.5) |
| `hdr` | obj |  | extension headers, relayed untouched except as noted: `sig` (§9), `airc-link`, `airc-passport` (§11.4, §12.5) |

### 4.3 `bind` → `bound`

Client face only.

```json
{"t":"bind","names":["alice/","bob/research","carol/*"]}
{"t":"bound","names":["//example.com/alice/","//example.com/bob/research","//example.com/carol/*"]}
```

- Each name is relative to the realm and MUST lie inside one of the client's
  namespaces (§5). A trailing `/` may name one of the namespaces itself. A
  reserved segment is refused unless configured.
- An endpoint bind (`bob/research`) receives that endpoint **and everything
  beneath it** (delegation, §2.5).
- A namespace bind (`alice/`) receives every endpoint beneath the prefix.
- `*` and `**` are accepted as in §2.7.
- The server replies with the accepted absolute patterns, or `error bad_bind`
  / `error forbidden` and closes.
- A bind flushes the spool for its namespace (§7.4).

**Resolving client selection.** When a message arrives for a local address
`A`, the server picks among all binds of all connected clients:

```
score(bind B for A):
  if B pattern-matches A (§2.7):
      literal = number of segments of B that are not "*"/"**"
      score = literal*2 + (1 if B has no "*" and no trailing "/" else 0)
  else if B is an exact endpoint (no "*", no trailing "/")
       and A lies strictly beneath B's path in the same authority:
      score = literal*2                       ; delegation
  else: not a candidate
choose the candidate with the highest score; ties: the most recently bound
```

### 4.4 `ack`

Every party that accepts a `msg` MUST answer exactly one `ack` for it over
the same connection.

| field | type | R | meaning |
|---|---|---|---|
| `id` | str | R | the message id |
| `status` | str | R | `delivered`, `queued` or `failed` |
| `code` | str |  | error class (§14.1); SHOULD be present on `failed` and `queued` |
| `by` | str |  | URI of the resolver that produced the status: `airc://R/_resolver` for a server, `airc://R/ns/_resolver` for a relay, the endpoint's own URI for an endpoint |
| `reason` | str |  | human-readable |

- **delivered**: the destination endpoint, or its relay, has taken the
  message. Not a read receipt.
- **queued**: a server has taken responsibility and holds a copy it will keep
  trying to deliver until the ttl expires.
- **failed**: the message will not be delivered.

Ack propagation and timeouts are in §6.6.

### 4.5 `ping` / `pong`

```json
{"t":"ping","n":7}   {"t":"pong","n":7}
```

`n` is optional and echoed.

### 4.6 `link` → `linked`

Client face only. Manages links (§11) for endpoints the client may act for.

| field | type | ops | meaning |
|---|---|---|---|
| `op` | str | R | `offer`, `accept`, `decline`, `revoke`, `list` |
| `as` | str | all but `list` | the acting endpoint (local form allowed). MUST be inside the client's namespaces. |
| `peer` | str | `offer` | the other endpoint |
| `label` | str | `offer` | ≤ 200 chars, informational |
| `direction` | str | `offer` | `both` (default), `a-initiates`, `b-initiates` |
| `expires` | str/int | `offer` | lifetime: seconds, or `30d`/`12h`/`45m`/`2w` |
| `id` | str | accept/decline/revoke | link id |
| `all` | bool | `list` | administrator connections only: every link in the realm |

`linked`:

| field | type | meaning |
|---|---|---|
| `ok` | bool | |
| `link` | obj | the link record (§11.1) after the operation |
| `links` | list | for `list` |
| `delivery` | str | ack status of the control message sent to the other side |
| `reason`, `code` | str | on failure |

### 4.7 `passport` → `passported`

Client face only. Manages passports (§12).

| field | type | ops | meaning |
|---|---|---|---|
| `op` | str | R | `new`, `list`, `revoke`, `redeem` |
| `as` | str | all but `list` | the acting endpoint |
| `label` | str | `new` | ≤ 200 chars |
| `expires` | str/int | `new` | passport lifetime (default 7 days, capped by the realm) |
| `uses` | int | `new` | redemptions allowed (default 1, capped) |
| `direction` | str | `new` | direction of the links it creates |
| `to` | str | `new` | pattern (§2.7) the redeemer must match |
| `link_ttl` | str/int | `new` | lifetime of created links (default 30 days) |
| `id` | str | `revoke` | passport id |
| `and_links` | bool | `revoke` | also revoke every link the passport created |
| `passport` | str | `redeem` | any rendering (§12.2) |
| `all` | bool | `list` | administrators: every passport |

`passported`: `ok`; for `new`: `passport` (record without the token hash),
`line`, `file`, and `url` when the realm configures a web base; for `list`:
`passports`; for `redeem`: `status`, `reason`, `code`, and `link` on success;
for `revoke`: `passport`, `revoked_links`.

### 4.8 `error`

```json
{"t":"error","code":"forbidden","reason":"alice/ is outside your namespaces"}
```

Sent before closing a connection for a protocol violation. Codes in §14.2.

### 4.9 `subscribe` → `subscribed`, `event`

Client face only. Live audit stream (§13).

```json
{"t":"subscribe","kinds":["msg","link"]}
{"t":"subscribed","kinds":["msg","link"],"scope":["alice"]}
{"t":"event","ts":1790380000.123,"kind":"msg.delivered","id":"…","from":"airc://…","to":"airc://…","status":"delivered","hop":"client","bytes":42}
```

`kinds` filters by full kind or by prefix (`msg`). `scope` is the list of
namespaces the subscriber sees, or `all` for an administrator.

### 4.10 `stat` → `stats` (optional)

A client MAY send `{"t":"stat"}` and receive `{"t":"stats", ...}` with
implementation-defined counters. Servers MAY restrict it.

---

## 5. Client identity and namespaces

A client never asserts its own identity. The server derives the set of
**namespaces** a connection may act for from the transport and tells the
client in `welcome.ns`:

- **Unix socket**: the peer uid (`SO_PEERCRED`, `LOCAL_PEERCRED`) looked up
  in the operator's `uid → namespaces` table.
- **Loopback TCP**: the owning uid of the remote socket (Linux: `/proc/net/tcp`),
  same table.
- **Remote clients (HTTPS binding)**: the key in the `Authorization` header
  (§3.4.2), looked up in the realm's `key → namespaces` table. Keys are
  registered out of band (operator configuration) or, on a hosted realm,
  through workspace registration (§3.4.4); the protocol only requires that
  the table exist.
- **Anything else**: no namespaces.

A server MUST enforce, for every client-face operation:

- `msg.from`, after canonicalization, lies inside one of the connection's
  namespaces (`ns/…` with at least one segment after the namespace);
- every `bind` name lies inside one (a namespace bind may name it exactly);
- `as` in `link` and `passport` frames lies inside one.

Consequently the `from` on a message leaving a server is a statement the
*server* makes. Identity is **namespace-granular**: the server proves which
namespace (user) a message came from; the endpoint name within it is asserted
by that user's process. Policy between endpoints of different namespaces is
enforceable; between endpoints of one namespace it is advisory.

An **administrator connection** (root, or the account the server runs as,
plus configured uids) with no namespaces of its own may list every link,
passport and event.

---

## 6. Routing

### 6.1 Overview

On receiving `msg` M over connection C, a server for realm R performs, in
order: validate → canonicalize and authenticate origin → expiry and dedupe →
control-traffic dispatch → policy → deliver → record → ack.

### 6.2 Validate, canonicalize, authenticate

```
validate types of id, from, to, body, ts, ttl, via, hdr (§4.2); else ack failed bad_address
if C is a client:
    ctx = C.namespaces[0] or ""
    frm = canonical(from, realm=R, ns=ctx); to = canonical(to, realm=R, ns=ctx)
    require frm.realm == R and frm inside C.namespaces      ; else failed refused
    via = []
else (C is a peer that said realm P in hello):
    frm = canonical(from); to = canonical(to)                 ; local forms invalid -> failed bad_address
    require frm.realm == P                                     ; else failed refused (no relaying for others)
    require to.realm == R                                      ; else failed refused (no transit)
    unless P is a trusted-unsigned peer:
        keys = published keys of P (§8.2); require non-empty   ; else failed refused
        require verify_message(M, keys) (§9.3)                 ; else failed refused
rewrite M.from, M.to to the canonical URI forms (fragments kept)
if expired(M): failed expired
if id seen (§7.3): ack delivered, code duplicate
```

### 6.3 Control traffic

Before policy:

- `type == application/airc-passport+json` and `to == //R/_passport` →
  passport redemption (§12.5).
- `type == application/airc-link+json` → link control (§11.4). Admitted
  independently of message policy, subject to §11.5.

### 6.4 Policy and local delivery

```
allowed, why = policy.check(frm, to)        (§10)
if not allowed: failed refused
if to.realm == R and to has no explicit port:
    if to is a namespace reference: failed namespace_not_endpoint   ; fan-out not offered
    if to.path == ["_resolver"]: ack delivered (health check), body ignored
    if to is reserved and no client is bound for it: failed no_such_path
    client = select(to)                       (§4.3)
    if client:
        forward M to client; wait for its ack (§6.6); relay that ack
    else if to.path[0] is a namespace this realm knows (any configured namespace):
        spool M under key to.path[0]; ack queued endpoint_offline
    else:
        failed no_such_path, by airc://R/_resolver
else:
    remote delivery (§6.5)
```

### 6.5 Remote delivery

```
if R in M.via or len(M.via) >= 8: failed routing_loop
M.via.append(R)
if server has a signing key and M.hdr.sig is absent: sign M (§9.3)
key = to.authority
link = an authenticated peer link to key (§8.4), dialling if needed
if none:
    if DNS had no authority at all: failed no_such_authority
    else: spool M under key; ack queued authority_unreachable
forward M over link; wait for ack (§6.6); relay it
if the link dropped before an ack: spool M; ack queued authority_unreachable
```

### 6.6 Acks, timeout, settlement

When a server forwards M to a next hop it waits at most **15 s** for that
hop's ack.

- Ack arrives: relay it upstream unchanged (status, code, by, reason).
- Timeout: spool M under its destination key, ack upstream `queued no_ack`,
  and keep M's id as *in flight*. When the late ack eventually arrives, mark
  the id *settled*; a spool flush (§7.4) MUST drop a settled message instead
  of resending it, and MUST NOT resend a message still in flight.
- Next hop's ack is `failed` with a reason beginning `link`: treat as a
  dropped link (spool, `queued`).

On `delivered` or `queued` the server records the id as seen (§7.3) and
notes the pair (from, to) for the reply window (§10.3).

---

## 7. The spool

### 7.1 Keys

A queued message is filed under a **destination key**: the authority string
(`partner.example`, `partner.example:7001`) for a remote destination, or the
first path segment (the namespace) for a local one.

### 7.2 Expiry

A message is expired when `ts + ttl < now` (ttl default 86400). Expired
messages are dropped when read from the spool and never delivered.

### 7.3 Duplicate suppression

A server keeps the ids of messages it has acked `delivered` or `queued` for
at least 2 × the default ttl (172 800 s). A message whose id is known is
acked `delivered` with code `duplicate` and not delivered again, so origins
MAY retry freely.

### 7.4 Flushing

A server MUST attempt delivery of a key's backlog:

- when a client binds inside that namespace;
- when an *authenticated* peer link to that authority comes up, in either
  direction;
- periodically (reference: every 30 s) for every key with a backlog.

Flushing preserves order per key, requeues what still cannot be delivered,
and honours §6.6 settlement.

---

## 8. Discovery and peer links

### 8.1 Locating an authority

For authority `A` (host, optional port):

0. explicit port → connect to host:port; IP literal → connect directly, port
   2472; no DNS in either case (stream binding);
1. a static entry in the server's configuration (either binding);
2. DNS `SRV _airc-https._tcp.<host>` → HTTPS binding at target:port (default 443);
   DNS `SRV _airc._tcp.<host>` → stream binding at target:port;
3. `https://airc.<host>/` (HTTPS), or `A/AAAA airc.<host>` port 2472 (stream);
4. `https://<host>/`, or `A/AAAA <host>` port 2472.

A federated server MUST publish the HTTPS binding (step 2 or a working step
3/4) and MAY publish the stream binding as well.

Cache results for the DNS TTL (clamped to 30 s … 3600 s).

### 8.2 Published keys

A realm publishes one TXT record per signing key at `_airc.<realm>`:

```
_airc.example.com.  IN TXT "v=airc1 k=ed25519 p=<base64 public key>"
```

Fields are space- or semicolon-separated `key=value`. `k` defaults to
`ed25519`. Verifiers fetch all records and accept a signature under any
of them (§9.3). Cache for the TTL; cache a miss for 60 s. A static
`known_keys` table MAY replace DNS for named realms.

### 8.3 Transport security

Peer links MUST use TLS in production. The dialler verifies the accepting
server's certificate for the resolved target host against its trust store.
TLS MAY be terminated by a proxy in front of the server. Confidentiality and
destination-host authentication come from TLS; realm authentication comes
from §9.

### 8.4 Opening and using a peer link

Dialling server D, to realm T:

```
connect (TLS) to the resolved endpoint
nonce = random
hello = {proto, role:"peer", realm:D, agent, ts:now, nonce, sig: sign_hello(D, T, ts, nonce)}   (§9.4)
welcome = read; require welcome.realm == T
unless T is trusted-unsigned: require verify_welcome(welcome, T, D, nonce, keys(T))
mark the link AUTHENTICATED; register it for T
```

Accepting server T, from a peer claiming realm P:

```
authenticated = P is trusted-unsigned
             or (hello.sig present and verify_hello(hello, T, keys(P)) with |now - hello.ts| <= 300 s)
register the link for P with that flag
welcome = {realm:T, proto, server, sig: sign_welcome(T, P, hello.nonce), kid}   (sig if T has a key)
if authenticated: flush T's spool for P over this link
```

Rules:

- A server MUST only *send* to a realm over an authenticated link (one it
  dialled, or an inbound one whose hello verified). It MAY *receive*
  individually signed messages over an unauthenticated inbound link. This
  is what stops an impostor from being handed a realm's queued mail.
- A server SHOULD accept several simultaneous links from one realm and MAY
  close a dialled link idle for 600 s.
- Acks travel back over the link the message arrived on.

---

## 9. Signing

### 9.1 Keys

Each realm holds one or more ed25519 keys (RFC 8032). The public half is
published per §8.2. The **kid** of a key is the first 8 hex digits of
SHA-256(raw 32-byte public key).

### 9.2 Signature format

```
sigstring = alg ":" kid ":" base64(signature)
alg       = "ed25519"
```

`hdr["sig"]` is one sigstring or a list of them. A verifier tries each
sigstring whose `alg` is on its **allowlist** (default `["ed25519"]`; the
value `none` is never valid) against every published key of the origin
realm, ordering keys with a matching kid first; any success verifies.

**Agility without negotiation.** Nothing about algorithms is negotiated on
the wire. To adopt a new algorithm a realm publishes a second key
(`k=<new>`), carries two signatures per message, and retires the old key
once its peers have added the new algorithm to their allowlists. A message
cannot select a weaker algorithm than a verifier permits.

### 9.3 Message signatures

Canonical string (UTF-8; fields joined by `\n`, no trailing newline):

```
id
from                         (canonical URI as it appears on the wire, fragment included)
to
ts_ms                        (round(ts × 1000), decimal integer)
type                         ("text/plain" when absent)
re                           ("" when absent)
body
```

The origin server signs every message leaving its realm and places the
result in `hdr["sig"]`. A receiving server MUST refuse (`failed refused`) a
peer message with a missing or invalid signature unless the origin realm is
configured as trusted-unsigned. Fields not covered (`ttl`, `via`, other
`hdr` entries) are routing metadata a hop may change.

### 9.4 Hello and welcome signatures

```
hello:    "airc-hello\n"   + dialler_realm + "\n" + target_realm + "\n" + ts + "\n" + nonce
welcome:  "airc-welcome\n" + acceptor_realm + "\n" + dialler_realm + "\n" + nonce
```

`ts` is the decimal integer from the hello. The target realm in the hello
binds it to one destination, so a captured hello cannot be replayed to a
different realm; the 300 s window bounds replay to the same realm, which
gains an attacker nothing beyond an authenticated-but-empty link.

---

## 10. Policy

### 10.1 Rules

A realm's policy is an ordered list of rules plus a default:

```json
{"from": "//example.com/**", "to": "//example.com/**", "allow": true, "note": "local fleets"}
{"links": true}
{"from": "*", "to": "//product.example/support", "allow": true}
{"from": "//partner.example/**", "to": "//example.com/alice/**", "allow": true, "mode": "reply", "window": 86400}
{"from": "*", "to": "*", "allow": false}
```

| field | meaning |
|---|---|
| `from`, `to` | patterns (§2.7); default `*` |
| `allow` | bool; default true |
| `mode` | `any` (default) or `reply` |
| `window` | seconds, for `reply`; default 86400 |
| `links` | `true` marks where links (§11) are evaluated |
| `note` | informational |

A rule that allows a whole realm (`//realm/**`) trusts everyone that realm
admits. For a hosted realm with open registration (§3.4.4) that is every
stranger who signs up, so such rules SHOULD NOT name a hosted realm: grant
exact endpoints with links (§11) or passports (§12) instead.

### 10.2 Evaluation

```
if no rule has links:true: d = link_decision(from, to); if d: return d
for rule in rules:
    if rule.links: d = link_decision(from, to); if d: return d; continue
    if not match(rule.from, from) or not match(rule.to, to): continue
    if rule.mode == "reply" and not in_reply_window(from, to, rule.window): continue
    return rule.allow
return default_allow
```

Both the origin server (egress) and the destination server (ingress)
evaluate their own policy; the ingress decision protects the realm.

### 10.3 Reply window

The deciding server records, for every message it acks `delivered` or
`queued`, the time the pair (from, to) last passed. `in_reply_window(a, b,
w)` is true when `b` messaged `a` within the last `w` seconds. This is how
"they may answer us but not start a conversation" is expressed without a
second mechanism.

---

## 11. Links

A link is a standing grant between two endpoints, recorded at both
endpoints' servers and enforced by each independently.

### 11.1 Record

```json
{"id":"L7f3a9c1e","a":"airc://example.com/alice/x","b":"airc://partner.example/support/desk",
 "direction":"both","label":"vendor","created":1790380000.1,"expires":1792972000.1,
 "state":"active","offered_by":"airc://example.com/alice/x",
 "origin":{"passport":"P3f9a1c2e","issued_by":"…","redeemed_by":"…"},
 "accepted_at":…,"declined_at":…,"revoked_at":…,"revoked_by":"…"}
```

- `id`: `L` + 8 lower-case hex, chosen by the offerer or issuer.
- `a`, `b`: endpoint keys (§2.2). `b` MAY be a namespace reference
  (trailing `/`) when the offerer chose to link to a whole namespace.
- `direction`: `both`, `a-initiates`, `b-initiates`.
- `state`: `pending` → `active` | `declined`; any → `revoked`. States only
  move forward; a revoked link never leaves that state.
- `expires`: null or a time after which the link has no effect.

### 11.2 Operations and who may perform them

| op | by | precondition | result |
|---|---|---|---|
| offer | endpoint a | a ≠ b; a has < 20 pending offers | record `pending`, `offered_by = a`; control `offer` to b |
| accept | endpoint b (the offeree) | state `pending`; actor ≠ offerer | `active`; control `accept` to a |
| decline | endpoint b | state `pending` | `declined`; control `decline` to a |
| revoke | a or b | state `pending` or `active` | `revoked`, `revoked_by`; control `revoke` to the other |
| grant | the issuer of a passport (§12.5) | — | record created `active`; control `grant` to b |

A client acts through the `link` frame (§4.6); the server performs the
state change locally first, then sends the control message.

### 11.3 Decision rule

Given from F and to T, collect all records joining F and T (either order; a
namespace-side `b` matches any endpoint beneath it), **newest first** by
`created`:

```
for rec in records:
    if rec.state == "revoked": return DENY ("link <id> revoked")
    if rec.state != "active" or rec expired: continue
    F_is_a = rec.a == F or (rec.a is a namespace and F beneath it)
    if rec.direction == "both"
       or (rec.direction == "a-initiates" and F_is_a)
       or (rec.direction == "b-initiates" and not F_is_a): return ALLOW
    if in_reply_window(F, T): return ALLOW
    return DENY ("<id> is <direction>; F may only reply")
return NO DECISION
```

The newest decisive record wins: a revocation blocks until the two endpoints
agree again (a new accepted offer, or a redeemed passport), and no longer.

### 11.4 Control messages

Link control travels as ordinary `msg` frames:

- `type`: `application/airc-link+json`
- `from`: the acting endpoint; `to`: the other endpoint
- `body`: `{"action": "offer"|"accept"|"decline"|"revoke"|"grant", "link": <record>}`, optionally with
  `"e2e": {"ik": …}`, the acting endpoint's identity key (§19.9)
- `hdr["airc-link"]`: the link id

A server receiving one for a local endpoint MUST:

1. validate the record (§11.1) and require `from` and `to` to be its `a`
   and `b` (either order);
2. apply the action with the same preconditions as §11.2 (`offer`: state
   `pending`, `offered_by == from`, realm accepts offers; `accept`/`decline`:
   only from the offeree; `grant`: `a == from`, `b == to`, state `active`;
   `revoke`: any party, and a revoke for a link never seen is stored, since it
   can only deny);
3. store/merge the record (states forward-only);
4. ack `delivered` once stored, then notify the local endpoint with a plain
   `text/plain` message that names the link id and the commands available
   (accept/decline for an offer; revoke otherwise). Notification failures do
   not change the ack.

A notice is the server's own statement, not the other party's, so it comes
**from `airc://<realm>/_links`** of the recipient's realm. Where the server
signs what it delivers to clients (the HTTPS binding does), the notice is signed
with that realm's key, so it verifies like any message; over a local socket it
is unsigned, like every local message. It carries:

| header | meaning |
|---|---|
| `airc-link` | the link id |
| `airc-link-action` | `offer`, `accept`, `decline`, `revoke` or `grant` |
| `airc-peer` | the other party's endpoint key; a client shows the notice as about, and replies to, this endpoint |
| `airc-passport` | on a redemption notice (§12.5), the passport id |

A server notifies for a local actor too (both ends in one realm), in the same
form.

`auto_accept` patterns: a server MAY be configured to accept offers for
matching local endpoints immediately; it then sets `active`, sends `accept`,
and tells the endpoint the link opened automatically.

### 11.5 Admission

Link control is admitted regardless of message policy so that a stranger can
ask (the **knock** is simply an `offer`). A server MAY refuse all offers
(`accept_offers: false`), MUST cap pending offers per offerer (20), and
SHOULD rate-limit offers per source.

---

## 12. Passports

A passport is a bearer invitation, issued by the server of an inviting
endpoint, that creates an active link when redeemed. It is validated only by
the issuer, so it needs no cryptography, can be revoked instantly, and every
redemption is visible to the issuer.

### 12.1 Record (issuer side)

```json
{"id":"P3f9a1c2e","token_hash":"<sha256 hex of the token>","endpoint":"airc://example.com/alice/x",
 "label":"Bob's reviewer","direction":"both","created":…,"expires":…,"uses":1,"used":[{"redeemer":"airc://…","at":…,"link":"L…"}],
 "bind_to":"//partner.example/**","link_ttl":2592000,"state":"active","issued_by":"airc://example.com/alice/x"}
```

- `id`: `P` + 8 hex. The token itself is ≥ 128 bits from a CSPRNG, base62,
  shown to the issuer once and stored only as SHA-256.
- `state`: `active` → `exhausted` (all uses consumed) | `revoked`.

### 12.2 Renderings

```
line  = "airc-passport:1;" "i=" issuer ";" [ "e=" endpoint-path ";" ] [ "k=" ifp ";" ] "t=" token   ; k= per §19.9
url   = <web base> "#" line                       ; token stays in the fragment, never sent to the web server
file  = {"airc_passport":1,"issuer":…,"endpoint":"airc://…","token":…,"label":…,"direction":…,"expires":"<ISO 8601>","uses":…,"redeem":"airc://<issuer>/_passport"}
```

A redeemer MUST accept all three. Only `issuer` and `token` are needed to
redeem; the rest is for display.

### 12.3 Issuance

Through `passport op=new` (§4.7). The realm decides who may issue
(`agents`: any endpoint for itself; `owner-only`: administrator connections;
`off`). Defaults: 1 use, 7 days, direction `both`, link lifetime 30 days;
caps: 100 uses, 90 days. `to` binds the passport to redeemers matching a
pattern.

### 12.4 Redemption request

The redeemer's client sends `passport op=redeem` with `as` = the redeeming
endpoint. Its server sends:

- `type`: `application/airc-passport+json`
- `from`: the redeemer; `to`: `airc://<issuer>/_passport`
- `body`: `{"action":"redeem","token":"<token>","redeemer":"<redeemer key>"}`
- `ttl`: SHOULD be short (600 s)

Locally if the issuer is this realm, else via remote delivery (§6.5, so it is
signed and spooled like anything else). The ack to this message is the
result.

### 12.5 Redemption (issuer)

```
if source has ≥ 20 failed redemptions in the last hour: failed refused ("too many failed redemptions")
rec = record whose token_hash == sha256(token)
why = first of:
      rec is None                              -> passport_unknown
      rec.state == revoked                     -> passport_revoked
      rec.expires < now                        -> passport_expired
      len(rec.used) >= rec.uses                -> passport_exhausted
      rec.bind_to and not match(bind_to, from) -> passport_not_for_you
      from == rec.endpoint                     -> passport_not_for_you
if why: count a failure; failed refused, reason = why with "_" -> " "
link = new record: a = rec.endpoint, b = from, direction = rec.direction, label = rec.label,
       expires = now + rec.link_ttl, state = active, offered_by = a,
       origin = {passport: rec.id, issued_by: rec.issued_by, redeemed_by: from}
store link; append to rec.used; exhausted if uses consumed
send control grant(link) to from and WAIT for its ack        ; the redeemer's server records it first
notify rec.endpoint (a notice from //<realm>/_links, §11.4: who redeemed, link id, uses left, how to revoke)
ack delivered, reason "link <id> granted"
```

`_passport` is reserved and handled by the server itself; a message of
another type to it is `failed no_such_path`.

### 12.6 Revocation

`passport op=revoke` sets `revoked`; with `and_links`, every link whose
`origin.passport` is this id and is `pending` or `active` is revoked and
`revoke` controls are sent to the other sides.

---

## 13. The audit stream

A server keeps an append-only log of events, without message bodies, and
streams the same records to subscribers (§4.9).

Event fields: `ts`, `kind`, and as applicable `id`, `from`, `to`, `status`,
`code`, `reason`, `by`, `hop` (`client`, `peer`, `local`, `spool`), `type`
(when not `text/plain`), `re`, `bytes` (body length), `link`, `passport`,
`endpoint`, `realm`, `authenticated`, `outbound`, `label`, `state`, `uses`,
`uses_left`.

Kinds: `msg.delivered`, `msg.queued`, `msg.failed`, `msg.flushed`,
`link.offer`, `link.accept`, `link.decline`, `link.revoke`, `link.grant`,
`passport.issue`, `passport.redeem` (with `status: failed` and `code` on a
refused attempt), `passport.revoke`, `peer.up`, `peer.down`.

A subscriber sees an event when any of its `from`, `to`, `endpoint`, `realm`
lies inside one of the subscriber's namespaces; an administrator sees all.
Body previews, if a site wants them, are written by its relay at delivery
time.

---

## 14. Codes and reasons

### 14.1 Ack `code`

| code | status | meaning | produced by |
|---|---|---|---|
| `no_such_authority` | failed | DNS has nothing for the realm | sender's server |
| `authority_unreachable` | queued | resolved but nothing answered, or link dropped | sender's server |
| `no_such_path` | failed | a resolver does not know the next segment | the last resolver that owned a prefix |
| `path_gone` | failed | existed and was torn down | the delegating endpoint |
| `not_delegated` | failed | owner does not accept sub-paths | the owning endpoint |
| `namespace_not_endpoint` | failed | destination ends in `/` and fan-out is refused | the resolver for it |
| `stale_instance` | failed | fragment does not match the current incarnation | the endpoint |
| `refused` | failed | identity, signature or policy refused it; passport refusals | whoever refused |
| `bad_address` | failed | malformed address or frame content | the server |
| `expired` | failed | ttl passed | the server |
| `routing_loop` | failed | realm already in `via`, or 8 hops | the server |
| `no_ack` | queued | next hop silent for 15 s; spooled | the server |
| `endpoint_offline` | queued or failed | nothing bound (queued) / agent not running (failed, from a relay) | server or relay |
| `duplicate` | delivered | id already seen | the server |
| `pushed` | queued | pushed to the recipient's open stream; the recipient's ack will follow asynchronously | the server |
| `e2e_no_key` | failed | the recipient has no private key for the envelope's `kid` (§19.6) | the recipient's client |
| `e2e_decrypt` | failed | the envelope did not open (§19.6) | the recipient's client |
| `e2e_sender` | failed | the plaintext's sender key or signature failed (§19.6) | the recipient's client |
| `e2e_required` | failed | the endpoint accepts only encrypted messages (§19.7) | the recipient's server |

Passport refusals use `refused` with reason `passport unknown`, `passport
expired`, `passport exhausted`, `passport revoked`, `passport not for you`.

### 14.2 `error` frame codes

`expected_hello`, `bad_proto`, `wrong_face`, `bad_frame`, `bad_json`,
`frame_too_large`, `bad_bind`, `forbidden`, `unexpected_frame`,
`no_welcome`, `realm_mismatch`, `peer_unverified`.

---

## 15. Constants

| name | value |
|---|---|
| default peer port | 2472 |
| max frame | 1 048 576 bytes |
| default message ttl | 86 400 s |
| dedupe retention | 172 800 s |
| max hops (`via`) | 8 |
| next-hop ack timeout | 15 s |
| spool retry interval | 30 s |
| dialled peer link idle close | 600 s |
| hello timestamp skew | 300 s |
| DNS cache clamp | 30 … 3600 s; misses 60 s |
| pending link offers per offerer | 20 |
| passport defaults / caps | 1 use (cap 100); 7 days (cap 90); link ttl 30 days |
| failed redemptions per source per hour | 20 |
| label length | 200 chars |
| link id / passport id | `L`/`P` + 8 hex |
| E2E encryption key lifetime | default 7 days, at most 30 (§19.8) |
| E2E key record cache | at most 3600 s (§19.3) |

Values other than the port, frame size, hop limit and id formats are
recommendations an operator may tune; peers do not depend on them.

---

## 16. Security requirements

1. **Never take identity from the sender.** Derive namespaces from the
   transport (§5); rewrite `from`.
2. **State what an address proves.** Namespace-granular on a shared host.
3. **Verify origins before opening the peer face.** Signed messages, signed
   hello, signed welcome, published keys (§9). Send only over authenticated
   links (§8.4).
4. **Mark foreign origins for the reader.** A relay MUST make a message from
   another namespace or realm recognisable as foreign to the agent that
   reads it (a banner line in the text, and a sender name the agent cannot
   confuse with a local peer). Text from elsewhere is an injection vector.
5. **Admit control traffic narrowly.** Offers and redemptions cross policy;
   cap and rate-limit them; bodies are attacker-controlled text and MUST be
   shown to agents with the same foreign framing.
6. **Keep secrets out of logs.** Passport tokens are stored hashed and shown
   once; the audit stream carries no bodies; the passport web page keeps the
   token in the URL fragment.
7. **Bound resources.** Frame size, hops, pending offers, redemption
   failures, spool by ttl, subscriber queues.
8. **Never let a key change pass silently.** A client using end-to-end
   encryption stops and asks when a peer's identity key changes without
   succession (§19.4); servers are exactly the parties it guards against.
9. **Prefer failing loudly.** A message that cannot be delivered is `failed`
   or `queued` with a code; there is no silent drop.

---

## 17. Conformance

### 17.1 Profiles

| profile | MUST implement |
|---|---|
| **Client** | §3; `hello`/`welcome`, `msg`, `ack`, `ping`/`pong`, `error`; local-form addressing (§2.3) |
| **Relay** | Client, plus `bind` for a namespace, delegation of sub-paths to the agent, truthful acks with `code`/`by`, the foreign-origin banner (§16.4) |
| **Server** | §2–§7, §10–§13, client face, `_resolver`, `_passport`, admin listing |
| **Federated server** | Server, plus §8–§9: discovery, TLS, signing, authenticated links; the HTTPS peer face (§3.4.1) is mandatory, the stream peer face optional |
| **Hosted realm** | Federated server, plus the HTTPS client face (§3.4.2) with registered client keys, the three delivery modes, and workspace registration (§3.4.4) |
| **E2E** (optional, any client or relay; servers: the key directory) | §19: key records, pins, HPKE envelope, sender signature, rotation; servers route envelopes unchanged and serve `/airc/v0/keys` |

### 17.2 Checklist

- [ ] Addresses: all rows of §18.1 parse or fail as stated.
- [ ] Framing: 1 MiB limit enforced; blank lines ignored; unknown `t` → `error`.
- [ ] Identity: `from` outside the connection's namespaces refused; binds likewise.
- [ ] Bind selection follows §4.3 scoring, including delegation.
- [ ] Routing follows §6, including `via`, `_resolver`, namespace refusal.
- [ ] Every accepted `msg` gets exactly one `ack`; `queued` always means a copy is held.
- [ ] Ack timeout spools and settles (§6.6); no double delivery on a late ack.
- [ ] Spool flushes on bind, on authenticated link up, and periodically; expired dropped; duplicates acked `duplicate`.
- [ ] Discovery order §8.1; keys from `_airc.<realm>` TXT.
- [ ] Signatures verify against §18.2; unsigned/invalid/tampered peer messages refused; unauthenticated inbound links never used to send.
- [ ] Policy evaluation order §10.2; reply window §10.3.
- [ ] Links: state machine §11.2; decision rule §11.3 (newest decisive wins); controls §11.4.
- [ ] Passports: renderings §12.2 parse; redemption order §12.5; grant recorded at the redeemer before the ack; revocation with `and_links`.
- [ ] Events written without bodies; subscriber scoping.
- [ ] Reserved names not bindable by clients.

---

## 18. Test vectors

### 18.1 Addresses

| input (context realm `example.com`, namespace `alice`) | canonical / result |
|---|---|
| `net-ops` | `airc://example.com/alice/net-ops` |
| `bob/net-ops` | `airc://example.com/bob/net-ops` |
| `//Example.COM/Alice/Net-Ops` | `airc://example.com/alice/net-ops` |
| `airc://x.org/a/b` | `airc://x.org/a/b` |
| `//example.com:2472/a` | `airc://example.com/a` |
| `//example.com:7001/a` | `airc://example.com:7001/a` |
| `//[2001:DB8::1]/a` | `airc://[2001:db8::1]/a` |
| `//192.0.2.1/a` | `airc://192.0.2.1/a` |
| `//h.com/alice/research/` | `airc://h.com/alice/research/` (namespace; ≠ `//h.com/alice/research`) |
| `//h.com/a/b#run-42` | `airc://h.com/a/b#run-42`; key `airc://h.com/a/b`; equivalent to `//h.com/a/b#x` |
| `//h.com/_resolver` | reserved |
| `a.b`, `-a`, `a-`, `airc:foo`, `//h`, `//h/a//b`, `/x`, `//h.com:99999/a`, `//[::zz]/a`, `a b` | error |

Patterns against `airc://example.com/alice/aih`:

| pattern | match |
|---|---|
| `*` | yes |
| `//example.com/alice/*` | yes |
| `//*/alice/aih` | yes |
| `//example.com/**` | yes |
| `//example.com/alice/` | yes |
| `//example.com/alice/aih/x` | no |
| `//example.com/alice` | no |
| `//example.com/bob/*` | no |

`//example.com/alice/` against `airc://example.com/alice` (the endpoint):
no.

### 18.2 Signing

Private key seed (32 bytes, base64): `AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=`
(bytes 0x01 … 0x20).
Public key: `ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=`, kid `65b60673`.
DNS: `_airc.example.com. IN TXT "v=airc1 k=ed25519 p=ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ="`

Message:

```json
{"id":"0f5a3c2e-9b1d-4e77-a6c8-1d2e3f4a5b6c","from":"airc://example.com/alice/x",
 "to":"airc://partner.example/support/desk","ts":1790380000.123,"body":"hello","type":"text/plain"}
```

Canonical string (`\n` shown literally):

```
0f5a3c2e-9b1d-4e77-a6c8-1d2e3f4a5b6c\nairc://example.com/alice/x\nairc://partner.example/support/desk\n1790380000123\ntext/plain\n\nhello
```

Signature: `ed25519:65b60673:oyBI45ZxQdCsc8sPRs4SMLnfo5jHuqHHuOQ9SG6WIhu+le9DbGRBzmrZQj+sli7/PNwS2aJWL4l+KVx2q/b3BA==`

Hello from `example.com` to `partner.example`, `ts` 1790380000, nonce
`6f1d2c3b4a596877`. Canonical: `airc-hello\nexample.com\npartner.example\n1790380000\n6f1d2c3b4a596877`.
Signature: `ed25519:65b60673:U/YtYxDq0VDtg88OOULLf3Q1pJQr8frOI/pTV+mJRO8mawiFaj/E9aZHL3izuNMHesk/f4YXirxUYvjsqO1KCg==`

Welcome from `partner.example` to `example.com` with that nonce, signed by
the same key (for the vector): canonical
`airc-welcome\npartner.example\nexample.com\n6f1d2c3b4a596877`.
Signature: `ed25519:65b60673:BnwUly7WPUSiAm2Hbwp1j9Aeqz6PcTRCr2E5Qq+bvT2BR48ujTUuyOKJ1ULDlAx9VzuqAQjydQe/IJAhC668DQ==`

### 18.3 Passport

Line: `airc-passport:1;i=example.com;e=alice/x;t=Zk3fQ9uT2mH7wLp4Rb8yNc6VdX1aS0gE`
Parses to issuer `example.com`, endpoint `alice/x`, token as given.
`token_hash` = SHA-256 of the token = `3f5fb3a4eca12051704aa76cca72b0d1a6aec1161b6046a8f547eb811ed808ba`.
URL form: `https://airc.example/p/#airc-passport:1;i=example.com;e=alice/x;t=Zk3fQ9uT2mH7wLp4Rb8yNc6VdX1aS0gE`.

### 18.4 Link decision

Records between X and Y, newest first, and the expected decision for X → Y:

| records | decision |
|---|---|
| none | no decision |
| `active both` | allow |
| `active a-initiates` (X is a) | allow |
| `active a-initiates` (X is b), Y has not messaged X | deny |
| `active a-initiates` (X is b), Y messaged X 10 min ago | allow (reply) |
| `revoked` | deny |
| `active` (newer), `revoked` (older) | allow |
| `pending` (newer), `revoked` (older) | deny |
| `declined` only | no decision |
| `active` but expired | no decision |

### 18.5 Bind selection

Binds `//r/alice/` (client A), `//r/alice/research` (client B),
`//r/alice/research/spider1` (client C):

| destination | selected |
|---|---|
| `//r/alice/aih` | A (namespace, score 2) |
| `//r/alice/research` | B (exact, score 5) |
| `//r/alice/research/spider1` | C (exact, score 7) |
| `//r/alice/research/spider2` | B (delegation, score 4) over A (namespace, 2) |

---

## 19. End-to-end encryption

Optional in the protocol, automatic in practice. A client that implements
this section creates and publishes its keys itself (§19.2, §19.3) and
encrypts every message to a peer that has published a valid key record;
neither the user nor the agent takes a step. Encryption is not mandatory
because it would shut out every client that has not implemented it yet, and
every peer that has no keys; an endpoint that wants it anyway can require it
(§19.7). Forward secrecy beyond key rotation (§19.8) is deferred.

An encrypted message is an ordinary `msg` whose body
only the recipient endpoint can read. Servers route it, apply policy, spool
it and ack it exactly as before; they never need, and never get, the
plaintext. The sender also signs the plaintext with its own endpoint key, so
the recipient learns which endpoint wrote it, not only which realm sent it
(§9 signatures vouch for realms).

### 19.1 What it protects, and what it does not

| protected | not protected |
|---|---|
| the body, its media type, and `re` | `from`, `to`, `id`, `ts`, `ttl`, `via`, the body's size: servers need them to route |
| against every server on the path, including a hosted realm's operator (§3.4.4) | who is linked to whom (links and passports are server records) |
| against a server forging a message in an endpoint's name | anything after decryption: the endpoint, and wherever an agent's session is processed, sees the plaintext |
| past messages, once the encryption key that sealed them is retired and destroyed (§19.8) | past messages still sealed to a key that leaks before it is retired; there is no per-message ratchet |

Link and passport control messages (§11.4, §12.4), notices (§11.4) and the
audit stream (§13) stay in the clear: servers must read them.

### 19.2 Keys

An endpoint that supports encryption holds:

- an **identity key** `ik`: ed25519, long-lived. Its **fingerprint** `ifp` is
  the first 32 lower-case hex digits (128 bits) of SHA-256 over the raw
  32-byte public key; shown to people in groups of four
  (`3f9a 1c2e 7b4d …`).
- one or more **encryption keys**: X25519 (RFC 7748), each with a `kid` (first
  8 hex digits of SHA-256 over the raw public key) and an expiry
  `not_after` (Unix seconds), each vouched for by the identity key.

The public halves form a **key record**:

```json
{"t":"e2e-keys","endpoint":"airc://airc.dev/alice/agent","ik":"<base64 ed25519 public key>",
 "keys":[{"kid":"4c1d2e3f","x25519":"<base64 X25519 public key>","not_after":1791000000}],
 "ts":1790380000,"sig":"<base64 ed25519 signature by ik>"}
```

`sig` is over these lines joined by `\n` (UTF-8, no trailing newline), one
`kid`/`x25519`/`not_after` triple per key in the order listed:

```
airc-e2e-keys
<endpoint>
<ik>
<ts>
<kid>
<x25519>
<not_after>
...
```

A record is valid when `sig` verifies under `ik`, `endpoint` is the endpoint
it was fetched for, and at least one key has `not_after` in the future. The
newest valid record (by `ts`) wins.

**Identity key succession.** A record replacing `ik` MAY carry
`"succession":{"ik_old":"<base64>","sig_old":"<base64 signature by ik_old over the same canonical string>"}`.
A client that had pinned `ik_old` accepts the new `ik` without a warning
(§19.4). Without it, a changed `ik` is a key change.

### 19.3 Publication and lookup

The endpoint's server keeps a directory of key records. It stores what
endpoints publish and serves it to anyone; clients never trust it for
authenticity (§19.4), only for availability.

| face | request | response |
|---|---|---|
| HTTPS client (§3.4.2) | `POST /airc/v0/keys` with a key record | `{"t":"keyed","ok":true}`; the record's `endpoint` MUST lie in the client's namespaces and `sig` MUST verify |
| HTTPS peer (§3.4.1), unauthenticated | `GET /airc/v0/keys?endpoint=<uri>` | the key record, verbatim, or 404 |
| stream client | frame `{"t":"keys","op":"publish","record":{…}}` or `{"t":"keys","op":"get","endpoint":"<uri>"}` | `{"t":"keyed","ok":…,"record":{…},"reason":…}` |

For `get` on an endpoint in another realm, the server fetches it from that
realm's peer face (discovered per §3.4.1) and returns it verbatim. Servers
MAY cache records for up to one hour.

### 19.4 Trusting a key

A client keeps a **pin** per peer endpoint: `(ik, source, first seen)`. Before
encrypting to a peer, or accepting its signature, it compares the record's
`ik` with the pin.

Keys are pinned from links and passports, the moments two endpoints agree
to talk. Sources, strongest first:

1. **verified**: the two people or agents compared fingerprints over another
   channel. Clients SHOULD offer a way to show both fingerprints.
2. **passport**: the passport carried the issuer endpoint's fingerprint
   (§19.9). The pin is as trustworthy as the channel the passport travelled.
3. **link**: the key arrived with a link control message at the moment the
   endpoints agreed to talk (§19.9). A server on the path could have altered
   it at that moment, but not unnoticed afterwards.
4. **first use**: only for a peer with neither a link nor a passport (traffic
   a realm's rules allow, such as endpoints of one organisation): the first
   record fetched from the directory.

A client sends its own identity key with every link control it originates
and puts its fingerprint in every passport it issues, so the pins above
exist without anyone asking for them.

With no pin, the client pins the record's `ik` with the best source it has.
When a record's `ik` differs from the pin (and no valid succession links
them), the client MUST NOT encrypt to it or accept messages signed by it
without an explicit decision by the endpoint's operator, and MUST say which
peer's key changed. A key change is also the signature of a server attack.

### 19.5 The encrypted message

The outer frame is a `msg` (§4.2) with:

- `type`: `application/airc-e2e+json`
- `re`: absent (it travels inside)
- `body`: the JSON envelope below, as a string

```json
{"v":1,"suite":"hpke-x25519-sha256-chacha20poly1305","kid":"4c1d2e3f","enc":"<base64>","ct":"<base64>"}
```

- `kid`: the recipient's encryption key used.
- Sealing is **HPKE base mode** (RFC 9180, single-shot) with KEM
  DHKEM(X25519, HKDF-SHA256) `0x0020`, KDF HKDF-SHA256 `0x0001`, AEAD
  ChaCha20-Poly1305 `0x0003`. `enc` is the encapsulated key, `ct` the
  ciphertext with its tag.
- HPKE `info` and the AEAD `aad` are both these lines joined by `\n`, which
  binds the ciphertext to its envelope (it cannot be replayed under another
  id or to another recipient):

```
airc-e2e/1
<id>
<from>
<to>
```

The plaintext is UTF-8 JSON:

```json
{"type":"text/plain","body":"hello","re":"cf0a7537-cecc-45aa-a7a8-0362cfdca73c",
 "sender":{"ik":"<base64 ed25519 public key>","sig":"<base64>"}}
```

`sender.sig` is ed25519 by the sender's identity key over these lines joined
by `\n`:

```
airc-e2e-msg
<id>
<from>
<to>
<type>
<re or empty>
<body>
```

A sender picks the recipient's valid encryption key with the latest
`not_after`, and refuses to encrypt to a record whose `ik` fails §19.4.

### 19.6 Receiving

The recipient's client (the relay, for an agent behind one):

1. finds the private key for `kid`; if it has none: ack `failed`, code
   `e2e_no_key` (the sender refetches the record and retries once);
2. opens the ciphertext with `info`/`aad` built from the outer frame; on
   failure: ack `failed`, code `e2e_decrypt`;
3. checks `sender.ik` against its pin for `from` (§19.4, pinning on first use)
   and verifies `sender.sig`; on failure: ack `failed`, code `e2e_sender`, and
   the plaintext MUST NOT be shown as coming from `from`;
4. delivers `type`, `body` and `re` as if they had been the outer fields, and
   tells the reader the message was end-to-end encrypted and from whom it is
   authenticated. A relay keeps the foreign-origin banner (§16.4): encryption
   authenticates the sender, it does not make the sender trusted.

### 19.7 Servers

- MUST route, spool, dedupe and ack `application/airc-e2e+json` messages
  unchanged, and MUST NOT require access to the body.
- Policy (§10), links and passports apply to the envelope as for any message.
- MAY let an endpoint require encryption: plaintext `msg` frames to it
  (other than control types and notices) are refused with `failed`
  `e2e_required`. An endpoint states this in its key record with
  `"require": true`, which senders SHOULD honour before sending.

### 19.8 Rotation

- Encryption keys: `not_after` at most 30 days ahead (default 7). A client
  publishes the next key before the current one expires and keeps a retired
  key's private half only as long as messages sealed to it can still arrive
  (the largest `ttl` it accepts, default one day past `not_after`), then
  destroys it. Destroying retired keys is what protects past messages.
- Identity keys: rotated rarely, with succession (§19.2).

### 19.9 Keys in links and passports

- **Links.** A link control message (§11.4) MAY carry the acting endpoint's
  identity key: `body.e2e = {"ik":"<base64>"}`. The receiving server passes
  it to its endpoint in the notice as `hdr["airc-peer-ik"]`; the endpoint pins
  it with source `link`.
- **Passports.** The one-line form gains an optional field before `t=`:
  `k=<ifp>` (the issuer endpoint's fingerprint); the file form carries
  `"ifp"`. On redemption the redeemer fetches the issuer endpoint's record,
  and pins it with source `passport` only if its `ik` matches `ifp`; on a
  mismatch it MUST NOT pin and SHOULD warn.

### 19.10 Test vectors

Generated from the reference implementation. Its HPKE also reproduces RFC
9180 Appendix A.2.1 (base mode, this suite) exactly; check yours against that
first.

Recipient identity key: ed25519 seed `0x01…0x20` (the key of §18.2), endpoint `airc://example.com/alice/x`.
Its encryption key: X25519 private key = DeriveKeyPair(ikm `0x21…0x40`) of RFC 9180 §7.1.3.

| item | value |
|---|---|
| `ik` | `ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=` |
| `ifp` | `65b60673d6ed884bf01c2c222d82ada0` |
| encryption key `x25519` | `+JORnd4/sXcnPAewcCyAtO/QfIg9Mo7zSeAlrs7WFQ4=` |
| its `kid` | `7222582a` |

Canonical key-record string (`\n` between lines):

```
airc-e2e-keys
airc://example.com/alice/x
ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=
1790380000
7222582a
+JORnd4/sXcnPAewcCyAtO/QfIg9Mo7zSeAlrs7WFQ4=
1791000000
```

Key record:

```json
{"t": "e2e-keys", "endpoint": "airc://example.com/alice/x", "ik": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", "keys": [{"kid": "7222582a", "x25519": "+JORnd4/sXcnPAewcCyAtO/QfIg9Mo7zSeAlrs7WFQ4=", "not_after": 1791000000}], "ts": 1790380000, "sig": "WpE/KYxDX5J5ORo0uVPIgJ6jKTJAkyMJcY/5+/kAbYOpWkgXLGsGJVqTgLbOjtQzLNG0i5AWhOGadOdHszx0Bg=="}
```

Sender identity key: ed25519 seed `0x41…0x60`, endpoint `airc://partner.example/support/desk`; `ik` = `rcFAEfgtHFbZVqpPnXPYhYNhpgYEhSXg0Ixjjcdd2Mc=`.
Message: `id` `00000000-0000-4000-8000-000000000001`, `from` `airc://partner.example/support/desk`, `to` `airc://example.com/alice/x`, `type` `text/plain`, no `re`, body `hello`.

Canonical plaintext-signature string:

```
airc-e2e-msg
00000000-0000-4000-8000-000000000001
airc://partner.example/support/desk
airc://example.com/alice/x
text/plain

hello
```

Plaintext (these exact bytes are sealed):

```json
{"type":"text/plain","body":"hello","re":"","sender":{"ik":"rcFAEfgtHFbZVqpPnXPYhYNhpgYEhSXg0Ixjjcdd2Mc=","sig":"uid97CCM0KFzbx2iwML0WKJI7rj/bvsB3LGa06zVZXdco9OAua7s204lwFejOGc2mqI2Qqak+Ntxasy9tckNBw=="}}
```

HPKE `info` = `aad`:

```
airc-e2e/1
00000000-0000-4000-8000-000000000001
airc://partner.example/support/desk
airc://example.com/alice/x
```

Sealed with the ephemeral key DeriveKeyPair(ikm `0x61…0x80`); envelope (the outer `body`):

```json
{"v": 1, "suite": "hpke-x25519-sha256-chacha20poly1305", "kid": "7222582a", "enc": "F4ex6a5rz7HWQUcWmsH1jcKPvABOvYgKggPdLXwWhm0=", "ct": "OfVbc0tMAE4/tMY2hITTuymzK4K6rfpcWYRNTC1hmlS7fgdwszUnmYfF4cFxEOLQ4w2a/OmQYg2xvgHWAfmB4n6rI3R18nzlJi/DMqoDgqs1iCbq8NQY4rp44X747DsaJ1RpEkIrg7Ezuvf4Q8PlT+lNM1PU03hRxlQ87mTscxclLK1IBi3YZ0PuWiIBVzFM95Z+4SBv4mxMe1UbY9MiiTQP/Q1KBHkb4hIWkNmQFyHTgmCZ1j3dCBqVcFX703qS+H/c1Jda3E6sTszHFtBtDjxeGlS+u6UUwTji2g=="}
```

Passport line with a fingerprint:

```
airc-passport:1;i=example.com;e=alice/x;k=65b60673d6ed884bf01c2c222d82ada0;t=Zk3fQ9uT2mH7wLp4Rb8yNc6VdX1aS0gE
```

---

## Appendix A. Complete exchanges

### A.1 Cross-realm message

```
# client "alice/x" (uid → namespace alice) on example.com
C> {"t":"hello","proto":"airc/0.1","role":"client","agent":"demo/1"}
S> {"t":"welcome","realm":"example.com","ns":["alice"],"proto":"airc/0.1","server":"aircd/0.1.0"}
C> {"t":"msg","id":"5b1e…","from":"x","to":"//partner.example/support/desk","ts":1790380000.1,"body":"hello"}

# example.com resolves _airc._tcp.partner.example, dials it over TLS
A> {"t":"hello","proto":"airc/0.1","role":"peer","realm":"example.com","agent":"aircd/0.1.0","ts":1790380000,"nonce":"…","sig":"ed25519:…"}
B> {"t":"welcome","realm":"partner.example","proto":"airc/0.1","server":"aircd/0.1.0","sig":"ed25519:…","kid":"…"}
A> {"t":"msg","id":"5b1e…","from":"airc://example.com/alice/x","to":"airc://partner.example/support/desk",
    "ts":1790380000.1,"body":"hello","type":"text/plain","ttl":86400,"via":["example.com"],"hdr":{"sig":"ed25519:…"}}

# partner.example forwards to the client bound for support/desk, which acks
B> {"t":"ack","id":"5b1e…","status":"delivered"}
S> {"t":"ack","id":"5b1e…","status":"delivered"}
```

### A.2 Link offer and accept, same realm

```
X> {"t":"link","op":"offer","as":"alice/x","peer":"bob/y","label":"q4"}
S> {"t":"linked","ok":true,"link":{"id":"L1a2b3c4d","state":"pending",…},"delivery":"delivered"}
S>(to Y) {"t":"msg","from":"airc://example.com/alice/x","to":"airc://example.com/bob/y","type":"text/plain",
          "body":"[airc: link offer L1a2b3c4d from alice/x (q4): they ask to open a message link with y, direction both. Accept: airc link accept L1a2b3c4d --as bob/y   Decline: airc link decline L1a2b3c4d --as bob/y]",
          "hdr":{"airc-link":"L1a2b3c4d","airc-link-action":"offer"}, …}
Y> {"t":"ack","id":"…","status":"delivered"}
Y> {"t":"link","op":"accept","as":"bob/y","id":"L1a2b3c4d"}
S> {"t":"linked","ok":true,"link":{"id":"L1a2b3c4d","state":"active",…},"delivery":"delivered"}
S>(to X) notice "[airc: link L1a2b3c4d with bob/y accepted.]"
```

### A.3 Passport redemption across realms

```
X> {"t":"passport","op":"new","as":"alice/x","label":"vendor","to":"//partner.example/**"}
S> {"t":"passported","ok":true,"passport":{"id":"P9e8d7c6b",…},"line":"airc-passport:1;i=example.com;e=alice/x;t=…"}
   (the line reaches the other party by any channel)
D> {"t":"passport","op":"redeem","as":"support/desk","passport":"airc-passport:1;i=example.com;e=alice/x;t=…"}
B→A msg type application/airc-passport+json to airc://example.com/_passport, body {"action":"redeem","token":"…","redeemer":"airc://partner.example/support/desk"}
A: validates, creates link L… active, sends A→B control grant(L…), waits for B's ack (B stores the link, notifies desk)
A→B ack {"status":"delivered","reason":"link L… granted"}
B> {"t":"passported","ok":true,"status":"delivered","reason":"link L… granted","link":{…}}
A notifies alice/x: "[airc: support/desk redeemed your passport P9e8d7c6b (vendor): link L… is open, …]"
```

---

*Changes since draft 0.2: the HTTPS binding (§3.4), mandatory for federated
servers; remote client identity by registered key (§5); the `pushed` ack
code; discovery of the HTTPS binding (§8.1); the Hosted realm profile.*

*Changes since draft 0.1: addresses restated in full (ports, IP literals,
namespaces, fragments, delegation, reserved names); links, passports, signing
and the audit stream specified with algorithms; ack codes and reasons made a
registry; constants table; conformance profiles; test vectors.*
