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:

/state/... without an agent prefix is explicitly not accepted — it is ambiguous from the host's perspective.

Defense-in-depth layers (in order):

  1. Allow-list prefix check — rejects without touching the filesystem if the path doesn't match either root.
  2. No symlinks below the matched root — each path component is checked with symlink_metadata before canonicalize. A sub-agent that plants ln -s /other/secret /agents/me/state/peek can't proxy another agent's file through this endpoint (canonicalize would happily resolve the symlink to a still-within-allow-list path).
  3. Canonicalize as belt-and-braces — resolves ../. traversal and rejects if the result escapes the roots.
  4. state/ subdir constraint — under AGENTS_ROOT, the second path component must be state/. Applied, proposed git repos and config dirs are off-limits.
  5. World-readable check — file must have mode & 0o004 set. A 0600 file inside state/ 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:

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 interfacePrivRequest 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