Commit graph hyperhive/nix/modules
Author SHA1 Message Date
müde
8349e6f621 feat(dashboard): node-aware queue render + buildSlots option
each queue card now shows its DAG's node chain (per-node state, step,
build-log link), fixing 'queue jumps don't show on the dashboard'.
live-log panel keys off the running node's log. new
services.hyperhive.c0re.buildSlots option (default 1) threads the
concurrent nix-build count into serve.json.
2026-07-06 20:36:57 +02:00
atlas
23f13d9e71 chore(docs,nix): fix stale 'localhost via shared netns' claims in forge/matrix
Agents in private netns can't reach host loopback directly; they
use forge.<domain> and matrix.<domain> via the gateway. Update four
locations that still claimed direct loopback access:

- hive-forge.nix: module header + openFirewall description
- hive-matrix.nix: openFirewall description
- docs/matrix.md: container design bullet + openFirewall section
2026-07-04 22:54:38 +02:00
atlas
d7bed0052c chore(nix): fix stale 'localhost via shared netns' claim in forge openFirewall doc
Agents in private netns reach the forge at forge.<domain> via the
gateway, not via localhost directly. The conclusion (firewall only
matters for external access) is correct; the mechanism description
was stale from the pre-isolation era.
2026-07-04 22:54:38 +02:00
atlas
4947037454 chore(nix): trim stale body from deprecated isolateContainers option doc
The option is a no-op (marked DEPRECATED — ignored); the Prerequisite,
Migration, and Rust counterpart sections all described the migration that
already completed. Strip to just the deprecation notice.

