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.
- Wrap table in .schedules-table-wrap (overflow-x: auto) so it
never exceeds the page width on narrow viewports
- Add .schedules-table-next-col (width: 8em) and
.schedules-table-every-col (width: 7em) with white-space: nowrap
to stop those columns from stealing space from the body column
- Align visual style with the perm page tables: unified border on
all cells (was: border-top row separator only), bg-elev thead,
font-size 0.82em (was 0.9em), min-width: 100% on the table itself
Drop the 'h-sock :8421' meta span from the agent card head.
ContainerView.container is now #[serde(skip)] since no frontend
code reads it; the field stays in Rust for internal use.
c.port remains serialized — used for fallback agent UI URLs.
hive_sh4re::Request uses #[serde(tag = "cmd")] so Wake serialises as
{"cmd":"wake",...}. The wake.rs in hive-matrix-mcp was sending
{"kind":"wake",...} — the harness could not deserialise the message
and silently discarded every incoming matrix event.
Also: drain the server's response line instead of just shutting the
write half. Without the drain the server got ECONNRESET writing back,
which logged a spurious error even though the wake itself was now
processed.
Root cause of the matrix notification blackhole reported in #1087.
add QueueKind::PermChange — dashboard tool-group and capability
handlers no longer write the shared JSON files inline. instead they
enqueue a PermChange entry; the FIFO worker applies the file write
then calls rebuild_agent so the updated env var takes effect.
concurrent batch-apply actions for different agents previously raced
on tool-groups.json / capabilities.json (last write wins, earlier
change silently dropped). serialising through the queue prevents this.
dedup check extended with perm-type discriminant so tool-groups and
capabilities changes for the same agent are kept as distinct entries
and never collapse into one slot.
- hive-sh4re: ToolGroup::description() and Capability::description() return
short human-readable strings for each variant
- hive-c0re: ToolGroupsSnapshot and CapabilitiesSnapshot now include a
`descriptions` map (name → description); get_capabilities now iterates
Capability::ALL instead of hardcoding the list
- tabs.js: renderToolGroups + renderCapabilities use descriptions[name] as
the column header title attribute (native browser tooltip on hover)
Argus flagged that since PR #1054 the nginx reload path is
`gateway_systemctl(["reload", "nginx"])` = `systemctl -M hive-gateway reload nginx`.
Replace the stale `systemd-run --machine=hive-gateway` reference.
Four modules were absent: capabilities.rs, build_logs.rs,
gateway_nginx.rs, priv_client.rs. Added at their natural
positions alongside related modules.
Remove web_tools from the C4P4B1L1T13S table in dashboard.md and add
it to the T00L GR0UPS section with a note that it gates Claude built-ins
rather than MCP tools. Fix turn-loop.md to say 'tool-group-gated' and
'web_tools tool group' throughout.
The hardcoded vec was a maintenance hazard — any new capability added
to Capability::ALL would silently be omitted from the permissions UI
column list until get_capabilities was manually updated.
Now both the GET and POST handlers derive their known-capability lists
from the same Capability::ALL source of truth.