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:
parent
309cdc7546
commit
1f602d5fda
9 changed files with 242 additions and 0 deletions
|
|
@ -315,6 +315,7 @@ async fn run_approval_init_config(
|
|||
) -> Result<()> {
|
||||
let result: Result<()> = async {
|
||||
lifecycle::setup_proposed(&proposed_dir, &approval.agent).await?;
|
||||
lifecycle::ensure_agent_state_subvolume(&approval.agent).await?;
|
||||
lifecycle::ensure_claude_dir(&claude_dir)?;
|
||||
lifecycle::ensure_state_dir(¬es_dir)?;
|
||||
Ok(())
|
||||
|
|
@ -659,6 +660,13 @@ pub async fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) -> Resul
|
|||
let _ = std::fs::remove_dir_all(&runtime);
|
||||
}
|
||||
if purge {
|
||||
// The state root may be a btrfs subvolume (#1762): a subvolume root
|
||||
// can't be removed with rmdir/`remove_dir_all`, so delete it via
|
||||
// hive-priv (root) first. No-op for plain-dir agents — the loop below
|
||||
// then handles the plain-dir state root plus the applied dir.
|
||||
if let Err(e) = crate::priv_client::delete_agent_subvolume(name).await {
|
||||
tracing::warn!(error = ?e, %name, "purge: delete state subvolume failed");
|
||||
}
|
||||
for dir in [
|
||||
Coordinator::agent_state_root(name),
|
||||
Coordinator::agent_applied_dir(name),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,27 @@ pub async fn control_infra_container(container: &str, action: InfraAction) -> Re
|
|||
.await?)
|
||||
}
|
||||
|
||||
/// Ensure the agent's persistent state root is a btrfs subvolume when the
|
||||
/// host FS supports it (via hive-priv, which runs as root). Idempotent and
|
||||
/// progressive: a no-op when the root already exists or the FS isn't btrfs.
|
||||
/// Safe to call on every provision.
|
||||
pub async fn ensure_agent_subvolume(agent_name: &str) -> Result<()> {
|
||||
ok(call(&PrivRequest::EnsureAgentSubvolume {
|
||||
agent_name: agent_name.to_owned(),
|
||||
})
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Delete the agent's state root iff it is a btrfs subvolume (purge path
|
||||
/// only). No-op for plain dirs / missing paths — hive-c0re's own
|
||||
/// `remove_dir_all` handles those.
|
||||
pub async fn delete_agent_subvolume(agent_name: &str) -> Result<()> {
|
||||
ok(call(&PrivRequest::DeleteAgentSubvolume {
|
||||
agent_name: agent_name.to_owned(),
|
||||
})
|
||||
.await?)
|
||||
}
|
||||
|
||||
fn check(resp: PrivResponse) -> Result<(String, String)> {
|
||||
if resp.ok {
|
||||
Ok((resp.stdout, resp.stderr))
|
||||
|
|
|
|||
Loading…
Reference in a new issue