Under network isolation, agents lost DNS resolution of the hive's own
zones (forge.<domain>, matrix.<domain>): the hyperhive-isolated-dns
oneshot points resolv.conf at the bridge resolver, but resolvconf
regenerated it from host-tracking right after (dhcpcd re-triggering that
when the veth comes up), reverting to the non-authoritative host
resolver. Agents then couldn't reach the in-hive forge or matrix through
the gateway, which broke an isolate-by-default rollout.
Take resolvconf and dhcpcd out of the resolv.conf loop: disable
resolvconf and tell dhcpcd not to touch resolv.conf (without disabling
dhcpcd itself, so the veth still gets its address). Then the last writer
owns it -- the nixos-container host-copy in shared netns, or the oneshot
in isolated mode. Mirrors the approach the matrix container already
takes. Eval-checked; flake check passes.
Follow-up to the isolated-container egress fix. The hyperhive-isolated-dns
oneshot (which rewrites resolv.conf to the bridge resolver) was ordered
before network-online.target + tea-login, but NOT relative to the harness
(hive-ag3nt) or the matrix daemon. hive-ag3nt only declares
`after network.target`, so on first boot its first-turn api.anthropic.com
lookup could race ahead of the resolv.conf rewrite and error (self-heals
next turn, but flaps the first one).
Add hive-ag3nt.service + hive-matrix-daemon.service to the oneshot's
`before` so DNS is fixed before any network consumer starts. The matrix
entry is a harmless no-op when matrix is disabled (unit absent).
Caught by damocles in review of the parent PR.
When isolateContainers=true, claude (and all egress) broke in every
container: agents came up with an IP but no way off the bridge subnet.
Two container-side gaps, both confirmed against nixpkgs
nixos-containers.nix:
1. No default route. hive-priv wrote HOST_ADDRESS= empty in the nspawn
conf. nixos-container's container-side setup only installs
`ip route add default via $HOST_ADDRESS` when HOST_ADDRESS is
non-empty, so the container had an address but no gateway -> nothing
off-subnet (incl. api.anthropic.com) was reachable. Fix: write
HOST_ADDRESS=<bridge-ip>. In bridge mode the host-side address/route
setup is skipped, so this only affects the container's default route.
2. No usable resolver. nixos-container copies the host's /etc/resolv.conf
into the container at every start; the host resolver (e.g. 127.0.0.53)
is unreachable from a private netns and isn't authoritative for the
hive's own zones. Fix: hive-priv drops a marker carrying the gateway
IP only when isolated, and a new harness-base oneshot
(hyperhive-isolated-dns) rewrites resolv.conf to point at the bridge
dnsmasq. Inert in shared-netns mode (no marker), so the shared
container toplevel does the right thing in both modes.
The gateway IP is the address part of HIVE_NETWORK_SUBNET (the bridge IP
verbatim, honouring a non-.1 operator override), via a new validated
bridge_gateway_ip() helper with unit tests.
Unblocks defaulting isolation on.
Match the harness reader landed in #1600 (configured_effort reads
HIVE_DEFAULT_EFFORT, mirroring HIVE_DEFAULT_MODEL). Was
HYPERHIVE_EFFORT_LEVEL in the first cut; damocles standardized the
name on the HIVE_DEFAULT_* convention.
Per-agent enum option (medium|high|xhigh, default medium) rendered into
the HYPERHIVE_EFFORT_LEVEL env var. The harness resolves effort as
operator-override-file -> this env -> medium and passes it to
claude --effort at turn launch (harness read+apply tracked separately).
Same declarative-default + runtime-override shape as hyperhive.model.
Umbrella: operator-controlled claude effort.
Adds a per-agent hyperhive.availableModels list option (default
[ haiku sonnet opus ]) rendered into the HIVE_AVAILABLE_MODELS env var
(comma-separated) so the per-agent web UI model quick-picker lists
exactly the configured models instead of a hardcoded set. Operators set
a shared default hive-wide or narrow it per-agent.
An assertion guards that hyperhive.model is present in the list so the
picker can always offer the model the agent is actually running.
ensure_claude_dir creates the dir as 0755 but cannot re-chmod after
hive-agent-user-migrate chowns it to the agent user (EPERM — non-owner).
The activation script runs as root and can always chmod it. Add an explicit
'chmod 755 $homeDir/.claude' after the existing chown so existing 0700
dirs from pre-fix containers are corrected on the next container boot.
Without this, all agents with pre-existing .claude dirs show 'needs login'
in the dashboard even with working sessions, because hive-core cannot list
the 0700 dir owned by a different user.
matrix-avatar-sync ran on every boot (RemainAfterExit=false + path
trigger), uploading a fresh PNG each time. Every upload mints a new
mxc:// URI, which triggers a profile state event in every joined room
— resulting in timeline spam even when the avatar hasn't changed.
Fix: before uploading, compute sha256sum of /etc/hyperhive/icon.svg
and compare against the last-synced hash stored in
$HYPERHIVE_STATE_DIR/matrix-avatar-icon-hash. Skip the upload if the
hash matches. Write the hash after a successful avatar_url PUT so
subsequent boots are no-ops until the icon file changes.
Hash file lives in the agent's state dir — survives restart, cleared
on purge (so purge + re-provision gets a fresh upload). Delete to force
re-upload manually.
Closes#1231
bash and matrix MCP entries were both assigned to the same attribute in the
same attrset literal, causing nix evaluation to fail with "attribute already
defined". Merged both into a single lib.mkMerge list under one assignment.
- restore count in get_loose_ends: 'N local task(s):' instead of bare 'local task(s):'
- add cross-crate coupling comment to both mcp_loose_ends_dir() copies
- add comment in hive-bash-daemon service env explaining HYPERHIVE_HARNESS_DIR
is already injected via systemd.globalEnvironment by the meta flake
The migration chown stomped agents/<name>/config (the proposed-config repo)
to hive-core like everything else, but hive-agent-user-migrate only re-chowned
state/harness/.claude on boot, leaving config core-owned. config is owned by
the editing parent/manager agent (core only pulls from it), so chown it to
this user too. RO self-mount in an agent's own container fails harmlessly.
On first agent deployment, the container boots before hive-c0re has
provisioned the forge-token. forge-avatar-sync was exiting early with
"no forge-token found", and RemainAfterExit=true prevented systemd
from ever re-running it — avatar never got uploaded until the next
container reboot.
Add a systemd.paths.forge-avatar-sync unit (PathExistsGlob on the
forge-token file) to re-fire the service once the token arrives, and
set RemainAfterExit=false to allow the re-fire. Mirrors the existing
matrix-avatar-sync pattern exactly.
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).
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.
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).
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.
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: '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.
iris's #718 scope: move substantive design context from `#` comment
blocks in `nix/` to corresponding `docs/` files, leave short
references in code. iris handed it back to me on #10114 since
nix/ is my lane + #775 established the pattern.
First pass — three highest-density blocks in harness-base.nix:
1. **First-boot agent-user migration** (~70 lines → `~20 lines code +
short ref` in the activation script). Substantive prose moves to
new `docs/persistence.md::First-boot agent-user migration (post-#658)`
section explaining the 4 steps the script performs + the eventual
removability of the marker-guarded body.
2. **nix-daemon `sandbox-fallback = true`** (10-line block → 5-line
ref). New `docs/gotchas.md::Containerized nix-daemon needs
sandbox-fallback = true` section covers the user-namespaces
rationale + nixpkgs-default override.
3. **Matrix daemon + token-arrival trigger** (~50 lines across two
systemd units → ~10 lines code + short refs). New
`docs/persistence.md::Matrix per-agent daemon + token-arrival
trigger` covers the socket-path rationale, the runtime-dir
ownership story, and the first-boot ordering pattern.
Net: harness-base.nix -84 lines, docs +74 lines. Substantive design
context moves to durable docs; in-code refs follow iris's pattern
from her #712 batches (`see docs/<file>::<section>`).
Follow-ups: hive-c0re.nix, hive-forge.nix, hive-matrix.nix (already
trimmed via #775 but a couple of remaining blocks could go), and
the smaller files in #718's scope table. Shipping this first to get
the pattern reviewed before larger batches.
Verified: `nix eval` on agent-base toplevel still resolves.
systemd.services.<name>.path appends /bin to each entry, so the
literal '/run/wrappers/bin' here was being expanded to
'/run/wrappers/bin/bin' inside the unit's PATH — a path that
doesn't exist. 'which sudo' then fell back to
'/run/current-system/sw/bin/sudo' (the non-setuid nix-store binary)
and refused with 'must be owned by uid 0 and have the setuid bit
set' on every agent, despite hyperhive.user.passwordlessSudo = true.
Verified on this container post-rebuild:
PATH includes /run/wrappers/bin/bin (non-existent)
/run/wrappers/bin/sudo exists with mode r-s--x--x (real setuid)
but `sudo` resolves to /run/current-system/sw/bin/sudo and fails.
Fix: drop the trailing /bin from both entries. systemd appends it.
The /run/current-system/sw entry was already correctly
expanding to /run/current-system/sw/bin (because of the same
auto-append), which is why everything else on PATH worked despite
the broken wrappers entry — only sudo (the one binary that needs
the wrapper dir) was affected.