Add a systemd.sockets.hive-c0re unit that holds /run/hyperhive/host.sock
before hive-c0re starts. hive-c0re serve() detects LISTEN_FDS via the
listenfd crate and accepts the systemd-handed fd instead of calling bind().
Falls back to the existing bind path when LISTEN_FDS is absent so direct
invocation and CI are unaffected.
Benefits: hivectl can connect the moment the socket unit activates (no
racy window), and a hive-c0re restart never drops the socket inode.
Part of the docs-migration chore (issue #708). Remove GitHub issue
numbers from inline comments, option descriptions, and rustdoc —
these are contextless noise for anyone reading the code without
access to the original discussions. Replace with prose that captures
the same rationale directly.
No functional change. Build still clean (cargo check passes).
The nixpkgs gitea-actions-runner module uses tokenFile as a systemd
EnvironmentFile, loaded before any ExecStartPre. Our previous
ExecStartPre override was too late — file didn't exist yet.
Fix: tmpfiles pre-creates /run/hive-ci/runner-token with a placeholder
on every boot. A new hive-ci-register.service oneshot (gated on
ConditionPathExists=!/var/lib/gitea-runner/hive/.runner) fetches the
real token and overwrites before gitea-runner-hive.service starts,
wired via After/Wants. On subsequent boots the placeholder is harmless
— nixpkgs register step exits early when .runner already exists.
Closes#918.
Per-instance package option does not exist in the nixpkgs module.
Package is configured at services.gitea-actions-runner.package.
Fixes build failure on main. Closes#916.
nix flake check pulls rust tools hermetically. curl/jq in preStart
use absolute store paths. nix is part of any NixOS system by default.
git is the only package genuinely needed at runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the manual runnerTokenFile operator step. On first boot the
container fetches a runner registration token from Forgejo's admin
API using hive-c0re's existing admin token
(/var/lib/hyperhive/forge-core-token). A preStart script writes the
token to /run/hive-ci/runner-token; on subsequent boots it writes a
dummy (registered .runner creds take precedence anyway).
Operator bootstrap is now just `services.hyperhive.ci.enable = true`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `services.hyperhive.ci` NixOS module that spins up a `hive-ci`
nixos-container running `gitea-actions-runner` against the hive-forge
Forgejo instance. Off by default; opt in with `ci.enable = true` after
generating a runner registration token in Forgejo.
Also adds `.forgejo/workflows/ci.yml` with four jobs: nix flake check,
formatting (nix fmt + cargo fmt), cargo test, and cargo clippy. Jobs
target the `hive-ci` runner label.
Container design mirrors hive-forge (shared host netns, non-ephemeral
state, loopback reach to forge). sandbox-fallback = true since nspawn
containers can't create user-namespaces for nix sandbox.
Closes#175.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
environment.variables writes to /etc/environment (PAM sessions only)
and is not visible to systemd service units. The correct path for
env vars that need to reach all systemd services is
systemd.globalEnvironment (sets DefaultEnvironment in systemd.conf),
which is the pattern established by #608 for HYPERHIVE_STATE_DIR.
Also update the inline comment reference from environment.variables
to systemd.globalEnvironment.
Services started by systemd in a gui-enabled container didn't have
WAYLAND_DISPLAY set, so Wayland clients couldn't find the compositor.
libwayland would fall back to a headless display or error out, leaving
apps running invisibly while the VNC session showed a blank weston
desktop (the double-screen problem).
Fix in weston-vnc.nix:
- Pass --socket=wayland-0 to weston so the socket name is
deterministic (weston normally picks any free wayland-N name).
- Set WAYLAND_DISPLAY=wayland-0 and XDG_RUNTIME_DIR=/run/user/0 as
global environment.variables gated on hyperhive.gui.enable, so
every service in the container inherits them automatically.
- Update gui.json to include wayland_display for tooling that reads it.
Update docs/gotchas.md with the rationale and pointer to #540.
The #872 path-unit approach was silently broken: IN_MOVED_TO from an
atomic rename on the host does not propagate across the nspawn
mount-namespace boundary into the container, so the watcher inside
the gateway container never fired.
Fix: after each agents.conf write, c0re calls
`systemd-run --machine=hive-gateway -- nginx -s reload` from the host.
The reload is best-effort (logged on failure, not fatal).
Remove the now-unused `hive-gateway-agents-conf.path` +
`hive-gateway-nginx-reload.service` from the gateway container config.
Update docs/gateway.md + comments to reflect the host-side approach.
Dashboard links to the forge were hardcoded as http://<hostname>:3000,
which breaks when the operator accesses the dashboard through
hive-gateway (forge is served at forge.<domain> with no port).
- nix/modules/hive-c0re.nix: inject HIVE_FORGE_PUBLIC_URL when
forge.behindGateway = true (e.g. http://forge.pr1ma.darkest.space)
- hive-c0re/src/dashboard.rs: expose forge_url: Option<String> in
StateSnapshot, reading from HIVE_FORGE_PUBLIC_URL
- frontend/packages/dashboard/src/tabs.js: use state.forge_url when
present; fall back to http://<hostname>:3000 for gateway-off /
local-dev deploys
docs/web-ui.md (1315 lines) split into three sub-files:
- docs/web-ui/shape.md — shared SPA skeleton, SSE multiplexing,
Worker-death self-heal, terminal pane, listener bind, relative
paths, atomic repaint, side panel
- docs/web-ui/dashboard.md — SW4RM/Y3R/SYST3M/SCH3DUL3S/S3TT1NGS
tabs, container row, topology tree, selection bar, approval card,
dashboard endpoints + event channel
- docs/web-ui/agent.md — header, terminal, composer, inbox, live
view, slash commands, per-agent endpoints, stats page
docs/web-ui.md replaced with a thin index linking all three.
Section anchors in docs (gateway.md, gotchas.md), Rust doc comments
(hive-ag3nt/src/web_ui.rs), and nix/templates/weston-vnc.nix updated
to point at the correct sub-file. README and CLAUDE.md file-map
updated with sub-file links. Inline // comments in frontend source
left unchanged (they reference the index which redirects to the right
sub-file).
- gateway_nginx.rs: HOST_CONF_PATH → /var/lib/hyperhive/gateway/agents.conf
- hive-gateway.nix: hostPath = /var/lib/hyperhive/gateway (not whole parent
dir — avoids exposing forge tokens or other credentials to the gateway
container)
- tmpfiles: add /var/lib/hyperhive/gateway/ dir rule + seed agents.conf there
- scrub "(#869)" from hive-gateway-nginx-reload service description
Replace eval-time per-agent nginx location baking with a runtime
include file. c0re writes /var/lib/hyperhive/agents.conf (nginx
location blocks, UDS or TCP per agent) on every topology change and
on the 10s marker poll. The gateway container bind-mounts
/var/lib/hyperhive/ at /run/hive-state/ and nginx includes
/run/hive-state/agents.conf. A systemd path unit inside the container
watches the file for changes and fires `nginx -s reload` on each
atomic rename from c0re — no nixos-rebuild switch needed when agents
start, stop, or flip useUnixSocket.
- new hive-c0re/src/gateway_nginx.rs: write() + render()
- lib.rs + meta.rs + agent_sockets::spawn_poll: hook in write()
- hive-gateway.nix: drop agentPortsTable/agentSocketsTable/
agentUpstreamFor/lib.mapAttrs', add /run/hive-state bind-mount,
include directive, systemd path unit + reload service, tmpfiles
for /var/lib/hyperhive + agents.conf seed
- docs/gateway.md: update vhost table + Per-agent UDS section
nginx-pre-start runs the config test as the nginx user; a 0600
root-owned key fails that check with BIO_new_file Permission denied
even though the master process would later load it as root.
Parent /var/lib/hive-gateway came out 0700, blocking the nginx-user
worker from reaching the 0755 tls/ subdir and surfacing as a generic
cert-load failure at nginx start.
When `services.hyperhive.gateway.enable` is on (default), the c0re
NixOS module now sets `HIVE_GATEWAY_ENABLED=1` on the service env.
`/api/state` exposes the flag as `gateway_enabled`; the SW4RM tab's
container-row renderer flips three link sites (primary agent-name
link, favicon fetch, `container`-kind nav-strip links) from the
legacy `http://<host>:<port>/` direct TCP shape to same-origin
`/agent/<name>/` URLs — the gateway proxies them to the per-agent
harness via `agent-ports.json` or `agent-sockets.json` (#784 / #815).
Gateway-off deploys keep the direct TCP fallback so local-dev /
operator opt-out keeps working.
`forge`-kind nav-strip links still resolve against `:3000` (separate
sub-domain transition, tracked by `forge.behindGateway`);
`external`-kind links are already absolute.
Mirrors the `HIVE_MATRIX_GUI_ENABLED` env→snapshot-flag pattern.
Docs updated: `docs/web-ui.md::Container row` + new
`docs/gateway.md::Dashboard link shape` section.
forge/matrix subdomain address rules are redundant when those are
sub-domains of hyperhive.domain (dnsmasq's /<domain>/ matches
sub-domains). Keep explicit for the cross-domain case (e.g.
forge.domain = "git.example.com") + add a comment explaining
the trade-off.
Stand up the host-side bridge interface + per-agent DNS resolver
ahead of #14 (netns isolation). Mara on #805#11541: "we need it
before #14 so nothing breaks when we switch over". v1 ships the
endpoint live but containers stay on shared host netns — when #14
flips them to private netns the DNS contract is already there.
Shape:
- new `nix/modules/hive-network.nix` with `services.hyperhive.network.*`
options (enable + bridgeName + bridgeIp + bridgePrefixLength +
upstreamDns). Default off. Imported from `hive-c0re.nix`.
- bridge interface via `networking.bridges` (no slave NICs at v1;
per-agent veth pairs attach once #14 lands).
- bridge IP assigned via `networking.interfaces`.
- `networking.firewall.interfaces.<bridge>.allowed{UDP,TCP}Ports =
[ 53 ]` opens the resolver on the bridge interface only —
other interfaces stay closed.
- dnsmasq config added to the existing `hive-gateway` container
(mara on #805:10957: "put the resolver into the gateway container").
Listens only on `bridgeName` + `lo`; authoritative for
`<hive-domain>`, `forge.<hive>`, `matrix.<hive>` answering with
the bridge IP; forwards everything else to upstream.
`resolveLocalQueries = false` keeps the gateway container's own
resolver untouched.
Asserts `services.hyperhive.domain != null` + `gateway.enable =
true` — both required for the resolver to be meaningful.
Docs: new `docs/network.md` covering v1 vs v2 split, container shape
rationale, default addressing, resolver behaviour, firewall posture.
`nix flake check` clean.
Two new sections housing rationale that was inline in
`harness-base.nix`:
- `docs/conventions.md::Best-effort oneshot services` — shape
contract shared by `tea-login`, `forge-avatar-sync`, and
`matrix-avatar-sync`: always exit 0, no `set -e`, skip silently
on missing prerequisites, wired to multi-user.target,
re-runnable. Plus the artefact-under-agent-home + service-stays-
root posture and the operator-visible-via-journalctl trade-off.
- `docs/persistence.md::matrix-avatar-sync` — the two-step `media
upload` → `set avatar_url` matrix-spec dance + why
`RemainAfterExit = false` (so the `.path` watcher's re-fire on
token appearance actually re-executes the unit).
In-code comments trim to one-liner purpose + pointers; the script
bodies stay (they're the actual implementation) but their inline
`#` rationale comments collapse where the docs cover them.
`description = ''…''` blocks (operator-facing options docs)
preserved per iris #718.
`nix flake check` clean; `nix fmt` clean.
Defensive against the fresh-boot-before-any-agent-spawn window where
the bind-mount source wouldn't exist from c0re's per-agent
`set_nspawn_flags` mkdir chain yet. nspawn auto-creates missing
sources, so this is intent-explicit rather than functional fix.
Switch per-agent gateway upstreams from TCP loopback to unix-domain
socket when the agent has opted in via `hyperhive.web.useUnixSocket`
(#822). Coexists with the TCP path during rollout.
Changes:
- New `agentSocketsFile` option (default
`/var/lib/hyperhive/agent-sockets.json`) — c0re writes the map
there via `hive_c0re::agent_sockets::write` (#809).
- `agentSocketsTable = lib.importJSON ...` (graceful empty when
file missing).
- `agentUpstreamFor name port` picks `http://unix:<path>:/` when the
socket has a JSON entry AND the file exists at eval time; else
`http://127.0.0.1:<port>/`. Path-exists gate guards against
c0re's blanket-emit shape during the canary window (agents in
`agent-sockets.json` who haven't actually flipped have no
bound socket on disk → fall back to TCP). Damocles will ship a
`.bound` marker filter on the c0re side (#784 step 2d
follow-up); once that's in, the path-exists check is redundant
but harmless. Step 4 drops it entirely along with the TCP
fallback.
- `containers.hive-gateway.bindMounts."/run/hive-agent"` —
read-only, unconditional. Inert when no agents have opted in.
Required so nginx inside the gateway container can `connect(2)`
to the per-agent sockets damocles's #813 bind-mounts into agent
containers at the same paths.
Docs:
- `docs/gateway.md::Per-agent UDS upstream (#784)` — full rollout
flow, subdir-bind rationale (damocles #813), eval-time gate
explainer, step 4 drop plan.
`nix flake check` clean; `nix fmt` clean.
Canary plan: once #822 (`useUnixSocket` option) lands + this PR
merges, manager flips atlas's agent.nix to `useUnixSocket = true`
via the config-update flow. End-to-end validation against atlas
before broader rollout.
closes#815. last harness-side piece of the #784 phase 2 rollout.
new option `hyperhive.web.useUnixSocket` (default false). When true,
the harness service env gets HIVE_WEB_SOCKET set to the canonical
/run/hive-agent/${userName}/web.sock path — making web_ui::serve
bind a UnixListener (PR #800) instead of TCP. Path matches
hive_c0re::agent_sockets::socket_path_for(name) so the c0re
bind-mount (#813) and the gateway's eventual upstream config all
derive from the same canonical shape (no triangulation drift).
Default false so an agent's web UI keeps binding TCP until the
per-agent flip is explicit. Rollout shape:
1. flip one canary agent (atlas volunteered) to true via agent.nix
2. validate atlas's gateway-side step 3 against that canary
3. flip remaining agents per-agent as the gateway side soaks
4. eventually drop this option once everyone's on unix (step 4 of #784)
Sub-agent-only by design — manager UI serves at / via the c0re
dashboard upstream, never via /agent/<name>/, so the per-agent
unix path is irrelevant for it (the env var is set unconditionally
for code simplicity; the manager's bind socket would just sit
unused).
Move the harness systemd unit rationale (role-driven unit name,
manager-only forge defaults, PATH wrapper-dir trick, env vars,
standalone-eval fallbacks, RuntimeDirectory + User= reasoning)
from `nix/templates/harness-base.nix` to a new
`docs/agent-hierarchy.md::Harness systemd unit shape (per-role)`
section.
In-code comments trim to short purpose statements + pointers; the
PATH /bin auto-append behaviour is already documented in
docs/gotchas.md, so the harness file just cross-refs both.
`description = ''…''` blocks (operator-facing options docs)
preserved per iris #718.
`nix flake check` clean; `nix fmt` clean (after formatter pass).
mara on #755: "e.g. /agent/name should show an error page stating
that the agent could not be found if missing in json or that it is
not reachable if we get a connection error. we dont want a fully
generic fallback, only for routes already special cased in the
nginx config."
Adds two static HTML pages built at deploy time via
`pkgs.runCommand "hyperhive-agent-error-pages"`:
- **not-found.html** — served when `/agent/<unknown>/...` hits the
bare `/agent/` catch-all. The catch-all `return 404`s, and
`error_page 404 = /__hive_agent_not_found` rewrites to the static
page.
- **unreachable.html** — served when `/agent/<known>/...` proxy_pass
to the harness returns 502 / 503 / 504. `proxy_intercept_errors
on` + `error_page 502 503 504 = /__hive_agent_unreachable` on each
per-agent location block rewrites to the static page.
Mechanics:
- `agentErrorPagesDir` (in the `let` block) is a `runCommand` that
emits two HTML files using a `<<EOF` heredoc — no template engine
needed.
- Two `internal` nginx locations (`= /__hive_agent_not_found`,
`= /__hive_agent_unreachable`) `alias` the exact files. `internal`
keeps the URIs unreachable from direct operator request — only
nginx's own error-handling can hit them.
- Per-agent location blocks pick up the `error_page` directive
through the existing `lib.mapAttrs'` over `agentPortsTable`. No
per-agent generated content; same static page for all.
- `/agent/` catch-all generates from a tiny optionalAttrs alongside
the per-agent block — both are no-op when the agent table is
empty (matches the pre-#15 shape).
Pages: minimal inline CSS, catppuccin palette matching the
dashboard (`#1e1e2e` bg, `#cdd6f4` text, `#cba6f7` not-found heading,
`#f9e2af` unreachable heading). No frontend-dist dependency — render
even when hive-c0re is down. Both link back to `/`.
Per mara's "only for routes already special cased" — scope stays
narrow. Forge / matrix / fluffychat keep nginx defaults; extending
the custom-error pattern to other vhosts is a separate follow-up
if/when needed.
Verified:
- nginx location attrset has `["/", "/agent/", "= /__hive_agent_not_found", "= /__hive_agent_unreachable"]`
- container toplevel builds clean (`nixos-system-hive-gateway-26.05pre-git`)
- `docs/gateway.md::Per-agent error pages` section captures the
design + rationale + intentional narrowness
Closes#755.
Continues #718 docs-extraction. Three more blocks moved to
`docs/gateway.md` (which already houses the gateway architecture
story from #775):
1. **Firewall posture (gateway on vs off)** — was a 22-line block
above `networking.firewall = lib.mkIf ...` in hive-c0re.nix.
Trimmed to 3-line ref. New `docs/gateway.md::Firewall posture
(host-level)` section covers the gateway-on / gateway-off
trade-off + why dashboard port stays loopback-only.
2. **`HIVE_FORGE_URL` loopback rationale** — was a 14-line block
above the env-var assignment. Trimmed to 5-line ref. New
`docs/gateway.md::HIVE_FORGE_URL: loopback for in-cluster,
sub-domain for the operator` section covers the in-cluster vs
external split + why agent containers can't use the sub-domain.
3. **hive-forge container shape** — was a 15-line top-of-`config`
block in hive-forge.nix explaining the nixos-container + host
netns choices. Trimmed to 4-line ref. New
`docs/gateway.md::hive-forge container shape` section captures
the same content with state-dir + wipe-via-destroy notes.
Net: hive-c0re.nix -29 lines, hive-forge.nix -11 lines, gateway.md
+44 lines. Same pattern as #782 (first pass) per iris's #10114
guidance — substantive WHY moves to docs as named sub-paragraphs,
in-code shrinks to `// see docs/<file>::<section>` refs.
Verified: `nix eval` on agent-base toplevel still resolves
cleanly; firewall posture unchanged (still 0 ports opened in the
gateway-on case + the same 8100..8999 range in the gateway-off
case).
Continues #718. Follow-up batches: remaining harness-base.nix
blocks, nix/docs/default.nix, nix/assets.nix, nix/templates/weston-vnc.nix.
mara: 'if we replace it with one thing, that should be named more
generic so we dont have to change it for future additions'.
extract the BASH_ENV plumbing into a shared shape:
- new internal option `hyperhive._bashEnvFragments` (types.lines)
accumulates shell snippets across feature modules.
- file path is now `/etc/hyperhive/bash-env.sh` (was the
cargo-specific bash-cargo-short.sh).
- the file + BASH_ENV + interactiveShellInit are gated on
`_bashEnvFragments != """ so a fully feature-disabled agent has
no overhead.
cargo function moves to a `lib.mkIf cargo.shortMessages` contribution
to `_bashEnvFragments` — same behaviour, no rename when the next
hook (nix-env helper, claude-cmd helpers, whatever) lands.
closes#777. saves tokens by collapsing per-crate progress lines into
warning/error summaries when claude (or the operator) runs cargo
inside an agent container.
implementation: /etc/hyperhive/bash-cargo-short.sh defines a 'cargo'
bash function that injects '--message-format short' on compile
subcommands (build/check/clippy/test/run/doc/bench/install/rustc/fix).
loaded via BASH_ENV in non-interactive shells (claude's Bash tool
runs 'bash -c') and via programs.bash.interactiveShellInit in
interactive shells (operator SSH inside the container).
handles the '+toolchain' selector (cargo +nightly build), skips
injection when the caller already passes --message-format (any
form), leaves third-party cargo-* subcommands alone.
new option: hyperhive.cargo.shortMessages (default true) — agents
that parse cargo json output should set false.