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.
This commit is contained in:
atlas 2026-09-25 02:12:26 +02:00 • committed by mara
commit 85ba45b2de
5 changed files with 90 additions and 52 deletions

View file

@ -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 agent has one; a `null` URL with no operator-declared account is the
"this agent has no matrix" state. "this agent has no matrix" state.
**First-boot ordering**: hive-c0re provisions the matrix token AFTER **First-boot ordering**: a token can arrive after the container comes
agent containers come up. Without the path-trigger sibling 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 = (`systemd.paths.hive-matrix-daemon`, `PathExistsGlob =
<this agent's state dir>/matrix-token*` — the trailing `*` also catches <this agent's state dir>/matrix-token*` — the trailing `*` also catches
a secondary multi-account token like `matrix-token-ccc`), the daemon 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 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 of the token re-fire the service so the daemon comes alive in the
same boot cycle as 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 restart the daemon re-runs each account's bring-up, which sets the
avatar (see below). avatar (see below).

View file

@ -44,7 +44,8 @@ hivectl agent ruth rebuild
You don't need to do anything else. The controller's next pass creates 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 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 the pass. A hive without a swarm secret store has no path to a forge token
for ruth at all. for ruth at all.
@ -237,10 +238,6 @@ control: [`swarm/ui.md`](../swarm/ui.md).
# Ensure the appservice's sender account exists first # Ensure the appservice's sender account exists first
hivectl matrix sync-admin 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) # Invite the operator to the hive Space (and optionally to rooms)
hivectl matrix invite mara hivectl matrix invite mara
hivectl matrix invite @mara:yourserver --room '#hive-chat:yourserver' 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 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 Swarm SSO creates the human operator's own matrix account instead of
a manual `hivectl` step — see _Swarm SSO_ above (`swarmctl user add`). a manual `hivectl` step — see _Swarm SSO_ above (`swarmctl user add`).

View file

