From d02dad99d5386e8528362e3ec9dc2d2b98d12274 Mon Sep 17 00:00:00 2001 From: James Carnegie Date: Mon, 20 Jul 2026 11:56:47 +0100 Subject: [PATCH] fix(tool-sandbox): skip missing fs_write_file grants instead of denying Signed-off-by: James Carnegie --- crates/nono-cli/src/tool-sandbox/mod.rs | 48 +++++++++++++++++++ .../src/tool-sandbox/platform/linux.rs | 2 +- .../src/tool-sandbox/platform/macos.rs | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/nono-cli/src/tool-sandbox/mod.rs b/crates/nono-cli/src/tool-sandbox/mod.rs index da229627e..3261d43ab 100644 --- a/crates/nono-cli/src/tool-sandbox/mod.rs +++ b/crates/nono-cli/src/tool-sandbox/mod.rs @@ -173,6 +173,54 @@ pub(crate) fn agent_can_write( && (path.starts_with(policy_root) || caps_grant(outer_caps, path, nono::AccessMode::Write)) } +// A policy's fs_write_file entries are best-effort: not every candidate +// path exists on every machine (e.g. a log file some other tool creates +// lazily). Skip a missing one rather than denying the whole command — +// mirrors add_optional_dir/add_optional_read_file in each platform module. +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) fn add_optional_write_file( + caps: &mut nono::CapabilitySet, + path: std::path::PathBuf, +) -> nono::Result<()> { + match nono::FsCapability::new_file(&path, nono::AccessMode::ReadWrite) { + Ok(capability) => { + caps.add_fs(capability); + Ok(()) + } + Err(nono::NonoError::PathNotFound(_)) => Ok(()), + Err(err) => Err(err), + } +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[cfg(test)] +mod add_optional_write_file_tests { + use super::add_optional_write_file; + use nono::CapabilitySet; + use std::path::PathBuf; + + #[test] + fn missing_path_is_skipped_not_an_error() { + let mut caps = CapabilitySet::new(); + + let result = add_optional_write_file(&mut caps, PathBuf::from("/no/such/path.log")); + + assert!(result.is_ok()); + assert!(caps.fs_capabilities().is_empty()); + } + + #[test] + fn existing_path_is_granted() { + let mut caps = CapabilitySet::new(); + let file = tempfile::NamedTempFile::new().expect("tempfile"); + + let result = add_optional_write_file(&mut caps, file.path().to_path_buf()); + + assert!(result.is_ok()); + assert_eq!(caps.fs_capabilities().len(), 1); + } +} + #[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(test)] mod agent_can_write_tests { diff --git a/crates/nono-cli/src/tool-sandbox/platform/linux.rs b/crates/nono-cli/src/tool-sandbox/platform/linux.rs index 209721fce..c05eab385 100644 --- a/crates/nono-cli/src/tool-sandbox/platform/linux.rs +++ b/crates/nono-cli/src/tool-sandbox/platform/linux.rs @@ -3592,7 +3592,7 @@ fn add_policy_fs( if matches!(write_access(&path), AccessMode::Read) { add_optional_read_file(caps, path)?; } else { - caps.add_fs(FsCapability::new_file(path, AccessMode::ReadWrite)?); + super::add_optional_write_file(caps, path)?; } } Ok(()) diff --git a/crates/nono-cli/src/tool-sandbox/platform/macos.rs b/crates/nono-cli/src/tool-sandbox/platform/macos.rs index 6d91088b3..1e6d68f1f 100644 --- a/crates/nono-cli/src/tool-sandbox/platform/macos.rs +++ b/crates/nono-cli/src/tool-sandbox/platform/macos.rs @@ -3068,7 +3068,7 @@ fn add_policy_fs( if matches!(write_access(&path), AccessMode::Read) { add_optional_read_file(caps, path)?; } else { - caps.add_fs(FsCapability::new_file(path, AccessMode::ReadWrite)?); + super::add_optional_write_file(caps, path)?; } } Ok(())