docs: the swarm mints agent forge tokens; hive-c0re and tea-login no longer do

credentials.md gains the forge-token row and drops the claim that the
forge token never passes through the store. setup.md says plainly that
an agent spawned on the hive alone, ruth's bootstrap included, now gets
no forge user from anything. CLI references regenerated.

Refs #3782
This commit is contained in:
atlas 2026-09-24 16:43:54 +02:00 • committed by mara
commit abc942cff3
8 changed files with 96 additions and 43 deletions

View file

@ -25,14 +25,15 @@ operator has to place, and where.
### 1 · Forge
```bash
# Provision (or refresh) ruth's own forge account — do this first. Ruth's
# bootstrap bypasses the normal spawn-approval flow ("Spawn sub-agents"
# below), so unlike every other agent it does not get its forge account
# auto-provisioned — this manual step is still load-bearing.
hivectl forge create-user ruth
# Sub-agents spawned later (via the approval flow in "Spawn sub-agents")
# get their forge accounts auto-provisioned — nothing to run here for them.
# hive-c0re no longer creates agent forge users or mints agent tokens:
# swarm-controller does, for agents created at swarm level
# (`swarmctl agent create`), and stores the token in the swarm secret store,
# where the agent fetches it. An agent that only ever existed on this hive —
# ruth's bootstrap, or the hive's own spawn-approval flow — gets no forge
# user from anything yet. Create that user in the forge's admin UI; once the
# agent has a store identity (`swarmctl agent mint-identity`), swarm-controller
# mints its token within five minutes, or at once with:
swarmctl agent mint-forge-token ruth
```
Swarm SSO creates the human operator's own forge account instead of
@ -267,7 +268,9 @@ See [`tools/hivectl.md`](../tools/hivectl.md) for every `hivectl` verb.
<!-- vale write-good.Passive = NO -->
- **No forge admin token is stored in any agent state dir.** Agents
hold a regular agent token in their `forge-token` file; sensitive
hold a regular agent token, fetched from the swarm secret store into
`/run/hive-agent-forge-token/token` (or, for an agent without a store
identity, the `forge-token` file hive-c0re wrote before); sensitive
creds (the core token) live on the host.
- All config changes (forge PRs on `agent-configs/<name>`) go through
operator approval — agents can't unilaterally rebuild containers, by design.

View file