@ -129,31 +129,52 @@ a token.
the bind-mount path. It reads only `.yaml`/`.yml` entries from there, the bind-mount path. It reads only `.yaml`/`.yml` entries from there,
so the sibling credentials are invisible to it. The `.yaml` suffix on so the sibling credentials are invisible to it. The `.yaml` suffix on
the credential id is what makes this work. the credential id is what makes this work.
5. **hive-c0re** reads the appservice token and creates each account with 5. **hive-c0re** reads the appservice token and creates its own
one `POST /register` typed `m.login.application_service`, persisting `@hive-<hive>:` account, and the accounts an operator asks for with
the returned `access_token` to `<agent-state>/matrix-token`. It never `hivectl matrix create-user`. It never mints the token itself: the value
mints the token itself: the value has to be the one the rendered has to be the one the rendered registration names, and only the nix
registration names, and only the nix side writes that. side writes that.
6. **An account that exists but has lost its token file** is re-tokened 6. **Agents' accounts aren't this hive's.** `swarm-controller` creates each
by an appservice `POST /login` — no password and no admin rights one with the swarm's own appservice token (next section), stores its
involved. A stored-password login and an admin-room password reset token at `swarm/agents/<agent>/matrix/main`, and the agent's
remain behind that, for accounts created before the appservice existed `hive-matrix-daemon` reads it from there as the agent itself.
or named outside its namespace.
7. **hive-c0re restarts `hive-matrix-daemon`** for the agent ### The swarm's appservice, and its admin sender
immediately after writing the token so the daemon picks up the
new credential without waiting for a full container restart. If A second registration sits beside the hive's: id `swarm`, sender `@swarm`,
the restart fails (for example daemon not yet running on first boot) the same non-exclusive namespace. It's the swarm's identity on the
hive-c0re logs the error as a warning and the `.path`-trigger sibling homeserver, and `swarm-controller` creates every agent's account with it.
(`hive-matrix-daemon.path` watching for `matrix-token` appearance) tuwunel loads every `.yaml` in `appservice_dir` and refuses only a
brings the daemon up on the same boot cycle anyway. 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-<agent>`, 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 ### The appservice's sender account, and why it isn't an admin
`@hive-<hive>:<server_name>` is the appservice's own `sender_localpart`, `@hive-<hive>:<server_name>` is the appservice's own `sender_localpart`,
which the homeserver creates itself when it loads the registration — on a which the homeserver creates itself when it loads the registration — on a
zero-user database, inside startup, before the HTTP listener accepts zero-user database, inside startup, before the HTTP listener accepts
anything. It's an **ordinary account**: nothing promotes it, and the anything. It's an **ordinary account**: nothing promotes it; the
homeserver runs no `admin_execute` for it. homeserver's `admin_execute` promotes only `@swarm`.
**One account per hive.** The localpart carries the hive's name, so a 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 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, rehomed to the swarm tier rather than granted here; from the hive,
`@hive-<hive>:` has no admin sender to make that call with, so both get 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 admin room's refusal rather than an over-privileged credential that
every other call site would also carry. The one hive-side path that every other call site would also carry. The swarm's own sender is the
depends on them is the automatic password recovery for an agent that admin they need; moving them there is separate work.
has lost its stored password — the admin-sender limitation doesn't
touch the ordinary appservice re-login above.
<details><summary>Upgrading a hive that shared one sender account with every other hive</summary> <details><summary>Upgrading a hive that shared one sender account with every other hive</summary>
@ -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 that minted it; removing the registration token touches no
device, no account and no session. `login_with_password` stays on, so device, no account and no session. `login_with_password` stays on, so
the password fallback is still there too. 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 - **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 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 homeserver no longer promotes it, so a hive built fresh has an

View file

@ -55,8 +55,10 @@ strategy for every credential, including the mTLS leaf.
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
| store path | minter | reader — pulls at runtime, holds in memory | renewal | | store path | minter | reader — pulls at runtime, holds in memory | renewal |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `swarm/agents/<agent>/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/<agent>/matrix/<account>` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated | | `swarm/agents/<agent>/matrix/<account>` | `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/<agent>/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/<agent>/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/<agent>/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/<agent>/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/<agent>/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/agents/<agent>/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 |
@ -68,6 +70,19 @@ strategy for every credential, including the mTLS leaf.
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
**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 **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 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 can fetch from the store the credential it would need in order to fetch. A

View file

@ -88,10 +88,10 @@ cache), and an optional `homeserver` (defaults to
always named `main` and is always the primary; the matrix module always named `main` and is always the primary; the matrix module
declares it for you as an ordinary entry of this map, from declares it for you as an ordinary entry of this map, from
`services.hyperhive.agent.matrix.url` + agent state, so what you add `services.hyperhive.agent.matrix.url` + agent state, so what you add
here are the *further* accounts. hive-c0re pins its `tokenFile` to here are the *further* accounts. Its token comes from the swarm secret
`<state>/matrix-token` and provisions it there, and the store (`swarm-controller` mints it); its `tokenFile` stays pinned to
dashboard's link-account route refuses to create an account by that `<state>/matrix-token` as the fallback, and the dashboard's link-account
name. route refuses to create an account by that name.
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
`main` is present exactly when that URL is non-null, which is the whole `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 `{ type = "http"; url = ...; }`). Same shape as `hive-bash-daemon` and
the built-in `hyperhive` surface (`hive-mcp-http`) — claude reconnects the built-in `hyperhive` surface (`hive-mcp-http`) — claude reconnects
to the stable URL every turn instead of respawning a stdio child. to the stable URL every turn instead of respawning a stdio child.
Silently exits when `<state>/matrix-token` is absent (account not yet Silently exits when the primary account has no token yet; the
provisioned); the `systemd.paths.hive-matrix-daemon` watcher restarts `systemd.paths.hive-matrix-daemon` watcher restarts it when a token file
it the moment hive-c0re provisions the token. 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 Incoming room events wake the agent via `AgentRequest::Wake` with
`from: "matrix"`. The wake body format depends on the unread state: `from: "matrix"`. The wake body format depends on the unread state: