mara: "no, do split packages for the separate daemons" — instead of
grouping all daemon/harness/MCP bins into one hyperhive-daemon
derivation, each bin (hive-c0re, hive-priv, hive-agent,
hive-agent-mcp, hive-agent-wake, hive-bash-daemon, hive-bash-mcp,
hive-matrix-daemon, hive-matrix-mcp, hive-metric) is now its own
named package + overlay output, matching the existing hivectl /
hive-forge split. harness-base.nix's ExecStart/command lines now
point at the specific pkgs.hive-* each ExecStart needs, and
environment.systemPackages only carries the two bins actually
PATH-looked-up in-container (hive-agent-wake, hive-metric) instead
of the whole bundle.
Agent containers pulled in `pkgs.hyperhive` (packages.default: daemonBins +
hivectlPkg + hiveForgePkg) via environment.systemPackages and every
harness ExecStart. That shipped hivectl (a host-admin CLI dialing the
host admin socket, unreachable from inside a container, wrapped with
wireguard-tools for `hivectl wg`) plus a redundant second hive-forge
copy (already provided separately via hive-forge-tools.nix) into every
single agent's closure.
Expose the existing daemonBins split as packages.<system>.daemonBins
(mara: 'there should be split nix outputs already, use those' — no new
grouping derivation) + a hyperhive-daemon overlay entry, and repoint
harness-base.nix's systemPackages entry and all agent-run ExecStarts
(hive-agent, hive-agent-mcp, hive-bash-daemon, hive-matrix-daemon) plus
the bash/matrix MCP bridge commands at it instead of the full bundle.
nix flake check: daemonBins + agent-base-toplevel + ruth-toplevel all
evaluate cleanly (docs derivation failure is a preexisting dirty-tree
issue, unrelated).
- docs/network.md: LOCAL_ADDRESS is now empty (not deterministic-IP);
containers use dhcpcd + dnsmasq DHCP pool, not hash-derived static IPs
- nix/modules/hive-ci.nix: 'service containers' → 'all containers'
in the dnsmasq DHCP pool comment (agents also use the same pool)
All agent containers now receive their bridge IP dynamically via DHCP
from the dnsmasq pool instead of a hash-derived static address:
- nix/templates/harness-base.nix: networking.useDHCP = true
- nix/modules/hive-gateway.nix: expand DHCP pool to full usable range
(.2 to .254 on /24) — was last-14-IPs-only
- hive-sh4re/src/priv_proto.rs: remove agent_ip from NetworkIsolation
- hive-c0re/src/lifecycle/mod.rs: drop agent_network_ip + DHCP_POOL_SIZE
- hive-c0re/src/lifecycle/host_config.rs: remove agent_network_ip call
- hive-priv/src/main.rs: LOCAL_ADDRESS= empty (DHCP assigns IP);
HOST_ADDRESS still set so nixos-container installs default route
before the DHCP lease arrives
- nix/dhcp-pool-size: deleted (no longer needed)
The nix/dhcp-pool-size single-source-of-truth file and all associated
Rust/Nix dual-constant plumbing are gone — there is no static map.
bridge_gateway_ip() is retained (still needed for HOST_ADDRESS).
Closes#2363
The DHCP pool exclusion is now by construction (agent_slots = usable -
dhcpPoolSize), not a secondary-hash remap. Update two comment blocks
that still referenced the old approach.
Move the DHCP pool size constant out of the two separate definitions
(Nix literal + Rust const) into a shared data file: nix/dhcp-pool-size.
- nix/dhcp-pool-size: new file, contains '14'
- hive-gateway.nix: reads via builtins.readFile + toIntBase10
- lifecycle/mod.rs: parses via include_bytes! const block at compile time
Cargo automatically tracks include_bytes! as a file dependency so a
change to nix/dhcp-pool-size triggers recompilation without build.rs.
Add a DHCP pool to the gateway's dnsmasq so bridge-attached service
containers (hive-ci and future equivalents) get their addresses from
a proper DHCP server instead of a brittle static derivation.
gateway (hive-gateway.nix):
- Add IPv4 arithmetic helpers (ipToInt, intToIp, pow2) to compute the
DHCP pool range at nix eval time from bridgeIp + bridgePrefixLength.
- Reserve the last dhcpPoolSize (14) usable host addresses as the DHCP
pool (e.g. .241-.254 on a /24 with 10.42.0.0 network).
- Add dhcp-range and dhcp-leasefile to the dnsmasq settings block.
The pool is active whenever services.hyperhive.network.enable is true.
hive-ci (hive-ci.nix):
- Remove the ciBridgeIp / ciBridgeOctets static derivation and the
brittle top-of-/24 comment block.
- Switch networking.interfaces.eth0 to useDHCP = true so hive-ci gets
its address from the gateway DHCP pool.
lifecycle (mod.rs, tests.rs):
- Add DHCP_POOL_SIZE = 14 constant (must stay in sync with
dhcpPoolSize in hive-gateway.nix).
- Remap agents whose FNV-1a hash falls in the DHCP pool into the
agent-only window [2, dhcp_start - 1]. Only the rare agent whose
name hashes into the pool is affected; all others keep their IPs.
- Update and extend tests: agent range is now .2-.240 on /24;
add agent_network_ip_never_in_dhcp_pool covering 18 agent names.
Both webhook registrations (knowledge push + config-PR pull_request) now
use the public hive domain instead of loopback:
https://<HYPERHIVE_HIVE_DOMAIN>/webhook/{knowledge,config-pr}
This routes deliveries through the gateway, bypassing the Forgejo SSRF
guard that blocked loopback delivery and silently broke the config-PR
merge flow since launch.
Changes:
- webhook_secret: new module — auto-generate + persist a 32-byte HMAC
secret to STATE_ROOT/webhook-secret on first startup; verify
X-Hub-Signature-256 on every incoming webhook POST (HMAC-SHA256).
- forge/mod.rs: ensure_config_pr_webhook now takes hive_domain +
webhook_secret; sets secret in Forgejo hook config.
- workers/knowledge.rs: ensure_webhook same update.
- dashboard/webhook.rs: both handlers read raw Bytes first, verify HMAC,
then parse JSON. Returns 401 on signature mismatch.
- dashboard/mod.rs: AppState carries webhook_secret; serve() takes it.
- main.rs: load/generate secret at startup; pass to registration tasks
+ dashboard; add 5-minute config-PR polling fallback task.
- forge/config_pr_poll.rs: new — scan agent-configs/* for open PRs with
no pending MergeConfigPr approval; queue them. Idempotent.
- stores/approvals.rs: has_pending_merge_config_pr() for poll dedup.
- nix/modules/hive-gateway.nix: remove dashboardAuth from /webhook/
location (HMAC replaces basic auth for webhook endpoints; Forgejo
cannot send HTTP Basic credentials with webhook deliveries).
On a fresh deploy Persistent=true causes the weekly timer to fire
immediately. Without this ordering hive-tls-resign can race
hive-tls-ca before the CA key is written, failing the resign.
CA is always valid when this runs; resign is a no-op if the leaf
is still within its half-life margin.
Suggested by argus (review) + requested by mara.
The parked PR (#1934) triggered re-sign by restarting hive-tls-ca
(a RemainAfterExit oneshot), which mara flagged as a hack. This
replaces that with a dedicated hive-tls-resign.{service,timer}:
- hive-tls-ca unchanged — still handles boot-time CA generation +
initial leaf signing; RemainAfterExit, runs before the gateway.
- hive-tls-resign.service — runs the leaf re-sign directly (openssl
in its own PATH); does NOT touch hive-tls-ca. Re-signs only when
the leaf is within half its validity of expiry, so the weekly timer
catches short-lived (30d default) leaves well before they lapse.
Checks sha256 before/after; if the leaf rotated it propagates into
the running gateway via systemctl -M hive-gateway (mirroring how
hive-c0re reloads the gateway after agents.conf writes — the
documented correct approach; an inotify path unit inside the
container does not work across the nspawn mount-namespace boundary).
|| true on the propagation calls so a stopped gateway never fails
the unit (its next boot imports the already-rotated leaf).
- hive-tls-resign.timer — OnCalendar=weekly, Persistent=true (fires a
missed run on next boot so a host that was off on the scheduled day
catches up rather than letting the leaf lapse silently).
- leafValidityDays doc updated to reflect automatic renewal.
Closes#1933.
hive-ci is the one container on the hive bridge that used DHCP
(networking.interfaces.eth0.useDHCP = true, from #2310 / #2336). But the
bridge has no DHCP server: dnsmasq on the bridge is DNS-only and agent
containers get deterministic static IPs (lifecycle::agent_network_ip),
so the DHCP client never gets a lease and the container hangs at boot:
A start job is running for DHCP Client (5s / 1min 31s)
Assign a static address on eth0 (top host address of the subnet) plus a
default route via the bridge gateway, and drop useDHCP. Preserves the
#2310 netns isolation (no host-loopback reach) while letting the
container boot and reach the forge through the gateway.
Refs #2310, #2336.
nixos-containers.nix sets networking.useDHCP = false for every
container (to suppress the deprecated global DHCP warning). Setting
networking.useDHCP = true in the hive-ci inner config conflicts with
no priority differential → eval error on hosts running current main.
Fix: drop the global useDHCP = true and configure DHCP on eth0
(the inner veth interface name assigned by systemd-nspawn when
hostBridge is used). This is both conflict-free and the correct
modern NixOS practice for per-interface DHCP.
The container-level nix.settings.fallback = lib.mkForce true (added as an
interim safety net so a down remote builder degrades gracefully rather than
hard-failing CI) is rendered redundant by the host-daemon routing fix: with
NIX_REMOTE=daemon set for all systemd units in the container, every nix
build invocation routes through the host daemon, which already has its own
fallback policy. The container setting would only apply to nix invocations
that bypass the daemon entirely — a case that should not exist post-deploy.
NOTE: draft — gated on the daemon-routing change (PR that adds
NIX_REMOTE=daemon to systemd.globalEnvironment + hive-priv service env)
being deployed and confirmed working in production before merging.
Give hive-ci its own network namespace (privateNetwork=true, hostBridge)
instead of sharing the host netns. This closes the path by which a
prompt-injected PR nix build could reach the unauthenticated dashboard
at 127.0.0.1:7000.
The runner now reaches the forge through the gateway:
- networking.extraHosts maps forgeCfg.domain → bridgeIp
- runner url = http://${forgeCfg.domain} (gateway port 80, addSSL=true,
no HTTP→HTTPS redirect — HTTP is served alongside HTTPS)
- networking.useDHCP = true for the bridge-attached veth
- networking.nameservers = [bridgeIp] (dnsmasq on bridge, external fwd)
Ports 80 and 443 are already open on the bridge firewall (hive-network.nix).
The bridge→127.0.0.0/8 DROP rule (defence-in-depth) is unchanged — CI
can no longer reach 127.0.0.1:{7000 dashboard, forge httpPort, etc.}.
Requires forge.behindGateway=true (the default when hyperhive is enabled)
— asserted so misconfigured hosts get a clear build error.
Note: the host-side hive-ci-prefetch script still uses
127.0.0.1:${httpPort} directly (it runs on the host, not in the
container) — no change needed there.
reqwest has no UDS transport, so unix: upstreams dial the socket
directly with a raw hyper/1.1 client per request instead. http(s)://
upstreams are unaffected (still go through the existing reqwest path).
Adds hyper (client, http1), hyper-util (tokio IO adapter), and
http-body-util as direct hive-ag3nt dependencies - all three were
already present transitively via reqwest, this just uses them
directly for the new code path.
Agent containers currently mount /tmp as a 3.2 GB tmpfs (RAM-backed).
Large scratch writes (nix-develop shells, cargo build dirs) eat container
RAM unnecessarily and can exhaust the tmpfs, losing in-flight work.
Set boot.tmp.useTmpfs = false so /tmp is disk-backed instead.
boot.tmp.cleanOnBoot (default true) preserves the ephemeral-per-boot
semantics — /tmp is cleared on container start, same as before.
Closes#2264. Supersedes PR #2299 (TMPDIR redirect + state/tmp).
Both mdNode implementations (agent UI app.js, dashboard common.js)
assigned marked.parse() output straight to innerHTML with no
sanitizer. marked v5+ dropped its built-in sanitize option, and there
was no DOMPurify anywhere in frontend/, so markdown containing raw
HTML/script tags rendered live in the browser.
Both sinks receive untrusted input in practice: the agent UI's mdNode
renders recv tool_result bodies, assistant prose, and send/ask/answer
payloads sourced from peer agents and matrix-relayed messages (the
documented prompt-injection adversary); the dashboard's mdNode renders
agent-authored .md files served verbatim by GET /api/state-file
(the endpoint validates path, not content). Since the per-agent UI and
dashboard are same-origin behind the gateway with operator-authority
endpoints (approve/spawn/rebuild/destroy/answer-question), injected
script would run with the operator's session.
Fix: DOMPurify.sanitize() the marked.parse() output at both sinks
before assigning to innerHTML. Added dompurify as a dependency to
both the agent and dashboard npm workspaces, recomputed npmDepsHash
in nix/frontend.nix for the updated lockfile. Also corrected
docs/web-ui/shape.md, which claimed the markdown-rendering path was
XSS-safe by construction the same way the text-node-based linkify
path is — it isn't; it's safe because it's sanitized.
CSP hardening for the dashboard (no unsafe-inline) is a separate,
larger backend change (response headers in hive-c0re) and is left as
a fast-follow rather than folded into this fix.
Add [output] section to weston.ini with name=VNC-1 mode=1280x720.
The VNC backend's output is named VNC-1; the mode string sets the
compositor's framebuffer size that clients connect to.
Replace the hardcoded FORGE_HTTP const with forge_http_base() which
reads HIVE_FORGE_URL from the environment (already set unconditionally
by hive-c0re.nix to http://<forge.domain>). Add forge_git_url() helper
that inserts core:<token> credentials between scheme and authority for
git push/clone URLs.
All call sites updated:
- forge/mod.rs: api() OnceLock + new forge_git_url/forge_http_base fns
- forge/repos.rs: push_meta, push_config, ensure_meta_remote
- forge/pr_merge.rs: tokenised_repo_url delegate + test loosened
- workers/knowledge.rs: clone + push URLs
- socket_server/mod.rs: clone_url in RepoCreated response
No new env var: HIVE_FORGE_URL was already the right knob (mara).
Closes#1868. Closes#2174 (this supersedes the operators-team fix from
the closed#2218, which is re-applied in the ensure_operators_team call
that was already merged separately).
Adds hive-xdg-icons package (nix/packages/hive-xdg-icons.nix) that
rasterizes the branding SVG to all standard hicolor sizes (16–256px)
and installs /usr/share/pixmaps/hyperhive.png as a flat fallback.
Ships .desktop entries for hive-c0re and hive-ag3nt with NoDisplay=true
so desktop environments can match running processes to their icon without
cluttering the application launcher.
Narrow drv input: only ./branding/hyperhive.svg, so unrelated source
changes don't bust the cache.
Wired into environment.systemPackages in hive-c0re.nix (host side)
so the icons are present wherever hive-c0re is deployed.
Closes#2228
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.
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
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.
- Option doc: 'Default points at the in-host tuwunel (shared netns)'
→ updated to reflect that c0re forwards the gateway URL at runtime
- DNS oneshot comment: 'inert in shared-netns mode' → removed since
isolation is always on; oneshot always runs
Agents run in private netns (always-on isolation) and can't reach
host loopback directly. hive-c0re forwards HIVE_MATRIX_URL set to
`matrix.<domain>` via the gateway. Update two places that still
referenced the removed shared-netns path.