deploy: split the forge's host decisions out of swarm.forge

`swarm.*` is what a hive needs to be a *client* of the swarm. For the
forge that is what it IS from any hive's point of view: its package, the
names and ports it answers on, the URLs it advertises, and the client id
it is registered under. How it is served, what it mirrors and where its
host-local secrets sit are decisions of the machine running it, so
behindGateway, openFirewall, mirrors, sso.clientSecretFile and
hostSwarmControllerTokenFile move to `deploy.forgejo.*`.

Unlike the wireguard mesh this SPLITS a module rather than relocating a
whole namespace. `sso` splits with it: `clientId` stays because it must
match the id in authelia's register, while the secret beside it is a
path on one host. Moving the whole `sso` block for symmetry with `ci`
was considered and rejected on exactly that asymmetry.

Declared in hive-forge/default.nix under the `deploy.*` path, following
swarm-victorialogs.nix; deploy.nix carries only the renames. `mirrors`
renames in one entry rather than one per field — it is a single option
of a list-of-submodule type, so the rename carries its whole value,
where `ci` needed five because it is a plain attrset of options.

Readers outside the module: hive-ci.nix binds `deploy.forgejo` for its
behindGateway assertion; swarm-authelia.nix and swarm-controller.nix
read theirs off the `deployCfg` they already bind. hivectl's `open`
printed `services.hyperhive.forge.behindGateway` in an operator-facing
hint — a path that never existed, missing `swarm.` — and hive-c0re's
state_snapshot doc comment carried the same defect; both now name the
new path. The rendered docs put the two halves on separate pages, so
the five descriptions of staying options that explain themselves in
terms of `behindGateway` now qualify it in full.

module-eval gains a forge case configured entirely through the old
paths, asserting the rendered firewall ports and the mirror env var
c0re seeds from: the new paths evaluate fine without the shims, so
dropping them reads as a clean tree. All five old paths are defined in
the fixture, so removing any single shim entry fails the eval rather
than only the two the assertion reads.
This commit is contained in:
atlas 2026-09-07 01:38:53 +02:00 committed by mara
commit 1db2ac26a7
13 changed files with 211 additions and 122 deletions

View file

@ -2,7 +2,11 @@
Private Forgejo instance running in a nixos-container, used as the
swarm's persistent code-collaboration surface (issues, PRs, reviews,
attachments). Configured via `services.hyperhive.swarm.forge.*`. Container
attachments). Configured via `services.hyperhive.swarm.forge.*` (what the
forge IS to every hive — its package, names, ports, URLs) plus
`services.hyperhive.deploy.forgejo.*` (what the host running it decides —
how it is served, what it mirrors, where its host-local secrets sit).
Container
shape, ROOT_URL / sub-domain routing, and operator-vs-in-cluster URL
handling live in [`docs/networking/gateway.md`](../networking/gateway.md); this file owns the
per-agent integration story and the notification pump that wakes

View file

