Security model
State-file endpoint security model
GET /api/state-file?path=<p> serves files from agent state dirs and
the shared space to authenticated dashboard users (browser, operator).
Two allow-listed root prefixes are accepted; all other paths are rejected
before touching the filesystem:
/var/lib/hyperhive/agents/<n>/state/— per-agent durable notes (canonical host form or the in-container view/agents/<n>/state/)/var/lib/hyperhive/shared/— shared docs (/shared/in-container)
/state/... without an agent prefix is explicitly not accepted — it is
ambiguous from the host's perspective.
Defense-in-depth layers (in order):
- Allow-list prefix check — rejects without touching the filesystem if the path doesn't match either root.
- No symlinks below the matched root — each path component is
checked with
symlink_metadatabefore canonicalize. A sub-agent that plantsln -s /other/secret /agents/me/state/peekcan't proxy another agent's file through this endpoint (canonicalize would happily resolve the symlink to a still-within-allow-list path). - Canonicalize as belt-and-braces — resolves
../.traversal and rejects if the result escapes the roots. state/subdir constraint — underAGENTS_ROOT, the second path component must bestate/. Applied, proposed git repos and config dirs are off-limits.- World-readable check — file must have
mode & 0o004set. A0600file insidestate/would otherwise be accessible to any operator with dashboard access.
scan_validated_paths (broker-message ingest, linkifier) uses the same
resolve_state_path helper so security rules stay in sync — the
dashboard renders anchors only for tokens that passed the same checks the
read endpoint enforces.
Nix builds and credential isolation
Background
Agent containers bind-mount the host's nix-daemon socket. The host daemon may
have sandbox-fallback = false (strict NixOS defaults), which causes nix build
inside nspawn containers to fail — containers lack kernel user namespaces, so nix
cannot set up its build sandbox. harness-base.nix sets sandbox-fallback = true
so that builds fall back to unsandboxed execution rather than failing outright.
Threat model
Unsandboxed nix builds run as nixbld users (non-root, typically UIDs 30001-30010).
Without sandbox isolation, a build derivation's builder script has read access to
any file in the container that the nixbld user can read.
What is NOT exposed:
/home/<name>/.claude/— mode0700, owned by the per-agent user<name>. nixbld users cannot read it.$HYPERHIVE_STATE_DIR/forge-token(=/agents/<name>/state/forge-token) — written at mode0600byhive-c0re/src/forge.rsand chowned to the per-agent uid:gid bylifecycle::chown_to_agent. nixbld users cannot read it.
Policy: all credential files written to agent state directories MUST be mode
0600 or stricter. Do not create world-readable secret files in agent state dirs.
Long-term fix
The proper fix is to enable user namespaces inside nspawn containers
(--private-users=inherit in EXTRA_NSPAWN_FLAGS) so nix can set up its real
sandbox and sandbox-fallback becomes a true last resort. This requires verifying
bind-mount compatibility with user namespace UID mapping and is tracked as a TODO.
hive-c0re privilege separation
Background
hive-c0re runs as the unprivileged system user hive-core
(/var/lib/hyperhive owned by hive-core:hive-core). It cannot
directly invoke nixos-container, journalctl -M, or systemctl -M hive-gateway — those require root. hive-priv fills this gap.
hive-priv
hive-priv is a minimal privileged helper that runs as root, socket-activated
at /run/hive/priv.sock (mode 0660, group hive-core — only the
hive-core user can connect). hive-c0re calls it via priv_client
for every operation that genuinely requires root.
Narrow interface — PrivRequest variants map 1:1 to specific
known operations; there is no arbitrary command pass-through:
| Operation | What it runs |
|---|---|
StartContainer / StopContainer / KillContainer |
nixos-container start/stop/kill <name> |
CreateContainer / UpdateContainer |
nixos-container create/update <name> --flake <ref> |
DestroyContainer |
nixos-container destroy <name> |
ListContainers |
nixos-container list |
ReadContainerJournal |
journalctl -M <container> -n <n> [filters...] |
ReloadGatewayNginx |
systemctl -M hive-gateway reload/start/reset-failed nginx |
WriteNspawnFlags |
write /etc/nixos-containers/<container>.conf (bind-mount list + network isolation vars) |
WriteResourceLimits |
write CPUQuota=/MemoryMax= systemd drop-in for agent container |
RemoveServiceDropin |
remove container@<name>.service.d/ drop-in on destroy |
DaemonReload |
systemctl daemon-reload |
ChownSocketDir / ChmodSocketDir |
chown/chmod /run/hive-agent/<name>/ socket directory |
RunForgeAdmin |
nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin <args> |
WriteAgentForgeToken / WriteAgentMatrixToken |
write 0600 credential file into agent state dir |
RestartMatrixDaemon |
systemctl --machine=h-<name> restart hive-matrix-daemon.service |
Container allowlist — every request is validated against an
allowlist before any operation: only names matching h-<agent> (the
standard agent prefix), the manager container, or the known sibling
service containers (hive-gateway, hive-forge, hive-matrix,
hive-ci) are accepted. Arbitrary container names are rejected.
Socket-activated — systemd starts hive-priv on the first
incoming connection (LISTEN_FDS=1); it is not running between calls.
The ProtectSystem=strict + ReadWritePaths sandbox limits filesystem
writes to only the paths hive-priv legitimately needs.
Privilege boundary summary
| Component | Runs as | Privilege needed for |
|---|---|---|
hive-c0re |
hive-core |
broker, HTTP dashboard, scheduling, approvals |
hive-priv |
root |
container lifecycle, journal reads, bind mounts, cred writes |
hive-ag3nt (per-container) |
per-agent user | turn execution, MCP serving |