Watch
1
0
Fork
You've already forked souveraine
0

federation: document node enrolment trust boundary

This commit is contained in:
Fimeg 2026-07-12 15:34:41 -04:00
commit 2c14da0dbe
6 changed files with 150 additions and 10 deletions

View file

@ -9,6 +9,9 @@ How Souveraine is built, and why. For whoever reads it, human or agent. When thi
**identity**: who she is
- [identity](identity/01-seed-identity.md). Two keys, the glyph, reaching and consulting.
**federation**: how one being exists in more than one place
- [node enrolment](federation/01-node-enrollment.md). The missing ceremony for adding a device without copying an identity.
**nervous**: how she feels things happen
- [the nervous system](nervous/01-eventbus.md). One bus, one kind of message.

View file

@ -0,0 +1,68 @@
# Node enrolment
Souveraine can already exchange signed events between configured peers. It cannot yet add a device as a node of an existing agent. Copying `~/.souveraine/agents/<id>/` to a phone is therefore a migration shortcut, not federation: it copies the agent's private key and gives two machines indistinguishable authority.
This document is the design-of-record for closing that gap. It narrows the existing federation sketch into a first implementation boundary; it does not claim that the ceremony exists today.
## What a node is
One agent can have several independently operating nodes. They share an agent root identity but not a private signing key.
| Concern | Stored value | Why |
|---|---|---|
| Being | agent root public key | Stable personhood across every fork |
| Node | immutable random `node_id` | Stable Git and protocol identity; never derived from a hostname |
| Hardware | RedFlag device public-key fingerprint | Attestation and revocation target |
| Display | label such as `phone` or `hearth` | Human-readable and renameable |
| Memory | `nodes/<node_id>` Git ref | Independent history for later reconciliation |
`machine-id` and hostname are useful evidence and display hints, but neither is suitable as the durable node identifier. A reinstall can change the first; a rename can change the second.
## The ceremony
The device being added creates its node key locally. Its private key never leaves it.
1. **Invite.** The authority creates a single-use invitation for an agent and requested label.
2. **Request.** The device creates `node_id` and node key, then returns a CSR containing its public key and RedFlag hardware public-key fingerprint.
3. **Commission.** The authority signs the tuple `(agent root, node_id, node public key, hardware fingerprint, label, issued-at, expiry/revocation policy)`.
4. **Join.** The device stores the commission, creates or receives its `nodes/<node_id>` memory ref, and adds the federation endpoint.
5. **Operate.** Each event carries the node signature and commission. Git moves memory; the event stream only says that a branch changed.
The future command shape is intentionally small:
```text
souveraine node invite --agent Annie --label phone
souveraine node join --invite <one-time-token>
souveraine node status
souveraine node revoke <node-id>
```
The command names are provisional. The resulting wire formats and storage paths must be versioned before a phone depends on them.
## What exists today
- Machine and agent Ed25519 keys exist.
- Federation transport signs each event with the machine key.
- Peer keys are explicit configuration and inbound events now require one of those configured keys.
- Git-backed memory commits locally, but has no push, fetch, branch status, or reconciliation commands.
## What is deliberately not done
- No root-key commissioning authority or CSR format.
- No RedFlag hardware-key integration or revocation list.
- No per-node key storage, commission validation, or event-chain validation.
- No memory remote or branch-per-node sync.
- No archivist reconciliation policy.
Until these land, `packaging/deploy-phone.sh` defaults to copying no private key. `--clone-identity` remains only for an explicit same-instance migration; it is not an enrolment workflow.
## Implementation order
1. Define versioned `NodeId`, CSR, and commission structs plus round-trip/signature tests.
2. Add a local node-key store and `node status`; do not connect it to deployment yet.
3. Implement invite/join and persist trusted commissions.
4. Replace shared agent-key authentication for `reach` with node key plus commission verification.
5. Add memory remotes and `nodes/<node_id>` divergence status.
6. Let the archivist reconcile branch state according to Cloister policy.
This preserves the intended architecture: the phone is a real branch of the agent, not a thin client and not a copied private key.

View file

@ -6,7 +6,7 @@ Two keys, both Ed25519. One belongs to the machine, one belongs to the agent. Ke
The machine key lives at `~/.souveraine/seed-id/`. One per install. It signs the transport: this event left this machine.
The agent key lives at `~/.souveraine/agents/{id}/seed/`, beside her memory. It signs her acts. Because it sits inside the subtree that syncs during federation, it travels with her. The machine key stays behind. The agent key goes where she goes.
The agent key lives at `~/.souveraine/agents/{id}/seed/`, beside her memory. It signs her acts. Today the memory subtree — including this private key — is copied when a same-instance migration is explicitly requested. That is a temporary implementation, not the node model: a copied private key cannot distinguish a legitimate fork from a stolen duplicate.
Both are made the same way. `SeedId::load_or_generate` reads the 32-byte private key if it is there, or makes one from the OS random source and writes it `0600`. The struct only ever offers `sign` and `verify`. No path hands out the private key, and none sends it anywhere.
@ -28,8 +28,19 @@ If it matches, it is the same being reaching across machines. No gate. If it doe
The payload can claim whatever it likes about its intent. The signature decides, not the claim. The signed bytes are fixed: request, tool, target, prompt, joined by newlines. Change any field after signing and the check fails. The code is `src/core/identity/summon.rs`.
## The node model we are building toward
An agent root key names the being. A device must have its own non-exportable node key, preferably the hardware-bound key RedFlag already knows. Adding a phone is a commissioning ceremony, not a directory copy:
1. The phone generates a local node key and a stable, random `node_id`.
2. It presents a CSR to the agent's designated commissioning authority.
3. The authority signs a commission binding agent root, node key, `node_id`, and a human label such as `phone`.
4. The node writes to its own Git ref, `nodes/<node_id>`. The label is display metadata; hostname and machine ID are not Git identity.
The root key does not travel to a federated node. The commission lets a node sign its own events and later lets the archivist reconcile its branch with the other branches of the same agent. This protocol is not implemented yet; see [node enrolment](../federation/01-node-enrollment.md).
## Open edges
The agent key travels with her memory under git. Whoever can pull that memory can read the key. Encryption at rest is the fix; until then her identity is only as safe as the remote that holds it. Tracked in `docs/tasks/federation-summon.md`.
The current shared agent key is a migration convenience, not a secure federation mechanism. It must be replaced by per-node commissions before memory remotes are used for independent nodes. `packaging/deploy-phone.sh` now excludes both machine and agent private keys by default; `--clone-identity` is deliberately noisy because it is only for same-instance migration.
Replay is held off by request-id and a sixty-second window. The in-flight record lives in memory and does not survive a restart. Same task tracks the hardening.