matrix: one sender account and one sender token per hive
A swarm runs one homeserver and every hive on it logged in as the same `@hive:` localpart, holding the same access token out of one swarm-wide store path. That is one matrix identity for N hives: the homeserver cannot attribute an action to the hive that took it, and revoking one hive's standing revokes every hive's. Three changes, and the third is the one that makes the other two real: - **The localpart carries the hive's name** (`hive-<hive>`), derived in one place, `swarm_secret_client::matrix::hive_localpart`. `hive-matrix.nix` renders the same string as the appservice registration's `sender_localpart`, so the shared account stops being created rather than merely stops being used. - **The store path is templated by hive**, not a constant. The "a swarm runs one homeserver, so this is a constant rather than a parameter" rationale went with it; it stopped holding the moment two hives shared the homeserver it describes. - **The path moved out from under the grant every hive has.** It sat at `swarm/services/matrix/sender-token`, inside the `secret/data/swarm/services/*` read stanza `policy::render` gives every hive. It now sits under that hive's own stanza, `secret/data/swarm/hives/<hive>/*`, which interpolates the reader's name — so a hive reads its own token and is refused another's. The policy renderer itself is unchanged: narrowing the `services/*` grant would break the OIDC-secret read it exists for, and moving the credential is what this needed instead. A policy test walks the rendered stanzas and asserts none of hive alpha's covers hive beta's sender token, so a later stanza that widened it fails here. `swarm-matrix-ctl` takes a new required `MATRIX_MINT_HIVE` and writes that hive's path; its store grant in `swarm-bao.nix` follows, scoped to one hive's leaf via the new `deploy.bao.matrixCtlHiveName` (defaulting to this host's `hiveName`) rather than a `hives/*` wildcard, which would hand the matrix container every hive's token back. Migration: no outage at deploy. `ensure_hive_user` short-circuits on the local token file, so a hive keeps running on what it has; with no such file it reads the new per-hive path, finds nothing, and falls through to the existing register-or-appservice-login ladder against its own localpart — which needs only the per-hive `as_token` on local disk. The old shared object is read by nothing afterwards. Rooms do not follow the identity, and that is the one operator step; both ways out are written into `docs/integrations/matrix.md`. No admin standing is granted to the per-hive accounts: `admin_execute` stays empty and the assertion pinning it is untouched.
This commit is contained in:
parent
637b308d4d
commit
1261b525d6
14 changed files with 461 additions and 133 deletions
|
|
@ -149,19 +149,27 @@ a token.
|
|||
|
||||
### The appservice's sender account, and why it isn't an admin
|
||||
|
||||
`@hive:<server_name>` is the appservice's own `sender_localpart`, which
|
||||
the homeserver creates itself when it loads the registration — on a
|
||||
`@hive-<hive>:<server_name>` 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.
|
||||
|
||||
**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
|
||||
homeserver attributes an action to the hive that took it, and revoking one
|
||||
hive's standing leaves the others alone.
|
||||
|
||||
Its access token is the **sender token**, and it's the credential
|
||||
hive-c0re presents for every homeserver call it makes on the hive's
|
||||
behalf. `swarm-matrix-ctl` mints it inside the `hive-matrix`
|
||||
container and publishes it to `swarm/services/matrix/sender-token`; the
|
||||
hive reads it from there. The name says what it authenticates as — an
|
||||
account the appservice registration brings into being — rather than any
|
||||
privilege level, because it carries none.
|
||||
behalf. It's per hive for the same reason the account is:
|
||||
`swarm-matrix-ctl` mints it inside the `hive-matrix` container and
|
||||
publishes it to `swarm/hives/<hive>/matrix/sender-token`, and the hive
|
||||
reads it from there under its own certificate. That path sits inside the
|
||||
hive's own read grant (`swarm/hives/<hive>/*`), so a hive fetches its own
|
||||
token and gets a refusal on any other hive's. The name says what it
|
||||
authenticates as — an account the appservice registration brings into being
|
||||
— rather than any privilege level, because it carries none.
|
||||
|
||||
It needs no promotion for what the hive does with it. Creating the hive
|
||||
Space and the chat room, writing their hierarchy and join rules, and
|
||||
|
|
@ -175,13 +183,51 @@ and `hivectl matrix reset-password`. Both are `!admin …` messages into
|
|||
`#admins:<server_name>`, and tuwunel only treats a message as a command
|
||||
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
|
||||
`@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.
|
||||
|
||||
<details><summary>Upgrading a hive that shared one sender account with every other hive</summary>
|
||||
|
||||
Nothing to do, and no window where the hive is without an account.
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
- **The old shared value at `swarm/services/matrix/sender-token` is read by
|
||||
nothing.** `hive-c0re` and `swarm-matrix-ctl` both build the path from the
|
||||
same function, and it now carries the hive's name — so the old object
|
||||
stays in the store, unread, until an operator deletes it. Delete it or
|
||||
leave it; the accounts it authenticates as keep their own standing either
|
||||
way, since an access token lives on the device that minted it.
|
||||
- **The hive re-mints, per hive, on the next boot.** `ensure_hive_user` runs
|
||||
from the startup sweep. It short-circuits on the token file it already has
|
||||
— so the first boot after the upgrade keeps running on the shared token
|
||||
until that file goes — and on a hive with no such file it reads the new
|
||||
per-hive store path, finds nothing on a store that has never held one, and
|
||||
falls through to the register-or-appservice-login ladder against
|
||||
`@hive-<hive>:`. That ladder needs only the `as_token`, which is per hive
|
||||
and on local disk, so it works with or without a reachable store.
|
||||
- **To move a hive onto its own account now**, delete its sender-token file
|
||||
(`hivectl matrix sync-admin` re-provisions, or the next sweep does) — the
|
||||
ladder then registers `@hive-<hive>:` and persists that account's token.
|
||||
Only then does the hive stop presenting the shared one.
|
||||
- **The rooms the shared account created don't follow the new account, and
|
||||
this is the one step that needs a decision.** Membership is per account.
|
||||
`ensure_hive_space` takes the stored room id first, so the sweep hands the
|
||||
new account the old Space's id, the invite it then sends comes back
|
||||
refused (a non-member can't invite), and the sweep logs it and carries
|
||||
on — degraded, not crashed. Two ways out, both operator-chosen. Either invite
|
||||
`@hive-<hive>:` into the existing Space and chat room from a client, which
|
||||
keeps the history; or delete the hive's stored room-id files, after which
|
||||
the next sweep creates a Space and chat room owned by the new account and
|
||||
invites every agent into them. Do one of the two; leaving it gives a hive
|
||||
that provisions no rooms.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Upgrading a hive that used the registration token</summary>
|
||||
|
||||
Nothing to do, and nothing to time. The activation script mints the
|
||||
|
|
@ -248,8 +294,8 @@ Initial rollout settings:
|
|||
## Hive Matrix Space
|
||||
|
||||
On first boot, after hive-c0re provisions all agent accounts, it
|
||||
creates a private **Matrix Space** named `"hive"` using the `@hive:`
|
||||
account (`@hive:<server_name>`) and invites every provisioned agent
|
||||
creates a private **Matrix Space** named `"hive"` using its own hive
|
||||
account (`@hive-<hive>:<server_name>`) and invites every provisioned agent
|
||||
into it. This gives the operator a single Space in FluffyChat or any
|
||||
Matrix client that groups all agent-to-agent + operator rooms in one
|
||||
place.
|
||||
|
|
|
|||
Loading…
Reference in a new issue