From abc942cff36f554db5a629081b183de827e6c69f Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 24 Sep 2026 16:43:54 +0200 Subject: [PATCH] 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 --- docs/getting-started/setup.md | 21 ++++++++++-------- docs/integrations/forge.md | 26 ++++++++++++++-------- docs/swarm/credentials.md | 39 ++++++++++++++++++++++----------- docs/tools/forge.md | 3 ++- docs/tools/hivectl-cli.md | 8 +++---- docs/tools/hivectl.md | 7 +++--- docs/tools/swarmctl-cli.md | 24 ++++++++++++++++++++ docs/trust-boundary/security.md | 11 ++++++---- 8 files changed, 96 insertions(+), 43 deletions(-) diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index 5a6c685e..62b8ab01 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -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. - **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/`) go through operator approval — agents can't unilaterally rebuild containers, by design. diff --git a/docs/integrations/forge.md b/docs/integrations/forge.md index 0979081e..0c598116 100644 --- a/docs/integrations/forge.md +++ b/docs/integrations/forge.md @@ -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 -`/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//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 `/forge-token`, +the file hive-c0re wrote before. hive-c0re no longer creates agent users or +mints agent tokens; the `hyperhive-` 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. -- `/forge-token` missing or empty (agent has no forge +- no token in `$HIVE_FORGE_TOKEN_FILE` or `/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 diff --git a/docs/swarm/credentials.md b/docs/swarm/credentials.md index 3a05fb70..2350bdcf 100644 --- a/docs/swarm/credentials.md +++ b/docs/swarm/credentials.md @@ -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. -| 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/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/` | `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 | @@ -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 +``` + +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 diff --git a/docs/tools/forge.md b/docs/tools/forge.md index d84360f7..108dfa2d 100644 --- a/docs/tools/forge.md +++ b/docs/tools/forge.md @@ -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. diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 18881495..cf635f73 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -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 `` +* `create-user` — Create or refresh a non-agent Forgejo account + token for `` * `reconcile-config` — Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/` main ## `hivectl forge create-user` -Create or refresh the Forgejo account + token for ``. +Create or refresh a non-agent Forgejo account + token for ``. -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 `). **Usage:** `hivectl forge create-user [OPTIONS] ` ###### **Arguments:** -* `` — Forgejo username. For agents: the container/agent name (`` in `h-`; manager uses the literal `manager`). For humans: any forgejo username — `mara`, `damocles`, etc +* `` — Forgejo username of a human/other account — `mara`, `damocles`, etc ###### **Options:** diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 409d462b..47e56416 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -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 `/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 ` 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. diff --git a/docs/tools/swarmctl-cli.md b/docs/tools/swarmctl-cli.md index 96d0eb20..1810d02d 100644 --- a/docs/tools/swarmctl-cli.md +++ b/docs/tools/swarmctl-cli.md @@ -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] ` + +###### **Arguments:** + +* `` — Name of an agent that already exists + +###### **Options:** + +* `--controller-socket ` — 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 diff --git a/docs/trust-boundary/security.md b/docs/trust-boundary/security.md index 78a98465..ca8fe746 100644 --- a/docs/trust-boundary/security.md +++ b/docs/trust-boundary/security.md @@ -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//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@.service.d/` drop-in on destroy | | `DaemonReload` | `systemctl daemon-reload` | | `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin ` | -| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir | +| `WriteAgentMatrixToken` | write `0600` credential file into agent state dir | | `RestartMatrixDaemon` | `systemctl --machine=h- restart hive-matrix-daemon.service` | | `ControlInfraContainer` | `systemctl 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` |