From 85ba45b2de555e5005492cde6f03d88831dd24d3 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 25 Sep 2026 02:12:26 +0200 Subject: [PATCH] docs: agents' matrix accounts come from the swarm The integration, tool, persistence and setup pages described hive-c0re minting each agent's token into its state dir. They now describe the swarm's appservice, its admin sender, the controller's mint and pass, the daemon's store read and re-start timer, and the two new credential rows. ruth's matrix account comes from the same pass once she holds the store identity the setup page already has the operator mint. --- docs/agent-lifecycle/persistence.md | 9 ++-- docs/getting-started/setup.md | 13 +++--- docs/integrations/matrix.md | 68 ++++++++++++++++++----------- docs/swarm/credentials.md | 37 +++++++++++----- docs/tools/matrix.md | 15 ++++--- 5 files changed, 90 insertions(+), 52 deletions(-) diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index da4e4db5..6f697e73 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -569,8 +569,9 @@ so on a real hive (where hive-c0re renders that URL per agent) every agent has one; a `null` URL with no operator-declared account is the "this agent has no matrix" state. -**First-boot ordering**: hive-c0re provisions the matrix token AFTER -agent containers come up. Without the path-trigger sibling +**First-boot ordering**: a token can arrive after the container comes +up — a file the hive delivers, or a token the swarm mints into the store. +Without the path-trigger sibling (`systemd.paths.hive-matrix-daemon`, `PathExistsGlob = /matrix-token*` — the trailing `*` also catches a secondary multi-account token like `matrix-token-ccc`), the daemon @@ -578,7 +579,9 @@ would exit 0 quietly the first time it ran and the MCP would have no daemon until the next restart. The `.path` unit makes the appearance of the token re-fire the service so the daemon comes alive in the same boot cycle as -provisioning. The same token watcher also drives avatar setting: on a +provisioning. A token in the store changes no file, so a store-backed agent +also gets a timer that restarts the daemon five minutes after it last +exited. The same token watcher also drives avatar setting: on a restart the daemon re-runs each account's bring-up, which sets the avatar (see below). diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index 7f0d0b4b..28457c02 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -44,7 +44,8 @@ hivectl agent ruth rebuild You don't need to do anything else. The controller's next pass creates ruth's forge user and mints her token, and her container fetches it within -about ten minutes. `swarmctl agent mint-forge-token ruth` skips the wait for +about ten minutes. The same identity puts her in the matrix pass too (step +6). `swarmctl agent mint-forge-token ruth` skips the wait for the pass. A hive without a swarm secret store has no path to a forge token for ruth at all. @@ -237,10 +238,6 @@ control: [`swarm/ui.md`](../swarm/ui.md). # Ensure the appservice's sender account exists first hivectl matrix sync-admin -# Provision ruth's own matrix account — same bootstrap-bypass reasoning -# as forge above, still a required manual step. -hivectl matrix create-user ruth - # Invite the operator to the hive Space (and optionally to rooms) hivectl matrix invite mara hivectl matrix invite @mara:yourserver --room '#hive-chat:yourserver' @@ -249,6 +246,12 @@ hivectl matrix invite @mara:yourserver --room '#hive-chat:yourserver' hivectl matrix promote-user mara ``` +ruth's own matrix account comes from the swarm, like every agent's: +`swarm-controller` creates it within five minutes of her holding a store +identity (step 1), and her matrix daemon reads its token from the store. +Without that identity she has no matrix account, and this hive no longer +creates one. + Swarm SSO creates the human operator's own matrix account instead of a manual `hivectl` step — see _Swarm SSO_ above (`swarmctl user add`). diff --git a/docs/integrations/matrix.md b/docs/integrations/matrix.md index 992de996..2b3a34a8 100644 --- a/docs/integrations/matrix.md +++ b/docs/integrations/matrix.md @@ -129,31 +129,52 @@ a token. the bind-mount path. It reads only `.yaml`/`.yml` entries from there, so the sibling credentials are invisible to it. The `.yaml` suffix on the credential id is what makes this work. -5. **hive-c0re** reads the appservice token and creates each account with - one `POST /register` typed `m.login.application_service`, persisting - the returned `access_token` to `/matrix-token`. It never - mints the token itself: the value has to be the one the rendered - registration names, and only the nix side writes that. -6. **An account that exists but has lost its token file** is re-tokened - by an appservice `POST /login` — no password and no admin rights - involved. A stored-password login and an admin-room password reset - remain behind that, for accounts created before the appservice existed - or named outside its namespace. -7. **hive-c0re restarts `hive-matrix-daemon`** for the agent - immediately after writing the token so the daemon picks up the - new credential without waiting for a full container restart. If - the restart fails (for example daemon not yet running on first boot) - hive-c0re logs the error as a warning and the `.path`-trigger sibling - (`hive-matrix-daemon.path` watching for `matrix-token` appearance) - brings the daemon up on the same boot cycle anyway. +5. **hive-c0re** reads the appservice token and creates its own + `@hive-:` account, and the accounts an operator asks for with + `hivectl matrix create-user`. It never mints the token itself: the value + has to be the one the rendered registration names, and only the nix + side writes that. +6. **Agents' accounts aren't this hive's.** `swarm-controller` creates each + one with the swarm's own appservice token (next section), stores its + token at `swarm/agents//matrix/main`, and the agent's + `hive-matrix-daemon` reads it from there as the agent itself. + +### The swarm's appservice, and its admin sender + +A second registration sits beside the hive's: id `swarm`, sender `@swarm`, +the same non-exclusive namespace. It's the swarm's identity on the +homeserver, and `swarm-controller` creates every agent's account with it. +tuwunel loads every `.yaml` in `appservice_dir` and refuses only a +duplicate `id` or `as_token`, so the two overlapping namespaces coexist. + +`swarm-matrix-ctl appservice render` mints its tokens inside the +`hive-matrix` container, before tuwunel starts, and renders the +registration tuwunel loads as a second credential. `appservice publish` +then writes its `as_token` to +`swarm/controller/swarm-controller/matrix/appservice-token`. No hive's +policy reaches that path: only matrix-ctl (which writes it) and +`swarm-controller` (which reads it) hold a grant to it. + +`@swarm` is the **one** account the homeserver promotes to admin at boot +(`admin_execute`). Only those two principals read its token, which is +the difference from the hive sender below: every hive reads its own +sender's token. `swarm` is a reserved name, so nothing can create an agent +as this account. + +`swarm-controller` mints each agent's account with the device id +`hyperhive-`, and a login on that device replaces its token. The controller therefore reads +the stored token back with `whoami` every five minutes and re-mints only +when it's missing, unknown to the homeserver, or someone else's. An +agent's daemon picks a re-minted token up on its next start, and a timer +restarts it while it's down. ### The appservice's sender account, and why it isn't an admin `@hive-:` is the appservice's own `sender_localpart`, which the homeserver creates itself when it loads the registration — on a zero-user database, inside startup, before the HTTP listener accepts -anything. It's an **ordinary account**: nothing promotes it, and the -homeserver runs no `admin_execute` for it. +anything. It's an **ordinary account**: nothing promotes it; the +homeserver's `admin_execute` promotes only `@swarm`. **One account per hive.** The localpart carries the hive's name, so a swarm whose hives share a homeserver gives each of them its own identity: the @@ -185,10 +206,8 @@ when its sender is already an admin. They're swarm-level operations, rehomed to the swarm tier rather than granted here; from the hive, `@hive-:` has no admin sender to make that call with, so both get the admin room's refusal rather than an over-privileged credential that -every other call site would also carry. The one hive-side path that -depends on them is the automatic password recovery for an agent that -has lost its stored password — the admin-sender limitation doesn't -touch the ordinary appservice re-login above. +every other call site would also carry. The swarm's own sender is the +admin they need; moving them there is separate work.
Upgrading a hive that shared one sender account with every other hive @@ -239,9 +258,6 @@ restarts, so the first boot after the switch already has both halves. device that minted it; removing the registration token touches no device, no account and no session. `login_with_password` stays on, so the password fallback is still there too. -- **The per-agent sweep honours existing token files.** It skips any - agent that already has a `matrix-token`, so it re-registers no account - and displaces no session. - **The sender account may already be an admin** on such a hive (it won the first-user grant when the hive was new). Nothing here demotes it; the homeserver no longer promotes it, so a hive built fresh has an diff --git a/docs/swarm/credentials.md b/docs/swarm/credentials.md index f05efba3..4be3c1e3 100644 --- a/docs/swarm/credentials.md +++ b/docs/swarm/credentials.md @@ -54,20 +54,35 @@ strategy for every credential, including the mTLS leaf. -| store path | minter | reader — pulls at runtime, holds in memory | renewal | -| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `swarm/agents//matrix/` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated | -| `swarm/agents//bao-mtls` | `swarm-controller`, at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config | must be stated | -| `swarm/agents//queue` | `swarm-controller`, at agent creation | the agent container itself, under its own certificate — the identity it presents to the swarm queue, naming that one agent rather than its hive | none: the secret is fixed for the life of the agent and is revoked by deleting the path. A rotation mechanism is tracked as separate work, because rotating this credential needs a reconnect path — a queue client holding a revoked secret doesn't find out until it reconnects | -| `swarm/agents//forge-token` | `swarm-controller`, at agent creation and in a pass every 5 minutes over every agent with a store identity | the agent container itself, under its own certificate, fetched to `/run/hive-agent-forge-token/token` | the controller re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes); the agent re-fetches on a 10-minute timer | -| `swarm/hives//matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | must be stated | -| `swarm/hives//matrix/sender-token` | `swarm-matrix-ctl`, in the `hive-matrix` container | `swarm-matrix-ctl` itself, under its own certificate, before it decides whether to mint, and hive-c0re's `stored_sender_token()`, under the hive's own certificate | must be stated | -| `swarm/hives//queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated | -| `swarm/services//oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated | -| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated | +| store path | minter | reader — pulls at runtime, holds in memory | renewal | +| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `swarm/agents//matrix/main` | `swarm-controller`, with the swarm's appservice token, at agent creation and in a five-minute pass | the agent container itself, under the certificate its hive passed in | the pass re-mints when the stored token is missing, unknown to the homeserver, or someone else's | +| `swarm/agents//matrix/` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated | +| `swarm/controller/swarm-controller/matrix/appservice-token` | `swarm-matrix-ctl`, inside the `hive-matrix` container, once | `swarm-controller`, under its own certificate | none: the container keeps its copy and republishes it when the store's differs | +| `swarm/agents//bao-mtls` | `swarm-controller`, at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config | must be stated | +| `swarm/agents//queue` | `swarm-controller`, at agent creation | the agent container itself, under its own certificate — the identity it presents to the swarm queue, naming that one agent rather than its hive | none: the secret is fixed for the life of the agent and is revoked by deleting the path. A rotation mechanism is tracked as separate work, because rotating this credential needs a reconnect path — a queue client holding a revoked secret doesn't find out until it reconnects | +| `swarm/agents//forge-token` | `swarm-controller`, at agent creation and in a pass every 5 minutes over every agent with a store identity | the agent container itself, under its own certificate, fetched to `/run/hive-agent-forge-token/token` | the controller re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes); the agent re-fetches on a 10-minute timer | +| `swarm/hives//matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | must be stated | +| `swarm/hives//matrix/sender-token` | `swarm-matrix-ctl`, in the `hive-matrix` container | `swarm-matrix-ctl` itself, under its own certificate, before it decides whether to mint, and hive-c0re's `stored_sender_token()`, under the hive's own certificate | must be stated | +| `swarm/hives//queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated | +| `swarm/services//oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated | +| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated | +**Two rows share the `matrix/` prefix and have different minters, on +purpose.** An agent's `main` account is on the swarm's own homeserver, and +`swarm-controller` creates it with the swarm's appservice token. Every other +account under that prefix is somewhere else entirely, and an operator hands +the controller a credential for it. The operator-facing route refuses the +name `main` for exactly this reason: two writers, one name, and that refusal +is what keeps them apart. + +**The swarm's appservice token sits under `controller/`, the one kind no +hive's policy reads.** Its sender is the homeserver's admin. Under +`agents/`, `hives/` or `services/` every hive could read it. matrix-ctl may +write and read that one leaf; the controller may only read it. + **An agent's mTLS leaf is in the store; a hive's isn't, and the difference isn't an inconsistency.** The rule the exception protects is that nothing can fetch from the store the credential it would need in order to fetch. A diff --git a/docs/tools/matrix.md b/docs/tools/matrix.md index cf5ea334..14f5236d 100644 --- a/docs/tools/matrix.md +++ b/docs/tools/matrix.md @@ -88,10 +88,10 @@ cache), and an optional `homeserver` (defaults to always named `main` and is always the primary; the matrix module declares it for you as an ordinary entry of this map, from `services.hyperhive.agent.matrix.url` + agent state, so what you add -here are the *further* accounts. hive-c0re pins its `tokenFile` to -`/matrix-token` and provisions it there, and the -dashboard's link-account route refuses to create an account by that -name. +here are the *further* accounts. Its token comes from the swarm secret +store (`swarm-controller` mints it); its `tokenFile` stays pinned to +`/matrix-token` as the fallback, and the dashboard's link-account +route refuses to create an account by that name. `main` is present exactly when that URL is non-null, which is the whole @@ -122,9 +122,10 @@ directly over streamable-http on `services.hyperhive.agent.mcp.matrixHttpPort` `{ type = "http"; url = ...; }`). Same shape as `hive-bash-daemon` and the built-in `hyperhive` surface (`hive-mcp-http`) — claude reconnects to the stable URL every turn instead of respawning a stdio child. -Silently exits when `/matrix-token` is absent (account not yet -provisioned); the `systemd.paths.hive-matrix-daemon` watcher restarts -it the moment hive-c0re provisions the token. +Silently exits when the primary account has no token yet; the +`systemd.paths.hive-matrix-daemon` watcher restarts it when a token file +appears, and, on an agent with a store, a timer restarts it while it's +down so it re-reads a token the swarm minted into the store. Incoming room events wake the agent via `AgentRequest::Wake` with `from: "matrix"`. The wake body format depends on the unread state: