hive-c0re: scrub lifecycle + meta cookies (#715 batch 9)
This commit is contained in:
parent
6d7ec90843
commit
b0495c5167
2 changed files with 73 additions and 86 deletions
|
|
@ -20,16 +20,13 @@ pub const MANAGER_NAME: &str = "hm1nd";
|
|||
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
|
||||
|
||||
/// Where the per-agent Claude credentials dir mounts inside the
|
||||
/// container. Pre-#658 this was a constant (`/root/.claude`, because
|
||||
/// every agent ran as root). With the user-named-after-agent shape
|
||||
/// the harness service runs as a non-root unix user whose home is
|
||||
/// `/home/<agent>/`, so the mount path now varies per agent —
|
||||
/// `container_claude_mount(name)` returns `/home/<name>/.claude`
|
||||
/// for sub-agents and `/home/hm1nd/.claude` for the manager.
|
||||
/// `claude` inside the container reads `$HOME/.claude` and the
|
||||
/// service environment sets `HOME` to the same path, so the OAuth
|
||||
/// session survives container restarts the same way the constant
|
||||
/// did.
|
||||
/// container. The harness service runs as a non-root unix user
|
||||
/// whose home is `/home/<agent>/`, so the mount path varies per
|
||||
/// agent — `container_claude_mount(name)` returns
|
||||
/// `/home/<name>/.claude` for sub-agents and `/home/hm1nd/.claude`
|
||||
/// for the manager. `claude` inside the container reads
|
||||
/// `$HOME/.claude` and the service environment sets `HOME` to the
|
||||
/// same path, so the OAuth session survives container restarts.
|
||||
#[must_use]
|
||||
pub fn container_claude_mount(name: &str) -> String {
|
||||
format!("/home/{name}/.claude")
|
||||
|
|
@ -55,13 +52,12 @@ const DEFAULT_MEMORY_MAX: &str = "2G";
|
|||
const DEFAULT_CPU_QUOTA: &str = "50%";
|
||||
|
||||
/// Per-agent web UI port — `WEB_PORT_BASE + FNV-1a(name) %
|
||||
/// WEB_PORT_RANGE` for every agent including the manager (#753
|
||||
/// dropped the pre-#753 "manager pinned at 8000" special case so
|
||||
/// the port allocation rule reads the same for every name).
|
||||
/// Collisions are possible (birthday paradox at ~30 agents); the
|
||||
/// operator resolves them by renaming an agent (different hash →
|
||||
/// different port). Stable across hosts, restarts, and dashboard
|
||||
/// renders — no state-file dance.
|
||||
/// WEB_PORT_RANGE` for every agent including the manager. The port
|
||||
/// allocation rule reads the same for every name; collisions are
|
||||
/// possible (birthday paradox at ~30 agents) and the operator
|
||||
/// resolves them by renaming an agent (different hash → different
|
||||
/// port). Stable across hosts, restarts, and dashboard renders —
|
||||
/// no state-file dance.
|
||||
#[must_use]
|
||||
pub fn agent_web_port(name: &str) -> u16 {
|
||||
let mut hash: u32 = 2_166_136_261;
|
||||
|
|
@ -90,12 +86,12 @@ pub fn is_manager(name: &str) -> bool {
|
|||
/// Read the agent user's `(uid, gid)` from the container's nixos-managed
|
||||
/// `/etc/passwd`. Returns `None` when the container hasn't been built
|
||||
/// yet, the passwd file is unparseable, or the agent user is missing
|
||||
/// (e.g. legacy pre-#658 container that still runs as root).
|
||||
/// (e.g. legacy container that still runs as root).
|
||||
///
|
||||
/// Used by `forge` + `matrix` after writing per-agent state files so
|
||||
/// the bind-mounted host file ends up readable by the agent user
|
||||
/// without waiting for the next container activation to run the chown
|
||||
/// fixup (#673).
|
||||
/// fixup.
|
||||
///
|
||||
/// Notes:
|
||||
/// - Reads the *container-local* passwd at
|
||||
|
|
@ -130,7 +126,7 @@ pub fn agent_uid_gid(agent_name: &str) -> Option<(u32, u32)> {
|
|||
/// activation script in `harness-base.nix` is the steady-state safety
|
||||
/// net. Used by per-agent state writers in `forge` + `matrix` so the
|
||||
/// agent can read the file without waiting for the next container
|
||||
/// rebuild (#673).
|
||||
/// rebuild.
|
||||
pub fn chown_to_agent(name: &str, path: &Path, subsystem: &str) {
|
||||
let Some((uid, gid)) = agent_uid_gid(name) else {
|
||||
return;
|
||||
|
|
@ -406,7 +402,7 @@ pub async fn rebuild_no_meta(
|
|||
let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display());
|
||||
if container_exists(name).await {
|
||||
// Existing container: preserve the prior running state across
|
||||
// rebuild (closes #371) and apply both the new system profile
|
||||
// rebuild, and apply both the new system profile
|
||||
// AND any `/etc/nixos-containers/<c>.conf` / drop-in changes
|
||||
// in a single start rather than `update`'s reload-then-outer-
|
||||
// restart double-bounce.
|
||||
|
|
@ -428,16 +424,15 @@ pub async fn rebuild_no_meta(
|
|||
// Pre-build the system toplevel **before** stopping the
|
||||
// running container so the agent keeps serving its
|
||||
// previous generation while the eval + fetch + build
|
||||
// happens out-of-band (#706). `nixos-container update`
|
||||
// then finds the toplevel cached and skips straight to
|
||||
// the profile-swap + restart — downtime collapses to that
|
||||
// happens out-of-band. `nixos-container update` then
|
||||
// finds the toplevel cached and skips straight to the
|
||||
// profile-swap + restart — downtime collapses to that
|
||||
// window only. Build failures surface here, before we
|
||||
// touch the container.
|
||||
//
|
||||
// When the container is already stopped there's no
|
||||
// downtime to shave (mara on #721#9007) — let `update`
|
||||
// do the build inline rather than evaluating the flake
|
||||
// twice for nothing.
|
||||
// downtime to shave — let `update` do the build inline
|
||||
// rather than evaluating the flake twice for nothing.
|
||||
prebuild_toplevel(name, &flake_ref).await?;
|
||||
run(&["stop", &container]).await?;
|
||||
}
|
||||
|
|
@ -452,7 +447,7 @@ pub async fn rebuild_no_meta(
|
|||
// `nixos-container create` builds + creates atomically — if
|
||||
// the build fails, no container record is left around to
|
||||
// clean up — so a pre-build adds nothing but a duplicate
|
||||
// eval (mara on #721#9007).
|
||||
// eval.
|
||||
run(&["create", &container, "--flake", &flake_ref]).await?;
|
||||
set_nspawn_flags(&container, agent_dir, claude_dir, notes_dir)?;
|
||||
set_resource_limits(&container)?;
|
||||
|
|
@ -480,8 +475,7 @@ pub async fn rebuild_no_meta(
|
|||
/// back to `meta#<name>` (the shape `nixos-container update --flake
|
||||
/// meta#<name>` uses) makes nix look for `packages.<system>.<name>`,
|
||||
/// `legacyPackages.<system>.<name>`, or `<name>` at the flake root —
|
||||
/// none of which exist in the rendered meta flake (closes #735, the
|
||||
/// argus-prebuild regression after the first cold rebuild post-#721).
|
||||
/// none of which exist in the rendered meta flake.
|
||||
///
|
||||
/// Returns the same error shape as the other nixos-container
|
||||
/// helpers so callers can use `?` without translation.
|
||||
|
|
@ -1065,9 +1059,7 @@ fn set_nspawn_flags(
|
|||
// `claude` CLI (which reads `$HOME/.claude`) finds them. The
|
||||
// harness service's environment sets `HOME` to the same path
|
||||
// (`agent-base.nix` / `manager.nix`), so no `--setenv` plumbing
|
||||
// is needed here — the bind alone is enough. Pre-#658 the mount
|
||||
// was the constant `/root/.claude` because the service ran as
|
||||
// root.
|
||||
// is needed here — the bind alone is enough.
|
||||
let claude_mount = container_claude_mount(agent_name);
|
||||
|
||||
let mut binds = format!(
|
||||
|
|
@ -1079,8 +1071,7 @@ fn set_nspawn_flags(
|
|||
|
||||
// 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.
|
||||
// own state (along with every sub-agent's).
|
||||
if container != MANAGER_NAME {
|
||||
let _ = write!(
|
||||
binds,
|
||||
|
|
@ -1142,13 +1133,12 @@ fn set_nspawn_flags(
|
|||
std::fs::create_dir_all(&config_dir).with_context(|| format!("create {config_dir}"))?;
|
||||
let _ = write!(binds, " --bind-ro={config_dir}:/agents/{agent_name}/config");
|
||||
|
||||
// Per-agent socket subdir (#784 phase 2 step 2b). Bind-mounts
|
||||
// `/run/hive-agent/<name>/` into the container at the same
|
||||
// path so the harness's `HIVE_WEB_SOCKET` bind has a stable
|
||||
// location both sides can see. Sub-agents only — the
|
||||
// manager's UI is served at `/` via the c0re dashboard
|
||||
// upstream, not via `/agent/<name>/`, so it never needs the
|
||||
// per-agent socket dir.
|
||||
// Per-agent socket subdir. Bind-mounts `/run/hive-agent/<name>/`
|
||||
// into the container at the same path so the harness's
|
||||
// `HIVE_WEB_SOCKET` bind has a stable location both sides can
|
||||
// see. Sub-agents only — the manager's UI is served at `/`
|
||||
// via the c0re dashboard upstream, not via `/agent/<name>/`,
|
||||
// so it never needs the per-agent socket dir.
|
||||
//
|
||||
// Bind-mounting the SUBDIR (not the socket file) is mandatory:
|
||||
// the harness's `bind_unix` helper unlinks any stale socket
|
||||
|
|
@ -1161,8 +1151,8 @@ fn set_nspawn_flags(
|
|||
//
|
||||
// Per-agent dir (rather than a shared `/run/hive-agent/`
|
||||
// mount) means the agent's container only sees its own
|
||||
// subdir — never siblings' (mara on #800: "agents can only
|
||||
// access their own sockets").
|
||||
// subdir — never siblings'. See `docs/gateway.md::Per-agent
|
||||
// unix-socket upstream`.
|
||||
//
|
||||
// mkdir source defensively: nspawn refuses to start when the
|
||||
// bind source is missing, and on a fresh host `/run/hive-agent/`
|
||||
|
|
|
|||
Loading…
Reference in a new issue