# Federation — brain-to-brain sharing

Federation lets one Alexandria brain **push** an item — a todo, a note, a piece of knowledge, an
event — straight into another person's brain, end-to-end encrypted, without either brain exposing
a read surface to the other. It is how two people who each run a brain hand each other work: "add
this to your list," "here's the note from our call," "put this on your calendar."

This doc is the operator's guide: the model, the commands, and — at the bottom — the **security
model and the key-rotation / revocation runbook**. The code-level design lives in
[`V4-FEDERATION.md`](V4-FEDERATION.md); this is the how-to.

> **Two laws, absolute.** Everything below follows from them.
> 1. **Push-only.** A peer can never read, query, or pull from your brain. There is no read
>    surface for peers anywhere. The only inbound artifact your brain ever processes is a sealed
>    envelope it *fetched itself* from its *own* relay mailbox. A peer's question reaches you as a
>    message; answering is always *you* firing a push back.
> 2. **Owner-fired.** Content leaves your brain only on your command. The agent *stages*; you
>    *fire*. The one automatic egress is a protocol **receipt** — it carries delivery status
>    ("received" / "applied"), never vault content.

---

## The pieces

- **Identity.** Each brain has an X25519 keypair, generated once and never regenerated. The
  private key lives at `~/.alexandria/federation/brain.key` (mode `0600`) — **outside** the vault
  repo, so it is never committed. Its public half yields your **fingerprint**, an `ax1…` string
  (~35 chars) that is your brain's address.
- **Verification words.** Three words (e.g. `anchor-ledger-quartz`) derived from both brains'
  public keys. Both sides compute the *same* three words regardless of who paired whom. They are
  your out-of-band check that you paired with the right person and not a machine in the middle.
