Commit graph hyperhive/nix/host-modules/hive-ci.nix
Author SHA1 Message Date
atlas
9c601c4166 containers: put journal files on the host, where the collector looks
The swarm collector reads /var/log/journal and has never seen a single
container unit. mara's count-by-unit against VictoriaLogs returns four
units, all host-tier; openbao -- which runs inside the swarm-bao
container -- is absent.

Cause: nixos-containers.nix hardcodes `--link-journal=try-guest` for
every non-ephemeral container. With `guest`, the host's
/var/log/journal/<machine-id> is a SYMLINK into the container's transient
root; a reader in the host namespace cannot follow it, and it dangles as
soon as the container stops. `ls -la /var/log/journal/` on the host shows
one real directory and a pile of `-> /tmp/nspawn-root-*` links dating
back to May.

swarm-otel.nix asserted the opposite, and that assertion is why the
receiver's path was considered sufficient: it said the files "live here"
and are "bind-mounted into the guest rather than the other way round".
That describes `--link-journal=host`. The same sentence names the flag we
actually use. The flag was right and the behaviour it described was not,
so grepping for the flag confirmed the comment and taught nothing.

`containers.<name>.extraFlags` feeds EXTRA_NSPAWN_FLAGS, which the
invocation expands after the hardcoded flag, so `--link-journal=host`
wins. The comment now describes what the code does instead of the other
way round.

Two payoffs, and the smaller one is the one the issue is about:
container logs become collectable, and -- independently -- they become
durable at all, rather than dying with the container.

Ten identical edits because ten host-modules hand-roll their own
container block; that duplication is #3773, not something to invent an
abstraction for here.

NOT VERIFIED: that systemd-nspawn honours the last `--link-journal` of
two. Everything else here is read out of nixpkgs; that step is a claim
about its argument parsing which cannot be exercised without starting a
container. It is settled by deploying one and re-running the `ls`: the
machine-id entry becomes a real directory instead of a symlink.

Refs #3849
2026-09-02 23:00:30 +02:00
atlas
55145ff8e8 hive-ci: keep the two load-bearing properties, drop the retelling
The module header restated `docs/scheduler/ci.md`'s credential-isolation
paragraph nearly word for word, and re-told the nix-sandbox gotcha that
`docs/process/gotchas.md` owns — both immediately above lines that already
pointed at those docs.

Measured, not eyeballed: 6-gram intersection between this file's comments
and `docs/scheduler/ci.md`, 74 -> 24.

Kept, because neither is visible from the config that implements it and a
reader changing this file needs both:

  * the container has its own network namespace, not the host's, so a PR's
    nix build cannot reach host-loopback services — a prompt-injection
    boundary rather than a preference;
  * the forge ADMIN token never enters this container; only a registration
    token is handed in.

Each is now one sentence naming the property and its consequence, with the
mechanism behind a `docs/x.md::Section` pointer whose heading was checked
to exist (`Container isolation`, `Container design`).

Also restated in the present: the container is non-ephemeral because the
runner writes its registered credentials on first registration and reuses
them, which is why an ephemeral one would re-register every restart. That
was previously a description of what gitea-actions-runner does, with the
reason left implicit.

Comments only; no behaviour change. Refs #3901.
2026-09-02 09:03:15 +02:00
iris
07b62612b0 docs: restructure into topic subdirectories, collapse duplicated index
Per mara's go-ahead on hyperhive#3902 ("getting started is good, but
terminal rendering does not go in there i think"):

Moved 21 top-level docs/*.md files into 7 new topic subdirectories
(existing web-ui/, turn-loop/, swarm/, tools/, crates/ untouched):
  getting-started/  setup.md
  agent-lifecycle/  agent-hierarchy.md, approvals.md, persistence.md
  trust-boundary/   boundary.md, security.md
  integrations/     forge.md, matrix.md, github.md, knowledge.md
  networking/       gateway.md, network.md, snapshot-store.md
  scheduler/        jobq.md, coordinator.md, ci.md, observability.md
  process/          conventions.md, gotchas.md, pr-review-gate.md
  web-ui/           terminal-rendering.md (moved into the EXISTING dir,
                    per mara's correction to the original getting-started
                    guess -- it's UI implementation detail, not onboarding)

The physical layout now matches docs/README.md's own topical headers,
which already amounted to this taxonomy -- see the scoping comment on
the issue for the two findings that motivated this (a genuine
duplication between CLAUDE.md's old "Reading paths" list and
docs/README.md's grouped one, since drifted out of sync with each
other; and the flat layout not matching the grouping we already had).

Fixed every cross-reference this moved across the whole repo (~120
files: docs/ internal links at every depth, Rust doc comments, nix
module option docs, crate READMEs) -- verified two ways: a grep sweep
confirming zero remaining references to any old path, and a script
that resolves every markdown link in docs/**/*.md + CLAUDE.md +
README.md against the filesystem and reports anything that doesn't
exist (zero broken links).

Collapsed CLAUDE.md's "Reading paths" section (the duplicate) down to
a pointer at docs/README.md, now the single index. Rewrote
docs/README.md itself to use the new subdirectory paths and added the
one doc it was missing that CLAUDE.md's old copy had (pr-review-gate.md).

Classified all 22 docs/*.md files first via a haiku subagent (mara's
suggestion) on two axes -- proposed grouping and operator-vs-
implementation focus -- before finalizing the taxonomy; spot-checked
the report and found internal inconsistencies (its classification
table disagreed with its own summary section for a few files), so this
taxonomy is my original proposal + the one correction mara gave
directly, not a blind application of the subagent's table. The
operator-focus data it gathered is still useful for a follow-up
content pass (docs skewing 'mixed' rather than pure operator-facing),
not addressed in this PR -- structure only.

nix fmt clean, both pre-push lints clean.
2026-09-02 01:55:37 +02:00
atlas
36082f991f otel: ship the journals the dashboard can already show
The dashboard's log surface is an allow-list: the four harness units in
every agent container, the hive-ci / hive-forge / hive-matrix machines,
and three host units. `swarm.otel.journaldUnits` only named the host
three plus forgejo — so the units an operator can read in the UI were
absent from the store they are supposed to be queryable in, and nothing
reports that, because a name that matches nothing is not an error.

Adds the four agent units, the CI runner and the homeserver. The agent
units are declared by the hive-c0re module rather than by the
agent-modules that define them: those are a guest evaluation and cannot
reach a host option.

Closes #3818
2026-08-31 12:51:51 +02:00
atlas
daa6eb96f8 deploy: move the hive CA's knobs to deploy.hive-controller.tls
`services.hyperhive.tls.{stateDir,caValidityDays,leafValidityDays}` sat at
the top of `services.hyperhive`, which is meant to be everything about
hyperhive rather than the settings of one hive. Where the hive CA lives,
how long it lasts and how long the leaves it signs last are decisions of
the host holding the key — `deploy.*`, by the same rule as the switches
that moved before them.

`hive-controller` is hive-c0re's new name (mara on the issue), so the
knobs hang off the daemon that owns the CA rather than off a bare `tls`
at the root. mkRenamedOptionModule entries carry existing configs.

⚠️ Unlike the two switch renames, these names are NOT unique, so this was
swept by ALIAS BINDING rather than by identifier: hive-tls.nix alone holds
two options spelled `stateDir` — its own `cfg.stateDir` and the swarm CA's
`swarmCaCfg.stateDir`, four sites that must not move. Nine files bind an
alias to this config; the rename followed those bindings.

Two sites were invisible to the obvious check, and an unanchored sweep for
`hyperhive\.tls\b` is what found them: the option declaration (`= {` after
the path, so no trailing `.` or `;`) and the alias convention documented in
a comment in lib/hive-ca-trust.nix.

Also renamed the `<tls.stateDir>` shorthand in four docs and two Rust doc
comments, anchored on its delimiters — the new path contains the old one
as a substring, so an unanchored replace would have doubled the prefix.
2026-08-30 20:52:00 +02:00
atlas
4423da4899 deploy: move the forge CI runner toggle out of swarm
Last of the swarm.* -> deploy.* moves for the enable-shaped options.
Whether this host also runs the CI runner is a per-machine decision, so
it was sitting in the namespace that has to be identical swarm-wide.

Renames all five options, not just enable: name, concurrency, labels and
package all describe the runner THIS host would run, and leaving them
behind would keep the original defect for four more options. One rename
entry each, because ci is a plain attrset rather than a submodule type,
so there is no parent path to rename in one go.

deploy.forgejo is the name deploy.nix's own header already specifies,
including this exact case -- "ci (does this host run the runner too) is
exactly that shape". It is the only entry with no enable: the forge is
not optional, it deploys with hyperhive itself, so running the runner is
the only deployment decision it has.

Also fixes hive-c0re/src/forge/ci_runner.rs, whose doc comment named
services.hyperhive.forge.ci.enable -- missing the swarm. segment, so it
had never resolved to a real option.
2026-08-30 16:07:21 +02:00
atlas
d17aa254dd nix, hive-sh4re: name modules that exist in the stale harness-base refs
harness-base.nix has never existed in this tree. Four comments named it,
or a `harness-base` module, as the place to look:

- weston-vnc.nix: the agent user is declared and home-chowned by
  nix/agent-modules/user.nix
- hive-ci.nix: the sandbox-fallback reasoning lives in
  nix/agent-modules/default.nix -- which the very next comment block in
  the same file already cites correctly
- packages/default.nix: the per-bin consumer is
  nix/agent-modules/packages.nix
- hive-sh4re/src/assets.rs: HIVE_ASSETS_DIR is set by
  hive-c0re/environment.nix and agent-modules/default.nix +
  agent-service.nix, and the package is built by nix/packages/assets.nix
  -- not the equally nonexistent nix/assets.nix

assets.rs was twice declared out of scope on the sibling PR because it
names a module rather than a file. That distinction was real and
irrelevant: neither the module nor the nix/assets.nix path it points at
exists. Reading the wording is not checking the reference.

Every replacement path was verified to exist, with a deliberately bogus
path as a control.
2026-08-30 04:19:11 +02:00
atlas
5a5a4ddd15 ci: let the runner execute what it builds
The upstream gitea-actions-runner unit uses DynamicUser, and systemd
mounts a dynamic unit's state directory noexec. Measured from a job's
own /proc/self/mountinfo on the live runner, with /tmp as the control:
the state dir carries noexec, /tmp does not, and a script written and
chmod +x'd in the workspace fails execve with EACCES while the same
script in /tmp runs.

This was invisible for as long as every workflow compiled inside the
nix sandbox and executed nothing from the workspace. The first job that
built a binary into the runner's own target dir -- an instrumented
coverage run -- died on its first build script.

ExecPaths= re-mounts the state dir executable. Both spellings are
listed with the ignore-if-absent prefix because ExecPaths resolves
against the host root, where the dynamic-user layout makes
/var/lib/gitea-runner a symlink into private/.
2026-08-27 14:48:47 +02:00
atlas
4152000a3b hive-ci: run the runner credential precondition with full privileges
The token file is 0600 root-owned and the precondition ran as the unit's
dynamic gitea-runner user, so it could not read it -- and it did not fail
closed. `[ -s ]` succeeds on a stat alone, grep then died with EACCES,
and `! grep` turned that error into true, so the gate exited 0 on a file
it had never read. It has reported 'a real token is present' without
looking for its entire existence, which is why the runner kept retrying
instead of holding still and saying why.

Prefix the step with + so it runs with full privileges. This grants the
runner nothing new: tokenFile becomes EnvironmentFile=, which systemd
already reads as root before dropping privileges, so the payload never
passes through an unprivileged reader either way -- the + only lets the
gate observe what systemd observes.

Also assert readability separately and loudly, rather than folding it
into the placeholder test: an unreadable file and a real token produced
the same verdict, so 'I could not look' was indistinguishable from 'I
looked and it is fine'.
2026-08-26 22:47:31 +02:00
atlas
a7dbe732fb hive-ci: give the runner daemon the hive CA it needs for TLS
NODE_EXTRA_CA_CERTS beside it is Node's and additive. gitea-runner is Go,
whose trust store is replacing: it reads SSL_CERT_FILE and trusts only
what that names. So the container trusted the hive CA for its Node
actions and never for the daemon itself.

The daemon then fails startup with x509 unknown authority the moment it
reaches a TLS endpoint, crash-loops on Restart=on-failure, and no job is
picked up -- every pull request just sits at Waiting to run.

Not claimed as the whole cause of that outage: which address the daemon
holds is a separate question. The trust gap is real under every
explanation, and adding a CA to one unit cannot break a working path.
2026-08-26 18:30:24 +02:00
atlas
38c222fadd feat(nix): move the forge host options under services.hyperhive.swarm
Forgejo is a swarm-global service, so its operator-facing host options
move to services.hyperhive.swarm.forge (and .swarm.forge.ci) as the
first of the namespace consolidation.

Existing hive configs keep evaluating: swarm-renames.nix maps every
moved leaf with mkRenamedOptionModule, which also emits a deprecation
warning naming both the old and new path, so an operator is told what to
rename rather than discovering it from a failed eval.

The per-agent hyperhive.forge.url does NOT move. It is a client pointer
at whatever forge an agent talks to - it shares a word with the service
and nothing else, and the two are already documented as separate option
surfaces.

Verified by evaluating the host module, since no Rust gate evaluates
nix: setting the old paths and reading the new ones yields the values
(httpPort 3999, ci.concurrency 7), and config.warnings carries the
rename notice.
2026-08-05 03:44:53 +02:00
atlas
65a0686297 fix(#2673): set nix fallback in agent + CI containers
Container nix invocations hard-failed whenever the remote builder
muede-pc2 was unreachable, while the identical build on the host
succeeded. Both go through the same host nix-daemon, so the difference
looked impossible.

The cause is that `fallback` is a client-side option: the nix client
transmits it to the daemon per connection (`tryFallback` in setOptions),
so the caller decides whether a failed remote dispatch may degrade to a
local build — even when the build itself runs on the host daemon under
NIX_REMOTE=daemon. Only genuinely daemon-side settings such as
`builders` are inherited from the host. The containers never set
`fallback`, so they took nix's default of false.

Set it in the agent-container base module and in the CI container, and
correct the hive-ci comment that claimed fallback was inherited from
the host daemon along with buildMachines and max-jobs.

Verified in an agent container: `nix fmt` fails outright on the remote
store's connection reset, while the same command with fallback enabled
reports the same connection error and then builds locally and succeeds.
2026-07-25 19:36:31 +02:00
atlas
6559f3e7b5 fix(#2578): route hive-ci's nix through the host daemon (keep distributed builds + gain fallback)
CI's nix flake check ran in hive-ci's OWN in-container nix-daemon, which
offloads to the pc2 remote builder and HARD-FAILS when pc2 is
unreachable (Connection-reset) — reddening every PR's queue hive-wide.
The host daemon builds fine in the same situation (buildMachines +
max-jobs>=1 + fallback → local build when pc2 is down), and the agent
containers already route through it.

Give hive-ci the same wiring: bind-mount the host nix-daemon socket dir
into the container, set NIX_REMOTE=daemon, and disable the container's
own nix-daemon service + socket. Now CI builds through the host daemon —
pc2 offload when it's up, graceful local fallback when it's down. Drops
the now-moot in-container wait-nix-daemon precond. Needs an operator
rebuild to apply.
2026-07-20 18:55:50 +02:00
damocles
12d607fe5d fix hive-ci RestartSec eval conflict with lib.mkForce (#2535) 2026-07-16 17:00:46 +02:00
atlas
f4bcc59152 feat(#2415): move hive-ci runner registration off the boot-critical path (nix)
Completes #2415. Registration no longer gates container@hive-ci start:
- Retire the host-side hive-ci-prefetch.service (+ its 100-line script and
  the now-dead coreTokenPath/containerRoot let-bindings) — the forge round-trip
  it did on the boot path now lives in hive-c0re (forge/ci_runner.rs), run out
  of band during the startup sweep.
- Drop the container@hive-ci TimeoutStartSec = mkForce 180s band-aid that
  papered over that boot-path wait tripping the nspawn start timeout (the ~60s
  restart loop of #2410).
- gitea-runner-hive gains an ExecStartPre precond (ahead of the nix-daemon
  wait) that fails fast unless it is already registered (.runner present) or a
  real, non-placeholder token is in place — so missing creds just hold the
  runner down instead of blocking the container. Restart=on-failure (no start
  limit) self-heals it once hive-c0re writes the token; c0re's explicit restart
  is the primary path.
- Set HYPERHIVE_FORGE_CI_ENABLED=1 on hive-c0re.service so the sweep registers.
The tmpfiles TOKEN=placeholder seed + read-only bind-mount are unchanged; the
core admin token still never enters the container.
2026-07-16 12:48:23 +02:00
atlas
6c4ef5f798 refactor(#2427): extract shared hive-CA trust bind-mount helper
hive-ci and hive-forge both bind the runtime-generated hive CA cert
read-only and order their container@ unit after hive-tls-ca.service so
the bind source exists before nspawn sets the mount up — the same
bind-mount + ordering + rationale duplicated verbatim in two modules.

Extract that language-agnostic half into a pure helper,
nix/host-modules/lib/hive-ca-trust.nix, taking a container name and
returning { useSelfSigned, caContainerPath, bindMount, containerOrdering }.
The per-runtime consumption stays at each call site (hive-ci's additive
NODE_EXTRA_CA_CERTS, hive-forge's Go SSL_CERT_FILE concat). hive-ci folds
containerOrdering into its existing mkMerge alongside the TimeoutStartSec
bump.

The helper is a pure function, not a module: host-modules/default.nix is
an explicit aggregator (not a glob) and the docs eval imports that same
aggregator, so the lib/ file is never picked up as a module. A third
outbound-TLS-trusting container no longer means a third copy-paste.
2026-07-15 20:12:52 +02:00
damocles
ffde085c35 fix(#2433): mkforce container@hive-ci timeoutstartsec to resolve nixpkgs conflict 2026-07-14 19:59:32 +02:00
atlas
2bf6290aa0 fix(#2410): bump container@hive-ci TimeoutStartSec to survive slow runner registration
gitea-runner registration (hive-ci-prefetch, host-side) sits on the
boot-critical path -- nspawn readiness is gated on the runner
registering, itself a forge + core-token round trip that can wait up
to 60s for the core token. The default ~60s TimeoutStartSec can trip
mid-register (especially right after a .runner purge, since every
boot re-registers from scratch), killing the half-started container
and triggering a restart loop until the token/forge settle. Bump to
180s so one register attempt has room to finish.
2026-07-14 17:38:40 +02:00
müde
79fec131d6 docs: sweep archaeology comments to present state 2026-07-13 22:16:02 +02:00
müde
4a48ce5024 refactor: nix/host-modules + nix/agent-modules layout, update doc paths 2026-07-13 22:05:49 +02:00
Renamed from nix/modules/hive-ci.nix (Browse further)