hive-c0re: back agent state dirs with btrfs subvolumes

Progressive enhancement: a brand-new agent's state root under
/var/lib/hyperhive/agents is created as a btrfs subvolume when the host
filesystem is btrfs, otherwise it falls back to a plain directory. No
existing agent is auto-migrated — the new path only fires when the root
does not yet exist, so plain-dir agents are left untouched until an
explicit opt-in upgrade.

Two new privileged ops (subvolume create/delete are root-only):
EnsureAgentSubvolume statfs-gates on btrfs, creates the subvolume, and
chowns it to the hive-core user so the normal state/claude/harness
mkdirs succeed inside it; DeleteAgentSubvolume btrfs-subvolume-deletes
the root iff it is actually a subvolume. hive-c0re calls Ensure before
the per-agent dirs are created (spawn/rebuild/InitConfig) and Delete on
the purge path only — destroy keeps the subvolume for revival, matching
plain-dir semantics. btrfs-progs added to the hive-priv unit PATH.

Per-subvolume usage accounting + optional quota is a separate
follow-up.
This commit is contained in:
atlas 2026-06-19 12:28:17 +02:00 committed by mara
commit 1f602d5fda
9 changed files with 242 additions and 0 deletions

View file

@ -269,6 +269,7 @@ pub async fn spawn(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()>
}
setup_proposed(&paths.proposed_dir, name).await?;
setup_applied(&paths.applied_dir, Some(&paths.proposed_dir), name).await?;
ensure_agent_state_subvolume(name).await?;
ensure_claude_dir(&paths.claude_dir)?;
ensure_state_dir(&paths.notes_dir)?;
// Meta flake gets the new agent's input + nixosConfiguration
@ -459,6 +460,7 @@ pub async fn rebuild_no_meta(
);
}
setup_applied(&paths.applied_dir, None, name).await?;
ensure_agent_state_subvolume(name).await?;
ensure_claude_dir(&paths.claude_dir)?;
ensure_state_dir(&paths.notes_dir)?;
let container = container_name(name);
@ -871,6 +873,28 @@ pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
Ok(())
}
/// Ensure agent `name`'s persistent state root
/// (`/var/lib/hyperhive/agents/<name>`) is a btrfs subvolume — when the host
/// filesystem supports it — BEFORE the per-agent subdirs (`state/`, `claude/`,
/// `harness/`) are created by `ensure_state_dir` / `ensure_claude_dir`.
///
/// Progressive enhancement (the #1762 model): if the root already exists
/// (any agent provisioned before this landed, plain dir or subvol) it's left
/// exactly as-is — no auto-migration — and the priv round-trip is skipped. On
/// a non-btrfs host the priv op no-ops and the root is later created as a
/// plain dir by `ensure_*_dir`, identical to the old behaviour. Only a
/// brand-new agent on a btrfs host gets a real subvolume. Subvolume creation
/// is privileged, so it's delegated to hive-priv.
pub async fn ensure_agent_state_subvolume(name: &str) -> Result<()> {
let root = Path::new(HOST_AGENTS_ROOT).join(name);
if root.exists() {
return Ok(());
}
crate::priv_client::ensure_agent_subvolume(name)
.await
.with_context(|| format!("ensure btrfs subvolume for agent {name}"))
}
fn initial_agent_nix(name: &str) -> String {
format!(
"{{ config, pkgs, lib, ... }}:\n{{\n # Per-agent overrides for {name}. This is a regular NixOS module\n # — add packages, services, modules, imports as needed.\n #\n # imports = [ ./extra-module.nix ];\n # environment.systemPackages = with pkgs; [ ];\n}}\n",