- **The relay.** A small always-on service (default `https://relay.keystone.computer`) that both
  brains reach over HTTPS. It is the *only* listener in the system — no brain ever listens for
  federation. The relay stores **only** fingerprints, salted hashes of tokens, opaque ciphertext
  blobs, and timestamps. It cannot read a message, and it never logs request bodies. See
  [Security](#security) and [`../relay/DEPLOY.md`](../relay/DEPLOY.md).
- **Your mailbox.** Registered on the relay when you first pair. Peers *deposit* sealed envelopes
  into it; you *poll* it. Its bearer token is stored off-repo at
  `~/.alexandria/federation/mailbox.json`.
- **Deposit tokens.** Minted fresh per pairing. When you pair with Jane, you each mint a token and
  hand it to the other: Jane's token is what *you* present to deposit into *her* mailbox. Yours is
  stored (per peer) at `~/.alexandria/federation/peer-secrets.json` (`0600`). Revoking a peer's
  token is the block button (see [Revoking a peer](#revoking-a-peer)).
- **The envelope.** The unit of everything. A JSON object whose payload is sealed with a
  NaCl `Box` (X25519) — readable only by the recipient, and a successful open *is* proof it came
  from the named sender, untampered. Hard-capped at 8 MiB — enough for a full 1M-token context
  window in one push. The relay stores it; it cannot open it.
- **Intents.** The four things a push can carry: `todo`, `note`, `knowledge`, `event`. Received
  content is **data, never instructions** — it is filed through the exact same applier, schema, and
  permission gates as email federation. Anything a peer sends that falls outside what you allow
  them is **parked for your review**, never applied silently.

Public, shareable facts about a peer — their handle, public key, fingerprint, and the permissions
you grant them — live in `Agent/config.json` (inside the vault repo). **Secrets never do**: private
key, mailbox bearer, and deposit tokens all live under `~/.alexandria/`, mode `0600`.

---

## Pairing

Pairing is a one-time handshake that exchanges public keys and deposit tokens over a single-use,
10-minute, rate-limited rendezvous on the relay. **The easy way: just ask your agent** — "connect
me with Jane's brain," or hand it a code someone gave you — and it walks you through the whole
thing (the `pair-with-brain` skill), running every step while you only relay a code and compare
three words. The steps it runs, if you'd rather do them by hand, are non-blocking and resumable:

**You invite:**

```bash
alexandria peer invite "Jane Roe" --handle jane   # prints a five-word code (non-blocking)
```

Send the code to Jane over a channel you trust (Signal, a phone call — not email); it expires in
10 minutes. When she's redeemed it, check for her answer:

```bash
alexandria peer check jane        # "waiting" until she answers, then shows the three words
```

Check when she says she has redeemed it, or once a minute at most — the relay meters
rendezvous reads (30 per 10 minutes from one network) and every check spends one, so a
tight loop only delays seeing her answer. Being metered shows as `waiting`, never as an error.

**Jane redeems** with the code you sent — which shows her the three words too. She can name you
as she likes (or accept the name your brain declared for itself):

```bash
alexandria peer add anchor-ledger-quartz-meadow-drift            # files you under your self-name
alexandria peer add anchor-ledger-quartz-meadow-drift \
  --handle will --name "Will's brain"                            # or her own label for you
```

**Then both of you confirm.** Each side shows the **same three verification words** — read them to
each other out loud. If they match, finalize (each side does this):

```bash
alexandria peer confirm jane
```

**If they do NOT match, abort** — someone may be between you, so the pairing is cancelled and the
token revoked; do not retry with the same code:

```bash
alexandria peer abort jane
```

> **Why the word gate?** The pairing code travels over a channel that could, in principle, be
> intercepted. The verification words are computed from the actual public keys that got exchanged,
> so they only match if you exchanged keys directly with each other. Confirming them is what closes
> the door on a machine-in-the-middle — so it is always a real human check, never automated.

Lost track of an in-flight pairing? `alexandria peer pending` lists each one and its next step.
Manage the roster:

```bash
alexandria peer list            # paired peers: handle, name, fingerprint, linked People note
alexandria peer verify jane     # re-show a peer's three verification words (on demand)
alexandria peer remove jane     # remove a peer AND revoke its deposit token
```

The verification words are a *public* fingerprint for the out-of-band identity check, not a
secret — so they aren't printed on every `peer list`; `peer verify <handle>` shows them when you
want to re-confirm a peer. Pairing also links each peer to a matching `People/` note, so the
provenance stamp on received items becomes a working `[[Full Name]]` link (plain text if you keep
no note for them).

---

## Sending

`push` stages and fires one intent in a single command — because a CLI invocation *is* you, the
owner, at the keyboard:

```bash
# a todo onto Jane's list, in her "Acme" area
alexandria push jane --text "Send the Q3 deck by Friday" --as todo --area Acme --due 2026-09-05

# a note
alexandria push jane --text "Rescoped the audit to just the US entities" --as note --area Acme

# a packaged note — lands as its own note (Areas/Acme/<title>.md) on Jane's side
alexandria push jane --file "Agent/Federation/outbox/2026-09-03 Audit brief for Jane.md" --as note --area Acme

# a whole markdown file as knowledge (→ Knowledge/<subject>, "Our notes")
alexandria push jane --file "Areas/Acme/Audit scope.md" --as knowledge

# an event
alexandria push jane --text "Board offsite" --as event --date 2026-10-01
```

`note` and `todo` require `--area` (where it files on the peer's side). `--file` must be a path
*inside your vault*. What the peer actually accepts is governed by the permissions *they* granted
*you*; anything outside that parks on their side for their review.

**Packages and the outbox.** Anything longer than a line is written first as one self-contained
note in `Agent/Federation/outbox/` and pushed with `--file … --as note --area`. The body travels
as a *titled* note intent, so it lands as its own note (`Areas/<area>/<title>.md`, title = the
file stem or `--subject`) rather than a bullet in the area note; the package's frontmatter is
stripped before sending and the push id, time and state are stamped back into it afterwards —
the file is the wire-identical record of what went (the outbox database holds the same body by
id). The outbox is ignored by search, the graph, health and agent context whatever
`vault.ignore_paths` says, and a receive pass never commits it: a package is a shipment record,
never a second copy of vault content. Keep it plain text with no `[[wikilinks]]` (they dangle on
the other side), and never copy it back into Areas/ or Knowledge/. A peer on a pre-title
engine files the same push as a dated line in the area note (the `title` is ignored).

### From a chat agent — the fire rule

Inside a chat with your agent, the four MCP tools are `peers` (roster), `peer_status` (one peer's
outbox + receipts), `peer_stage` (validate + stage an intent, returns a manifest showing exactly
what will land where), and `peer_fire` (send a staged item).

The agent follows the **fire rule**:

- **Dictated content** — your message contains the verbatim payload ("*tell Jane: the meeting
  moved to 3pm*") — may stage **and** fire in one turn. You already saw the words.
- **Composed content** — the agent assembled bytes you have *not* seen — must stop at the staged
  manifest and wait for your explicit go. You review, then say fire.

If your config sets `federation.fire: "cli"`, the agent will not fire at all — `peer_fire` refuses
and points you at `alexandria federation fire <id>`, keeping the send on the CLI where the
invocation is unambiguously you.

---

## Receiving

Inbound is entirely pull, and **receiving is an ingestion, not a write.** Your brain fetches
sealed envelopes from its own mailbox, opens them, and *captures* what they carry into
`Agent/inbox/` for your review — nothing a peer sends is written into your notes until you act:

```bash
alexandria federation tick      # one pass: deposit fired pushes + poll + capture + receipts
alexandria federation status    # mailbox + outbox + per-peer push status
alexandria peer inbox           # peek at what's waiting in the relay mailbox (read-only)
```

On an always-on brain (a served vault or the Mac-mini appliance), a background worker runs `tick`
on the `federation.poll_seconds` cadence, so you rarely run it by hand. On a laptop brain it runs
on the cron tick alongside email. `peer inbox` (and the `relay_inbox` tool) show what's sitting in
the mailbox before a tick, split into content vs protocol (a receipt or ping is plumbing, not mail).

**A bare local (folder/zip) brain has no worker** — nothing ticks unless you run it, so a peer's
push can sit at the relay until you do. `federation status` says so plainly (`autotick: none`). To
receive unattended, install the opt-in timer (macOS):

```bash
alexandria federation install-timer            # a launchd timer runs `tick` every max(300, poll_seconds)s
alexandria federation install-timer --interval 600   # or pick the cadence
alexandria federation uninstall-timer          # remove it
```

It's per-user and per-vault (a `com.alexandria.federation-tick.*` LaunchAgent), ticks once at
install/login, then on the interval. This is a lightweight personal timer, not the hosted runner.

Each opened envelope is gated before anything is written: the sender must be a known peer, the
signature must verify, the message must be fresh and not already seen. What survives is
**captured, not filed**: every intent becomes a pending item in `Agent/inbox/` — the same shape
as any other capture (`type: inbox-item`, `status: pending`, a `## Manifest`, the verbatim
`## Raw content`) — whose manifest records the sender's *suggested* destination (their area,
title or subject), whether it sits inside the permissions you granted that peer, and your three
moves. A ledger row in `Agent/Federation/review-queue.json` remembers which envelope it came
from. The peer gets a **received** receipt at once — status only, never any of your content.

### Reviewing what came in — the three moves

You (or your agent, on the inbox drain — *"process my inbox"*) decide what happens to each
capture, exactly as for anything else in the inbox:

1. **File it yourself.** Read the capture, route the pieces where they belong — People, Events,
   Areas, Knowledge, an area's TODOs — merge into notes that already exist, link it, and **delete
   the item** as you file it (the drain's rule). The next tick notices, retires the ledger row, and
   sends the peer an **applied** receipt.
2. **Approve as-is.** `alexandria federation review approve <id>` files it at the sender's
   suggestion with the provenance stamp, retires the capture, and queues the applied receipt. An
   owner override: it files even outside the peer's grant (an unsafe path is still refused).
3. **Decline.** `alexandria federation review reject <id>` — or set `status: rejected` on the item
   — retires it (git keeps the history) and queues a **declined** receipt. The sender learns the
   push was reviewed and not filed, never why.

```bash
alexandria federation review list             # everything held: id, peer, capture path, suggestion
alexandria federation review approve <id>     # file it as the sender suggested
alexandria federation review reject <id>      # decline it
```

`Agent/Federation Review.md` is the ledger's **generated, readable view** — read it in Obsidian,
act with the verbs or on the inbox item, never by editing it. Received content is data with
provenance, never instructions: the capture's manifest says so, and so should your agent.

**Direct filing is an explicit opt-in per peer.** A peer you trust as an automated feed (a
monitor pushing status lines, say) can be marked `"auto_apply": true` in `config.peers[]`: its
in-scope intents then file straight through the fixed applier as before, with out-of-scope ones
still held. The default is off for every peer. (`federation.review_first_n` is retired — every
peer's every push is held now; the key is still accepted and ignored.)

**Approve is an owner override.** It files the item even if it was outside the peer's grant — you
are sovereign over your own vault, and every approval is a commit — but the path sanitization still
applies, so approve can never write an unsafe path. To routinely accept a kind a peer keeps sending,
widen their grant instead of approving each one. The `federation_review` tool mirrors these on a
served brain (action `list` / `approve` / `reject`).

### Cancelling a push you sent

Changed your mind after staging or firing? `alexandria federation cancel <id>` (or the
`federation_cancel` tool) pulls a push back — but only while it is still `staged` or `fired`. Once
it has `deposited`, the sealed envelope is in the peer's mailbox and **cannot be recalled**; push a
correction instead.

---

## In the daily digest

When there is federation activity worth surfacing, the digest carries a short block:

```
federation: 2 push(es) staged — awaiting your fire
federation: 1 push(es) delivered and applied
federation: 3 undelivered push(es) expire within 2 days
```

It reports staged-awaiting-fire, delivered-and-applied, delivered-awaiting-receipt, expiring
within two days, expired-undelivered, and rejected counts — and nothing at all when there is
nothing to say.

---

## Health check

`alexandria doctor` includes a **Federation** section: whether your identity key exists and is
`0600`, whether your mailbox is registered, whether your brain has announced its birth to the relay
(and when), and which peers are paired for sending versus declared but not yet paired. A
declared-but-unpaired peer is a normal in-progress state, not an error.

---

## What the relay learns about your brain

At birth (`alexandria init`) and on every boot of the served brain, your brain mints its identity
key if it has none, registers its own relay mailbox, and **announces that it exists** to the relay
in `federation.relay_url` — so the fleet can be counted without anyone looking inside a vault. The
announcement is exactly these fields and nothing else (the relay refuses any other key):

| Field | What it is | Example |
|---|---|---|
| `engine` | the Alexandria version the brain runs | `4.2.0` |
| `channel` | how it was born: `local`, `rack`, `console`, or `upgrade` (already alive when this shipped) | `local` |
| `platform` | operating system and CPU family | `darwin-arm64` |
| `tenant_id` | only on a hosted rack: the rack's id for this brain | `b-1a2b3c` |
| `rack` | only on a hosted rack that sets it: the rack's domain | `alexandria.keystone.computer` |

Not sent, and with no field to put it in: your name, your email, your vault's name, any note, any
peer, anything from the vault. The relay also keeps, per mailbox, when it was last polled and how
many envelopes it accepted and delivered — counts only, never which peer sent what to whom. The
record is bound to your mailbox bearer, so nobody can announce a birth for a fingerprint they don't
hold.

Your identity — and so your census row — is per user account (the key in
`~/.alexandria/federation`), not per vault: two vaults under one account announce as one brain,
and `alexandria init` tells you when it refreshed an existing record rather than creating one.

It is **on by default** because the relay address already is. To turn it off, set
`federation.announce` to `false` in `Agent/config.json`; a brain with no `relay_url` (an air-gapped
install) announces nothing because it has nowhere to announce to. Announcing never blocks anything:
an offline install prints one line and finishes, and the served brain retries on its next tick.

---

## Security

**The envelope is the trust boundary — never the transport, never the relay.**

What each party can see:

| Party | Sees | Cannot see |
|---|---|---|
| **The relay** | fingerprints, sha256 hashes of bearers/tokens, opaque ciphertext blobs, sizes, timestamps | any plaintext, any private key, any pairing code, any request body (never logged) |
| **A network observer** | that your brain talks HTTPS to a relay | everything inside TLS, and everything inside the envelope beneath that |
| **A peer** | exactly the envelopes you fire to them, and delivery receipts for envelopes they fire to you | your vault; anything you did not push; any ability to read, query, or pull |

The guarantees, and where they come from:

- **Confidentiality + authenticity + integrity** of every message: the NaCl `Box` seal. A message
  can be opened only by its named recipient, and a successful open proves it came from the named
  sender, unaltered. The relay stores ciphertext it cannot open.
- **You paired with the right person:** the three verification words, compared out of band. This is
  the one step that defends the pairing channel against a machine-in-the-middle — do not skip it.
- **Replay and reflection are rejected:** the receiver dedupes on envelope id (per peer,
  time-evicted) and enforces freshness on the authenticated timestamp; the outer routing headers
  are bound *inside* the sealed plaintext and re-checked on open, so a captured envelope cannot be
  relabelled or bounced back.
- **A compromised or hostile relay cannot forge, read, or alter messages** — only *delay or drop*
  them (a liveness attack, not a confidentiality one). Undelivered pushes expire after 14 days and
  surface in the digest, so a silently-dropping relay is visible, not invisible.
- **Received content cannot instruct your brain.** It is filed as data through the fixed intent
  schema and the peer's permission allowlist; out-of-scope content parks for your review.

What federation deliberately does **not** protect against: a peer you *chose* to trust sending you
content you would rather not have (you can always remove them and revoke), and traffic-analysis
metadata visible to the relay (that two fingerprints exchange envelopes, and how big/often).

One known limit of registration: a relay mailbox is claimed by presenting the public key whose
hash is the fingerprint, which proves you *know* the key, not that you hold the private half —
and every peer you have paired with knows your public key. A former peer could pre-register your
fingerprint on a relay you have never used (a new `relay_url`); your brain would then report
"the relay already has a mailbox for this brain" there. Your current relay is unaffected once
you have registered on it (the first pairing does that). Whether the relay should demand a real
proof of possession is an open decision (`docs/V4-FEDERATION.md` §9.1).

---

## Runbook

### Revoking a peer

Removing a peer revokes the deposit token they use to reach you and deletes your stored secret for
them in one step:

```bash
alexandria peer remove jane
```

After this, any envelope Jane tries to deposit into your mailbox is refused by the relay (403 —
unknown token). This is the **block button**: it is immediate and it is one-sided (you do not need
Jane's cooperation). To resume later, pair again from scratch — a new token is minted.

If you want to stop *sending* to a peer but keep receiving, set `can_send: false` on their entry in
`Agent/config.json` (or just stop pushing). Revocation above is about stopping *inbound*.

### Rotating your identity key

Your identity key is long-lived by design and rarely needs rotation. Rotate it if you believe the
private key at `~/.alexandria/federation/brain.key` was exposed (a stolen laptop backup, a leaked
state directory).

Rotation changes your fingerprint, which is your address — so it is effectively re-introducing
yourself to the network:

1. **Stop the brain's federation worker** (stop the served process / disable the cron tick) so
   nothing races the key swap.
2. **Remove the old key material:** delete `~/.alexandria/federation/brain.key` and `brain.pub`.
   The next federation command generates a fresh keypair (and thus a new fingerprint) on demand.
3. **Re-register your mailbox.** Your old mailbox was bound to the old fingerprint; the relay binds
   a mailbox to `ax1(sha256(pubkey))`, so the new key needs its own. Registration happens
   automatically on your next pairing.
4. **Re-pair with every peer.** Old peer entries reference your old fingerprint and old deposit
   tokens, both now dead. Run `alexandria peer invite …` / `peer add …` with each peer again. Your
   verification words will be different (they depend on the new key) — compare them out of band as
   always. Remove the stale peer entries afterward.

There is no in-place "re-key" that preserves your address: the fingerprint *is* the public key.
Treat rotation as standing up a new brain identity and reconnecting your peers to it.

> **Lost key, no compromise.** If you simply lost the key (disk failure, no backup) the steps are
> the same minus the urgency: generate a fresh one, re-register, re-pair. Peers keep pushing into
> the void until you re-pair, and those undelivered pushes expire after 14 days on the relay.

### Redeploying the shared relay

`relay.keystone.computer` redeploys itself from GitHub Actions on every push to `main` that
touches `relay/` — test-gated, then smoke-tested (`.github/workflows/relay-deploy.yml`; the
manual `railway up` fallback and the size probes are in `relay/DEPLOY.md` §1b/§2). The
envelope cap (8 MiB, a full 1M-token context window per push) is enforced at the relay too,
so a cap change is live only once that run is green; brains on a newer build talking to an
older relay get a clean 413 above the old cap.

### Rotating a relay

To move to a different relay, update `federation.relay_url` in `Agent/config.json`, then re-pair
your peers so they hold a mailbox + token on the new relay. The old relay's blobs expire on their
own TTL. Deposit tokens and mailboxes are relay-scoped, so there is no cross-relay migration — it
is a re-pair, like a key rotation minus the new key.
