# AIRC operations guide

Running the reference implementation on a host: install, configure, attach
fleets, verify, troubleshoot. For the protocol see [`airc-spec.md`](/transport.md); for names
see [`airc-addressing.md`](/addressing.md); for why the deployment is shaped this way see
[`design.md`](/design.md).

## 1. Components on a host

| what | runs as | unit | listens on |
|------|---------|------|------------|
| `aircd` realm server | system account `airc` | `aircd.service` (system) | `/run/airc/airc.sock` (clients); `:2472` (peers, when enabled) |
| `airc relay --backend co` per fleet | the fleet owner | `airc-relay.service` (user unit) | connects out to the socket |
| `co tell` hand-off | whoever calls `co tell` | — | connects out to the socket |

`/opt/airc` holds the copy the service and other fleets run (`aircd`, `airc`,
`scripts/airc/`). The source of truth is `apps/airc` in the tree; re-run the
installer to upgrade the copy.

## 2. Install and upgrade

```
sudo apps/airc/scripts/install.sh [realm]        # default realm oroboro.com
```

Idempotent. Creates the `airc` account, `/opt/airc`, `/etc/airc/aircd.json`
(only if absent), `/var/lib/airc/spool`, installs and (re)starts
`aircd.service`. Re-running upgrades `/opt/airc` and restarts the server;
the spool and config are left alone. Clients reconnect on their own (the
relay retries every 2 s; `co tell` is one-shot and reports failure if it
lands in the restart window).

### The relay and its backend

The relay is site-neutral: it binds `<fleet>/`, rewrites the sender, adds the
foreign-origin banner and the `[airc id=… re=… link=…]` header (always with
`--header ids`, otherwise only when there is something to say), and acks
truthfully. How text reaches an agent is a **backend** (`--backend`):

- `co` (default): the co tooling. Finds the agent directory, delivers through
  its channel socket, queues to `queued-messages.jsonl` while the agent is
  compacting (acking `queued`), auto-starts a stopped agent for senders of
  this fleet (`--autostart local`, the default; `all` includes foreign
  senders, `off` never), and writes the fleet's `_messages.log` line with a
  body preview when a message is delivered. Options via `--opt`: `agents_dir=`, `co_dir=`,
  `log=false`.
- `stdout`: prints deliveries; for demos and as the template for a new site.

A new site writes one module under `scripts/airc/backends/` with `exists`,
`deliver` and `describe`.

### Attach a fleet

As the fleet owner (needs `loginctl enable-linger <owner>`):

```
install -m 644 apps/airc/scripts/airc-relay.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now airc-relay.service
```

The template binds `<owner>/`, expects the fleet's agents at `~/agents` and
the co tools at `/opt/co/shellscr/co`, and runs `/opt/airc/airc`. Edit the
three `Environment=`/`ExecStart` lines for a fleet laid out differently
(rafael's fleet uses `/agents`, `~/milk/shellscr/co` and the tree's own
`apps/airc/airc`). Add `--autostart` to `ExecStart` if this fleet wants an
inbound message to boot a stopped agent.

The owner's uid must appear in `uid_namespaces` in the server config, else
the relay is refused with `forbidden`.

## 3. Configuration reference — `/etc/airc/aircd.json`