Also fix the bridgeIp description: 'once netns isolation lands' is past
tense — isolation is unconditional now. Rephrase to present tense.
2026-07-04 21:57:26 +02:00
atlas
1e986a0270 chore(nix): remove stale 'privsep phase 2' labels from comments
privsep is complete and always on. the phased-rollout framing
is historical — drop it from the inline comments, keeping the
accurate description of what the user + delegation actually do.
2026-07-04 21:04:18 +02:00
atlas
460ed50a80 chore(nix): remove stale phased-rollout comments in network + gateway modules 2026-07-04 20:44:50 +02:00
atlas
467d3fa82e fix(#1845): correct ProtectSystem=strict comment (nix called directly via NIX_REMOTE=daemon) 2026-07-04 13:44:42 +02:00
atlas
7e83d906b2 fix(#1845): upgrade hive-c0re ProtectSystem full → strict
hive-c0re never writes outside its StateDirectory + RuntimeDirectory:
  - /etc writes (nixos-containers) go through hive-priv, not c0re
  - /etc/hyperhive/serve.json is read-only at startup (written by the
    NixOS activation script, not by the daemon)
  - nix builds delegate to hive-priv via the privileged Unix socket;
    hive-c0re itself never invokes nix directly

So strict is safe: systemd keeps /var/lib/hyperhive (StateDirectory)
and /run/hyperhive (RuntimeDirectory) writable; everything else is
read-only, shrinking the writable attack surface to exactly what the
service needs.
2026-07-04 13:44:42 +02:00
atlas
3a4b8d9873 fix(#1748): route all nix invocations through the host daemon (NIX_REMOTE=daemon)
Root contexts (systemd services running as root, PID 1) default to
store=auto which resolves to the LOCAL nix store — bypassing the host
daemon, its remote builders (muede-pc2), and any prebuilt derivation
outputs already in the shared store. This causes spurious full
rebuilds of agent toplevels that the host already built and cached.

Two changes:

harness-base.nix:
- Add systemd.globalEnvironment.NIX_REMOTE = "daemon" — sets
  DefaultEnvironment in systemd.conf so every unit in the container
  inherits NIX_REMOTE=daemon. Non-root contexts already default to
  the daemon socket; this only matters for root services that would
  otherwise use the local store.
- Add NIX_REMOTE = "daemon" to environment.variables so interactive
  shells also have it set (redundant with /etc/profile.d/nix-daemon.sh
  but explicit and profile-agnostic).

hive-c0re.nix (hive-priv service):
- Add NIX_REMOTE = "daemon" to the service environment. hive-priv
  runs as root and invokes nixos-container update + nix prebuild; these
  must route through the host daemon so they see the shared store and
  remote builders, not a private local store.

The sandbox-fallback = true in harness-base.nix is kept as a belt-
and-suspenders fallback but becomes a no-op for the common case once
nix routes through the daemon (the daemon builds on the host where
sandboxing works).
2026-07-04 13:12:02 +02:00
atlas
3e5ddbd2c0 fix(#1845): add ProtectSystem=full to hive-c0re service hardening
ProtectSystem=full makes /usr, /etc, and /boot read-only. Safe for
hive-c0re because it never writes to those paths directly:
- /etc writes (e.g. /etc/nixos-containers) go through hive-priv
- /etc/hyperhive/serve.json reads are read-only
- All runtime writes go to StateDirectory (/var/lib/hyperhive) and
  RuntimeDirectory (/run/hyperhive), which remain writable under full

ProtectSystem=strict (everything read-only) is still deferred — it
requires auditing ReadWritePaths for every nix path c0re touches.
RestrictAddressFamilies deferred pending a watched deploy.

Also tightens the comment block to not mention a numbered issue.
2026-07-04 13:01:47 +02:00
atlas
ef2b3a735c fix: remove tracker tag from source comment (lint) 2026-07-04 13:01:47 +02:00
atlas
b3b1c7b288 fix(#1845): add safe systemd hardening directives to hive-c0re service
hive-c0re runs as the unprivileged hive-core user and delegates all
privileged operations to hive-priv via a Unix socket. Add the subset of
systemd sandboxing directives that are provably safe without a watched
deploy (no ProtectSystem / RestrictAddressFamilies, which require mapping
the full set of filesystem/network paths first):

- NoNewPrivileges: already unprivileged; no setuid/capability escalation
- PrivateTmp: nix subprocesses use StateDirectory temp paths, not /tmp
- ProtectHome: HOME is /var/lib/hyperhive (StateDirectory), not /home/*
- ProtectKernelTunables: no sysctl writes needed
- ProtectKernelLogs: logs via systemd-journal group, not /dev/kmsg
- ProtectControlGroups: cgroup writes go through hive-priv, not c0re
- RestrictNamespaces: namespace creation goes through hive-priv
- LockPersonality: no personality changes needed
- RestrictRealtime: no real-time scheduling

Follows the same pattern as hive-priv (PrivateTmp + ProtectHome already
present there). Deferred: ProtectSystem=strict + ReadWritePaths +
RestrictAddressFamilies — those need a watched deploy per the #1845 plan
since getting the ReadWritePaths wrong silently breaks runtime I/O.
2026-07-04 13:01:47 +02:00
atlas
ef4dd67e9a fix(#1747): stable hive-c0re ExecStart via /etc/hyperhive/serve.json
Previously, serveConfig was a pkgs.writeText store path embedded in
ExecStart. Any repo edit (gateway.nix, frontend, unrelated nix modules)
changes hyperhiveFlakeSource → serveConfig hash → ExecStart → systemd
restarts hive-c0re → StartupSweep → every agent rebuilt unnecessarily.

Fix: write the JSON to /etc/hyperhive/serve.json via environment.etc
(a managed /etc symlink to the store-path content). ExecStart now
references the stable /etc path, changing only when cfg.package (the
hive-c0re binary) changes. Unrelated module edits no longer bounce c0re
or trigger a cascade agent rebuild.

hive-c0re still restarts — and triggers the sweep — when its binary
changes, which is the correct invariant: a new release legitimately
needs to resync all agents with updated harness modules.
2026-07-04 12:52:47 +02:00
damocles
b5acd60cf5 feat(#2170): split docs/ into its own meta-flake input 2026-07-04 11:47:07 +02:00
atlas
cb0a66147a fix(#2172): gate CLAUDE_CODE_OTEL_DIAG_STDERR on hyperhive.otel.debug
CLAUDE_CODE_OTEL_DIAG_STDERR was always set when OTEL is enabled, making
OTEL SDK errors (e.g. 404 from a misconfigured collector endpoint) appear
in every agent's stderr unconditionally. Move it behind a new opt-in flag.

Changes:
- nix/modules/hive-c0re.nix: add services.hyperhive.otel.debug (bool,
  default false); wire to HYPERHIVE_OTEL_DEBUG env on hive-c0re unit.
- hive-c0re/src/meta.rs: add debug field to OtelConfig; read
  HYPERHIVE_OTEL_DEBUG; emit hyperhive.otel.debug = true when set.
- nix/templates/harness-base.nix: add hyperhive.otel.debug internal
  option; move CLAUDE_CODE_OTEL_DIAG_STDERR out of otelSettingsEnv into
  a debug-gated lib.optionalAttrs block.

Default behaviour: OTEL exports silently (no stderr noise). Operators
troubleshooting collector connectivity set services.hyperhive.otel.debug
= true to re-enable the diagnostic output.
2026-07-03 22:12:56 +02:00
atlas
ae41e39dfb refactor(#2111): remove nixpkgs-unstable, source claude-code from main nixpkgs 2026-07-01 19:32:46 +02:00
atlas
53df2c9598 rework(#2072): seed mirrors in c0re startup, not a host-side oneshot
Per mara: the mirror seeding belongs in hive-c0re's forge provisioning
sweep, where the core admin token + org-ensure already live — not a parallel
host-side nix oneshot.

- forge.rs: ensure_mirrors() reads HYPERHIVE_FORGE_MIRRORS (JSON list of
  {upstream,dest}), ensures each dest org (reuse ensure_org) + creates the
  pull-mirror via the migrate API (reuse forge_http, serde_json::json! body,
  409/existing = success). Called in ensure_all() right after the SEEDED_ORGS
  loop (token in scope, warn-and-continue like the other ensure_* steps).
- hive-forge.nix: forward effectiveMirrors to c0re via
  systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS; drop the
  forgejo-seed-mirrors.service + its script + the host-side core-token read.
  Keep the forge.mirrors option, DEFAULT_ACTIONS_URL=self (CI-gated), and the
  dest-shape / no-c0re-namespace-collision assertions.

Verified locally: nix parse + treefmt (incl rustfmt) clean; serde/serde_json
patterns mirror dashboard.rs. cargo build runs in CI (no cc in my container).
2026-06-29 00:26:41 +02:00
atlas
6f5dade9c9 treefmt: collapse the assertion-message interpolation to one line
nixfmt wanted the ${...} on a single line (verified locally:
nix build .#checks.x86_64-linux.formatting passes). The earlier CI 'docs'
failure was a transient muede-pc2 build hiccup — the docs check builds clean
locally on the same drv.
2026-06-29 00:26:41 +02:00
atlas
990868b2e7 address argus review on the mirror seed
- drop the three cross-ref tracker tags from comments/description (prose only)
- build the orgs + migrate JSON bodies with jq -n --arg (an upstream URL
  containing a quote no longer corrupts the request)
- don't auto-append the actions/checkout mirror when the operator already
  declared that dest (avoids a duplicate effectiveMirrors entry when CI is on)
2026-06-29 00:26:41 +02:00
atlas
4a3581a3d2 feat(#2072): auto-seed Forgejo pull-mirrors (DEFAULT_ACTIONS_URL=self for CI)
General-purpose mirror mechanism for the internal forge, per mara's call on
#2074 (real Forgejo pull-mirrors, nix-configured — not a pushed clone).

- services.hyperhive.forge.mirrors: list of { upstream, dest } pull-mirrors,
  any repo. Each is created as a real Forgejo pull-mirror (re-syncs from
  upstream), dest = <owner>/<repo> in its own org.
- When forge.ci.enable is set: an actions/checkout mirror is auto-appended +
  forgejo DEFAULT_ACTIONS_URL is pointed at this instance, so CI
  'uses: actions/checkout@vN' resolves on loopback — immune to a host-resolver
  blip that previously reded every checkout (the seed/re-sync needs external
  DNS, but that's off the CI critical path).
- forgejo-seed-mirrors.service: host-side oneshot (the core admin token never
  enters a container), modelled on hive-ci-prefetch — waits <=60s for the core
  token, then idempotently ensures each dest org + creates the pull-mirror via
  the migrate API. partOf the forge container so it re-ensures on restart.
- assertions: dest must be <owner>/<repo>; mirror orgs can't shadow the
  c0re-managed namespaces (config/shared/agents/core) so the seed never races
  hive-c0re's own provisioning.

Supersedes #2074 (the raw-clone stopgap) as the durable #2072 fix.
2026-06-29 00:26:41 +02:00
atlas
b0d099274e wip(#2072): forge.mirrors option + DEFAULT_ACTIONS_URL=self when CI on
General-purpose Forgejo pull-mirror config (services.hyperhive.forge.mirrors:
list of {upstream, dest}). When CI is enabled, auto-append an actions/checkout
mirror + point forgejo DEFAULT_ACTIONS_URL at this instance so CI's
actions/checkout@vN resolves on loopback (immune to host-resolver blips, #2072).

Seed oneshot (creates the dest orgs + pull-mirrors via the migrate API) is the
next commit.
2026-06-29 00:26:41 +02:00
damocles
c8ae3371e1 refactor(#2015): drop redundant domain != null checks (domain is required) + update docs 2026-06-27 21:02:55 +02:00
damocles
816d387c65 feat(#2015): drop issue tags from nix comments (tracker-tag lint) 2026-06-27 21:02:55 +02:00
damocles
3d39cc00d0 feat(#2015): require services.hyperhive.domain (assertion) when hyperhive enabled 2026-06-27 21:02:55 +02:00
damocles
09603b77b9 feat(#2006): rename manageRootAgent option to ruthless (invert sense) 2026-06-27 00:37:32 +02:00
damocles
4fa17de387 feat(#2006): add services.hyperhive.manageRootAgent to opt out of root-agent auto-management 2026-06-27 00:37:32 +02:00
damocles
cae1dd8147 feat(#2014): hivectl open verb + Urls host request for web surfaces 2026-06-27 00:21:50 +02:00
damocles
af10a4bfef fix(otel): cumulative metric temporality + metricIntervalMs knob (real metrics-export fix) 2026-06-24 20:28:37 +02:00
damocles
609438a889 fix(#1977): exposeHostPorts is firewall-only (drop conflicting loopback proxy) 2026-06-24 19:48:50 +02:00
damocles
fe68bfbadc fix(network): co-locate exposeHostPorts firewall opening with the proxies (argus review on #1972) 2026-06-24 19:28:29 +02:00
damocles
0172106559 feat(#1971): network.exposeHostPorts to reach host-loopback services from agents 2026-06-24 19:17:37 +02:00
atlas
14ae7367cd fix(systemd): set explicit SyslogIdentifier on hyperhive-declared units
Follow-up to the harness fix: any unit whose ExecStart is a generated
wrapper (a NixOS `script =` block or a writeShellScript) gets its journal
SyslogIdentifier derived from the wrapper's store-path basename — an
opaque, unstable name. Pin an explicit stable identifier on every unit
hyperhive declares.

script/wrapper units (leaked store paths): forgejo-gpg-init,
hive-gateway-self-signed-cert, hive-tls-ca, hive-ci-prefetch,
hyperhive-isolated-dns, tea-login, forge-avatar-sync,
matrix-avatar-sync, hive-dashboard-links.

direct-binary units (basename already sane; explicit for tidiness +
future-proofing): hive-c0re, hive-priv, hive-matrix-daemon,
hive-bash-daemon, weston.

Only changes the displayed line identifier; journalctl -u <unit>
filtering (keys on _SYSTEMD_UNIT) is unaffected.
2026-06-23 22:53:33 +02:00
damocles
21ec7dc23d feat(#1930): forward otel headers credential into agent containers via nspawn --load-credential 2026-06-23 21:16:18 +02:00
damocles
838cc9af9a feat(#1930): move otel stats export to host-level services.hyperhive.otel 2026-06-23 21:16:18 +02:00
atlas
4e4e9f8a60 hive-tls: leafValidityDays default 30 (per mara: ahead of the ~47-day proposal)
Short-lived leaf bounds the blast radius of a key compromise. Doc notes
the renewal caveat: re-sign is activation-only today, so a hive that
stays up >30 days without a rebuild would need a periodic re-sign timer
to avoid leaf expiry (follow-up).
2026-06-23 10:46:11 +02:00
atlas
5ed0288634 hive-tls: default leafValidityDays to 397 (was 3650/~10y)
A 10-year leaf is pointless given the leaf auto-rotates (re-signed
within 30 days of expiry) and is long enough to make browsers warn.
397 days sits just under the 398-day CA/Browser-Forum leaf cap (so
browsers accept it) while bounding the blast radius of a leaf-key
compromise. The re-sign runs at service activation (hive-tls-ca is a
oneshot, no timer), so 397 stays comfortably above any gateway
rebuild gap.
2026-06-23 10:46:11 +02:00
atlas
d4f106d590 hive-gateway: route dashboard by path, not Accept header
The dashboard vhost split static-vs-backend on the request Accept
header (map $http_accept $dashboard_spa_target), so the same URL
behaved differently by content-type — e.g. /api/state fetched with
Accept: text/html wrongly returned index.html.

Now that all hive-c0re routes live under /api/ plus the single
/webhook/knowledge endpoint, route by path instead: /api/ and
/webhook/ proxy to c0re (SSE settings on /api/), everything else
serves the dist with try_files $uri /index.html. Drops the
dashboard Accept-header map and the @c0re named location.

Updates docs/gateway.md accordingly.
2026-06-22 23:08:23 +02:00
atlas
f73bcc66eb hive-gateway.nix: nix fmt (collapse lib.optional block left dirty by the forge-fix) 2026-06-22 22:14:36 +02:00
müde
0a9bfe2b9b fix borked forge 2026-06-22 21:13:25 +02:00
atlas
95a556e280 fix(#1906): add git + openssh to the hive-priv build-env PATH
`nixos-container update <agent>` (run by the hive-priv root helper) invokes
nix, which shells out by bare name to `git` whenever it has to fetch or
re-resolve a git-source flake input, and to `ssh` to dispatch builds to
remote builders. The hive-priv service `path` had neither, so an agent whose
config carries a git-source input — or a stale flake.lock whose node URL no
longer matches the flake's declared input, forcing nix to re-resolve at eval
— fails the build with `executing "git": No such file or directory`, and
remote-builder dispatch fails with `Could not find executable 'ssh'`.

Add `pkgs.gitMinimal` + `pkgs.openssh` to the path. Latent-bug fix: nix
legitimately needs both for any git-source input / remote build. Surfaced by
the bitburner agent's build failing on a git-source input re-resolution.
2026-06-22 20:43:04 +02:00
atlas
2b4e928afe feat(#1867): make the internal forge mandatory (remove forge.enable)
The internal forge is the canonical store for the meta flake, every
agent's config repo, and the internal/* repos, so it can no longer be
optional. Remove the services.hyperhive.forge.enable toggle:

- hive-forge.nix: drop the `enable` option; the forge config now
  deploys gated on `services.hyperhive.enable` (it ships with hyperhive).
- hive-c0re.nix: HIVE_FORGE_URL env unconditional; forge-public-URL gate
  drops the enable check (keeps behindGateway).
- hive-gateway.nix: local /etc/hosts forge entry keyed on behindGateway.
- hive-ci.nix: drop the now-moot `forge.ci.enable requires forge.enable`
  assertion (forge is always present); reword the option doc.
- nix/docs/default.nix: drop the `forge.enable = mkForce false` stub
  (option gone); the options-doc eval stays light via hyperhive.enable.
- hive-c0re forge.rs / hivectl.rs: reword 'forge.enable = true' error
  text to 'wait for hive-c0re to start the container' (the runtime
  token-absent path is unchanged — it's a bootstrap-timing check, not
  the opt-out).
- docs/approvals.md, docs/ci.md: drop stale forge.enable references.

Migration: configs that set `services.hyperhive.forge.enable = false`
must drop the line — the forge is now mandatory.

Prereq/companion to #1838 (PR-based config flow, which assumes the forge
is always present).
2026-06-22 19:26:34 +02:00
atlas
edad6f863c feat(#1886): trust a peer hive's root CA hive-wide for self-signed federation
Add swarm.peers.<domain>.caCert (path to a peer hive's root CA PEM),
trusted everywhere the hive's own internal CA is — so a self-signed
peer hive can federate (matrix) and any in-hive consumer validates its
certs.

Mechanism (reuses the existing hive-CA embedding): the meta-flake
renderer embeds a LIST of CA files next to each agent's flake —
hive-ca.pem (the hive's own self-signed CA, when active) plus each peer
caCert as peer-ca-<N>.pem — and emits them all in
security.pki.certificateFiles, so every agent trusts them at build
time. The matrix container trusts the same peer CAs for federation TLS.
Nothing is installed in the host trust store; the certs live in the nix
store (no mutable host file).

- meta.rs: embedded_ca_files() = hive CA + peer CAs (from new
  HIVE_PEER_CA_PATHS env); ca_embed_state() tracks the list (content +
  add/remove); sync_agents materialises + stages the list; render emits
  the multi-entry certificateFiles. Tests cover hive-only / hive+peers
  / peers-only / none.
- hive-c0re.nix: HIVE_PEER_CA_PATHS service env (colon-joined caCerts);
  caCert / certFingerprint option docs updated to the hive-wide scope.
- hive-matrix.nix + docs/swarm.md: scope + comment updates.

certFingerprint stays the c0re-only leaf-pin path.
2026-06-22 16:54:44 +02:00
atlas
4db8a8cd3d feat(#1843): static-serve the dashboard via the gateway, hive-c0re API-only
nginx proxied `<hive>/` straight to hive-c0re:7000, and hive-c0re served the
dashboard dist itself via `tower_http::ServeDir` (from `HIVE_STATIC_DIR` baked
into its service env). So a frontend-only change rebuilt the hive-c0re unit and
restarted the core daemon — every operator session dropped its SSE stream for a
pure CSS/JS change.

The gateway nginx now static-serves the dashboard dist directly; hive-c0re's
dashboard router is API-only. The split uses the Accept-header SPA fallback (the
same `map $http_accept` pattern the matrix/agent vhosts already use), so no
backend prefix has to be enumerated: a browser navigation (Accept: text/html)
whose path is not an on-disk asset gets the SPA index.html; everything else
(every /api route, the bare action/mutation routes, the two SSE streams, the
knowledge webhook — all Accept != text/html) falls through `try_files` to the
`@c0re` named location and is reverse-proxied to hive-c0re. A new c0re route
needs no gateway change.

- hive-c0re.nix: expose the themed dist as a new internal read-only option
  `services.hyperhive.c0re.servedFrontend`; drop `HIVE_STATIC_DIR` from the
  service env (the router no longer serves files).
- hive-gateway.nix: read that option in host-module scope (dashboardDist),
  static-serve `dashboard/` with the Accept-header `try_files ... @c0re` split;
  `@c0re` carries `proxy_buffering off` + a 1d read timeout for the SSE streams
  and a duplicated auth_basic block (named locations do not inherit it). The
  dashboard map is unconditional; the matrix map stays gated on the matrix GUI.
- dashboard.rs: drop the ServeDir fallback + the HIVE_STATIC_DIR resolution; the
  router 404s unmatched paths (the gateway only proxies non-static requests).
- hive-c0re/Cargo.toml: drop the now-unused tower-http dependency.
- docs/gateway.md: document the dashboard static split + the `@c0re` fall-through.

The store path is reachable inside the gateway nspawn container (shared
/nix/store), mirroring how HIVE_AGENT_FRONTEND_DIR already exposes the per-agent
UIs. The gateway and c0re changes must land together (atomic cutover) or the
dashboard 404s — this needs a watched gateway + c0re rebuild.
2026-06-22 01:18:01 +02:00
atlas
86ad8bc914 hive-ci: build locally when a remote builder is unreachable
The CI container set `nix.settings.sandbox-fallback` but never
`nix.settings.fallback`, so a cache-miss build with an unreachable
remote build machine had no local fallback and hard-failed. A single
down or DNS-unresolvable `nix.buildMachines` host therefore turned every
fresh `nix flake check` red hive-wide, in ~30s, even for diffs that
can't affect the nix build (pure JS/markdown). Add
`nix.settings.fallback = true` so CI degrades to a slower local build
instead of failing.
2026-06-21 21:48:53 +02:00
atlas
1f602d5fda hive-c0re: back agent state dirs with btrfs subvolumes
Progressive enhancement: a brand-new agent's state root under
/var/lib/hyperhive/agents is created as a btrfs subvolume when the host
filesystem is btrfs, otherwise it falls back to a plain directory. No
existing agent is auto-migrated — the new path only fires when the root
does not yet exist, so plain-dir agents are left untouched until an
explicit opt-in upgrade.

Two new privileged ops (subvolume create/delete are root-only):
EnsureAgentSubvolume statfs-gates on btrfs, creates the subvolume, and
chowns it to the hive-core user so the normal state/claude/harness
mkdirs succeed inside it; DeleteAgentSubvolume btrfs-subvolume-deletes
the root iff it is actually a subvolume. hive-c0re calls Ensure before
the per-agent dirs are created (spawn/rebuild/InitConfig) and Delete on
the purge path only — destroy keeps the subvolume for revival, matching
plain-dir semantics. btrfs-progs added to the hive-priv unit PATH.

Per-subvolume usage accounting + optional quota is a separate
follow-up.
2026-06-19 13:46:39 +02:00
atlas
2efd95d0f5 nix(ci): pin the runner readiness gate to --store daemon
A bare `nix store ping` run as root with the daemon socket absent
resolves to a local store (root writes /nix/store directly) and pings
successfully, so the gate could false-pass at cold boot: nix-daemon.socket
carries ConditionPathIsReadWrite=/nix/var/nix/daemon-socket and is
condition-skipped until /nix/var goes read-write. systemd service units
don't source the profile that sets NIX_REMOTE=daemon, so auto-store
fallback to local is the real environment here.

Pin --store daemon so the readiness poll verifies the actual daemon
socket and the gate honours its wait-for-daemon contract instead of
passing against a local fallback while the daemon is still down.
2026-06-18 13:02:21 +02:00
atlas
3b8e77c1c1 nix(gateway): factor the self-signed condition into a shared option
Per review: the `tls.certDir == null && !tls.acme.enable` derivation was
duplicated in hive-gateway, hive-tls, and hive-ci. Expose it once as a
read-only internal option `services.hyperhive.gateway.useSelfSigned` (the
gateway module's single source of truth) and have hive-tls and hive-ci
consume it instead of re-deriving.

Eval-proven: gateway.useSelfSigned is true on the self-signed default /
false with tls.certDir, and the hive-tls (HIVE_TLS_CA_PATH) + hive-ci
(NODE_EXTRA_CA_CERTS) wiring derives correctly from it.
2026-06-18 01:03:55 +02:00
atlas
9f03cf31ba nix(ci): trust the hive CA in the runner's Node actions (self-signed TLS)
With self-signed TLS the gateway/forge serve a hive-CA-signed leaf and
forgejo's ROOT_URL is https://forge.<domain>. The CI runner's Node-based
actions (e.g. upload-artifact) POST to the ROOT_URL-derived artifact
endpoint and fail with "unable to verify the first certificate": Node uses
its own bundled CA bundle, not the system store, so it rejects the
self-signed chain. checkout etc. are fine — they hit the localhost http
registration URL.

Bind-mount the public hive CA cert (only ca.pem — never the hive-tls state
dir, which holds the CA + leaf private keys) into the hive-ci container and
set NODE_EXTRA_CA_CERTS on the runner service so every Node action trusts
it, hive-wide. Order container@hive-ci after the host hive-tls-ca service so
the cert exists before the bind-mount is set up. All gated on self-signed
mode; with an operator cert / ACME the public CA already validates and the
mount + env var are absent.

Eval-proven: self-signed → /run/hive-ca/ca.pem bind-mount (from
/var/lib/hive-tls/ca.pem), NODE_EXTRA_CA_CERTS=/run/hive-ca/ca.pem, and
container@hive-ci ordered after hive-tls-ca.service; certDir → all absent.
2026-06-18 00:17:53 +02:00
atlas
58cc720173 nix(ci): gate the actions runner on nix-daemon readiness
After a hive-ci restart the act_runner re-registers and immediately claims
any queued jobs, which can beat the in-container nix daemon coming up: its
nix-daemon.socket carries ConditionPathIsReadWrite=/nix/var/nix/daemon-socket
and is skipped until /nix/var is read-write, so the first nix-dependent
build dispatches into a cold daemon and hangs/retries (observed: a 55m48s
nix flake check vs the normal ~30s — a build-offload stall, not a code
failure; a non-nix step on the same runner passed in 1s, masking it).

Ordering the runner after/wants nix-daemon.socket does not help — a
condition-skipped unit satisfies systemd ordering immediately. Instead add
a blocking ExecStartPre that polls until the daemon actually answers
(nix store ping), placed with mkBefore so it runs ahead of the upstream
module's runner-registration ExecStartPre. The runner therefore cannot
register or claim jobs until nix is usable. Topology-agnostic (works
whether the daemon is in-container or a shared host socket); ~180s ceiling
then the unit fails cleanly rather than claiming jobs into a dead daemon.

Eval-proven: gitea-runner-hive's ExecStartPre is
[wait-nix-daemon, gitea-register-runner-hive] in that order.
2026-06-17 21:17:35 +02:00
atlas
1a3f82a459 nix(gateway): self-signed TLS as the implicit default, deprecate the toggle
Make self-signed TLS the gateway's default whenever no external TLS source
is configured, and deprecate the explicit selfSignedTls toggle. Self-signed
is now derived as `tls.certDir == null && !tls.acme.enable`, so an operator
selects a TLS mode by setting tls.certDir or tls.acme — or neither, for the
self-signed default. There is no http-only mode: matrix discovery hardcodes
https, so the gateway always terminates TLS.

The selfSignedTls option is kept as a deprecated no-op (warns when set to
false) so existing configs still eval. The two selfSignedTls mutual-
exclusion assertions and the HSTS-requires-TLS assertion are dropped — they
are impossible or vacuous now that self-signed is the floor. The hive-tls
module and the forge ROOT_URL scheme consume the derived value: the gateway
always terminates TLS, so behind the gateway the forge is always advertised
over https.

Updates docs/gateway.md (TLS-modes table, self-signed section, the removed
http-only section, firewall + discovery notes). Eval-proven: default →
self-signed (hive CA active, https ROOT_URL); tls.certDir → CA inactive;
selfSignedTls=false → deprecation warning fires.
2026-06-17 21:14:05 +02:00