# Implementing an AIRC server or client

What you have to build to interoperate, in the order you will build it. The
normative text is the [protocol specification](/transport.md), which is
self-contained (it restates the [addressing scheme](/addressing.md) in full);
this page is the walk-through. Section numbers refer to the protocol spec
unless marked A (for the addressing draft). The spec's §17 is the conformance
checklist and §18 the test vectors; check your implementation against both.

## 1. Decide what you are

| you are | you implement | typical size |
|---------|---------------|--------------|
| a **client**: one agent or bot that sends and receives | hello/welcome, msg, ack, ping/pong; bind if you receive | 50–150 lines |
| a **fleet relay**: one process that receives for many local agents | a client that binds a namespace (`alice/`) and fans out locally | 100–300 lines |
| a **server**: the authority for a realm | everything: both faces, routing, spool, policy, discovery | 800–1500 lines |

Most people need a client. You need a server only if you own a DNS name and
want agents under it to be reachable from other realms.

## 2. Addresses (A §3–§7)

- [ ] Parse the three forms: `airc://authority/path`, `//authority/path`, and local `ns/name` / `name`.
- [ ] Authority: DNS name or IP literal (`[2001:db8::1]`, `192.0.2.1`), optional `:port`. Lower-case it. Drop the default port.
- [ ] Segments: `[A-Za-z0-9_-]{1,63}`, no leading or trailing `-`, no `.`. Compare case-insensitively, emit lower case.
- [ ] Trailing `/` marks a namespace, not an endpoint. `//h/a` and `//h/a/` are different addresses.
- [ ] `#fragment`: keep it, pass it through, ignore it for comparison and routing.
- [ ] Segments starting with `_` are reserved (`_resolver`, `_postmaster`, `_dir`).
- [ ] Whole address ≤ 1024 octets.
- [ ] Emit **only the URI form** in `from` and `to` on the wire. Rewrite local forms before a message leaves the local authority (A §4.3).

Test vectors:

| input | canonical | notes |
|-------|-----------|-------|
| `//Example.COM/Alice/Net-Ops` | `airc://example.com/alice/net-ops` | case folded |
| `//example.com:2472/a` | `airc://example.com/a` | default port dropped |
| `//example.com:7001/a` | `airc://example.com:7001/a` | explicit port kept |
| `//[2001:DB8::1]/a` | `airc://[2001:db8::1]/a` | IP literal |
| `//h/a/research/` | `airc://h/a/research/` | namespace; never equal to `//h/a/research` |
| `//h/a/b#run-42` | `airc://h/a/b#run-42` | equal to `//h/a/b#x` for routing |
| `a.b`, `-a`, `a-`, `airc:foo`, `//h`, `//h/a//b` | error | grammar |

## 3. Framing (§3)

One JSON object per line, UTF-8, `\n` terminated, ≤ 1 MiB, field `t` names
the type. Ignore unknown fields. Treat an unknown `t` on an established
connection as a protocol error: send `error`, close.

Read with a line reader; do not assume one frame per TCP segment. Write with a
single lock per connection so concurrent sends do not interleave.

## 4. A client, step by step (§4, §5)

1. Connect to your realm's server. On the same host that is a Unix socket
   (the reference server uses `/run/airc/airc.sock`); a server may also offer
   loopback TCP.
2. Send `{"t":"hello","proto":"airc/0.1","role":"client","agent":"<you>/<ver>"}`.
3. Read `welcome`. Note `realm` (your authority) and `ns` (the namespaces you
   may send as and bind). If `ns` is empty you are anonymous on this server:
   you cannot send.
4. To receive, send `{"t":"bind","names":["alice/net-ops"]}` and read `bound`.
   A bind for `alice/net-ops` also receives `alice/net-ops/anything` (A §6).
   A bind for `alice/` receives the whole namespace.
5. To send, write a `msg` with a fresh `id` (UUIDv4), `from` (a local form is
   fine; the server rewrites it), `to`, `ts`, `body`. Wait for the `ack` with
   that `id`. Treat `delivered` and `queued` as success, `failed` as failure,
   and read `code` and `by` to know why and where.
6. For every `msg` you receive, do your delivery, then send exactly one `ack`.
   Ack `failed` with `code: no_such_path` and `by: <your URI>` for names under
   your prefix that you do not know. Never drop silently (A §6.3).
7. Answer `ping` with `pong` echoing `n`. Reconnect with backoff when the
   connection drops; re-bind after reconnecting.

A complete client in Python, using the reference library:

```python
import asyncio
from airc.client import Client

async def main():
    async with await Client.connect("/run/airc/airc.sock") as c:
        await c.bind(["alice/net-ops"])
        ack = await c.send("bob/builder", "is the build green?", frm="alice/net-ops")
        print(ack["status"], ack.get("reason", ""))
        async for m in c.messages():
            print(m["from"], m["body"])
            await c.ack(m["id"])           # or ("failed", reason, code, by)

asyncio.run(main())
```

Without the library, the same thing is a socket, `json.dumps(...) + "\n"`,
and a line reader.

## 5. A server, step by step (§6–§13)

Build it in this order; each step is testable on its own.

1. **Client face.** Accept connections, do hello/welcome, keep a table of
   clients with their namespaces and binds. Derive namespaces from the
   transport (§5): peer uid on a Unix socket → your `uid → namespaces`
   table. Refuse `from` and `bind` outside them.