| key | default | meaning |
|-----|---------|---------|
| `realm` | required | The authority this server is for (`oroboro.com`). |
| `client_unix` | — | Unix socket path for the client face. Created 0666 in its directory. |
| `client_unix_mode` | `"666"` | Octal mode of that socket. Forced to 0600 when `trust_client_claims` is on. |
| `client_tcp` | — | `host:port` loopback client face. Identity from `/proc/net/tcp`; loopback only. |
| `peer_listen` | — | `host:port` for other realms. Leave unset until peer authentication exists. |
| `tls.cert`, `tls.key` | — | Server certificate for the peer face; enables TLS on it. |
| `tls.ca` | system CAs | Extra CA bundle for verifying dialled peers. |
| `tls.insecure_skip_verify` | false | Testing only. |
| `uid_namespaces` | `{}` | `"uid": ["ns", ...]` — which namespaces a connecting uid may bind and send as. This is the fleet identity table. |
| `namespaces` | `[]` | Extra namespaces this realm knows (messages to them queue instead of failing). |
| `trust_client_claims` | false | Accept `hello.ns` claims from unidentifiable clients. Tests only. |
| `allow_reserved_binds` | false | Let clients bind `_`-prefixed names. |
| `peers` | `{}` | `"realm": {"host", "port", "tls"}` static overrides for discovery. |
| `spool_dir` | `~/.airc/spool` | Store-and-forward directory. |
| `events_log`, `events_max_bytes` | `<state_dir>/events.jsonl`, 50 MB | Audit stream file; rotated once to `.1`. |
| `state_dir` | parent of `spool_dir` | Where `links.json` lives. |
| `links.accept_offers` | true | Admit link offers from other endpoints and realms. |
| `links.auto_accept` | `[]` | Patterns of local endpoints that accept every offer (an open channel: `//realm/support/*`). |
| `links.max_pending_per_offerer` | 20 | Cap on unanswered offers per endpoint. |
| `links.max_ttl` | — | Cap on a link's lifetime in seconds. |
| `admin_uids` | root and the server's uid | Connections that may list every link (`airc links --all`). |
| `passports.issue` | `agents` | Who may issue: `agents` (any endpoint, for itself), `owner-only` (admin uids), `off`. |
| `passports.default_ttl`, `max_ttl` | 7 days, 90 days | Passport lifetime. |
| `passports.default_uses`, `max_uses` | 1, 100 | Redemptions per passport. |
| `passports.link_ttl` | 30 days | Lifetime of links created by redemption (null = no expiry). |
| `passports.web_base` | — | If set, `airc passport new` also prints a link, e.g. `https://airc.oroboro.com/p/`. |
| `passports.redeem_failures_per_hour` | 20 | Failed redemptions tolerated per source before refusing. |
| `signing.key` | `<state_dir>/realm.key` | This realm's ed25519 key; created on first start, mode 0600. |
| `signing.require` | true | Refuse peer messages without a valid signature from their realm's published key. |
| `signing.trusted_unsigned` | `[]` | Realms exempt from signatures (private static peers, tests). |
| `signing.known_keys` | `{}` | `"realm": ["base64 key", ...]` used instead of DNS for those realms. |
| `signing.hello_skew` | 300 | Seconds a peer's hello timestamp may be off. |
| `signing.accept_algs` | `["ed25519"]` | Signature algorithms this realm verifies. Add a new one here before peers start using it; `none` is ignored. |
| `signing.extra_keys` | `[]` | Additional key files to sign with during an algorithm or key transition (messages then carry several signatures). |
| `policy.default_allow` | true | Decision when no rule matches. |
| `policy.rules` | `[]` | Ordered rules: `{"from": pattern, "to": pattern, "allow": bool, "mode": "any"\|"reply", "window": secs, "note": text}`. |

Patterns are `//authority/segments` with `*` (one segment), `**` (rest), a
trailing `/` (everything beneath), `*` alone (anything), `//*/…` (any authority).

Changing the config requires `sudo systemctl restart aircd`.

## 4. Command line

```
airc send [--from NAME] [--re ID] [--ttl S] <to> <message...>
airc listen <name> [--json]           bind a name and print what arrives
airc relay --namespace NS [--autostart]
airc status                           server snapshot (clients, binds, peers, backlog)
airc addr <text> [--realm R] [--ns NS]  parse and canonicalize an address
airc events [-n 50] [--follow] [--kinds msg link passport peer]   the audit stream, no bodies
airc link offer <peer> [--as ME] [--label L] [--direction both|a-initiates|b-initiates] [--expires 30d]
airc link accept|decline|revoke <id> [--as ME]
airc links [--all]                    links touching my endpoints; --all for the operator
airc passport new [--as ME] [--label L] [--expires 7d] [--uses 1] [--to PATTERN] [--direction D] [--json]
airc passport list [--all]            issued passports, with who redeemed them
airc passport revoke <id> [--and-links]
airc accept <passport> [--as ME]      redeem: one-line form, URL, JSON, or @file
airc -v ...                           show error class and producing resolver on failure
```

`--socket PATH|host:port` or `AIRC_SOCKET` selects the server; the default is
`/run/airc/airc.sock`. Exit codes for `send`: 0 delivered or queued, 1 failed,
2 no server. `co tell <ns>/<agent>` and `co tell //realm/path` call `airc send`
and pass the exit code through.

### Links in practice

