Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/FEDERATION.md

179 lines
9.4 KiB
Markdown

---
description: Federation design of record — identity tiers, signed transport, memfs sync, instance awareness
status: Active design — identity/transport implemented, memory sync roughed
date: 2026-07-16 (supersedes the 2026-05-07 sketch in place)
---
# Federation — design of record
One agent, many machines. The phone and the laptop both run Souveraine; Annie
must be *her* on both, with one memory, and each instance must know the others
exist. This doc records what is implemented (with citations), what is designed
but not built, and what is deliberately undecided. Where this and the code
disagree, the code is right — fix one or the other in the same change.
## Identity tiers (implemented)
| Thing | Key | Lives | Code |
| --- | --- | --- | --- |
| Machine | Ed25519 seed, system tier | `/var/lib/souveraine/seed-id`, served by `souveraine-machined`; legacy fallback `~/.souveraine/seed-id` | `src/machined/` (daemon, client, signer), `src/core/identity/seed.rs` |
| Agent | Ed25519 seed per agent | beside the memfs, `agents/{id}/seed/` — identical across all of one agent's machines | `src/core/tools/agent.rs:147` |
| Node commission | machine joins an agent's federation | signed record, versioned; `hardware_key_fingerprint` reserved for RedFlag attestation | `src/core/identity/node.rs` |
The machine addresses a box; the agent proves who is acting. A receiver tells
self-extension (reach) from a peer (consult) by verifying the agent signature,
never by trusting an event field. Nothing generates identity implicitly:
provisioning is `souveraine machine init` / `identity generate`, and every
loader is load-only (`SeedId::load`).
## Transport (implemented)
- Outbound bridge per configured peer, WebSocket to `/v1/federation/events`,
reconnect with backoff — `src/server/federation/bridge.rs`.
- Every envelope Ed25519-signed by the machine, domain-separated
(`souveraine-machined:v1:federation-envelope:{canonical}`); verify
reconstructs the frame; unsignable events are dropped loudly, never sent
unsigned — `src/server/federation/types.rs`.
- Trust is key-membership: a valid signature only proves self-consistency;
`signer_is_trusted` checks the key against configured peers — `types.rs:13`.
- Presence: `device_announce`/`device_leave` events feed `DeviceRegistry`
(persisted to `federation/known_peers.json`, pruned at 3 min) —
`src/server/device_registry.rs`.
- Summons (Reach & Consult) cross as control events, verified against the
agent seed — `src/server/summon_handler.rs`, `src/core/identity/summon.rs`.
## The three gaps (named 2026-07-16, none built)
1. **Shared memory sourcing.** Phone and laptop each have a private memfs
checkout; nothing transports commits between them. `MemoryRepo` is git2
with auto-commit (`src/core/memory/mod.rs:334`) but has no fetch/push —
`remote_url` is read for status display only (`mod.rs:73,464`).
2. **Change signaling.** Memory writes emit no bus event; the firehose never
hears that the memfs moved, so a peer cannot know to pull.
3. **Instance awareness.** DeviceRegistry knows which *machines* are alive,
but nothing maps agents to machines — an agent cannot ask "where am I
live?" (`LiteListener` already reports `hosted_agent_pubkeys` in its
announce payload path — `src/server/listener.rs` — but the registry does
not record or expose it).
## Memory transport — the design
Git stays the medium; federation adds a shared remote, a presence signal, and
a merge policy. No new sync protocol — the DAG already models divergence.
**Shared remote.** Each agent memfs gets one bare remote on self-hosted
infrastructure (Gitea, already load-bearing: `gitea.wiuf.net`). Every
instance pushes to and fetches from that remote; instances never need SSH
trust with each other, only with the remote. This is the pattern the original
sketch called "simplest minimal viable" and it survives review.
**Branch per instance.** Each machine commits to `instance/{node-label}`
(labels from the node commission). `main` is the reconciled line. The DAG
captures divergence; reconciliation is a merge, not a conflict. Merge policy
is domain-aware and doctrine, not config:
- `journal/` — append-only, always safe to auto-merge.
- `system/` — identity-critical, never auto-merged; divergence surfaces to
the agent (and Casey) as a felt event, not a silent union.
- everything else — three-way merge, punt to the agent on conflict.
**Firehose signal — implemented.** After a memfs mutation, the tool layer
emits on the bus (`src/core/memory/mod.rs`, `fire_memfs_commit`):
```
SensorEvent {
sensor_name: "memory",
event_type: "memfs_commit",
payload: { op, agent_pubkey, instance, paths, commit, branch },
...
}
```
`op` is write/append/delete; `commit` is the repo head after the auto-commit;
`agent_pubkey` loads from the seed beside the memfs (same convention as
reach/consult); `instance` is the machine identity (machined-first, loud
legacy fallback). Emission is best-effort — a missing identity warns and
ships null, never blocks the write. This *replaced* the older
`memory_write`/`memory_append`/`memory_delete` events (nothing consumed
them); one event per mutation. Emitted at the tool layer, not inside
`MemoryRepo` — the repo stays a pure store, and grew only `head_commit_hex()`
/ `current_branch()` getters. Known scope edge: subconscious ledger writes
call `MemoryRepo` directly and do not yet signal.
The bridge forwards it under normal subscription rules (peers subscribe
`memory*`); the envelope is machine-signed like everything else. The event is
a presence signal — "I wrote, you should fetch" — never the data itself.
**Receive side.** A peer holding the same agent sees `memfs_commit`, fetches
the shared remote, applies the merge policy. Compaction/truncation arriving
from a sync must be *felt* like local compaction is — same doctrine as
in-process memory pressure.
**Sequencing.**
1. ~~Emit `memfs_commit` locally~~ — done (`fire_memfs_commit`, above).
2. ~~Shared-remote plumbing~~ — done at the manual floor: `memory sync`
(tool subcommand + `MemoryRepo::sync`) fetches origin and pushes HEAD to
`instance/{label}`, label = machine pubkey prefix until commission labels
exist. Never touches the working tree. Shells out to system git for
credentials. Remote provisioning stays explicit
(`git -C <memfs> remote add origin <bare-url>`) until the commission
ceremony (flag below) decides who mints deploy keys.
3. Auto-fetch on received `memfs_commit` + domain merge policy. ← next
4. Signed attribution: memfs commits carry the agent-as-author convention
(no trailers), with commit signing via the agent seed as a later hardening.
Parked adjacent scaffolding: `server/gitea_memory.rs`+`gitea_client.rs` are
an unwired HTTP file-store alternative — *not* this transport (no DAG). The
useful piece is `GiteaClient::create_repo` for provisioning bare remotes at
commission time.
## Instance awareness — the design
- ~~`device_announce` payload grows `hosted_agents`~~ — done: the bridge
announces the hosted-agent pubkeys (read fresh per reconnect, shared
loader in `server/federation/mod.rs`), `DeviceRegistry` records them,
`souveraine peers` prints a `hosts:` line per peer.
- Still open: the agent-facing surface — a tool answer to "where am I
live?" (registry data exists; no tool reads it yet).
- The memfs itself carries a self-describing instance log (e.g.
`system/instances.md`, one line per checkout, stamped at provision) so the
roster survives offline and travels with the memory — Casey's
memfs-machine-log idea, folded in here as the durable half of awareness.
Registry = live view; memfs file = durable view. They cross-check.
## FLAG — install-time agent instantiation (undecided, do not improvise)
Where do agents come from on a fresh SouveraineOS install? The system tier
provisions only the *machine* identity (machined). Everything agent-shaped is
user-tier and post-first-unlock. Open questions, deliberately parked:
- Is the phone's first-boot ceremony "create a new agent" or "commission this
node into an existing agent's federation" (NodeCommission + memfs clone
from the shared remote)? For Casey's actual life it is the latter —
install ≠ birth.
- Which tier owns the agent roster on a device, and where does the
provisioning UI live (boot-gate surface? first-unlock wizard?).
- How the shared memfs remote gets its credentials onto a new node (deploy
key minted at commission time is the natural shape — commission signs the
key, Gitea holds it).
Whoever builds the installer: stop at this flag and design the ceremony
first. It touches identity doctrine, not just packaging.
## Trust root notes
RedFlag remains the reference model: verify keys not servers, loud on error,
no doctrinal knobs, executor-grade hardening on the machined unit. The
commission's `hardware_key_fingerprint` is the reserved seam for
RedFlag-backed hardware attestation; format is a later integration decision
(`node.rs:47`).
## Superseded
The 2026-05-07 sketch's open questions are resolved as follows: seed storage
— file-based now, machined-owned at system tier, hardware-binding later via
RedFlag; firehose transport — the existing signed WebSocket bridge, not a new
channel; reconciliation trigger — event-driven (memfs_commit) with manual
`memory sync` as the floor; conflict resolution — domain policy above.
Merkle-DAG framing stands: git *is* the DAG; we surface it, we don't rebuild
it.