diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 84c782fd..61c06b7e 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -660,7 +660,7 @@ pub async fn destroy(coord: &Arc, 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 + // The state root may be a btrfs subvolume: 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. diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index b0c03169..f46c50fd 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -878,7 +878,7 @@ pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> { /// 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 +/// Progressive enhancement: 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 diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index ee5b6ab7..7bffe4d1 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -565,17 +565,32 @@ async fn ensure_agent_subvolume(agent_name: &str) -> Result<(String, String)> { ); } - // The subvol root is created root-owned; hive-c0re (the `hive-core` user) - // must be able to mkdir state/ claude/ harness/ inside it, exactly as it - // would in a plain dir. Match the owner of AGENT_STATE_ROOT (hive-core). - if let Ok(meta) = std::fs::metadata(&root) - && let Err(e) = std::os::unix::fs::chown(&agent_root, Some(meta.uid()), Some(meta.gid())) - { - tracing::warn!( - agent = %agent_name, - error = %e, - "ensure_agent_subvolume: chown subvol to agents-root owner failed" - ); + // The subvol root is created root-owned, but hive-c0re (the `hive-core` + // user) must be able to mkdir state/ claude/ harness/ inside it — exactly + // as it would in a plain dir. Chown it to AGENT_STATE_ROOT's owner + // (hive-core). This MUST succeed: a root-owned subvol would make the + // downstream dir creation fail with a confusing permission error, and the + // c0re-side exists-check would then skip re-running this op on retry, + // wedging the agent. So on any failure roll the subvol back and bail — the + // create path surfaces a clear error and a retry starts clean. + let chown_result = std::fs::metadata(&root) + .with_context(|| format!("stat agents root {} for ownership", root.display())) + .and_then(|meta| { + std::os::unix::fs::chown(&agent_root, Some(meta.uid()), Some(meta.gid())).with_context( + || format!("chown subvol {} to agents-root owner", agent_root.display()), + ) + }); + if let Err(e) = chown_result { + // Best-effort rollback so we never leave a root-owned subvol behind. + let _ = Command::new("btrfs") + .args(["subvolume", "delete"]) + .arg(&agent_root) + .output() + .await; + return Err(e.context(format!( + "rolled back subvolume {} after chown failed", + agent_root.display() + ))); } tracing::info!(agent = %agent_name, path = %agent_root.display(), "created agent state subvolume"); Ok((String::new(), String::new()))