drop legacy /state mount for manager (#604)

This commit is contained in:
damocles 2026-05-29 21:09:16 +02:00 committed by Mara
commit dc99e64b2d
4 changed files with 60 additions and 62 deletions

View file

@ -26,11 +26,6 @@ pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
/// Persistent across destroy/recreate so OAuth login survives. /// Persistent across destroy/recreate so OAuth login survives.
pub const CONTAINER_CLAUDE_MOUNT: &str = "/root/.claude"; pub const CONTAINER_CLAUDE_MOUNT: &str = "/root/.claude";
/// Mount point of the per-agent durable knowledge dir inside the container.
/// Agents are told (system prompt) to keep `notes.md` and any other scratch
/// state here; persists across destroy/recreate.
pub const CONTAINER_NOTES_MOUNT: &str = "/state";
/// Mount point of the shared directory accessible to all agents. /// Mount point of the shared directory accessible to all agents.
/// All agents can read/write here; agents should only put things they're /// All agents can read/write here; agents should only put things they're
/// willing to lose (other agents may delete them). /// willing to lose (other agents may delete them).
@ -865,25 +860,29 @@ fn set_nspawn_flags(
// below are gated on `container == MANAGER_NAME` anyway. // below are gated on `container == MANAGER_NAME` anyway.
let agent_name = container.strip_prefix(AGENT_PREFIX).unwrap_or(container); let agent_name = container.strip_prefix(AGENT_PREFIX).unwrap_or(container);
// Compute the in-container state mount point. Sub-agents get // Claude credentials always land at /root/.claude so the
// /agents/<name>/state; the manager keeps the legacy /state path. // `claude` CLI (which reads $HOME/.claude) finds them without
// Claude credentials always land at /root/.claude for all agents so // any HOME override.
// the `claude` CLI (which reads $HOME/.claude) finds them without any
// HOME override.
let notes_mount = if container == MANAGER_NAME {
CONTAINER_NOTES_MOUNT.to_owned()
} else {
format!("/agents/{agent_name}/state")
};
let claude_mount = CONTAINER_CLAUDE_MOUNT; let claude_mount = CONTAINER_CLAUDE_MOUNT;
let mut binds = format!( let mut binds = format!(
"--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{claude_mount} --bind={notes}:{notes_mount} --bind={shared}:{CONTAINER_SHARED_MOUNT}", "--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{claude_mount} --bind={shared}:{CONTAINER_SHARED_MOUNT}",
runtime = runtime_dir.display(), runtime = runtime_dir.display(),
claude = claude_dir.display(), claude = claude_dir.display(),
notes = notes_dir.display(),
shared = HOST_SHARED_ROOT, shared = HOST_SHARED_ROOT,
); );
// Per-agent state at `/agents/<container>/state`. Skipped for
// the manager — the `/agents` bind below already exposes its
// own state (along with every sub-agent's). Pre-#604 the manager
// had a bespoke `/state` legacy alias bind; that's gone.
if container != MANAGER_NAME {
let _ = write!(
binds,
" --bind={notes}:/agents/{agent_name}/state",
notes = notes_dir.display(),
);
}
if container == MANAGER_NAME { if container == MANAGER_NAME {
// systemd-nspawn refuses to start a container whose bind // systemd-nspawn refuses to start a container whose bind
// source doesn't exist. The meta repo is created by the // source doesn't exist. The meta repo is created by the

View file

@ -398,10 +398,19 @@ where
}; };
# Container-wide env: every service + co-process daemon can # Container-wide env: every service + co-process daemon can
# resolve the agent's durable state dir without hard-coding it. # resolve the agent's durable state dir without hard-coding it.
# `environment.variables` only writes /etc/environment (login
# shells); `systemd.globalEnvironment` is the analogue for
# systemd units so tea-login / forge-avatar-sync /
# matrix-avatar-sync etc. can read `$HYPERHIVE_STATE_DIR`
# without each service having to redeclare it (#604).
environment.variables = { environment.variables = {
HIVE_LABEL = name; HIVE_LABEL = name;
HYPERHIVE_STATE_DIR = "/agents/${name}/state"; HYPERHIVE_STATE_DIR = "/agents/${name}/state";
}; };
systemd.globalEnvironment = {
HIVE_LABEL = name;
HYPERHIVE_STATE_DIR = "/agents/${name}/state";
};
systemd.services.${service}.environment = parentEnv // { systemd.services.${service}.environment = parentEnv // {
HIVE_PORT = toString port; HIVE_PORT = toString port;
HIVE_LABEL = name; HIVE_LABEL = name;

View file

@ -196,14 +196,15 @@ pub fn write_payload(agent: &str, host_path: &Path, message: &str) -> Result<(),
} }
/// Container-visible state prefix the caller's `file_path` must live /// Container-visible state prefix the caller's `file_path` must live
/// under. Sub-agents see their state at `/agents/<name>/state/`; /// under. Every agent sees its state at `/agents/<container>/state/`
/// the manager keeps the legacy `/state/` mount (see /// (see `lifecycle::set_nspawn_flags`). Auto-file paths use the same
/// `lifecycle::set_nspawn_flags`). Auto-file paths use the same /// prefix so the round-trip is symmetric. The manager logical name
/// prefix so the round-trip is symmetric. /// maps to its container name (`hm1nd`) per `lifecycle::MANAGER_NAME` —
/// the pre-#604 legacy `/state/` alias is gone.
#[must_use] #[must_use]
pub fn container_state_prefix(agent: &str) -> String { pub fn container_state_prefix(agent: &str) -> String {
if agent == hive_sh4re::MANAGER_AGENT { if agent == hive_sh4re::MANAGER_AGENT {
"/state/".to_owned() format!("/agents/{}/state/", crate::lifecycle::MANAGER_NAME)
} else { } else {
format!("/agents/{agent}/state/") format!("/agents/{agent}/state/")
} }
@ -275,18 +276,23 @@ mod tests {
} }
#[test] #[test]
fn manager_uses_legacy_state_prefix() { fn manager_uses_container_name_prefix() {
// The manager container mounts its state at `/state/` (legacy), // Post-#604: manager's container view of its state is at
// not `/agents/manager/state/`. Same host path; different // `/agents/<MANAGER_NAME>/state/` (= `/agents/hm1nd/state/`),
// container-visible path. resolve_host_path needs to know. // same as every other agent — the legacy bare `/state/` mount
assert_eq!(container_state_prefix("manager"), "/state/"); // was dropped from lifecycle::set_nspawn_flags.
let p = resolve_host_path("manager", "/state/reminders/x.md").unwrap(); assert_eq!(container_state_prefix("manager"), "/agents/hm1nd/state/");
let p = resolve_host_path("manager", "/agents/hm1nd/state/reminders/x.md").unwrap();
// NB: the host path still resolves under `agents/manager/`
// (Coordinator::agent_notes_dir takes the broker LOGICAL name).
// That's a pre-existing manager-logical-vs-container-name
// discrepancy tracked separately in #162; out of scope here.
assert_eq!( assert_eq!(
p, p,
PathBuf::from("/var/lib/hyperhive/agents/manager/state/reminders/x.md") PathBuf::from("/var/lib/hyperhive/agents/manager/state/reminders/x.md")
); );
// And the sub-agent prefix must NOT be accepted for the manager. // And the legacy `/state/` prefix must NOT be accepted anymore.
assert!(resolve_host_path("manager", "/agents/manager/state/x.md").is_err()); assert!(resolve_host_path("manager", "/state/x.md").is_err());
} }
#[test] #[test]

View file

@ -684,19 +684,12 @@
# No `set -e`: any subshell failure must not propagate. # No `set -e`: any subshell failure must not propagate.
# A failed unit aborts `nixos-container update` which blocks rebuilds. # A failed unit aborts `nixos-container update` which blocks rebuilds.
FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url} FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url}
# Manager bind-mounts state at /state; sub-agents at # $HYPERHIVE_STATE_DIR is set system-wide by the meta flake
# /agents/<name>/state. Glob both — each container only sees # (systemd.globalEnvironment, /agents/<name>/state per agent
# its own mount, so there is exactly one hit (or zero when # including manager post-#604).
# the forge hasn't been seeded yet). TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token"
TOKEN_FILE="" if [ ! -f "$TOKEN_FILE" ]; then
for f in /state/forge-token /agents/*/state/forge-token; do echo "tea-login: no forge-token at $TOKEN_FILE; skipping"
if [ -f "$f" ]; then
TOKEN_FILE="$f"
break
fi
done
if [ -z "$TOKEN_FILE" ]; then
echo "tea-login: no forge-token found; skipping"
exit 0 exit 0
fi fi
TOKEN=$(cat "$TOKEN_FILE") TOKEN=$(cat "$TOKEN_FILE")
@ -764,14 +757,10 @@
exit 0 exit 0
fi fi
FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url} FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url}
TOKEN_FILE="" # $HYPERHIVE_STATE_DIR is set system-wide by the meta flake
for f in /state/forge-token /agents/*/state/forge-token; do # (systemd.globalEnvironment) to `/agents/<name>/state`.
if [ -f "$f" ]; then TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token"
TOKEN_FILE="$f" if [ ! -f "$TOKEN_FILE" ]; then
break
fi
done
if [ -z "$TOKEN_FILE" ]; then
echo "forge-avatar-sync: no forge-token found; skipping" echo "forge-avatar-sync: no forge-token found; skipping"
exit 0 exit 0
fi fi
@ -904,17 +893,12 @@
exit 0 exit 0
fi fi
# Token written by `hive-c0re::matrix::ensure_user_for` to the # Token written by `hive-c0re::matrix::ensure_user_for` to the
# agent's bind-mounted state dir. Either appears at the legacy # agent's bind-mounted state dir. $HYPERHIVE_STATE_DIR is set
# `/state/` (manager) or per-agent `/agents/<name>/state/` path. # system-wide by the meta flake (systemd.globalEnvironment) to
TOKEN_FILE="" # `/agents/<name>/state`.
for f in /state/matrix-token /agents/*/state/matrix-token; do TOKEN_FILE="$HYPERHIVE_STATE_DIR/matrix-token"
if [ -f "$f" ]; then if [ ! -f "$TOKEN_FILE" ]; then
TOKEN_FILE="$f" echo "matrix-avatar-sync: no matrix-token at $TOKEN_FILE; skipping"
break
fi
done
if [ -z "$TOKEN_FILE" ]; then
echo "matrix-avatar-sync: no matrix-token found; skipping"
exit 0 exit 0
fi fi
TOKEN=$(cat "$TOKEN_FILE") TOKEN=$(cat "$TOKEN_FILE")