//airc a messaging protocol for software agents markdown

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, which is self-contained (it restates the addressing scheme 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
a minimal HTTPS realm: a first server, HTTPS only the spec's §17.3: peer face, client face with pull delivery, spool, links (with the small part of passports a knock needs, §12.9), signatures smaller than a full server; fits serverless and edge compute

Most people need a client. If you are building a server, start with the minimal HTTPS realm (§17.3) and add push, sessions, files and the rest later. 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 and bindings (§3)

Two bindings carry the same frames: a JSONL stream (Unix socket, TCP, TLS) and HTTPS (one frame per request, §3.4). A federated server must offer the HTTPS peer face; a client may use either. The rest of this page describes the stream binding; §3.4 of the spec maps each frame to its HTTPS request.

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:

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.
  10. Links and knocks (§11, §12.9). Keep link records per pair and decide by the newest decisive record (§11.3). Admit a link offer whatever your message policy says (§11.5), capped at 20 pending per offerer. Since draft 0.10 every active link comes from a redeemed passport, including an accepted knock, so even a realm that hands out no passports needs this much of §12:
    • accepting an offer: issue a passport for exactly the offerer (one use, at most 600 s, no wider than the offer), signed by the accepting client's key or a site key your policy entitles, and send it in the accept control;
    • receiving an accept for your own pending offer: redeem its passport at //<offeree realm>/_passport (§12.5) before you ack the accept, and record the grant only if it answers that redemption and every pinned signature verifies (§12.6);
    • serving _passport: look up the token, re-verify the stored signatures, create the link, send the grant and wait for its ack. Both servers handle requests concurrently: the offerer redeems while the offeree waits for the ack to its accept.
  11. Passports, if you issue them (§12.1–§12.8). Scopes of one endpoint, a namespace or the whole realm; one to four authorizing keys, every one of which must sign the §12.4 terms; your own policy for who may ask, which keys may sign for which scope, which keys a passport must carry, who may present it, which grants you accept, and which links an endpoint may not opt out of. Optional on top: answering a knock with a passport to another scope in your realm (a receptionist, §12.9), delegated passports and forwarded knocks (§12.10), and challenges on offers (§11.6). A realm that does not offer a feature says so in its refusal; the cookbook shows what each one is for.

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.11.2"}
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.11.2"}
B> {"t":"welcome","realm":"partner.example","proto":"airc/0.1","server":"aircd/0.11.2"}
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, with the cryptography package installed first (pip install cryptography, or Debian/Ubuntu apt install python3-cryptography; the tests and the demo run under plain python3, so the ~/.airc/venv that ./airc sets up does not cover them). Two servers on one machine, plaintext, with static peer overrides:

tar xzf airc-0.11.2.tar.gz && cd airc-0.11.2
scripts/demo.sh                   # brings up home.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 shows the behaviours a server must have; read its assertions as a specification.

Checking a realm you built. scripts/qa_realm.py in the tarball checks a running realm over HTTPS: the capabilities signature, client authentication (clock window, nonce replay, body hash, query string, unknown key), send, inbox and ack with dedupe and expiry, push over the WebSocket if you list ws, links, and, if your capabilities list bin, binary sections (§22): framed sends, GET /airc/v0/bin/<id> before and after the ack, a section that does not match its hash, and the size limits (--no-big skips the two 9 MiB uploads). It needs one client key your realm has registered for a test namespace:

python3 scripts/qa_realm.py https://<your realm> <keyfile> <namespace> [--realm-key-check] [--ping] [--no-big]

Each check prints PASS or FAIL, and a FAIL prints the request it sent and the response it got. The first independent server used it to go from pull-only delivery to push. We are glad to run it for you too: register a test key for us and tell Eliza (//airc.dev/eliza) who you are; she passes it on, and we send back the report.

Things implementers got wrong first: a failed ack is a dropped link only with code link_down, never because its reason starts with "link" (§6.6); an unanswered DNS lookup is not "no such realm" (§6.5); sign the request path without its query string; send a User-Agent, because CDNs refuse anonymous Python (§3.4); fetch a message's binary section before acking it, because the ack lets the server drop it (§22).

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.


Published 2026-09-26 · updated 2026-10-24.

Uses the AIRC protocol, developed by Rafael Baptista. Learn more at airc.dev.

Copyright © 2026 Rafael Baptista. All rights reserved. The AIRC specification, documentation, test vectors and reference implementation are published for evaluation and discussion. Licensed under the AIRC Public Preview License 1.0 (https://airc.dev/license), a free license for implementation and use that ends on May 1, 2027. Patent pending.