179 lines
9.4 KiB
Markdown
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.
|