@ -17,7 +17,9 @@ each agent on relevant activity.
Two scope sets live in `hive-c0re::forge`:
**`TOKEN_SCOPES`** (per-agent tokens):
**`TOKEN_SCOPES`** (per-agent tokens, and `hivectl forge create-user`
accounts). swarm-controller mints agent tokens with a byte-identical copy,
`forge::agent_token::AGENT_TOKEN_SCOPES`, pinned by a test:
| Scope | Why |
| -------------------- | ------------------------------------------------------------------------------------------------------- |
@ -46,13 +48,19 @@ hive-c0re to re-mint with the new scopes.
## Per-agent forge accounts
Each agent gets its own Forgejo user + access token, provisioned at
boot by `hive-c0re::forge`. The provisioning flow is idempotent:
`hive-c0re::forge` reuses existing accounts + tokens, so container destroy/recreate
doesn't lose forge identity. It writes the token to
`<state>/forge-token` (one line, no trailing newline) inside the
agent container so `hive-forge` CLI + `forge_notify` poller can
read it without touching c0re's host-side credential store.
Each agent gets its own Forgejo user and access token. swarm-controller
creates the user when it creates the agent, and mints one token named
`swarm-agent` with the admin API into the swarm secret store at
`swarm/agents/<agent>/forge-token`. A pass at start and every five minutes
re-mints any agent's token that's missing or no longer matches the forge;
a rotation deletes the old `swarm-agent` token first, so each agent holds at
most one. The agent fetches the token under its own store certificate into
`/run/hive-agent-forge-token/token` (`nix/agent-modules/forge-token.nix`),
and `hive-forge`, the git credential helper, the `forge_notify` poller and
the avatar sync read it from there, falling back to `<state>/forge-token`,
the file hive-c0re wrote before. hive-c0re no longer creates agent users or
mints agent tokens; the `hyperhive-<unix-seconds>` tokens it minted stay on
the forge until removed.
Two things live in the `agent-configs` Forgejo organization:
@ -162,7 +170,7 @@ The poller starts disabled and stays that way for any of:
- `HIVE_FORGE_URL` not set (no forge configured for this hive), or
not parseable as a URL.
- `<state>/forge-token` missing or empty (agent has no forge
- no token in `$HIVE_FORGE_TOKEN_FILE` or `<state>/forge-token` (agent has no forge
account — pre-provisioning or destroy-without-purge race).
- Initial client construction fails (the typed `forgejo-api` client
for the API calls, or the plain reqwest client kept for the

View file

@ -42,9 +42,9 @@ to the store under its own name, pulling what it needs when it needs it. No
process reads a secret on another principal's behalf: the principal that
needs a value is the principal that authenticates for it.
Two per-agent credential files — the forge token and the github token — sit
outside this page: they're operator-supplied and never pass through the
store, so the table below doesn't govern them.
One per-agent credential file — the github token — sits outside this page:
it's operator-supplied and never passes through the store, so the table
below doesn't govern it.
**Per secret, the target specifies minter, reader, and renewal strategy.**
Those three are the contract, and the reader is a process pulling a store
@ -54,16 +54,17 @@ strategy for every credential, including the mTLS leaf.
<!-- vale write-good.Passive = NO -->
| 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>/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/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/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/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 |
| 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>/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>/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>/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/<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/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 -->
@ -95,6 +96,18 @@ value it holds, so running this against an already-migrated agent doesn't drop
its queue connection. The certificate half isn't: the agent gets a fresh leaf
and picks it up on its next boot.
The forge token needs no such step: `swarm-controller` checks every agent
that has a store identity at start and every five minutes, and mints a token
for any whose stored one is missing or stale. To check one agent now:
```sh
swarmctl agent mint-forge-token <agent>
```
An agent without a store identity gets no swarm token and keeps using the
`forge-token` file in its state dir, if it has one; run `mint-identity` for it
first.
⚠️ **Run this for every existing agent before deploying a hive-side change
that makes a container require a credential it may not have.** A container
whose credential is absent doesn't start — that's deliberate, and it's what

View file

@ -14,7 +14,8 @@ markdown-docs > docs/tools/forge-cli.md`.
## Credentials and repo defaults
- Credentials: `$HYPERHIVE_STATE_DIR/forge-token`
- Credentials: `$HIVE_FORGE_TOKEN_FILE` (the token the agent fetched from
the swarm secret store), else `$HYPERHIVE_STATE_DIR/forge-token`
- Active repo resolves, highest priority first: global `-r/--repo` flag
> the `origin` remote of the cwd's git checkout > `$HIVE_FORGE_REPO`
(last-resort override, unset by default) > a hard error.

View file

@ -103,22 +103,22 @@ Manual entry point to the same idempotent provisioning c0re runs at boot — for
###### **Subcommands:**
* `create-user` — Create or refresh the Forgejo account + token for `<name>`
* `create-user` — Create or refresh a non-agent Forgejo account + token for `<name>`
* `reconcile-config` — Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/<agent>` main
## `hivectl forge create-user`
Create or refresh the Forgejo account + token for `<name>`.
Create or refresh a non-agent Forgejo account + token for `<name>`.
For an existing agent, persists the token to its state dir; for a human/other account, prints the token to stdout. Set a password to enable forge web-UI login (otherwise it uses a random throwaway).
Prints the token to stdout. Set a password to enable forge web-UI login (otherwise it uses a random throwaway). Refused for an existing agent: swarm-controller mints an agent's token (`swarmctl agent mint-forge-token <agent>`).
**Usage:** `hivectl forge create-user [OPTIONS] <NAME>`
###### **Arguments:**
* `<NAME>` — Forgejo username. For agents: the container/agent name (`<n>` in `h-<n>`; manager uses the literal `manager`). For humans: any forgejo username — `mara`, `damocles`, etc
* `<NAME>` — Forgejo username of a human/other account — `mara`, `damocles`, etc
###### **Options:**

View file

@ -29,7 +29,7 @@ at boot. Useful for recovery, ad-hoc re-provisioning, or fixing a single
agent without bouncing the daemon.
```bash
hivectl forge create-user iris # provision (or refresh) forge account for agent `iris`
hivectl forge create-user iris # refused: `iris` is an agent; use `swarmctl agent mint-forge-token iris`
hivectl forge create-user mara # create forge account for a human user; prints token to stdout
hivectl forge create-user mara --password hunter2 # set a web-login password
hivectl forge create-user mara --password-stdin # read password from stdin (safer for scripting)
@ -40,8 +40,9 @@ hivectl forge reconcile-config iris --verbose # include the full diff, not
```
- For **agents** (name has a state dir under `/var/lib/hyperhive/agents/`):
`create-user` persists the token to `<state>/forge-token`. Re-running refreshes the
token (idempotent — scope always matches current `TOKEN_SCOPES`).
`create-user` refuses. swarm-controller mints an agent's token into
the swarm secret store; `swarmctl agent mint-forge-token <agent>` checks
and, if needed, re-mints it.
- For **non-agents** (humans): creates the account and prints the token to
stdout, creating no state dir. Re-running after account already exists
re-mints the token and prints it again — safe for password resets.

View file

@ -8,6 +8,7 @@ This document contains the help content for the `swarmctl` command-line program.
* [`swarmctl agent`↴](#swarmctl-agent)
* [`swarmctl agent create`↴](#swarmctl-agent-create)
* [`swarmctl agent mint-identity`↴](#swarmctl-agent-mint-identity)
* [`swarmctl agent mint-forge-token`↴](#swarmctl-agent-mint-forge-token)
* [`swarmctl user`↴](#swarmctl-user)
* [`swarmctl user add`↴](#swarmctl-user-add)
* [`swarmctl user update`↴](#swarmctl-user-update)
@ -45,6 +46,7 @@ Manage agents across the swarm
* `create` — Queue creation of a new agent on a hive in this swarm
* `mint-identity` — Queue a re-mint of an existing agent's identity at the swarm's secret store
* `mint-forge-token` — Check one agent's forge token, and mint it if it's missing or stale
@ -104,6 +106,28 @@ Queues and returns, the same way `agent create` does — watch the swarm UI's jo
## `swarmctl agent mint-forge-token`
Check one agent's forge token, and mint it if it's missing or stale.
swarm-controller does this for every agent with a store identity at start and every five minutes; this is for when waiting isn't an option. It leaves a current token alone.
Queues and returns, the same way `agent create` does — watch the swarm UI's job view for the outcome.
**Usage:** `swarmctl agent mint-forge-token [OPTIONS] <NAME>`
###### **Arguments:**
* `<NAME>` — Name of an agent that already exists
###### **Options:**
* `--controller-socket <PATH>` — swarm-controller's unix socket.
Supplied by the nix module that installs this binary, from the same `socketPath` option the daemon binds; falls back to `SWARM_CONTROLLER_SOCKET`.
## `swarmctl user`
Manage subjects in the swarm's SSO provider

View file

@ -236,9 +236,12 @@ token policy bounds file reads; network isolation bounds network reach.
this directory at a default mode is world-readable, which isn't
hypothetical: `plugins/` and `.last-cleanup` already are.
- `$HYPERHIVE_STATE_DIR/forge-token` (= `/agents/<name>/state/forge-token`)
— written at mode `0600` and chowned to the per-agent uid:gid (see
`hive-c0re/src/forge/mod.rs`'s module doc for exactly where). nixbld users
can't read it.
— the token hive-c0re wrote at mode `0600`, chowned to the per-agent
uid:gid, before swarm-controller took over minting. Nothing writes it any
more; it stays as the fallback for an agent without a store identity.
nixbld users can't read it. The swarm-minted token lives under
`/run/hive-agent-forge-token/` (`0700` directory, `0400` file), outside
the state dir.
**Policy**: all credential files written to agent state directories MUST be mode
`0600` or stricter. Don't create world-readable secret files in agent state dirs.
@ -291,7 +294,7 @@ known operations; there is no arbitrary command pass-through:
| `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy |
| `DaemonReload` | `systemctl daemon-reload` |
| `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin <args>` |
| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir |
| `WriteAgentMatrixToken` | write `0600` credential file into agent state dir |
| `RestartMatrixDaemon` | `systemctl --machine=h-<name> restart hive-matrix-daemon.service` |
| `ControlInfraContainer` | `systemctl <action> container@<container>.service` — the `InfraContainer` enum is the allowlist, and serde rejects unknown names at the wire boundary (`hive-c0re` has no variant, so no request can name it) |
| `SyncAgentTmpfiles` | write `/etc/tmpfiles.d/hyperhive-agents.conf` for the agent set, then `systemd-tmpfiles --create` |