When containers run in private netns (isolateContainers=true), host
loopback is unreachable so HIVE_FORGE_URL=http://127.0.0.1:3000 breaks.
- nix/modules/hive-network.nix: when isolateContainers is on + forge
is enabled, open forge.httpPort on the bridge interface so agents
can reach forgejo at bridgeIp:httpPort (forgejo binds 0.0.0.0)
- nix/modules/hive-c0re.nix: HIVE_FORGE_URL switches to bridge IP
when network.enable && isolateContainers; loopback path retained
when isolateContainers=false
- docs/network.md: add Forge access + Forge URL rows to effects table
- docs/gateway.md: rewrite HIVE_FORGE_URL section for both modes
Add opt-in WireGuard mesh support to services.hyperhive.swarm:
- swarm.peers.<domain>.wireguardPublicKey — peer's wg public key
- swarm.peers.<domain>.wireguardEndpoint — peer's UDP endpoint (optional)
- swarm.peers.<domain>.wireguardAddress — peer's mesh IP with prefix
- swarm.wireguard.enable — bring up wg-hive interface
- swarm.wireguard.privateKeyFile — path to host's wg private key
- swarm.wireguard.address — this host's mesh IP/prefix
- swarm.wireguard.listenPort — UDP listen port (default 51820)
- swarm.wireguard.persistentKeepalive — keepalive seconds (default 25)
When enabled, generates networking.wireguard.interfaces.wg-hive with
one peer entry per mesh-enabled swarm.peers entry. Opens listenPort
UDP on the host firewall. Adds wireguard_address to HYPERHIVE_PEERS
JSON so hive-c0re can use mesh IPs for intra-swarm routing.
Assertions guard against enable=true without privateKeyFile or address.
Also refactors networking.firewall.allowedTCPPortRanges from the
nested attrset form (which conflicted with the new allowedUDPPorts
line) to the per-attribute form.
docs/swarm.md: adds WireGuard setup section with key generation
commands, two-hive config example, NAT/keepalive notes.
- hive-sh4re: UnreadMatrix gains summary: String field (per-room breakdown)
- hive-matrix-mcp/protocol: add RoomUnread struct + UnreadSummary request
- hive-matrix-mcp/handlers: collect_unread() fetches per-room data;
single-unread rooms include truncated last-message body + sender;
multi-unread rooms carry count only
- hive-matrix-mcp/wake: format_unread_summary() builds wake body from
RoomUnread slice; terse one-liner for single-room/single-message,
bulleted list for multi-room; always appends read-hint
- hive-matrix-mcp/timeline: wake body now covers all rooms with unread
at fire time, not just the triggering event; falls back to per-event
teaser if notification counts haven't updated yet
- hive-ag3nt/mcp: matrix_unread_summary() replaces matrix_unread_rooms();
UnreadMatrix loose end carries per-room summary lines; render shows
room breakdown with sender: body for single-unread rooms
When the local matrix daemon has rooms with unread notifications,
get_loose_ends now prepends an UnreadMatrix entry to the output so
agents see pending matrix activity alongside questions/reminders
without message content being exposed.
Changes:
- hive-sh4re: add LooseEnd::UnreadMatrix { rooms: u32 } variant
- hive-matrix-mcp: add DaemonRequest::UnreadCount and handler that
counts joined rooms with notification_count > 0 (server-side push
notification counts, cached by matrix-sdk)
- hive-ag3nt/mcp: inject UnreadMatrix entry on self-queries by
connecting to /run/hive-matrix/socket (HIVE_MATRIX_SOCKET override);
best-effort — agents without matrix configured are unaffected
get_agent_links was proxying to http://127.0.0.1:{port}/api/state.
Since all agents now bind exclusively to a unix socket (HIVE_WEB_SOCKET
set unconditionally in harness-base.nix), the TCP fetch always fails
silently, returning [] — so the nav-strip icons on every card disappear.
Replace with container_view::build_nav_links(), which reconstructs the
same link list from disk:
- stats.html always (container kind)
- /{name} and /agent-configs/{name} when forge-token is present (forge kind)
- extras from hyperhive-dashboard-links.json (external kind)
The GUI screen link is intentionally omitted — /etc/hyperhive/gui.json
lives inside the agent container and is not host-visible. GUI agents
are rare; the omission is acceptable until a host-visible marker lands.
No new dependencies. reqwest is no longer used in dashboard.rs for
this handler (still used by forge.rs and hivectl.rs).
RELOAD_PENDING stays true on any failed reload, and reload_if_pending
fires on every 10-second spawn_poll tick. When the gateway is down or
nginx config is bad this hammers systemctl indefinitely.
Fix: track LAST_FAILED_RELOAD (unix timestamp). reload_if_pending backs
off to one attempt per RELOAD_RETRY_SECS (30s) after a failure. Fresh
write() calls reset the backoff so new topology changes still attempt
reload immediately.
Fixes#1113.
rusqlite was compiled with features=["bundled"] which embeds the
SQLite C source and compiles it via the cc crate on every fresh
dependency build. libsqlite3 is already in nativeBuildInputs (pkgs.sqlite
+ pkgs.pkg-config) so the system library is always available in the
nix sandbox. Dropping bundled removes the C compilation step from
the dep tree.
sync_agent called ensure_user_email on every tick, which sent
PATCH /api/v1/admin/users/{name} without the required login_name
field. Forgejo's EditUserOption handler was resetting use_custom_avatar
to false on each call, clobbering the avatar uploaded by the
forge-avatar-sync one-shot service.
Two-part fix:
- add login_name to the PATCH body (Forgejo EditUserOption requires it)
- write EMAIL_ALIGNED_MARKER_PREFIX{name} on first success; skip the
PATCH on all subsequent sync_agent ticks
Marker follows the same one-shot pattern as CORE_AVATAR_MARKER and
CONFIG_ORG_AVATAR_MARKER. Delete the marker to force re-alignment.
Two argus review fixups:
1. Move isolateContainers assertion to an unconditional `lib.mkIf
cfg.isolateContainers` arm. The prior placement inside
`mkIf (enable && isolateContainers)` was dead code — the assertion
could never fire because both flags were already true by the time the
block activated. Now `isolateContainers=true; enable=false;` raises
a NixOS assertion error at eval time.
2. Add comment on HIVE_NETWORK_SUBNET noting it carries the host-side
bridge IP (e.g. "10.42.0.1/24"), not the canonical network address
("10.42.0.0/24"). Rust consumer must normalize via bitwise AND before
subnet membership checks or address arithmetic.
Adds `services.hyperhive.network.isolateContainers` (bool, default
false). When enabled alongside `network.enable`, activates:
- IP forwarding + NAT masquerade so isolated agents reach the internet
- nftables DROP rule blocking bridge-subnet → loopback (defence-in-depth
against compromised agent reaching the c0re dashboard)
- `HIVE_NETWORK_ISOLATION`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET`
injected into the hive-c0re service env; the Rust lifecycle reads these
to set `PRIVATE_NETWORK`, `LOCAL_ADDRESS`, and `HOST_BRIDGE` in each
agent container's conf
Config block rewritten as `lib.mkMerge [...]` — the prior `lib.mkIf //
lib.mkIf` pattern was invalid nix (mkIf returns a tagged value, not an
attrset; // on it is a type error). See docs/network.md for full design.
Remove the deployed sha plain-text badge from the card head.
Add a forge-linked menu item to the three-dot menu instead:
'deployed:abc123def012 ↗' opens agent-configs/<name>/commit/<sha>
on the hive forge in a new tab.
Only shown when both c.deployed_sha and forgeBase are present.
buildAgentMenu now takes forgeBase as second arg; call site passes
the same forgeBase computed for nav-strip links.
CSS: add text-decoration: none + box-sizing to .agent-menu-item
so <a> link items render identically to the <button> items.
Privsep splits repo ownership: hive-priv (root) fetches the hive-core-owned
meta/applied repos via nix, and hive-c0re (hive-core) fetches the agent-owned
proposed-config repos. git/libgit2's dubious-ownership guard rejects both
('repository path is not owned by current user'), failing every rebuild.
Install a root-trusted gitconfig with safe.directory=* in each service's
HOME; both already have read access — this only satisfies the guard.
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.
Two privsep follow-ups, both in the hive-c0re service:
1. The migration 'chown -R hive-core /var/lib/hyperhive' (which only
started running once ExecStartPre stopped failing 203/EXEC) stomped every
agent's bind-mounted creds — agents/<name>/{claude,state,harness,config}
are owned by the per-agent/manager users — logging all agents out with no
way back in. Scope it to everything *except* agents/, plus the agents/
dir node itself so c0re can still create new per-agent subdirs. Each
container's hive-agent-user-migrate activation chowns the contents back.
2. nix (prebuild 'nix build', flake-check, meta eval in c0re; nixos-container
update->nix in priv) writes its cache under $HOME/.cache. Both services
run as users with no home -> HOME=/var/empty (unwritable) -> Lix cache
init fails, rebuilds error out. Set HOME to each service's StateDirectory
(adding one for hive-priv).
hive-priv had no PATH, so the nixos-container it runs for every container
op (incl. lifecycle::list) failed ENOENT. build_all swallows that into an
empty list — the 'no managed containers' symptom. Give the helper the
minimal set nixos-container shells out to (nixos-container, nix, util-linux,
e2fsprogs) on top of the systemd/coreutils/findutils already in the unit
PATH, rather than the whole system profile.
Also route forge/matrix is_present() through priv_client::list_containers
instead of spawning nixos-container directly from unprivileged hive-c0re.
The state-migration chown invoked ${pkgs.coreutils}/bin/sh, which does not
exist (coreutils ships chown, not sh), so ExecStartPre exited 203/EXEC on
every boot. hive-c0re hit its start limit and never came up, so the gateway
returned 502 Bad Gateway. Call chown directly with the +- prefix (run as
root, tolerate failure) instead of going through a shell for '|| true'.
The privsep drop to the hive-core user left four journalctl -M <container>
call sites shelling out directly. -M enters the container namespace via the
machine bus, which needs root, so all container-journal reads failed with
Permission denied. Add a ReadContainerJournal verb to hive-priv and route
dashboard get_journal, manager get_logs, the rebuild-failure journal tail,
and the agent host-journal -M path through it. Host-journal reads (no -M)
stay direct via systemd-journal group membership.