The handbook's sections were agent-lifecycle, integrations, swarm, tools, trust-boundary and web-ui, which do not follow how the system is built, so a reader could not tell which layer a page is about. Its pages now sit in swarm/, hives/ and agents/, with the accounts an agent can hold outside the system in external-accounts/. - swarm/: forge, matrix and the knowledge repo join the swarm pages, and so does the swarmctl reference. - hives/: the dashboard, hivectl and its reference, and the operator/agent boundary, with a README that takes over the old web-ui and tools pages. - agents/: the roster, persistence, config changes, the agent's own web page (was web-ui/agent) and the security model, with a README. - external-accounts/: GitHub and GitLab. Pages are moved whole; none is split yet. Every reference follows: relative links in the docs, `docs/2-handbook/...` paths in code comments, the nix checks that regenerate the CLI references and the ignore list, and docs/README.md, whose handbook section is now by layer.
3.7 KiB
Socket mechanics behind the operator/agent boundary
How systemd activates hive-priv and how the per-agent socket directory
gets its owner and mode, the two pieces of the boundary described in
the operator/agent boundary.
hive-priv socket activation
hive-priv is always socket-activated by the hive-priv.socket
systemd unit. The unit binds /run/hive/priv.sock with
SocketGroup=hive-core and mode 0660 and passes the ready listener
to the helper as fd 3 (LISTEN_FDS). The helper requires this and
bails if it isn't socket-activated.
⚠️ Intentionally, no self-bind fallback exists: if hive-priv bound
the socket itself, it would create the file owned by root's primary
group rather than hive-core, and a hive-core client couldn't
connect the way the socket unit's SocketGroup grant intends.
Requiring socket activation everywhere keeps dev and prod on the
exact same path, so the group grant always holds.
the per-agent socket dir
Three principals that share no group reach /run/hive-agent/<name>/,
which is why its mode is what it's:
| principal | reaches | needs |
|---|---|---|
| the agent's harness | binds + unlinks agent.sock, web.sock |
owner, rwx |
hive-c0re |
dials agent.sock (todo wakes) |
traverse |
| the gateway's nginx | dials web.sock |
traverse |
The last two land in "other," so the dir is 0751, owned by the
agent's container uid/gid — o=--x is traverse without listing, and
both sockets are 0666, which is all a dialer needs.
One mechanism creates it, one sets its owner. Before every start,
hive-priv creates the dir when missing (EnsureAgentSocketDir, 0751 root:root, no symlink followed) and leaves an existing one alone. The
container's own activation (hive-agent-user-migrate) then chowns it to
the agent user and sets 0751. Without a user namespace
that uid is the host inode's owner. (A namespaced agent, see "User namespace" in
docs/2-handbook/agents/persistence.md, gets the dir from EnsureAgentSocketDir
owned by its data owner instead, because the container can't chown through
the id map.) Don't add another host-side chown or chmod:
two owners of one path revert each other. Until the container activates,
the dir is 0751 root: nothing but root can plant a socket in it.
The mode is load-bearing, not cosmetic. Write permission on a
directory is what confers the right to unlink its entries, whoever owns
them, and the sticky bit is the only thing that would restrain that (it
isn't set here). A world-writable socket dir therefore lets anything
able to reach the path delete an agent's socket and bind its own. On the
host, any non-root process whose /run is writable can do that, such as
a login session or dnsmasq. nginx only dials: ProtectSystem=strict makes its view
of /run read-only. Dropping o=w removes that permission rather than
qualifying it.
⚠️ The gateway's nginx and dnsmasq are host services, next to
hive-c0re (see docs/4-implementation/networking/gateway.md) — there is no namespace between
them and the rest of the host. That costs no network isolation: nginx
binds the host's :80/:443 and reaches localhost upstreams, which
would require opening a netns anyway.
🔑 It does mean nothing implicitly scopes the privileged reload verb —
see docs/4-implementation/trust-boundary/hive-c0re-privilege.md for
how PrivRequest::ReloadGatewayNginx's containment works.
⚠️ This dir has exactly one writer, so it needs no world write at all.