state + federation design of record
machined system tier landed in souveraine (5039163/116562a); boot-gate vs session-lock doctrine on record. FEDERATION.md moves here from souveraine's gitignored docs tree, rewritten: implemented pieces cited, memfs transport roughed (shared bare remote, branch-per-instance, domain merge policy, memfs_commit firehose signal), instance awareness design, and the install-time agent instantiation flag.
This commit is contained in:
parent
921b03eaee
commit
0da372b314
2 changed files with 194 additions and 2 deletions
158
docs/FEDERATION.md
Normal file
158
docs/FEDERATION.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
---
|
||||
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.** After a memfs commit, the server emits on the bus:
|
||||
|
||||
```
|
||||
SensorEvent {
|
||||
sensor_name: "memory",
|
||||
event_type: "memfs_commit",
|
||||
payload: { agent_pubkey, instance, paths, commit, branch },
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Emitted at the server/tool layer (where `ToolContext.event_bus` already
|
||||
lives), not inside `MemoryRepo` — the repo stays a pure store. 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 (bus only — surfaces/inspector benefit even
|
||||
before any peer exists).
|
||||
2. Shared-remote plumbing: `memory sync` (manual push/fetch vs the bare
|
||||
remote, per-instance branch), remote provisioned per agent.
|
||||
3. Auto-fetch on received `memfs_commit` + domain merge policy.
|
||||
4. Signed attribution: memfs commits carry the agent-as-author convention
|
||||
(no trailers), with commit signing via the agent seed as a later hardening.
|
||||
|
||||
## Instance awareness — the design
|
||||
|
||||
- `device_announce` payload grows `hosted_agents: [agent_pubkey, ...]`
|
||||
(the lite listener already computes this list; the full server should too).
|
||||
- `DeviceRegistry` records it; `souveraine peers` shows which agents live
|
||||
where; the agent-facing surface is a tool answer to "where am I live?".
|
||||
- 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.
|
||||
Loading…
Reference in a new issue