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.
Security fixes landed without updating the security-model docs; bring them
current as present-tense invariants:
- security.md: matrix identities are public / cross-agent-readable via
GetAgentMeta while the token stays private; name validation extends to
GetAgentMeta; the nix-build blast radius has a network dimension (hive-ci
netns) alongside the 0600 token policy.
- boundary.md: the core dashboard /api has no app-layer auth — protection is
the gateway plus network isolation; stated as a standing invariant.
- ci.md: hive-ci runs in its own private netns behind the bridge, reaches the
forge via the gateway, and cannot reach host-loopback (dashboard/forge port).
- knowledge.md: /knowledge excludes .git via tmpfs so the core token in
.git/config isn't readable by agents.
Refs #2308, #2310, #2313.
claude emits a started tick then a completed tick per plugin install;
previously each got its own row (loading... then done) instead of one
line updating in place. Same coalescing pattern already used for
thinking_tokens and the generic status tick.
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.
The is_descendant_of and apply_set_parent cycle detection both used
hand-rolled 32-hop bounded ancestor walks. Correct in practice (no
real hive exceeds 32 levels) but carried an arbitrary ceiling and were
harder to reason about than proven graph primitives.
Changes:
- Add build_graph(): converts BTreeMap<name, parent|null> → DiGraph
with parent→child edges + BTreeMap<name, NodeIndex> index
- Add is_descendant_of_in(): pure (no disk I/O), uses
petgraph::algo::has_path_connecting from ancestor to candidate
- Rewrite is_descendant_of(): delegates to is_descendant_of_in(&read())
- Rewrite apply_set_parent() cycle detection: build_graph() + speculative
edge + is_cyclic_directed(); no depth limit
- Add tests for is_descendant_of_in (self, direct child, grandchild,
parent-is-not-child, sibling, unknown)
petgraph was already a workspace dep (used elsewhere). On-disk format
unchanged (flat JSON map). Public API surface unchanged.
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.
Claude's generic `system/status` subtype carries no detail beyond the
bare label, and previously each tick got its own note row. During a
compaction pass (which emits a burst of these with no other signal
before the completed compact_boundary) this looked like a wall of
identical noise followed by silence, making a routine compaction look
stuck.
Collapse consecutive status ticks into one updating row, same pattern
already used for the thinking_tokens counter.
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.
The knowledge repo is cloned with credentials embedded in .git/config;
those credentials survive on disk and the entire LOCAL_DIR (including
.git/) is bind-mounted read-only into every agent container. Any
agent (or prompt-injected PR build) can read the site-admin token via
cat /knowledge/.git/config.
Defense-in-depth layer: in write_nspawn_flags, append
--tmpfs=/knowledge/.git after the --bind-ro=.../knowledge flag whenever
the /knowledge bind mount is present. systemd-nspawn processes the tmpfs
after the bind, overlaying an empty in-memory directory over .git/ inside
the container. Agents see the working-tree documents but not the repo
metadata or stored credentials.
This is independent of the complementary fix in workers/knowledge.rs
(stop embedding credentials in .git/config in the first place). Both
layers together provide defense-in-depth: even if a future credential
accidentally reaches .git/config, it stays off the container floor.
Closes part of issue 2308 (bind-mount isolation layer).
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.