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.
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.
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.
claude --effort now accepts low, medium, high, xhigh, max.
Expand EFFORT_LEVELS from 3 to 5, update NixOS enum + description,
and fix the stale doc comment in web_ui.rs.
Closes#2220
The docs evaluation referenced self.nixosConfigurations.agent-base and
self.nixosModules.default — both tied to self's full flake store path.
Every commit produces a new self hash (even when only Rust or frontend
files change), so docs drvs change on every commit and the remote
builder (muede-pc2) must rebuild them from scratch each time.
Fix: use builtins.path on the nix/ directory with
builtins.unsafeDiscardStringContext to produce a content-addressed store
path (nixSrc) that only changes when .nix files change. Evaluate both
hostEval and agentEval from nixSrc using stub package args (pkgs.emptyFile /
pkgs.emptyDirectory) instead of self.overlays.default, which avoids
pulling in the Rust and frontend build closures.
Now docs drvs only change when a .nix file changes. PRs that touch only
Rust or frontend code hit the remote builder cache instead of forcing a
rebuild. Closes the disk-pressure-induced CI failure loop on muede-pc2.
Forge URLs in transformOptions updated to prepend nix/ prefix correctly
(nixSrc is a copy of nix/, so stripped paths need nix/ re-added).
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.
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).
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.
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.
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.
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.
Add `baseClaudeEnv` (always-on) and extend `otelSettingsEnv` with the
env vars from #2149:
Always-on (every agent):
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
- CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1
- CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1 (recovers MCP-flap mid-turn)
- CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=1
- CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1
- CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX=hive+<agent>
- DISABLE_AUTOUPDATER=1 / DISABLE_UPDATES=1 (nix owns packages)
- DISABLE_INSTALL_GITHUB_APP_COMMAND=1
- DO_NOT_TRACK=1
- ENABLE_CLAUDEAI_MCP_SERVERS=0 (hive supplies its own)
- FORCE_AUTOUPDATE_PLUGINS=1
OTEL-gated (when hyperhive.otel.enable):
- CLAUDE_CODE_ENABLE_FEEDBACK_SURVEY_FOR_OTEL=1
- CLAUDE_CODE_OTEL_DIAG_STDERR=1
- OTEL_METRICS_INCLUDE_VERSION=1
Because baseClaudeEnv includes per-agent values
(CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX = "hive+${userName}"),
jq is now always run at build time — the OTEL-off branch that returned
the static asset verbatim is removed.