`--as` defaults to `$CO_SESSION_NAME`, so from an agent's shell
`airc link offer sofia/aih --label "release work"` offers on the agent's own
behalf. The other endpoint receives a message naming the link id and the two
commands to answer it. An active link allows traffic between the two
endpoints even in a default-deny realm; a revoked one denies it even in a
default-allow realm, which is how an agent blocks a peer without an operator.
Records are in `<state_dir>/links.json` on each server; `airc links --all`
run as root or the `airc` user lists the realm's.

### Passports in practice

`airc passport new --label "Bob's reviewer"` prints a one-line passport (and
a link when `web_base` is set). Send either to the other party by any channel;
their agent runs `airc accept '<line>'` and both agents are told the link is
open. The token is shown once and stored only as a hash; `airc passport list`
shows state and who redeemed. `airc passport revoke <id> --and-links` closes
the passport and every link it created, on both sides.

## 5. Verify

```
airc status                                          # both relays bound?
co tell sofia/aih "ping"                             # from a rafael agent
sudo -u sofia env CO_SESSION_NAME=aih /opt/airc/airc send rafael/airc "pong"
airc send //oroboro.com/_resolver ping               # server health, no relay involved
sudo journalctl -u aircd -n 20                       # one line per routed message
systemctl --user status airc-relay                   # per fleet
```

## 6. Troubleshooting

| symptom | meaning | fix |
|---------|---------|-----|
| `cannot reach server at /run/airc/airc.sock` (exit 2) | aircd down or socket not world-connectable | `sudo systemctl status aircd`; `ls -l /run/airc` |
| `Failed ... (no such path X/Y)` | first segment is not a known namespace, or the relay says no such agent dir | check `uid_namespaces`; `ls <agents_dir>` in that fleet |
| `Queued ... (no resolver bound for sofia/aih)` | sofia's relay is not connected | `systemctl --user status airc-relay` as sofia; message delivers when it binds |
| `Failed ... (sofia/aih is not running (channel unreachable))` | agent dir exists, no live channel socket, and the sender is foreign | start the agent in that fleet, or run its relay with `--autostart all` |
| `Failed ... (you may not send as ...)` | `--from` outside the caller's namespaces | it is doing its job |
| `Queued ... (no ack from next hop within timeout; spooled)` | relay took >15 s (cold auto-start) | nothing; the spooled copy is dropped when the late ack lands, resent otherwise |
| relay log `forbidden: sofia/ is outside your namespaces []` | uid not in `uid_namespaces` | add it, restart aircd |
| duplicate delivery after an aircd restart | in-memory late-ack table lost while a copy was spooled | known 0.1 limitation; rare |

The spool is plain files: `/var/lib/airc/spool/queue/<key>.jsonl`, one message
per line; `seen.jsonl` is the dedupe log. Deleting a queue file drops those
messages.

## 7. Opening the peer face (federation)

Federation needs `python3-cryptography` on the host (present on Debian by
default) and four things per realm:

1. **A key.** Start aircd once with `peer_listen` set (or run
   `aircd -c /etc/airc/aircd.json --show-key`); it creates
   `/var/lib/airc/realm.key` and prints the DNS record to publish.
2. **DNS.** Publish the key and the service location:
   ```
   _airc.oroboro.com.        IN TXT "v=airc1 k=ed25519 p=<public key from --show-key>"
   _airc._tcp.oroboro.com.   IN SRV 0 0 2472 airc.oroboro.com.
   ```
   To rotate, publish the new key alongside the old, restart with the new
   `signing.key`, and remove the old record after the longest message ttl.
3. **TLS on 2472.** Either give aircd `tls.cert`/`tls.key` (a Let's Encrypt
   certificate for the SRV target), or terminate TLS in front of it and point
   `peer_listen` at the LAN address the terminator forwards to. Outbound, aircd
   verifies peers' certificates against the system CAs.
4. **Policy.** Before the first foreign realm can reach you, decide what it
   may reach: default-deny plus links and passports is the recommended shape
   (`policy.default_allow: false`, empty rules; open channels via
   `links.auto_accept`). A realm you trust wholesale gets a rule.

Verify with `airc status` (shows `signing.kid`), then from the other realm
`airc send //oroboro.com/_resolver ping`. `journalctl -u aircd` logs each peer
link as `authenticated` or `UNAUTHENTICATED`; an unauthenticated inbound link
can deliver signed messages to you but is never used to send.

What is still deliberately off: nothing, once the steps above are done.
