From 4e3ab9e50667fdc2cb59a023ac6ae4ea7c63ae26 Mon Sep 17 00:00:00 2001 From: shiva Date: Sat, 15 Aug 2026 11:55:32 +0530 Subject: [PATCH] fix(agentflare-jobs): bind ~/.agentflare read-write into job sandboxes Dispatched jobs run inside a bwrap sandbox with a read-only root, and ~/.agentflare (the MCP server's own sqlite state dir) wasn't in the bind-mount allowlist at all -- every write through it (item done, comment create, vent) failed silently with EROFS. Two dispatched jobs (items #112, #116) each finished real work but couldn't report it, leaving the tracker stuck showing not-done despite merge-ready PRs. Add a --bind-try (writable, skipped if missing) mount for ~/.agentflare alongside the existing home-dir binds, same pattern as the read-only HOME_CACHE_DIRS entries but writable since this dir needs to persist state back to the host rather than being contained per-job. Agentflare-Agent: claude-code Agentflare-Branch: task/120-fix-dispatched-job-sandbox-mounts-agentf Agentflare-Item: 120 --- .../agentflare-jobs/src/sandbox/bwrap/mod.rs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/crates/agentflare-jobs/src/sandbox/bwrap/mod.rs b/crates/agentflare-jobs/src/sandbox/bwrap/mod.rs index f568135c..18193634 100644 --- a/crates/agentflare-jobs/src/sandbox/bwrap/mod.rs +++ b/crates/agentflare-jobs/src/sandbox/bwrap/mod.rs @@ -40,6 +40,20 @@ const HOME_CACHE_DIRS: &[&str] = &[".cargo", ".rustup", ".cache", ".npm"]; /// since this one dir needs both. const OPENCODE_DATA_DIR_RELATIVE: &str = ".local/share/opencode"; +/// The agentflare MCP server's own state dir, relative to `$HOME` -- holds +/// `agentflare.db`, the sqlite store every `item`/`comment`/`vent` MCP call +/// writes through. Unlike `HOME_CACHE_DIRS`, this one must be writable: a +/// dispatched job's whole job-completion signal (`item action=done`, +/// `comment action=create`, even `vent` for reporting a sandbox problem like +/// this one) is an MCP call into this same DB, so read-only here means the +/// agent finishes real work with no way to report it (item #120 -- confirmed +/// live via `EROFS` on `touch ~/.agentflare/probe` and two dispatched jobs +/// stuck showing not-done despite merge-ready PRs). Bound with `--bind-try` +/// (writable, silently skipped if missing) rather than `--bind`, matching +/// the "missing dir is fine, wrong dir is not" fallback the other optional +/// home binds use. +const AGENTFLARE_DATA_DIR_RELATIVE: &str = ".agentflare"; + /// Whether `command` (the resolved binary about to be run, e.g. /// `/home/user/.opencode/bin/opencode`) is opencode -- matched on the final /// path component so it doesn't care whether the caller passed a bare name @@ -171,6 +185,14 @@ fn build_bwrap_args_with_home( } } + let agentflare_dir = Path::new(home).join(AGENTFLARE_DATA_DIR_RELATIVE); + if agentflare_dir.exists() { + let agentflare_str = path_to_string(&agentflare_dir); + bwrap_args.push("--bind-try".to_string()); + bwrap_args.push(agentflare_str.clone()); + bwrap_args.push(agentflare_str); + } + if is_opencode(command) { let opencode_dir = Path::new(home).join(OPENCODE_DATA_DIR_RELATIVE); let opencode_str = path_to_string(&opencode_dir); @@ -513,6 +535,34 @@ mod tests { assert_eq!(args[idx - 1], "--ro-bind-try"); } + #[test] + fn agentflare_data_dir_bound_read_write_when_present() { + // Item #120: `~/.agentflare` holds the sqlite DB every `item`/ + // `comment`/`vent` MCP call writes through, so a dispatched job + // needs write access to report its own completion -- read-only (or + // missing entirely) means the job finishes real work with no way to + // signal it, exactly what happened live on items #112 and #116. + let dir = tempfile::tempdir().unwrap(); + std::fs::create_dir(dir.path().join(AGENTFLARE_DATA_DIR_RELATIVE)).unwrap(); + let home = std::ffi::OsString::from(dir.path()); + let args = build_bwrap_args_with_home(None, "true", &[], Some(&home), false); + let agentflare_path = path_to_string(&dir.path().join(AGENTFLARE_DATA_DIR_RELATIVE)); + let idx = args + .iter() + .position(|a| a == &agentflare_path) + .expect(".agentflare dir bound"); + assert_eq!(args[idx - 1], "--bind-try"); + } + + #[test] + fn agentflare_data_dir_skipped_when_absent() { + let dir = tempfile::tempdir().unwrap(); + let home = std::ffi::OsString::from(dir.path()); + let args = build_bwrap_args_with_home(None, "true", &[], Some(&home), false); + let agentflare_path = path_to_string(&dir.path().join(AGENTFLARE_DATA_DIR_RELATIVE)); + assert!(!args.iter().any(|a| a == &agentflare_path)); + } + #[test] fn opencode_data_dir_overlaid_when_present() { let dir = tempfile::tempdir().unwrap();