Compare commits
43 changed files with 329 additions and 1902 deletions
|
|
@ -24,30 +24,17 @@ operator has to place, and where.
|
||||||
|
|
||||||
### 1 · Forge
|
### 1 · Forge
|
||||||
|
|
||||||
hive-c0re no longer creates agent forge users or mints agent tokens.
|
|
||||||
swarm-controller does, for every agent that holds a store identity: a pass
|
|
||||||
at start and every five minutes creates the forge user if it's missing,
|
|
||||||
then mints the token into the swarm secret store, where the agent fetches
|
|
||||||
it under that identity. An agent created with `swarmctl agent create` gets
|
|
||||||
its identity then. Ruth doesn't: hive-c0re creates her on its own at
|
|
||||||
startup, so she needs her identity minted by hand, once.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On the swarm-controller host: give ruth her store identity. <hive> is the
|
# Provision (or refresh) ruth's own forge account — do this first. Ruth's
|
||||||
# name of the hive she runs on.
|
# bootstrap bypasses the normal spawn-approval flow ("Spawn sub-agents"
|
||||||
swarmctl agent mint-identity ruth --hive <hive>
|
# 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
|
||||||
|
|
||||||
# On ruth's hive: re-apply her container config, which is when hive-c0re
|
# Sub-agents spawned later (via the approval flow in "Spawn sub-agents")
|
||||||
# hands the new identity to the container.
|
# get their forge accounts auto-provisioned — nothing to run here for them.
|
||||||
hivectl agent ruth rebuild
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
|
||||||
about ten minutes. `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
|
|
||||||
for ruth at all.
|
|
||||||
|
|
||||||
Swarm SSO creates the human operator's own forge account instead of
|
Swarm SSO creates the human operator's own forge account instead of
|
||||||
a manual `hivectl` step — see _Swarm SSO_ below (`swarmctl user add`).
|
a manual `hivectl` step — see _Swarm SSO_ below (`swarmctl user add`).
|
||||||
|
|
||||||
|
|
@ -280,9 +267,7 @@ See [`tools/hivectl.md`](../tools/hivectl.md) for every `hivectl` verb.
|
||||||
<!-- vale write-good.Passive = NO -->
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- **No forge admin token is stored in any agent state dir.** Agents
|
- **No forge admin token is stored in any agent state dir.** Agents
|
||||||
hold a regular agent token, fetched from the swarm secret store into
|
hold a regular agent token in their `forge-token` file; sensitive
|
||||||
`/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.
|
creds (the core token) live on the host.
|
||||||
- All config changes (forge PRs on `agent-configs/<name>`) go through
|
- All config changes (forge PRs on `agent-configs/<name>`) go through
|
||||||
operator approval — agents can't unilaterally rebuild containers, by design.
|
operator approval — agents can't unilaterally rebuild containers, by design.
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,7 @@ each agent on relevant activity.
|
||||||
|
|
||||||
Two scope sets live in `hive-c0re::forge`:
|
Two scope sets live in `hive-c0re::forge`:
|
||||||
|
|
||||||
**`TOKEN_SCOPES`** (per-agent tokens, and `hivectl forge create-user`
|
**`TOKEN_SCOPES`** (per-agent tokens):
|
||||||
accounts). swarm-controller mints agent tokens with a byte-identical copy,
|
|
||||||
`forge::agent_token::AGENT_TOKEN_SCOPES`, pinned by a test:
|
|
||||||
|
|
||||||
| Scope | Why |
|
| Scope | Why |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------------------- |
|
| -------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||||
|
|
@ -48,20 +46,13 @@ hive-c0re to re-mint with the new scopes.
|
||||||
|
|
||||||
## Per-agent forge accounts
|
## Per-agent forge accounts
|
||||||
|
|
||||||
Each agent gets its own Forgejo user and access token. swarm-controller
|
Each agent gets its own Forgejo user + access token, provisioned at
|
||||||
creates the user when it creates the agent, and mints one token named
|
boot by `hive-c0re::forge`. The provisioning flow is idempotent:
|
||||||
`swarm-agent` with the admin API into the swarm secret store at
|
`hive-c0re::forge` reuses existing accounts + tokens, so container destroy/recreate
|
||||||
`swarm/agents/<agent>/forge-token`. A pass at start and every five minutes
|
doesn't lose forge identity. It writes the token to
|
||||||
covers every agent with a store identity: it creates a missing forge user and
|
`<state>/forge-token` (one line, no trailing newline) inside the
|
||||||
re-mints any token that's missing or no longer matches the forge;
|
agent container so `hive-forge` CLI + `forge_notify` poller can
|
||||||
a rotation deletes the old `swarm-agent` token first, so each agent holds at
|
read it without touching c0re's host-side credential store.
|
||||||
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:
|
Two things live in the `agent-configs` Forgejo organization:
|
||||||
|
|
||||||
|
|
@ -171,7 +162,7 @@ The poller starts disabled and stays that way for any of:
|
||||||
|
|
||||||
- `HIVE_FORGE_URL` not set (no forge configured for this hive), or
|
- `HIVE_FORGE_URL` not set (no forge configured for this hive), or
|
||||||
not parseable as a URL.
|
not parseable as a URL.
|
||||||
- no token in `$HIVE_FORGE_TOKEN_FILE` or `<state>/forge-token` (agent has no forge
|
- `<state>/forge-token` missing or empty (agent has no forge
|
||||||
account — pre-provisioning or destroy-without-purge race).
|
account — pre-provisioning or destroy-without-purge race).
|
||||||
- Initial client construction fails (the typed `forgejo-api` client
|
- Initial client construction fails (the typed `forgejo-api` client
|
||||||
for the API calls, or the plain reqwest client kept for the
|
for the API calls, or the plain reqwest client kept for the
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ wiring is runtime:
|
||||||
- the `hyperhive-isolated-dns` oneshot (`nix/agent-modules/network.nix`), gated on that
|
- the `hyperhive-isolated-dns` oneshot (`nix/agent-modules/network.nix`), gated on that
|
||||||
marker, rewrites `/etc/resolv.conf` to `nameserver <gateway-ip>` at boot.
|
marker, rewrites `/etc/resolv.conf` to `nameserver <gateway-ip>` at boot.
|
||||||
It's ordered `before` the harness (`hive-ag3nt`), the matrix daemon, and
|
It's ordered `before` the harness (`hive-ag3nt`), the matrix daemon, and
|
||||||
the forge-token fetch so the resolver is correct before the first DNS lookup.
|
`tea-login` so the resolver is correct before the first DNS lookup.
|
||||||
|
|
||||||
**Why isolation is safe**: hive-c0re's control-plane sockets are unix
|
**Why isolation is safe**: hive-c0re's control-plane sockets are unix
|
||||||
domain sockets bind-mounted into containers, not network listeners — see
|
domain sockets bind-mounted into containers, not network listeners — see
|
||||||
|
|
|
||||||
|
|
@ -502,9 +502,13 @@ sparingly) or build just the suspect check, for example `nix build
|
||||||
## Best-effort oneshot services
|
## Best-effort oneshot services
|
||||||
|
|
||||||
The harness ships a family of one-shot systemd services that
|
The harness ships a family of one-shot systemd services that
|
||||||
configure agent-side surfaces from values delivered to the agent at
|
configure agent-side surfaces from values hive-c0re writes into
|
||||||
provisioning time:
|
the state dir at provisioning time:
|
||||||
|
|
||||||
|
- `tea-login` — writes `~/.config/tea/config.yml` from the
|
||||||
|
`forge-token` written by `hive-c0re::forge::ensure_user_for`,
|
||||||
|
so `tea repos create` / `tea pulls create` work without
|
||||||
|
interactive prompts.
|
||||||
- `forge-avatar-sync` — uploads `services.hyperhive.agent.icon` SVG to the
|
- `forge-avatar-sync` — uploads `services.hyperhive.agent.icon` SVG to the
|
||||||
agent's Forgejo profile, so the icon shows up on commits / PRs /
|
agent's Forgejo profile, so the icon shows up on commits / PRs /
|
||||||
issue comments.
|
issue comments.
|
||||||
|
|
@ -534,10 +538,13 @@ Shape contract — every one of these:
|
||||||
by the `.path` watchers that re-fire on token appearance (see
|
by the `.path` watchers that re-fire on token appearance (see
|
||||||
`docs/agent-lifecycle/persistence.md::Matrix per-agent daemon`).
|
`docs/agent-lifecycle/persistence.md::Matrix per-agent daemon`).
|
||||||
|
|
||||||
The service stays root-owned so the bootstrap ordering doesn't need a
|
The artefact lives under the agent user's home where applicable
|
||||||
user-existence check before each fire.
|
(`~/.config/tea/config.yml`) and is chown'd to that user, but the
|
||||||
|
service itself stays root-owned so the bootstrap ordering doesn't
|
||||||
|
need a user-existence check before each fire.
|
||||||
|
|
||||||
This pattern keeps the rebuild path resilient: any failure inside
|
This pattern keeps the rebuild path resilient: any failure inside
|
||||||
these services degrades the corresponding surface (no avatar) but never blocks the container from coming up. The
|
these services degrades the corresponding surface (no tea config,
|
||||||
|
no avatar) but never blocks the container from coming up. The
|
||||||
operator notices through `journalctl -u <unit>` rather than a
|
operator notices through `journalctl -u <unit>` rather than a
|
||||||
broken switch-to-configuration.
|
broken switch-to-configuration.
|
||||||
|
|
|
||||||
|
|
@ -448,7 +448,7 @@ connects to the compositor at `127.0.0.1:<vnc_port>`.
|
||||||
must never block on weston signalling readiness. A misconfigured
|
must never block on weston signalling readiness. A misconfigured
|
||||||
weston degrades to a `Restart=on-failure` loop visible in
|
weston degrades to a `Restart=on-failure` loop visible in
|
||||||
`journalctl`, it doesn't abort the `nixos-container update`.
|
`journalctl`, it doesn't abort the `nixos-container update`.
|
||||||
Same reasoning as the best-effort oneshots in `docs/process/conventions.md`.
|
Same reasoning as the `tea-login` unit in `nix/agent-modules/forge.nix`.
|
||||||
- **`[core] idle-time=0`**: disables weston's 300-second idle
|
- **`[core] idle-time=0`**: disables weston's 300-second idle
|
||||||
timeout. Without it the VNC desktop fades to black and
|
timeout. Without it the VNC desktop fades to black and
|
||||||
desktop-shell shows its click-to-unlock screen — useless for an
|
desktop-shell shows its click-to-unlock screen — useless for an
|
||||||
|
|
|
||||||
|
|
@ -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
|
process reads a secret on another principal's behalf: the principal that
|
||||||
needs a value is the principal that authenticates for it.
|
needs a value is the principal that authenticates for it.
|
||||||
|
|
||||||
One per-agent credential file — the github token — sits outside this page:
|
Two per-agent credential files — the forge token and the github token — sit
|
||||||
it's operator-supplied and never passes through the store, so the table
|
outside this page: they're operator-supplied and never pass through the
|
||||||
below doesn't govern it.
|
store, so the table below doesn't govern them.
|
||||||
|
|
||||||
**Per secret, the target specifies minter, reader, and renewal strategy.**
|
**Per secret, the target specifies minter, reader, and renewal strategy.**
|
||||||
Those three are the contract, and the reader is a process pulling a store
|
Those three are the contract, and the reader is a process pulling a store
|
||||||
|
|
@ -54,17 +54,16 @@ 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/<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>/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/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/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>/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/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 |
|
||||||
| `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 |
|
||||||
| _(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 -->
|
||||||
|
|
||||||
|
|
@ -96,19 +95,6 @@ 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
|
its queue connection. The certificate half isn't: the agent gets a fresh leaf
|
||||||
and picks it up on its next boot.
|
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. For any whose
|
|
||||||
stored token is missing or stale it creates the forge user if there isn't one,
|
|
||||||
then mints the token. 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
|
⚠️ **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
|
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
|
whose credential is absent doesn't start — that's deliberate, and it's what
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ markdown-docs > docs/tools/forge-cli.md`.
|
||||||
|
|
||||||
## Credentials and repo defaults
|
## Credentials and repo defaults
|
||||||
|
|
||||||
- Credentials: `$HIVE_FORGE_TOKEN_FILE` (the token the agent fetched from
|
- Credentials: `$HYPERHIVE_STATE_DIR/forge-token`
|
||||||
the swarm secret store), else `$HYPERHIVE_STATE_DIR/forge-token`
|
|
||||||
- Active repo resolves, highest priority first: global `-r/--repo` flag
|
- Active repo resolves, highest priority first: global `-r/--repo` flag
|
||||||
> the `origin` remote of the cwd's git checkout > `$HIVE_FORGE_REPO`
|
> the `origin` remote of the cwd's git checkout > `$HIVE_FORGE_REPO`
|
||||||
(last-resort override, unset by default) > a hard error.
|
(last-resort override, unset by default) > a hard error.
|
||||||
|
|
|
||||||
|
|
@ -103,22 +103,22 @@ Manual entry point to the same idempotent provisioning c0re runs at boot — for
|
||||||
|
|
||||||
###### **Subcommands:**
|
###### **Subcommands:**
|
||||||
|
|
||||||
* `create-user` — Create or refresh a non-agent Forgejo account + token for `<name>`
|
* `create-user` — Create or refresh the 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
|
* `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`
|
## `hivectl forge create-user`
|
||||||
|
|
||||||
Create or refresh a non-agent Forgejo account + token for `<name>`.
|
Create or refresh the Forgejo account + token for `<name>`.
|
||||||
|
|
||||||
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>`).
|
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).
|
||||||
|
|
||||||
**Usage:** `hivectl forge create-user [OPTIONS] <NAME>`
|
**Usage:** `hivectl forge create-user [OPTIONS] <NAME>`
|
||||||
|
|
||||||
###### **Arguments:**
|
###### **Arguments:**
|
||||||
|
|
||||||
* `<NAME>` — Forgejo username of a human/other account — `mara`, `damocles`, etc
|
* `<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
|
||||||
|
|
||||||
###### **Options:**
|
###### **Options:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ at boot. Useful for recovery, ad-hoc re-provisioning, or fixing a single
|
||||||
agent without bouncing the daemon.
|
agent without bouncing the daemon.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hivectl forge create-user iris # refused: `iris` is an agent; use `swarmctl agent mint-forge-token iris`
|
hivectl forge create-user iris # provision (or refresh) forge account for agent `iris`
|
||||||
hivectl forge create-user mara # create forge account for a human user; prints token to stdout
|
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 hunter2 # set a web-login password
|
||||||
hivectl forge create-user mara --password-stdin # read password from stdin (safer for scripting)
|
hivectl forge create-user mara --password-stdin # read password from stdin (safer for scripting)
|
||||||
|
|
@ -40,9 +40,8 @@ hivectl forge reconcile-config iris --verbose # include the full diff, not
|
||||||
```
|
```
|
||||||
|
|
||||||
- For **agents** (name has a state dir under `/var/lib/hyperhive/agents/`):
|
- For **agents** (name has a state dir under `/var/lib/hyperhive/agents/`):
|
||||||
`create-user` refuses. swarm-controller mints an agent's token into
|
`create-user` persists the token to `<state>/forge-token`. Re-running refreshes the
|
||||||
the swarm secret store; `swarmctl agent mint-forge-token <agent>` checks
|
token (idempotent — scope always matches current `TOKEN_SCOPES`).
|
||||||
and, if needed, re-mints it.
|
|
||||||
- For **non-agents** (humans): creates the account and prints the token to
|
- For **non-agents** (humans): creates the account and prints the token to
|
||||||
stdout, creating no state dir. Re-running after account already exists
|
stdout, creating no state dir. Re-running after account already exists
|
||||||
re-mints the token and prints it again — safe for password resets.
|
re-mints the token and prints it again — safe for password resets.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ This document contains the help content for the `swarmctl` command-line program.
|
||||||
* [`swarmctl agent`↴](#swarmctl-agent)
|
* [`swarmctl agent`↴](#swarmctl-agent)
|
||||||
* [`swarmctl agent create`↴](#swarmctl-agent-create)
|
* [`swarmctl agent create`↴](#swarmctl-agent-create)
|
||||||
* [`swarmctl agent mint-identity`↴](#swarmctl-agent-mint-identity)
|
* [`swarmctl agent mint-identity`↴](#swarmctl-agent-mint-identity)
|
||||||
* [`swarmctl agent mint-forge-token`↴](#swarmctl-agent-mint-forge-token)
|
|
||||||
* [`swarmctl user`↴](#swarmctl-user)
|
* [`swarmctl user`↴](#swarmctl-user)
|
||||||
* [`swarmctl user add`↴](#swarmctl-user-add)
|
* [`swarmctl user add`↴](#swarmctl-user-add)
|
||||||
* [`swarmctl user update`↴](#swarmctl-user-update)
|
* [`swarmctl user update`↴](#swarmctl-user-update)
|
||||||
|
|
@ -46,7 +45,6 @@ Manage agents across the swarm
|
||||||
|
|
||||||
* `create` — Queue creation of a new agent on a hive in this 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-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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -106,28 +104,6 @@ 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`
|
## `swarmctl user`
|
||||||
|
|
||||||
Manage subjects in the swarm's SSO provider
|
Manage subjects in the swarm's SSO provider
|
||||||
|
|
|
||||||
|
|
@ -236,12 +236,9 @@ token policy bounds file reads; network isolation bounds network reach.
|
||||||
this directory at a default mode is world-readable, which isn't
|
this directory at a default mode is world-readable, which isn't
|
||||||
hypothetical: `plugins/` and `.last-cleanup` already are.
|
hypothetical: `plugins/` and `.last-cleanup` already are.
|
||||||
- `$HYPERHIVE_STATE_DIR/forge-token` (= `/agents/<name>/state/forge-token`)
|
- `$HYPERHIVE_STATE_DIR/forge-token` (= `/agents/<name>/state/forge-token`)
|
||||||
— the token hive-c0re wrote at mode `0600`, chowned to the per-agent
|
— written at mode `0600` and chowned to the per-agent uid:gid (see
|
||||||
uid:gid, before swarm-controller took over minting. Nothing writes it any
|
`hive-c0re/src/forge/mod.rs`'s module doc for exactly where). nixbld users
|
||||||
more; it stays as the fallback for an agent without a store identity.
|
can't read it.
|
||||||
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
|
**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.
|
`0600` or stricter. Don't create world-readable secret files in agent state dirs.
|
||||||
|
|
@ -294,7 +291,7 @@ known operations; there is no arbitrary command pass-through:
|
||||||
| `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy |
|
| `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy |
|
||||||
| `DaemonReload` | `systemctl daemon-reload` |
|
| `DaemonReload` | `systemctl daemon-reload` |
|
||||||
| `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin <args>` |
|
| `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin <args>` |
|
||||||
| `WriteAgentMatrixToken` | write `0600` credential file into agent state dir |
|
| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir |
|
||||||
| `RestartMatrixDaemon` | `systemctl --machine=h-<name> restart hive-matrix-daemon.service` |
|
| `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) |
|
| `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` |
|
| `SyncAgentTmpfiles` | write `/etc/tmpfiles.d/hyperhive-agents.conf` for the agent set, then `systemd-tmpfiles --create` |
|
||||||
|
|
|
||||||
|
|
@ -137,9 +137,11 @@ services.hyperhive.agent.forge.url = "http://forge.example:3000"; # default: nu
|
||||||
services.hyperhive.agent.matrix.url = "https://matrix.example"; # default: null
|
services.hyperhive.agent.matrix.url = "https://matrix.example"; # default: null
|
||||||
```
|
```
|
||||||
|
|
||||||
**`services.hyperhive.agent.forge.url`** — base URL of the Forgejo instance. It scopes
|
**`services.hyperhive.agent.forge.url`** — base URL of the Forgejo instance. Used by
|
||||||
the git credential helper to this forge and is where `forge-avatar-sync`
|
a one-shot boot unit (`tea-login`) that writes `~/.config/tea/config.yml`
|
||||||
uploads the agent's icon. Override when the agent should connect to a
|
directly from the agent's `forge-token`, so `tea` and `hive-forge`
|
||||||
|
work without an interactive auth step. The unit is a no-op when
|
||||||
|
`forge-token` is absent. Override when the agent should connect to a
|
||||||
Forgejo on a different host or port (for example a swarm peer's forge).
|
Forgejo on a different host or port (for example a swarm peer's forge).
|
||||||
Validated: must be an `http://` or `https://` URL, or `null`.
|
Validated: must be an `http://` or `https://` URL, or `null`.
|
||||||
|
|
||||||
|
|
@ -147,7 +149,7 @@ Validated: must be an `http://` or `https://` URL, or `null`.
|
||||||
loopback default would only ever be correct when the forge shares the
|
loopback default would only ever be correct when the forge shares the
|
||||||
agent's network namespace, and inside a container `localhost` is the
|
agent's network namespace, and inside a container `localhost` is the
|
||||||
agent itself, so the default was a value that built fine and then talked
|
agent itself, so the default was a value that built fine and then talked
|
||||||
to the wrong machine. With `null` the credential helper and `forge-avatar-sync`
|
to the wrong machine. With `null` the `tea-login` and `forge-avatar-sync`
|
||||||
units aren't generated at all: an absent integration rather than a
|
units aren't generated at all: an absent integration rather than a
|
||||||
misdirected one. You don't normally set this — hive-c0re renders the
|
misdirected one. You don't normally set this — hive-c0re renders the
|
||||||
host's real forge URL into every agent, and refuses to write a meta
|
host's real forge URL into every agent, and refuses to write a meta
|
||||||
|
|
|
||||||
|
|
@ -351,12 +351,7 @@ fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either copy of the forge token: the one fetched from the swarm secret
|
if crate::paths::state_dir().join("forge-token").is_file() {
|
||||||
// store (`HIVE_FORGE_TOKEN_FILE`), or the state-dir file the hive used
|
|
||||||
// to write.
|
|
||||||
let fetched_token = std::env::var_os("HIVE_FORGE_TOKEN_FILE")
|
|
||||||
.is_some_and(|p| std::path::Path::new(&p).is_file());
|
|
||||||
if fetched_token || crate::paths::state_dir().join("forge-token").is_file() {
|
|
||||||
links.push(AgentLink {
|
links.push(AgentLink {
|
||||||
url: format!("/{label}"),
|
url: format!("/{label}"),
|
||||||
icon: "🔨".to_owned(),
|
icon: "🔨".to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -527,7 +527,7 @@ pub(crate) async fn resolve_approval_dag(
|
||||||
let mut terminal_tag = None;
|
let mut terminal_tag = None;
|
||||||
match approval.kind {
|
match approval.kind {
|
||||||
ApprovalKind::Spawn => {
|
ApprovalKind::Spawn => {
|
||||||
// Post-spawn forge bookkeeping (config repo mirror, meta
|
// Post-spawn forge bookkeeping (user, config repo mirror, meta
|
||||||
// access) — warn-only, then the resolution events + a rescan so
|
// access) — warn-only, then the resolution events + a rescan so
|
||||||
// the dashboard reflects the post-spawn state either way.
|
// the dashboard reflects the post-spawn state either way.
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
|
|
@ -605,10 +605,13 @@ fn fetch_approval_for_worker(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forge bookkeeping run once after the very first container spawn:
|
/// Forge bookkeeping run once after the very first container spawn:
|
||||||
/// mirror the applied repo and grant read access to core/meta. The
|
/// create the per-agent forge user, mirror the applied repo, and grant
|
||||||
/// agent's forge user and token are swarm-controller's, not this
|
/// read access to core/meta. Also rescans containers so the dashboard
|
||||||
/// hive's. Also rescans containers so the dashboard reflects the post-spawn state.
|
/// reflects the post-spawn state.
|
||||||
async fn forge_after_first_spawn(coord: &Arc<Coordinator>, agent: &str) {
|
async fn forge_after_first_spawn(coord: &Arc<Coordinator>, agent: &str) {
|
||||||
|
if let Err(e) = crate::forge::ensure_user_for(agent).await {
|
||||||
|
tracing::warn!(%agent, error = ?e, "forge: ensure_user after first spawn failed");
|
||||||
|
}
|
||||||
if let Err(e) = crate::forge::ensure_config_repo(agent).await {
|
if let Err(e) = crate::forge::ensure_config_repo(agent).await {
|
||||||
tracing::warn!(%agent, error = ?e, "forge: ensure_config_repo after first spawn failed");
|
tracing::warn!(%agent, error = ?e, "forge: ensure_config_repo after first spawn failed");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Optional Forgejo wiring — per-agent account alignment,
|
//! Optional Forgejo wiring — per-agent user + token provisioning,
|
||||||
//! config-repo mirroring, meta read-access grants. Also seeds
|
//! config-repo mirroring, meta read-access grants. Also seeds
|
||||||
//! `internal/docs` — a private repo every agent gets read-only
|
//! `internal/docs` — a private repo every agent gets read-only
|
||||||
//! collaborator access to for operator-curated shared content.
|
//! collaborator access to for operator-curated shared content.
|
||||||
|
|
@ -21,7 +21,7 @@ pub use repos::{
|
||||||
ensure_meta_remote, ensure_repo, ensure_shared_docs_repo, fast_forward_applied_main,
|
ensure_meta_remote, ensure_repo, ensure_shared_docs_repo, fast_forward_applied_main,
|
||||||
fetch_config_main_into_applied, meta_read_access, push_config, push_meta, shared_docs_access,
|
fetch_config_main_into_applied, meta_read_access, push_config, push_meta, shared_docs_access,
|
||||||
};
|
};
|
||||||
pub use users::{core_token, provision_user_token};
|
pub use users::{core_token, ensure_user_for, provision_user_token};
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
@ -212,9 +212,9 @@ pub(crate) fn api(token: &str) -> Result<Forgejo> {
|
||||||
Forgejo::new(Auth::Token(token), url).context("build forgejo api client")
|
Forgejo::new(Auth::Token(token), url).context("build forgejo api client")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-agent forge sync: align the agent's forge account (email,
|
/// Per-agent forge sync: ensure the agent has a forgejo user + token,
|
||||||
/// repo-creation lockdown), mirror its config repo, grant read access
|
/// a mirrored config repo, read access to `core/meta`, and the `meta`
|
||||||
/// to `core/meta`, and wire the `meta` remote in its proposed repo. All operations are idempotent; failures
|
/// remote in its proposed repo. All operations are idempotent; failures
|
||||||
/// are logged as warnings but don't abort the caller.
|
/// are logged as warnings but don't abort the caller.
|
||||||
///
|
///
|
||||||
/// `core_token` is `core_token()` — passed in so callers that already
|
/// `core_token` is `core_token()` — passed in so callers that already
|
||||||
|
|
@ -228,10 +228,11 @@ pub(crate) fn api(token: &str) -> Result<Forgejo> {
|
||||||
/// dashboard warning (see [`ensure_all`]); the rebuild path ignores it and
|
/// dashboard warning (see [`ensure_all`]); the rebuild path ignores it and
|
||||||
/// relies on the journal `warn!` lines alone (a rebuild is its own retry).
|
/// relies on the journal `warn!` lines alone (a rebuild is its own retry).
|
||||||
pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool {
|
pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool {
|
||||||
// The agent's forge user and token are swarm-controller's now
|
|
||||||
// (`MintAgentForgeToken`); this hive no longer creates or re-mints
|
|
||||||
// either.
|
|
||||||
let mut ok = true;
|
let mut ok = true;
|
||||||
|
if let Err(e) = ensure_user_for(name).await {
|
||||||
|
tracing::warn!(%name, error = ?e, "forge: ensure_user failed");
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
// Align email to match the git user.email set by meta::render_flake
|
// Align email to match the git user.email set by meta::render_flake
|
||||||
// so commits link to the agent's Forgejo profile. Best-effort;
|
// so commits link to the agent's Forgejo profile. Best-effort;
|
||||||
// also patches up agents created before this fix (old @hive.local).
|
// also patches up agents created before this fix (old @hive.local).
|
||||||
|
|
|
||||||
|
|
@ -313,8 +313,8 @@ pub(super) async fn ensure_repo_creation_disabled(name: &str) {
|
||||||
/// a monotonic clock so re-issuing doesn't collide with an existing
|
/// a monotonic clock so re-issuing doesn't collide with an existing
|
||||||
/// token of the same name in the DB. `scopes` is the scope string
|
/// token of the same name in the DB. `scopes` is the scope string
|
||||||
/// passed to `forgejo admin user generate-access-token --scopes`;
|
/// passed to `forgejo admin user generate-access-token --scopes`;
|
||||||
/// use `TOKEN_SCOPES` for `hivectl forge create-user` accounts and
|
/// use `TOKEN_SCOPES` for agents, `CORE_TOKEN_SCOPES` for the
|
||||||
/// `CORE_TOKEN_SCOPES` for the bootstrap `core` user.
|
/// bootstrap `core` user.
|
||||||
async fn mint_token(name: &str, scopes: &str) -> Result<String> {
|
async fn mint_token(name: &str, scopes: &str) -> Result<String> {
|
||||||
let token_name = format!(
|
let token_name = format!(
|
||||||
"{TOKEN_NAME_PREFIX}-{}",
|
"{TOKEN_NAME_PREFIX}-{}",
|
||||||
|
|
@ -349,6 +349,16 @@ async fn mint_token(name: &str, scopes: &str) -> Result<String> {
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mint a fresh Forgejo access token for an agent and write it to the
|
||||||
|
/// agent's state dir via hive-priv. hive-c0re runs unprivileged and
|
||||||
|
/// cannot write to agent-owned (0755) state directories directly.
|
||||||
|
async fn mint_and_persist_agent_token(name: &str) -> Result<()> {
|
||||||
|
let token = mint_token(name, TOKEN_SCOPES).await?;
|
||||||
|
crate::priv_client::write_agent_forge_token(name, &token)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("write forge-token for {name} via hive-priv"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Mint a fresh Forgejo access token for the `core` admin user and
|
/// Mint a fresh Forgejo access token for the `core` admin user and
|
||||||
/// write it directly to `path`. Unlike agent tokens this path is owned
|
/// write it directly to `path`. Unlike agent tokens this path is owned
|
||||||
/// by hive-c0re itself (under `/var/lib/hyperhive/`), so a direct
|
/// by hive-c0re itself (under `/var/lib/hyperhive/`), so a direct
|
||||||
|
|
@ -366,11 +376,24 @@ async fn mint_and_persist_core_token(path: &Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure `name` has a forgejo user + token file. Always re-mints the
|
||||||
|
/// token so the on-disk file always reflects the current `TOKEN_SCOPES`.
|
||||||
|
/// Safe to call on every spawn and on every hive-c0re startup.
|
||||||
|
pub async fn ensure_user_for(name: &str) -> Result<()> {
|
||||||
|
if !is_present().await {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
ensure_user_exists(name, false, None).await?;
|
||||||
|
ensure_user_email(name).await;
|
||||||
|
mint_and_persist_agent_token(name).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Provision a forgejo user for `name` and return the freshly-minted
|
/// Provision a forgejo user for `name` and return the freshly-minted
|
||||||
/// token, which is **not** persisted to disk — the caller is responsible
|
/// token. Unlike [`ensure_user_for`], the token is **not** persisted to
|
||||||
/// for storing it. Used by `hivectl forge create-user` for human
|
/// disk — the caller is responsible for storing it. Used by `hivectl
|
||||||
/// (non-agent) accounts. Agent tokens are minted by swarm-controller
|
/// forge create-user` for human (non-agent) accounts so we don't create
|
||||||
/// (`swarm-controller/src/forge/agent_token.rs`), not here.
|
/// stray `/var/lib/hyperhive/agents/<name>/` directories for users that
|
||||||
|
/// aren't agents.
|
||||||
///
|
///
|
||||||
/// `password` picks the account password. `None` keeps the existing
|
/// `password` picks the account password. `None` keeps the existing
|
||||||
/// random-throwaway shape (caller doesn't need web UI access — token
|
/// random-throwaway shape (caller doesn't need web UI access — token
|
||||||
|
|
|
||||||
|
|
@ -642,7 +642,7 @@ const FORWARDED_VARS: &[&str] = &[
|
||||||
/// Map of forwarded env var -> the agent option carrying the same value.
|
/// Map of forwarded env var -> the agent option carrying the same value.
|
||||||
///
|
///
|
||||||
/// Both exist because they're consumed at different times: the option is baked
|
/// Both exist because they're consumed at different times: the option is baked
|
||||||
/// into scripts at build time (forge-avatar-sync bakes `FORGE_URL` from it), the env
|
/// into scripts at build time (tea-login bakes `FORGE_URL` from it), the env
|
||||||
/// var is read at runtime. Setting only one leaves the other on its default,
|
/// var is read at runtime. Setting only one leaves the other on its default,
|
||||||
/// which is how a hive ends up with two disagreeing answers for one value.
|
/// which is how a hive ends up with two disagreeing answers for one value.
|
||||||
///
|
///
|
||||||
|
|
@ -1242,7 +1242,7 @@ where
|
||||||
# resolve the agent's durable state dir without hard-coding it.
|
# resolve the agent's durable state dir without hard-coding it.
|
||||||
# `environment.variables` only writes /etc/environment (login
|
# `environment.variables` only writes /etc/environment (login
|
||||||
# shells); `systemd.globalEnvironment` is the analogue for
|
# shells); `systemd.globalEnvironment` is the analogue for
|
||||||
# systemd units so forge-avatar-sync /
|
# systemd units so tea-login / forge-avatar-sync /
|
||||||
# hive-matrix-daemon etc. can read `$HYPERHIVE_STATE_DIR`
|
# hive-matrix-daemon etc. can read `$HYPERHIVE_STATE_DIR`
|
||||||
# without each service having to redeclare it.
|
# without each service having to redeclare it.
|
||||||
environment.variables = {
|
environment.variables = {
|
||||||
|
|
@ -1259,7 +1259,7 @@ where
|
||||||
// Forwarded vars (HIVE_FORGE_URL etc.) also go into globalEnvironment,
|
// Forwarded vars (HIVE_FORGE_URL etc.) also go into globalEnvironment,
|
||||||
// not just the harness service env below, so EVERY service + shell in
|
// not just the harness service env below, so EVERY service + shell in
|
||||||
// the container inherits them — crucially the bash-task runner (where
|
// the container inherits them — crucially the bash-task runner (where
|
||||||
// `hive-forge` + `git` actually run), plus the matrix daemon, forge-avatar-sync
|
// `hive-forge` + `git` actually run), plus the matrix daemon, tea-login
|
||||||
// and interactive shells. Scoped to the harness service alone they were
|
// and interactive shells. Scoped to the harness service alone they were
|
||||||
// invisible to bash tasks: harmless in shared netns (the localhost
|
// invisible to bash tasks: harmless in shared netns (the localhost
|
||||||
// default works) but broken under isolation, where the in-cluster
|
// default works) but broken under isolation, where the in-cluster
|
||||||
|
|
@ -2081,7 +2081,7 @@ mod tests {
|
||||||
fn render_flake_sets_service_url_options_from_forwarded_env() {
|
fn render_flake_sets_service_url_options_from_forwarded_env() {
|
||||||
// The forwarded env vars must ALSO become option assignments, because
|
// The forwarded env vars must ALSO become option assignments, because
|
||||||
// the two are consumed at different times: the option is baked into
|
// the two are consumed at different times: the option is baked into
|
||||||
// scripts at build time (forge-avatar-sync's FORGE_URL), the env var is read at
|
// scripts at build time (tea-login's FORGE_URL), the env var is read at
|
||||||
// runtime. Emitting only the env var leaves the option on its default,
|
// runtime. Emitting only the env var leaves the option on its default,
|
||||||
// which is how a hive ends up with two disagreeing answers for the same
|
// which is how a hive ends up with two disagreeing answers for the same
|
||||||
// URL.
|
// URL.
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,18 @@ pub async fn set_agent_paused(agent_name: &str, paused: bool) -> Result<()> {
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write the Forgejo access token for `agent_name` to
|
||||||
|
/// `<agent_state_root>/<agent_name>/state/forge-token` via hive-priv
|
||||||
|
/// (running as root). The file is written 0600 and chowned to the agent
|
||||||
|
/// user so it is readable from inside the agent container.
|
||||||
|
pub async fn write_agent_forge_token(agent_name: &str, token: &str) -> Result<()> {
|
||||||
|
ok(call(&PrivRequest::WriteAgentForgeToken {
|
||||||
|
agent_name: agent_name.to_owned(),
|
||||||
|
token: token.to_owned(),
|
||||||
|
})
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Write a Matrix access token for `agent_name` via hive-priv (running as
|
/// Write a Matrix access token for `agent_name` via hive-priv (running as
|
||||||
/// root). `account: None` writes the hive-internal
|
/// root). `account: None` writes the hive-internal
|
||||||
/// `<state>/matrix-token`; `account: Some(name)` writes
|
/// `<state>/matrix-token`; `account: Some(name)` writes
|
||||||
|
|
|
||||||
|
|
@ -600,30 +600,36 @@ async fn handle_forge_create_user(
|
||||||
"hive-forge container not running — wait for hive-c0re to start it before provisioning forge users"
|
"hive-forge container not running — wait for hive-c0re to start it before provisioning forge users"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let mut out = Vec::new();
|
||||||
if agent_exists(name)? {
|
if agent_exists(name)? {
|
||||||
// An agent's forge user and token are swarm-controller's: it mints
|
if password.is_some() {
|
||||||
// the token into the swarm secret store and the agent fetches it
|
// Agents authenticate by API token, never by password — refuse
|
||||||
// from there. A hive-minted token would be one the swarm neither
|
// rather than silently dropping a supplied one.
|
||||||
// tracks nor rotates.
|
anyhow::bail!(
|
||||||
anyhow::bail!(
|
"forge create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via API token"
|
||||||
"forge create-user: '{name}' is an agent; its forge token is minted by \
|
);
|
||||||
swarm-controller — run `swarmctl agent mint-forge-token {name}` on the \
|
}
|
||||||
controller host"
|
crate::forge::ensure_user_for(name.as_str())
|
||||||
);
|
.await
|
||||||
}
|
.with_context(|| format!("forge create-user {name}"))?;
|
||||||
let token = crate::forge::provision_user_token(name.as_str(), password)
|
let path = Coordinator::agent_notes_dir(name).join("forge-token");
|
||||||
.await
|
out.push(format!("forge: provisioned agent user '{name}'"));
|
||||||
.with_context(|| format!("forge create-user {name}"))?;
|
out.push(format!("token persisted at: {}", path.display()));
|
||||||
let mut out = vec![
|
|
||||||
format!("forge: provisioned user '{name}' (not an agent — token not persisted)"),
|
|
||||||
format!("token: {token}"),
|
|
||||||
];
|
|
||||||
if password.is_some() {
|
|
||||||
out.push("password: set as supplied — use it to log into the forge web UI".to_owned());
|
|
||||||
} else {
|
} else {
|
||||||
out.push(
|
let token = crate::forge::provision_user_token(name.as_str(), password)
|
||||||
"password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(),
|
.await
|
||||||
);
|
.with_context(|| format!("forge create-user {name}"))?;
|
||||||
|
out.push(format!(
|
||||||
|
"forge: provisioned user '{name}' (not an agent — token not persisted)"
|
||||||
|
));
|
||||||
|
out.push(format!("token: {token}"));
|
||||||
|
if password.is_some() {
|
||||||
|
out.push("password: set as supplied — use it to log into the forge web UI".to_owned());
|
||||||
|
} else {
|
||||||
|
out.push(
|
||||||
|
"password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(HostResponse::messages(out))
|
Ok(HostResponse::messages(out))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,17 +37,23 @@ async fn forgejo_loop(state_dir: String, socket: std::path::PathBuf) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let token_paths = hive_forge_notify::forge_token_paths(&state_dir);
|
let token_path = format!("{state_dir}/forge-token");
|
||||||
// Retry reading the token to handle races where the agent's fetch of
|
// Retry reading the token to handle races where hive-priv provisions
|
||||||
// it from the swarm secret store lands after this unit starts, or where
|
// it after the harness starts, or where a parent-container chown
|
||||||
// a parent-container chown briefly makes the file unreadable.
|
// briefly makes the file unreadable.
|
||||||
let mut token = {
|
let token = {
|
||||||
let mut attempts = 0u32;
|
let mut attempts = 0u32;
|
||||||
loop {
|
loop {
|
||||||
if let Some(t) = hive_forge_notify::read_first_token(&token_paths) {
|
match tokio::fs::read_to_string(&token_path).await {
|
||||||
break t;
|
Ok(t) => {
|
||||||
|
let t = t.trim().to_owned();
|
||||||
|
if !t.is_empty() {
|
||||||
|
break t;
|
||||||
|
}
|
||||||
|
debug!("forge_notify: empty forge token at {token_path}");
|
||||||
|
}
|
||||||
|
Err(e) => debug!("forge_notify: cannot read token at {token_path}: {e}"),
|
||||||
}
|
}
|
||||||
debug!(?token_paths, "forge_notify: no forge token yet");
|
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
if attempts >= TOKEN_RETRY_MAX {
|
if attempts >= TOKEN_RETRY_MAX {
|
||||||
debug!(
|
debug!(
|
||||||
|
|
@ -59,7 +65,7 @@ async fn forgejo_loop(state_dir: String, socket: std::path::PathBuf) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(mut source) = ForgejoSource::new(&forge_url, &token) else {
|
let Some(source) = ForgejoSource::new(&forge_url, &token) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -105,18 +111,6 @@ async fn forgejo_loop(state_dir: String, socket: std::path::PathBuf) {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
// The swarm rotates the token when it goes stale, and the agent's
|
|
||||||
// fetch replaces the file; pick the new one up rather than polling
|
|
||||||
// with a revoked token for the rest of this process's life.
|
|
||||||
if let Some(t) = hive_forge_notify::read_first_token(&token_paths)
|
|
||||||
&& t != token
|
|
||||||
&& let Some(s) = ForgejoSource::new(&forge_url, &t)
|
|
||||||
{
|
|
||||||
info!("forge_notify: forge token changed on disk; using the new one");
|
|
||||||
token = t;
|
|
||||||
source = s;
|
|
||||||
own_login.clear();
|
|
||||||
}
|
|
||||||
if own_login.is_empty() {
|
if own_login.is_empty() {
|
||||||
own_login = resolve_own_login(&client, &source).await;
|
own_login = resolve_own_login(&client, &source).await;
|
||||||
if !own_login.is_empty() {
|
if !own_login.is_empty() {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue