fix(#2733): write the agent pause marker via hive-priv

`Coordinator::set_paused` wrote the marker directly with `std::fs::write`
from hive-c0re, which runs as the unprivileged `hive-core` user. The
agent's harness dir is chowned to the agent user on every container boot
(`user.nix`'s activation chown), mode 0755 — so hive-core can stat the
marker but gets EACCES creating or unlinking it. Pause therefore only
ever worked on an agent that had never booted; the read side works
because a stat needs traverse, not write, which is why the paused pill
and `is_paused` looked healthy.

Route both directions through hive-priv, the root helper that already
owns the other writes into agent-owned directories:

- `PrivRequest::SetAgentPaused { agent_name, paused }`, with the marker
  filename constant moved to hive-priv-sock. That is the narrowest crate
  all three sides share (hive-priv deliberately does not depend on
  hive-sh4re, which re-exports it for the in-container resolver). A
  private copy on any one side would break pause silently, since every
  reader just sees "no marker".
- `write_agent_state_file` generalised to `write_agent_dir_file`, taking
  the target directory: `state/` and `harness/` are both agent-owned,
  which is the same reason both need root.
- resume unlinks via `remove_file`, which acts on the leaf and never
  follows a symlink — an agent could otherwise plant a link at the
  marker path and have root delete an arbitrary file.

`Coordinator::set_paused` becomes an async round-trip; its three call
sites were already async. Both directions stay idempotent because the
dashboard toggle and `hivectl pause|resume` fire without reading the
current state first.
This commit is contained in:
atlas 2026-07-26 23:50:05 +02:00 committed by mara
commit de09628c7c
9 changed files with 193 additions and 36 deletions

View file

@ -9,6 +9,7 @@ workspace = true
[dependencies]
chrono.workspace = true
hive-priv-sock.workspace = true
hive-types.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -29,11 +29,15 @@ pub fn harness_dir() -> PathBuf {
PathBuf::from(format!("/agents/{label}/harness"))
}
/// File name of the pause marker inside the harness dir. Shared so the
/// in-container resolver below and hive-c0re's host-side one (which
/// builds the same path from `/var/lib/hyperhive/agents/{name}/harness`)
/// cannot drift apart.
pub const PAUSED_MARKER_FILE: &str = "paused";
/// File name of the pause marker inside the harness dir. Re-exported from
/// `hive-priv-sock`, which owns the definition because hive-priv (root) is
/// the component that actually creates and unlinks the marker — hive-c0re
/// runs unprivileged and cannot write to the agent-owned harness dir — and
/// hive-priv deliberately does not depend on this crate. Shared so the
/// in-container resolver below, hive-c0re's host-side one (which builds the
/// same path from `/var/lib/hyperhive/agents/{name}/harness`) and the
/// privileged writer cannot drift apart.
pub use hive_priv_sock::PAUSED_MARKER_FILE;
/// Marker file whose presence means "this agent is paused": the harness
/// keeps serving its web UI and MCP daemons but drives no turns, so