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:
parent
89a5dd752c
commit
85ba45b2de
5 changed files with 90 additions and 52 deletions
|
|
@ -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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -54,20 +54,35 @@ 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/<account>` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | must be stated |
|
| `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>/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>/matrix/<account>` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | 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/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>/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>/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/hives/<hive>/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/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/hives/<hive>/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/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/hives/<hive>/queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated |
|
| `swarm/hives/<hive>/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/services/<clientId>/oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | must be stated |
|
| `swarm/hives/<hive>/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 |
|
||||||
| _(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 |
|
| `swarm/hives/<hive>/queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated |
|
||||||
|
| `swarm/services/<clientId>/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 |
|
||||||
|
|
||||||
<!-- 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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue