mirror of
https://github.com/openai/codex.git
synced 2026-04-30 11:21:34 +03:00
seatbelt: honor split filesystem sandbox policies (#13448)
## Why After `#13440` and `#13445`, macOS Seatbelt policy generation was still deriving filesystem and network behavior from the legacy `SandboxPolicy` projection. That projection loses explicit unreadable carveouts and conflates split network decisions, so the generated Seatbelt policy could still be wider than the split policy that Codex had already computed. ## What changed - added Seatbelt entrypoints that accept `FileSystemSandboxPolicy` and `NetworkSandboxPolicy` directly - built read and write policy stanzas from access roots plus excluded subpaths so explicit unreadable carveouts survive into the generated Seatbelt policy - switched network policy generation to consult `NetworkSandboxPolicy` directly - failed closed when managed-network or proxy-constrained sessions do not yield usable loopback proxy endpoints - updated the macOS callers and test helpers that now need to carry the split policies explicitly ## Verification - added regression coverage in `core/src/seatbelt.rs` for unreadable carveouts under both full-disk and scoped-readable policies - verified the current PR state with `just clippy` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13448). * #13453 * #13452 * #13451 * #13449 * __->__ #13448 * #13445 * #13440 * #13439 --------- Co-authored-by: viyatb-oai <viyatb@openai.com>
This commit is contained in:
@@ -208,14 +208,17 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let requested_write = test.workspace_path("requested-but-unused.txt");
|
||||
let requested_dir = test.workspace_path("requested-dir");
|
||||
fs::create_dir_all(&requested_dir)?;
|
||||
let requested_dir_canonical = requested_dir.canonicalize()?;
|
||||
let requested_write = requested_dir.join("requested-but-unused.txt");
|
||||
let _ = fs::remove_file(&requested_write);
|
||||
let call_id = "request_permissions_skip_approval";
|
||||
let command = "touch requested-but-unused.txt";
|
||||
let command = "touch requested-dir/requested-but-unused.txt";
|
||||
let requested_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(&requested_write)]),
|
||||
write: Some(vec![absolute_path(&requested_dir_canonical)]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -292,6 +295,7 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul
|
||||
|
||||
let nested_dir = test.workspace_path("nested");
|
||||
fs::create_dir_all(&nested_dir)?;
|
||||
let nested_dir_canonical = nested_dir.canonicalize()?;
|
||||
let requested_write = nested_dir.join("relative-write.txt");
|
||||
let _ = fs::remove_file(&requested_write);
|
||||
|
||||
@@ -300,7 +304,7 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul
|
||||
let expected_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![absolute_path(&requested_write)]),
|
||||
write: Some(vec![absolute_path(&nested_dir_canonical)]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -310,7 +314,7 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul
|
||||
Some("nested"),
|
||||
json!({
|
||||
"file_system": {
|
||||
"write": ["./relative-write.txt"],
|
||||
"write": ["."],
|
||||
},
|
||||
}),
|
||||
)?;
|
||||
@@ -366,7 +370,8 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn read_only_with_additional_permissions_widens_to_unrequested_cwd_write() -> Result<()> {
|
||||
async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_cwd_write()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
@@ -440,16 +445,18 @@ async fn read_only_with_additional_permissions_widens_to_unrequested_cwd_write()
|
||||
|
||||
let result = parse_result(&results.single_request().function_call_output(call_id));
|
||||
assert!(
|
||||
result.exit_code.is_none() || result.exit_code == Some(0),
|
||||
"unexpected exit code/output: {:?} {}",
|
||||
result.exit_code != Some(0),
|
||||
"unrequested cwd write should stay denied: {:?} {}",
|
||||
result.exit_code,
|
||||
result.stdout
|
||||
);
|
||||
assert!(result.stdout.contains("cwd-widened"));
|
||||
assert_eq!(fs::read_to_string(&unrequested_write)?, "cwd-widened");
|
||||
assert!(
|
||||
!requested_write.exists(),
|
||||
"only the unrequested cwd path should have been written"
|
||||
"requested path should remain untouched when the command targets an unrequested file"
|
||||
);
|
||||
assert!(
|
||||
!unrequested_write.exists(),
|
||||
"unrequested cwd write should remain blocked"
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(unrequested_write);
|
||||
@@ -459,7 +466,8 @@ async fn read_only_with_additional_permissions_widens_to_unrequested_cwd_write()
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn read_only_with_additional_permissions_widens_to_unrequested_tmp_write() -> Result<()> {
|
||||
async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_tmp_write()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
@@ -534,16 +542,18 @@ async fn read_only_with_additional_permissions_widens_to_unrequested_tmp_write()
|
||||
|
||||
let result = parse_result(&results.single_request().function_call_output(call_id));
|
||||
assert!(
|
||||
result.exit_code.is_none() || result.exit_code == Some(0),
|
||||
"unexpected exit code/output: {:?} {}",
|
||||
result.exit_code != Some(0),
|
||||
"unrequested tmp write should stay denied: {:?} {}",
|
||||
result.exit_code,
|
||||
result.stdout
|
||||
);
|
||||
assert!(result.stdout.contains("tmp-widened"));
|
||||
assert_eq!(fs::read_to_string(&tmp_write)?, "tmp-widened");
|
||||
assert!(
|
||||
!requested_write.exists(),
|
||||
"only the unrequested tmp path should have been written"
|
||||
"requested path should remain untouched when the command targets an unrequested file"
|
||||
);
|
||||
assert!(
|
||||
!tmp_write.exists(),
|
||||
"unrequested tmp write should remain blocked"
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(tmp_write);
|
||||
|
||||
Reference in New Issue
Block a user