drop legacy /state mount for manager (#604)
This commit is contained in:
parent
7aadf38483
commit
dc99e64b2d
4 changed files with 60 additions and 62 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue