Compare commits

...

1 Commits

Author SHA1 Message Date
Eva Wong
6003ea43ed Preserve exec add-dir roots for macOS sandbox 2026-05-01 14:53:22 -07:00
2 changed files with 146 additions and 4 deletions

View File

@@ -81,6 +81,9 @@ use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ReviewTarget;
@@ -132,6 +135,7 @@ pub use exec_events::TurnStartedEvent;
pub use exec_events::Usage;
pub use exec_events::WebSearchItem;
use serde_json::Value;
use serde_json::json;
use std::collections::HashMap;
use std::io::IsTerminal;
use std::io::Read;
@@ -1016,10 +1020,86 @@ fn sandbox_mode_from_permission_profile(
}
fn config_request_overrides_from_config(config: &Config) -> Option<HashMap<String, Value>> {
config
.active_profile
.as_ref()
.map(|profile| HashMap::from([("profile".to_string(), Value::String(profile.clone()))]))
let mut overrides = HashMap::new();
if let Some(profile) = config.active_profile.as_ref() {
overrides.insert("profile".to_string(), Value::String(profile.clone()));
}
add_workspace_write_request_overrides(config, &mut overrides);
(!overrides.is_empty()).then_some(overrides)
}
fn add_workspace_write_request_overrides(config: &Config, overrides: &mut HashMap<String, Value>) {
if config.permissions.active_permission_profile().is_some() {
return;
}
let permission_profile = config.permissions.permission_profile();
let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions();
if !has_writable_project_roots_entry(&file_system_policy) {
return;
}
let writable_roots = explicit_writable_roots(&file_system_policy);
let network_access = network_policy.is_enabled();
let exclude_tmpdir_env_var = !has_writable_special_entry(&file_system_policy, |value| {
matches!(value, FileSystemSpecialPath::Tmpdir)
});
let exclude_slash_tmp = !has_writable_special_entry(&file_system_policy, |value| {
matches!(value, FileSystemSpecialPath::SlashTmp)
});
if writable_roots.is_empty() && !network_access && !exclude_tmpdir_env_var && !exclude_slash_tmp
{
return;
}
overrides.insert(
"sandbox_workspace_write".to_string(),
json!({
"writable_roots": writable_roots,
"network_access": network_access,
"exclude_tmpdir_env_var": exclude_tmpdir_env_var,
"exclude_slash_tmp": exclude_slash_tmp,
}),
);
}
fn explicit_writable_roots(file_system_policy: &FileSystemSandboxPolicy) -> Vec<AbsolutePathBuf> {
let mut writable_roots = Vec::new();
for entry in &file_system_policy.entries {
let FileSystemPath::Path { path } = &entry.path else {
continue;
};
if !entry.access.can_write() {
continue;
}
if writable_roots.iter().any(|existing| existing == path) {
continue;
}
writable_roots.push(path.clone());
}
writable_roots
}
fn has_writable_project_roots_entry(file_system_policy: &FileSystemSandboxPolicy) -> bool {
has_writable_special_entry(file_system_policy, |value| {
matches!(
value,
FileSystemSpecialPath::ProjectRoots { subpath } if subpath.is_none()
)
})
}
fn has_writable_special_entry(
file_system_policy: &FileSystemSandboxPolicy,
matches_special_path: impl Fn(&FileSystemSpecialPath) -> bool,
) -> bool {
file_system_policy.entries.iter().any(|entry| {
entry.access.can_write()
&& matches!(
&entry.path,
FileSystemPath::Special { value } if matches_special_path(value)
)
})
}
fn approvals_reviewer_override_from_config(

View File

@@ -422,6 +422,68 @@ async fn thread_lifecycle_params_include_legacy_sandbox_when_no_active_profile()
assert_eq!(resume_params.permissions, None);
}
#[cfg(not(target_os = "windows"))]
#[tokio::test]
async fn thread_lifecycle_params_preserve_workspace_write_roots() {
let codex_home = tempdir().expect("create temp codex home");
let workspace = tempdir().expect("create temp workspace");
let frontend = workspace.path().join("frontend");
let backend = workspace.path().join("backend");
std::fs::create_dir_all(&frontend).expect("create frontend dir");
std::fs::create_dir_all(&backend).expect("create backend dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(frontend),
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
additional_writable_roots: vec![backend.clone()],
..Default::default()
})
.build()
.await
.expect("build config with workspace-write root");
let start_params = thread_start_params_from_config(&config);
let resume_params = thread_resume_params_from_config(&config, "thread-id".to_string());
let expected_backend = backend.abs().as_path().to_string_lossy().into_owned();
for config_overrides in [
start_params
.config
.as_ref()
.expect("start config overrides"),
resume_params
.config
.as_ref()
.expect("resume config overrides"),
] {
let sandbox_workspace_write = config_overrides
.get("sandbox_workspace_write")
.expect("sandbox workspace-write overrides");
let writable_roots = sandbox_workspace_write
.get("writable_roots")
.and_then(serde_json::Value::as_array)
.expect("writable roots array");
assert!(
writable_roots
.iter()
.any(|root| root.as_str() == Some(expected_backend.as_str())),
"expected writable_roots to include {expected_backend}, got {writable_roots:?}"
);
}
assert_eq!(
start_params.sandbox,
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
);
assert_eq!(start_params.permissions, None);
assert_eq!(
resume_params.sandbox,
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
);
assert_eq!(resume_params.permissions, None);
}
#[tokio::test]
async fn session_configured_from_thread_response_uses_review_policy_from_response() {
let codex_home = tempdir().expect("create temp codex home");