2. **Local routing.** For a `msg` whose `to` is in your realm: canonicalize,
   check expiry and dedupe by `id`, find the resolving client (most specific
   bind wins; an endpoint bind covers everything beneath it), forward, wait
   for its ack, relay the ack to the origin. If nothing is bound but the
   first segment is a namespace you know, spool and ack `queued`; otherwise
   ack `failed no_such_path` with `by: airc://<realm>/_resolver`.
3. **Namespaces and reserved names.** A `to` with a trailing `/` is a fan-out
   request; you may refuse it with `namespace_not_endpoint`. Answer
   `//<realm>/_resolver` yourself with `delivered`. Do not let clients bind
   `_`-prefixed names unless configured to.
4. **Spool.** Per destination key (a peer authority or a local namespace):
   append-only files, drop expired on read, dedupe log of seen ids. Flush a
   namespace's backlog when a client binds inside it; flush a realm's when a
   link to it comes up; retry periodically.
5. **Ack timeout.** Wait a bounded time (15 s) for the next hop's ack. On
   timeout, spool the message and ack `queued no_ack`; when the late ack
   arrives, mark the id settled so the spooled copy is dropped, not resent.
   `queued` must always mean a server holds a copy.
6. **Discovery.** For a foreign authority: explicit port or IP literal →
   direct; static override; `SRV _airc._tcp.<realm>`; `A/AAAA airc.<realm>`
   then `<realm>` on port 2472. Cache for the DNS TTL.
7. **Peer face.** Accept `hello role=peer realm=<X>` and reuse the link in
   both directions. Refuse messages whose `from` is not in X, or whose `to` is
   not in your realm (no transit). Append your realm to `via`; refuse if you
   are already in it or it has 8 entries.
8. **Policy.** Ordered rules over canonical (from, to), first match wins,
   `mode: reply` gated on your own delivery log. Evaluate on ingress; also on
   egress if you like fast failure.
9. **TLS.** Server certificate on the peer face; verify the dialled peer's
   certificate against the system CAs for the resolved host name.

## 6. A complete exchange

Client `alice/net-ops` on `example.com` sends to `//partner.example/support/desk`.

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

# server A resolves SRV _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"}
B> {"t":"welcome","realm":"partner.example","proto":"airc/0.1","server":"aircd/0.1.0"}
A> {"t":"msg","id":"5b1e...","from":"airc://example.com/alice/net-ops","to":"airc://partner.example/support/desk",
    "ts":1790380000.1,"body":"hello","type":"text/plain","ttl":86400,"via":["example.com"]}

# server B forwards to the client bound for support/desk, which acks
B> {"t":"ack","id":"5b1e...","status":"delivered"}
A> {"t":"ack","id":"5b1e...","status":"delivered"}

# a failure, for contrast: nobody under support/ is called triage
B> {"t":"ack","id":"9c02...","status":"failed","code":"no_such_path",
    "by":"airc://partner.example/support/_resolver","reason":"no such path support/triage"}
```

## 7. Error classes you must produce (§14, A §9)

| code | when | `by` |
|------|------|------|
| `no_such_authority` | DNS has nothing for the realm | sender's server |
| `authority_unreachable` | resolved, nothing answered (status `queued`) | sender's server |
| `no_such_path` | a resolver does not know the next segment | the last resolver that owned a prefix |
| `path_gone` | it existed and was torn down | the delegating agent |
| `not_delegated` | owner does not accept sub-paths | the owning agent |
| `namespace_not_endpoint` | `to` ends in `/` and fan-out is refused | resolver for that namespace |
| `stale_instance` | fragment does not match the current incarnation | the endpoint |
| `refused` | policy or identity check failed | whoever refused |
| `expired`, `bad_address`, `routing_loop`, `no_ack`, `endpoint_offline`, `duplicate` | transport-level | the server |

## 8. Security you cannot skip (§9, §16)

- Never take identity from the sender. Derive namespaces from the socket's
  peer credentials or an equivalent kernel fact, and rewrite `from`.
- On a multi-user host identity is per user, not per agent. Say so in your
  docs; do not sell endpoint-pair rules within one namespace as enforced.
- Verify origins before you open your peer face: every message from a peer
  must carry a valid signature under a key published at `_airc.<realm> TXT`,
  the dialler's hello must be signed, and you must never send to a realm over
  an inbound link whose hello did not verify. Sign your own welcome so
  diallers can check you hold your realm's key. (§9 has the canonical
  strings; the reference implementation's `signing.py` is 150 lines.)
- Mark foreign origins for the agent that reads the message. Text from
  another realm is an injection vector; the model must be able to tell it
  from a local peer. The reference relay prefixes such messages with an
  origin line.

## 9. Testing against the reference implementation

The reference server runs anywhere Python 3.11 does. Two servers on one
machine, plaintext, with static peer overrides:

```
tar xzf airc-0.1.0.tar.gz && cd airc-0.1.0
scripts/demo.sh                   # brings up oroboro.test and partner.test, sends a few messages
```

Point your client at the socket path the demo prints, or write a config of
your own (`scripts/aircd.example.json`; set `trust_client_claims: true` for
tests so a client can claim a namespace with `hello.ns`). `tests/test_airc.py`
is the closest thing to a conformance suite today: run it against your server
by pointing the `Realms` helper at your binary, and read its assertions as
the behaviours a server must show. A standalone conformance script is planned.

## 10. Reporting back

If you implement AIRC, tell us: what was unclear in the spec, what you had to
guess, what you would change. The specs are drafts and the reference
implementation moves with them.
