From dc99e64b2d107436d21c53b859bb3a3485ce917f Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 29 May 2026 21:09:16 +0200 Subject: [PATCH] drop legacy /state mount for manager (#604) --- hive-c0re/src/lifecycle.rs | 33 ++++++++++---------- hive-c0re/src/meta.rs | 9 ++++++ hive-c0re/src/reminder_scheduler.rs | 32 +++++++++++-------- nix/templates/harness-base.nix | 48 ++++++++++------------------- 4 files changed, 60 insertions(+), 62 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index cb12cb55..6e6f05c7 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -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//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//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 diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 5b4f1167..fe496f91 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -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; diff --git a/hive-c0re/src/reminder_scheduler.rs b/hive-c0re/src/reminder_scheduler.rs index 92d16e82..5d315e90 100644 --- a/hive-c0re/src/reminder_scheduler.rs +++ b/hive-c0re/src/reminder_scheduler.rs @@ -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//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//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//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] diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 468b04e2..720b6633 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -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//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//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//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//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//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")