Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ci-runner: run the forge CI runner as a swarm service

The CI runner was a hive deploy module: hive-ci.nix ran it beside the
forge, and hive-c0re registered it through the forge's admin API with
the `core` token, writing the registration token through hive-priv. It
could only run on the forge's host, and it kept an admin-scoped forge
token on a hive.

Now the runner is a swarm service:

- swarm-ci-runner.nix runs it in a `swarm-ci-runner` container on the
  host that sets deploy.ci-runner.enable, reaching the forge by name.
  It needs neither the forge nor a hive there.
- swarm-controller reads the forge's runner registration token with its
  own admin token and writes it to swarm/services/ci-runner/
  registration-token whenever it differs from what it last wrote. Its
  grant on that path is create/update only.
- The runner's own leaf (CN swarm-ci-runner, role and policy of the same
  name) reads that one path. swarm-bao-ci-runner.service fetches it into
  the runner's EnvironmentFile and restarts the runner on a change.
- A pre-start step removes .runner when the daemon's journal since its
  last start holds Forgejo's "unregistered runner" refusal (the daemon
  exits 1 when its startup Declare gets it), or when .runner names
  another forge address; upstream's pre-start step then registers again.

Removed from the hive side: hive-ci.nix and deploy.forgejo.ci.*,
forge/ci_runner.rs, PrivRequest::RegisterCiRunner and its handler and
ReadWritePaths entry, InfraContainer::Ci with hivectl's --ci and the
dashboard's hive-ci journal entry, HYPERHIVE_FORGE_CI_ENABLED, and
read:admin,write:admin from CORE_TOKEN_SCOPES: nothing else in hive-c0re
calls /api/v1/admin/*. The old ci options, under both swarm.forge.ci and
deploy.forgejo.ci, now fail evaluation naming deploy.ci-runner.

The forge's half of the old ci.enable, the actions/checkout mirror and
DEFAULT_ACTIONS_URL = self, is deploy.forgejo.actionsFromSelf.

Closes #4954
This commit is contained in:
atlas 2026-10-05 02:25:05 +02:00 • committed by mara
commit e2da5509ae
60 changed files with 1088 additions and 1201 deletions

View file

@ -56,13 +56,13 @@ Concretely, the core daemon's dashboard `/api` carries **no
application-layer authentication** — the core daemon serves operator-authority
routes unauthenticated at the HTTP layer. Their protection is entirely (a) the
gateway, which fronts all operator traffic and is where operator auth lives,
and (b) network isolation, which keeps agents — and `hive-ci`'s untrusted PR
builds — off host-loopback so nothing can reach `127.0.0.1:<dashboard_port>`
and (b) network isolation, which keeps agents — and the CI runner's untrusted
PR builds — off host-loopback so nothing can reach `127.0.0.1:<dashboard_port>`
directly. This is deliberate given the load-bearing role of network isolation
above, but it's a standing invariant: the `/api` must never bind to a
non-loopback address or get exposed outside the gateway, and every new
operator-authority route inherits that assumption. hyperhive treats `hive-ci`
like an agent for this purpose — it runs untrusted PR code and is
operator-authority route inherits that assumption. hyperhive treats
`swarm-ci-runner` like an agent for this purpose — it runs untrusted PR code and is
netns-isolated for the same reason.
The boundary rests on three layers:

View file

@ -205,9 +205,9 @@ Unsandboxed nix builds run as `nixbld` users (non-root, typically UIDs 30001-300
Without sandbox isolation, a build derivation's builder script has read access to
any file in the container that the nixbld user can read.
The blast radius also has a **network** dimension. hive-ci runs its unsandboxed
builds of untrusted PR code in its own private netns behind the hive bridge: a
build reaches the forge only through the gateway and can't reach host-loopback
The blast radius also has a **network** dimension. `swarm-ci-runner` runs its
unsandboxed builds of untrusted PR code in its own private netns behind the
bridge: a build reaches the forge by name and can't reach host-loopback
services — including the core dashboard at `127.0.0.1:<dashboard_port>`, which
has no application-layer auth of its own (see [`docs/scheduler/ci.md`](../scheduler/ci.md)). The `0600`
token policy bounds file reads; network isolation bounds network reach.
@ -286,7 +286,6 @@ known operations; there is no arbitrary command pass-through:
| `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` | unlink `/etc/tmpfiles.d/hyperhive-agents.conf` and return `Ok` |
| `SetAgentPaused` | create / remove the `<state>/<name>/harness/paused` marker that parks an agent's turn loop |
| `RegisterCiRunner` | write `/run/hive-ci/runner-token` (host path, root-owned) then `systemctl --machine=hive-ci restart gitea-runner-hive.service`. Only the registration token crosses; the forge admin token never enters the container |
| `EnsureAgentSubvolume` | `btrfs subvolume create <state>/<name>` for a new agent — no-op when the path exists or the filesystem isn't btrfs |
| `UpgradeAgentSubvolume` | migrate an existing plain state dir into a subvolume: create, `cp -a --reflink=auto`, atomic swap. Operator opt-in, and the caller stops the agent first |
| `DeleteAgentSubvolume` | `btrfs subvolume delete <state>/<name>` — purge path only, never a plain destroy |
@ -301,7 +300,7 @@ known operations; there is no arbitrary command pass-through:
**Container allowlist** — `hive-priv` validates every request against
an allowlist before any operation: the allowlist accepts only names
matching the agent-name convention (char-validated) or the known
sibling service containers (`hive-forge`, `hive-matrix`, `hive-ci`),
sibling service containers (`hive-forge`, `hive-matrix`),
and rejects arbitrary container names. `hive-gateway` is a host unit,
not a container, so it's not in this list — see `ReloadGatewayNginx`
above for how the hard-coded unit name scopes its access instead.