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.
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.
/// All agents can read/write here; agents should only put things they're
/// willing to lose (other agents may delete them).
@ -865,25 +860,29 @@ fn set_nspawn_flags(
// below are gated on `container == MANAGER_NAME` anyway.
let agent_name = container.strip_prefix(AGENT_PREFIX).unwrap_or(container);
// Compute the in-container state mount point. Sub-agents get
// /agents/<name>/state; the manager keeps the legacy /state path.
// Claude credentials always land at /root/.claude for all agents so
// 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")
};
// Claude credentials always land at /root/.claude so the
// `claude` CLI (which reads $HOME/.claude) finds them without
// any HOME override.
let claude_mount = CONTAINER_CLAUDE_MOUNT;
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(),
claude = claude_dir.display(),
notes = notes_dir.display(),
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 {
// systemd-nspawn refuses to start a container whose bind
// 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
# 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 = {
HIVE_LABEL = name;
HYPERHIVE_STATE_DIR = "/agents/${name}/state";
};
systemd.globalEnvironment = {
HIVE_LABEL = name;
HYPERHIVE_STATE_DIR = "/agents/${name}/state";
};
systemd.services.${service}.environment = parentEnv // {
HIVE_PORT = toString port;
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
/// under. Sub-agents see their state at `/agents/<name>/state/`;
/// the manager keeps the legacy `/state/` mount (see
/// `lifecycle::set_nspawn_flags`). Auto-file paths use the same
/// prefix so the round-trip is symmetric.
/// under. Every agent sees its state at `/agents/<container>/state/`
/// (see `lifecycle::set_nspawn_flags`). Auto-file paths use the same
/// prefix so the round-trip is symmetric. The manager logical name
/// maps to its container name (`hm1nd`) per `lifecycle::MANAGER_NAME` —
/// the pre-#604 legacy `/state/` alias is gone.
#[must_use]
pub fn container_state_prefix(agent: &str) -> String {
if agent == hive_sh4re::MANAGER_AGENT {
"/state/".to_owned()
format!("/agents/{}/state/", crate::lifecycle::MANAGER_NAME)
} else {
format!("/agents/{agent}/state/")
}
@ -275,18 +276,23 @@ mod tests {
}
#[test]
fn manager_uses_legacy_state_prefix() {
// The manager container mounts its state at `/state/` (legacy),
// not `/agents/manager/state/`. Same host path; different
// container-visible path. resolve_host_path needs to know.
assert_eq!(container_state_prefix("manager"), "/state/");
let p = resolve_host_path("manager", "/state/reminders/x.md").unwrap();
fn manager_uses_container_name_prefix() {
// Post-#604: manager's container view of its state is at
// `/agents/<MANAGER_NAME>/state/` (= `/agents/hm1nd/state/`),
// same as every other agent — the legacy bare `/state/` mount
// was dropped from lifecycle::set_nspawn_flags.
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!(
p,
PathBuf::from("/var/lib/hyperhive/agents/manager/state/reminders/x.md")
);
// And the sub-agent prefix must NOT be accepted for the manager.
assert!(resolve_host_path("manager", "/agents/manager/state/x.md").is_err());
// And the legacy `/state/` prefix must NOT be accepted anymore.
assert!(resolve_host_path("manager", "/state/x.md").is_err());
}
#[test]

View file

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