@ -10,7 +10,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
| `<hive>/agent/<name>/` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) |
| `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` |
| `<hive>/matrix/` (deprecated) | `_` | 301 → `chat.<swarm>/` | `matrix.gui.enable` |
| `forge.<swarm>/` | `forge.<swarm>` | forgejo (`3000`) | `forge.behindGateway` |
| `forge.<swarm>/` | `forge.<swarm>` | forgejo (`3000`) | `deploy.forgejo.behindGateway` |
| `chat.<swarm>/_matrix/*` | `chat.<swarm>` | tuwunel (`8008`) | `matrix.gatewayHost != null` |
| `chat.<swarm>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` |
| `chat.<swarm>/config.json` | `chat.<swarm>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
@ -62,7 +62,7 @@ Each location carries a duplicated `auth_basic` block (separate locations don't
`services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`:
- `<hive-domain>``127.0.0.1`
- `forge.<swarm>``127.0.0.1` (when forge.behindGateway)
- `forge.<swarm>``127.0.0.1` (when deploy.forgejo.behindGateway)
- `chat.<swarm>``127.0.0.1` (when matrix.gatewayHost set)
- `auth.<swarm>``127.0.0.1` (when deploy.authelia)
@ -159,7 +159,7 @@ flip together: the primary agent-name link, the favicon fetch
(`<url>/icon`), and the nav-strip `container`-kind links from
`DashboardState.links` (`GET /api/dashboard-state`). `forge`-kind nav-strip links still
resolve against `http://<host>:3000` (separate sub-domain transition
tracked by `forge.behindGateway`); `external`-kind links are
tracked by `deploy.forgejo.behindGateway`); `external`-kind links are
already absolute. See `docs/web-ui/dashboard.md::Container row` for the
frontend-side derivation.
@ -333,10 +333,12 @@ container.
```nix
services.hyperhive.swarm.forge = {
httpPort = 3000; # default — HTTP listener; outside hyperhive's 7000/8100-8999 range
sshPort = 2222; # default — git-over-SSH; kept off 22 so it doesn't collide with the host openssh
openFirewall = false; # default — expose httpPort + sshPort to the host firewall
httpPort = 3000; # default — HTTP listener; outside hyperhive's 7000/8100-8999 range
sshPort = 2222; # default — git-over-SSH; kept off 22 so it doesn't collide with the host openssh
};
# Which ports the forge answers on is swarm-wide; whether THIS host opens
# them in its firewall is a deployment decision, so it lives under deploy.*.
services.hyperhive.deploy.forgejo.openFirewall = false; # default
```
`httpPort` (default **3000**) is the port Forgejo's HTTP server binds to.
@ -357,7 +359,7 @@ the bridge), not the raw port, so no firewall hole is needed. Flip to
(not behind the gateway).
- External git clients that push/pull via SSH directly to the host.
Forgejo served through the gateway (`forge.behindGateway = true`) does
Forgejo served through the gateway (`deploy.forgejo.behindGateway = true`) does
not need `openFirewall` — the gateway's own `openFirewall` option covers
that path.
@ -373,8 +375,8 @@ covers most cases:
| Shape | Auto-derived `ROOT_URL` |
|---|---|
| `behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
| `behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
| `deploy.forgejo.behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
| `deploy.forgejo.behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
The gateway always terminates TLS, so the `behindGateway = true` case is
always advertised over `https://`; only the direct (`behindGateway =

View file

@ -100,7 +100,7 @@ build can't hold the runner's single slot indefinitely).
## Container design
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It cannot reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `forge.behindGateway = true`.
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It cannot reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `deploy.forgejo.behindGateway = true`.
- **Non-ephemeral**: runner credentials persist across restarts (written to container's stateDir on first registration, reused thereafter).
- **Sandbox fallback**: nspawn containers can't create user-namespaces, so nix's sandboxing would always fail. Module sets `nix.settings.sandbox-fallback = true` in the container — nix builds run unsandboxed (safe because the container is already isolated). See `docs/process/gotchas.md`.
- **Credential isolation**: the forge admin token (`forge-core-token`) never enters the container. hive-c0re holds it and performs all forge API calls (runner validation + registration-token mint, in `forge/ci_runner.rs`); via hive-priv it writes only the runner registration token to the host env-file `/run/hive-ci/runner-token`, which the container bind-mounts read-only.
@ -150,10 +150,10 @@ forge provisioning sweep (orgs, agent accounts, etc.), so mirror
seeding lives in the same place rather than a separate host-side unit.
**General-purpose mirrors**: you can pre-seed any external repo as a
pull-mirror via `services.hyperhive.swarm.forge.mirrors`:
pull-mirror via `services.hyperhive.deploy.forgejo.mirrors`:
```nix
services.hyperhive.swarm.forge.mirrors = [
services.hyperhive.deploy.forgejo.mirrors = [
{ upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; }
{ upstream = "https://github.com/example/tool"; dest = "mirrors/tool"; }
];

View file

@ -314,6 +314,6 @@ an error.
A surface has no URL when it isn't browser-reachable: `home` needs
`services.hyperhive.domain`; `forge` needs
`services.hyperhive.swarm.forge.behindGateway = true`; `matrix` needs
`services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs
`services.hyperhive.swarm.matrix.gui.enable = true`. In those cases the command
exits with a hint naming the option to set.