diff --git a/crates/application/src/plugin/mod.rs b/crates/application/src/plugin/mod.rs index 7cc52c91..18b01136 100644 --- a/crates/application/src/plugin/mod.rs +++ b/crates/application/src/plugin/mod.rs @@ -2443,6 +2443,10 @@ fn workspace_exec_sandbox( abs_root: root.as_str().to_owned(), access: project_access, }]; + allowed.push(domain::PathGrant { + abs_root: "/dev/null".to_owned(), + access: domain::PathAccess::RO | domain::PathAccess::RW, + }); for system_root in ["/bin", "/usr", "/lib", "/lib64", "/etc"] { allowed.push(domain::PathGrant { abs_root: system_root.to_owned(), @@ -6063,6 +6067,29 @@ mod tests { } } + #[test] + fn workspace_exec_read_sandbox_grants_dev_null_without_widening_project_access() { + let root = ProjectPath::new("/workspace/example").unwrap(); + let sandbox = workspace_exec_sandbox(&root, PluginWorkspaceExecEffect::Read); + + assert_eq!( + sandbox + .allowed + .iter() + .find(|grant| grant.abs_root == root.as_str()) + .map(|grant| grant.access), + Some(domain::PathAccess::RO | domain::PathAccess::EXEC) + ); + assert_eq!( + sandbox + .allowed + .iter() + .find(|grant| grant.abs_root == "/dev/null") + .map(|grant| grant.access), + Some(domain::PathAccess::RO | domain::PathAccess::RW) + ); + } + #[tokio::test] async fn workspace_exec_allows_matching_rule_clamps_limits_and_audits_metadata() { let (project_id, tool, runner, audit) = workspace_exec_fixture(); diff --git a/crates/infrastructure/src/workspace_tool.rs b/crates/infrastructure/src/workspace_tool.rs index 0f0d3c25..dbb5ccc6 100644 --- a/crates/infrastructure/src/workspace_tool.rs +++ b/crates/infrastructure/src/workspace_tool.rs @@ -263,4 +263,18 @@ mod tests { assert!(result.stdout_truncated); assert!(!result.stderr_truncated); } + + #[cfg(unix)] + #[tokio::test] + async fn local_runner_allows_cli_to_open_dev_null_for_read_and_write() { + let runner = LocalWorkspaceToolRunner::new(Arc::new(crate::sandbox::NoopSandbox)); + let result = runner + .run(spec(&["-c", "exec 3<>/dev/null; printf ok"], 1_000, 32)) + .await + .unwrap(); + + assert_eq!(result.exit_code, Some(0)); + assert_eq!(result.stdout, b"ok"); + assert!(result.stderr.is_empty()); + } }