mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Enforce managed deny-read rules in the Windows sandbox (#38660)
## Why Windows sandbox requests must preserve managed filesystem deny rules across every execution path and setup refresh. Unsupported policies should fail closed instead of allowing a command to run without the requested protection. ## What changed - Resolve Windows filesystem overrides while constructing each sandbox execution request so both `shell_command` and `exec_command` enforce exact-path and glob deny-read entries. - Carry resolved deny-read paths into Windows sandbox setup refreshes, including workspace-relative entries. - Reject unelevated restricted-token requests that cannot enforce deny-read rules. - Reject recursive globs rooted at a filesystem root unless `glob_scan_max_depth` bounds their expansion. ## Testing Added coverage for deny-read enforcement through both command runtimes, setup refresh resolution, restricted-token failure, and bounded root-level glob scans. GitOrigin-RevId: 33dfa4d1a45b14850cbf58f9173717e3e707d9bf
This commit is contained in:
@@ -45,10 +45,13 @@ pub(crate) use codex_sandboxing::is_likely_sandbox_denied;
|
||||
#[cfg(test)]
|
||||
use codex_sandboxing::permission_profile_supports_windows_restricted_token_sandbox;
|
||||
use codex_sandboxing::record_filesystem_sandbox_violation;
|
||||
#[cfg(test)]
|
||||
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
|
||||
#[cfg(test)]
|
||||
use codex_sandboxing::resolve_windows_restricted_token_filesystem_overrides;
|
||||
#[cfg(test)]
|
||||
use codex_sandboxing::unsupported_windows_restricted_token_sandbox_reason;
|
||||
#[cfg(any(test, target_os = "windows"))]
|
||||
use codex_sandboxing::windows_sandbox_uses_elevated_backend;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -379,7 +382,7 @@ pub fn build_exec_request(
|
||||
expiration,
|
||||
capture_policy,
|
||||
};
|
||||
let mut exec_req = manager
|
||||
let request = manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command,
|
||||
permissions: permission_profile,
|
||||
@@ -393,38 +396,13 @@ pub fn build_exec_request(
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
})
|
||||
.map(|request| {
|
||||
let windows_sandbox_workspace_roots = if windows_sandbox_workspace_roots.is_empty() {
|
||||
vec![sandbox_cwd.clone()]
|
||||
} else {
|
||||
windows_sandbox_workspace_roots.to_vec()
|
||||
};
|
||||
ExecRequest::from_sandbox_exec_request(
|
||||
request,
|
||||
options,
|
||||
windows_sandbox_workspace_roots,
|
||||
)
|
||||
})
|
||||
.map_err(CodexErr::from)?;
|
||||
let use_windows_elevated_backend =
|
||||
windows_sandbox_uses_elevated_backend(exec_req.windows_sandbox_level);
|
||||
exec_req.windows_sandbox_filesystem_overrides = if use_windows_elevated_backend {
|
||||
resolve_windows_elevated_filesystem_overrides(
|
||||
exec_req.sandbox,
|
||||
&exec_req.permission_profile,
|
||||
sandbox_cwd,
|
||||
use_windows_elevated_backend,
|
||||
)
|
||||
let windows_sandbox_workspace_roots = if windows_sandbox_workspace_roots.is_empty() {
|
||||
vec![sandbox_cwd.clone()]
|
||||
} else {
|
||||
resolve_windows_restricted_token_filesystem_overrides(
|
||||
exec_req.sandbox,
|
||||
&exec_req.permission_profile,
|
||||
sandbox_cwd,
|
||||
exec_req.windows_sandbox_level,
|
||||
)
|
||||
}
|
||||
.map_err(CodexErr::UnsupportedOperation)?;
|
||||
Ok(exec_req)
|
||||
windows_sandbox_workspace_roots.to_vec()
|
||||
};
|
||||
ExecRequest::from_sandbox_exec_request(request, options, windows_sandbox_workspace_roots)
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_exec_request(
|
||||
|
||||
@@ -19,14 +19,19 @@ use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
pub use codex_protocol::models::SandboxPermissions;
|
||||
use codex_sandboxing::SandboxExecRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
|
||||
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
|
||||
use codex_sandboxing::resolve_windows_restricted_token_filesystem_overrides;
|
||||
use codex_sandboxing::windows_sandbox_uses_elevated_backend;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use codex_utils_string::truncate_middle_with_token_budget;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -112,7 +117,7 @@ impl ExecRequest {
|
||||
request: SandboxExecRequest,
|
||||
options: ExecOptions,
|
||||
windows_sandbox_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
) -> Self {
|
||||
) -> Result<Self, CodexErr> {
|
||||
let SandboxExecRequest {
|
||||
command,
|
||||
cwd,
|
||||
@@ -131,6 +136,36 @@ impl ExecRequest {
|
||||
expiration,
|
||||
capture_policy,
|
||||
} = options;
|
||||
let windows_sandbox_filesystem_overrides = if sandbox == SandboxType::WindowsRestrictedToken
|
||||
{
|
||||
let sandbox_policy_cwd = windows_sandbox_policy_cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| CodexErr::InvalidRequest(format!("invalid sandbox cwd: {err}")))?;
|
||||
let use_windows_elevated_backend =
|
||||
windows_sandbox_uses_elevated_backend(windows_sandbox_level);
|
||||
if use_windows_elevated_backend {
|
||||
resolve_windows_elevated_filesystem_overrides(
|
||||
sandbox,
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
use_windows_elevated_backend,
|
||||
)
|
||||
} else {
|
||||
resolve_windows_restricted_token_filesystem_overrides(
|
||||
sandbox,
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
windows_sandbox_level,
|
||||
)
|
||||
}
|
||||
.map_err(|error| {
|
||||
CodexErr::UnsupportedOperation(
|
||||
truncate_middle_with_token_budget(&error, /*max_tokens*/ 900).0,
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let network_sandbox_policy = permission_profile.network_sandbox_policy();
|
||||
if !network_sandbox_policy.is_enabled() {
|
||||
env.insert(
|
||||
@@ -142,7 +177,7 @@ impl ExecRequest {
|
||||
if sandbox == SandboxType::MacosSeatbelt {
|
||||
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||||
}
|
||||
Self {
|
||||
Ok(Self {
|
||||
command,
|
||||
cwd,
|
||||
env,
|
||||
@@ -157,13 +192,13 @@ impl ExecRequest {
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
permission_profile,
|
||||
windows_sandbox_filesystem_overrides: None,
|
||||
windows_sandbox_filesystem_overrides,
|
||||
arg0,
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_managed_network: None,
|
||||
exec_server_network_proxy: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -981,7 +981,7 @@ impl CoreShellCommandExecutor {
|
||||
exec_request,
|
||||
options,
|
||||
self.windows_sandbox_workspace_roots.clone(),
|
||||
);
|
||||
)?;
|
||||
if let Some(network) = exec_request.network.as_ref() {
|
||||
network
|
||||
.apply_to_env_for_optional_environment(
|
||||
|
||||
@@ -453,11 +453,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
.iter()
|
||||
.map(PathUri::to_abs_path)
|
||||
.collect::<std::io::Result<Vec<_>>>()?;
|
||||
Ok(crate::sandboxing::ExecRequest::from_sandbox_exec_request(
|
||||
request,
|
||||
options,
|
||||
workspace_roots,
|
||||
))
|
||||
crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options, workspace_roots)
|
||||
}
|
||||
|
||||
pub fn env_for_exec_server(
|
||||
@@ -487,8 +483,11 @@ impl<'a> SandboxAttempt<'a> {
|
||||
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
||||
})
|
||||
.map_err(CodexErr::from)?;
|
||||
let mut exec_request =
|
||||
crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options, Vec::new());
|
||||
let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request(
|
||||
request,
|
||||
options,
|
||||
Vec::new(),
|
||||
)?;
|
||||
exec_request.exec_server_managed_network = managed_network;
|
||||
if self.sandbox_requested {
|
||||
exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {
|
||||
|
||||
@@ -5,6 +5,8 @@ use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
@@ -202,6 +204,107 @@ fn deny_read_blocks_explicit_escalation_and_policy_bypass() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_sandbox_env_preserves_denied_reads_or_rejects_unsupported_backend() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("create sandbox workspace");
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(
|
||||
dunce::canonicalize(temp_dir.path()).expect("canonicalize sandbox workspace"),
|
||||
)
|
||||
.expect("absolute sandbox workspace");
|
||||
let denied_path = cwd.join("blocked");
|
||||
std::fs::create_dir_all(denied_path.as_path()).expect("create denied directory");
|
||||
let denied_path = AbsolutePathBuf::from_absolute_path(
|
||||
dunce::canonicalize(denied_path.as_path()).expect("canonicalize denied directory"),
|
||||
)
|
||||
.expect("absolute denied directory");
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: denied_path.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
]);
|
||||
let permissions = codex_protocol::models::PermissionProfile::from_runtime_permissions(
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let manager = SandboxManager::new();
|
||||
let mut attempt = SandboxAttempt {
|
||||
sandbox: SandboxType::WindowsRestrictedToken,
|
||||
sandbox_requested: true,
|
||||
permissions: &permissions,
|
||||
exec_server_permissions: &permissions,
|
||||
enforce_managed_network: false,
|
||||
manager: &manager,
|
||||
sandbox_cwd: &cwd_uri,
|
||||
workspace_roots: std::slice::from_ref(&cwd_uri),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Elevated,
|
||||
windows_sandbox_private_desktop: false,
|
||||
network_denial_cancellation_token: None,
|
||||
network_proxy: None,
|
||||
};
|
||||
let command = || SandboxCommand {
|
||||
program: "cmd.exe".into(),
|
||||
args: vec!["/C".to_string(), "echo sandboxed".to_string()],
|
||||
cwd: cwd_uri.clone(),
|
||||
env: HashMap::new(),
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
};
|
||||
let options = || crate::sandboxing::ExecOptions {
|
||||
expiration: crate::exec::ExecExpiration::DefaultTimeout,
|
||||
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
|
||||
};
|
||||
|
||||
let request = attempt
|
||||
.env_for(
|
||||
command(),
|
||||
options(),
|
||||
/*network*/ None,
|
||||
/*environment_id*/ None,
|
||||
)
|
||||
.expect("prepare elevated Windows sandbox request");
|
||||
let overrides = request
|
||||
.windows_sandbox_filesystem_overrides
|
||||
.expect("elevated Windows sandbox should preserve deny-read overrides");
|
||||
assert_eq!(overrides.additional_deny_read_paths, vec![denied_path]);
|
||||
assert_eq!(request.windows_sandbox_workspace_roots, vec![cwd]);
|
||||
|
||||
attempt.windows_sandbox_level =
|
||||
codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken;
|
||||
let error = attempt
|
||||
.env_for(
|
||||
command(),
|
||||
options(),
|
||||
/*network*/ None,
|
||||
/*environment_id*/ None,
|
||||
)
|
||||
.expect_err("restricted-token Windows sandbox cannot enforce deny-read restrictions");
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"unsupported operation: windows unelevated restricted-token sandbox cannot enforce deny-read restrictions directly; refusing to run unsandboxed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_core::exec::ExecParams;
|
||||
use codex_core::exec::process_exec_tool_call;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_core::windows_sandbox::sandbox_setup_is_complete;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -14,7 +15,16 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::test_codex::TestCodexHarness;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use serial_test::serial;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
@@ -383,3 +393,156 @@ async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyh
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial(codex_home)]
|
||||
async fn windows_elevated_shell_and_unified_exec_enforce_managed_deny_reads() -> anyhow::Result<()>
|
||||
{
|
||||
let codex_home =
|
||||
codex_home_for_windows_sandbox_test("windows-elevated-tool-runtime-deny-read-codex-home")?;
|
||||
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str());
|
||||
stage_windows_sandbox_helpers()?;
|
||||
|
||||
let configured_codex_home = dunce::canonicalize(codex_home.path())?.abs();
|
||||
let builder = test_codex()
|
||||
.with_windows_cmd_shell()
|
||||
.with_config(move |config| {
|
||||
config.codex_home = configured_codex_home;
|
||||
config.set_windows_elevated_sandbox_enabled(true);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow unified exec");
|
||||
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: config.cwd.join("exact-secret.txt"),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
missing_path_behavior: None,
|
||||
},
|
||||
]);
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
))
|
||||
.expect("set managed deny-read permission profile");
|
||||
});
|
||||
let harness = TestCodexHarness::with_builder(builder).await?;
|
||||
harness
|
||||
.write_file("secret.env", "glob secret should remain private\n")
|
||||
.await?;
|
||||
harness
|
||||
.write_file("exact-secret.txt", "exact secret should remain private\n")
|
||||
.await?;
|
||||
harness.write_file("public.txt", "public ok\n").await?;
|
||||
|
||||
let command = concat!(
|
||||
"(type secret.env 1>NUL 2>NUL && echo GLOB-READ || echo GLOB-DENIED) & ",
|
||||
"(type exact-secret.txt 1>NUL 2>NUL && echo EXACT-READ || echo EXACT-DENIED) & ",
|
||||
"type public.txt"
|
||||
);
|
||||
let shell_call_id = "windows-managed-deny-read-shell-command";
|
||||
let unified_call_id = "windows-managed-deny-read-exec-command";
|
||||
let shell_args = json!({
|
||||
"command": command,
|
||||
"timeout_ms": 30_000,
|
||||
"login": false,
|
||||
});
|
||||
let unified_args = json!({
|
||||
"cmd": command,
|
||||
"yield_time_ms": 30_000,
|
||||
"tty": false,
|
||||
"login": false,
|
||||
});
|
||||
mount_sse_sequence(
|
||||
harness.server(),
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-windows-shell-deny-read"),
|
||||
ev_function_call(
|
||||
shell_call_id,
|
||||
"shell_command",
|
||||
&serde_json::to_string(&shell_args)?,
|
||||
),
|
||||
ev_completed("resp-windows-shell-deny-read"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-windows-unified-deny-read"),
|
||||
ev_function_call(
|
||||
unified_call_id,
|
||||
"exec_command",
|
||||
&serde_json::to_string(&unified_args)?,
|
||||
),
|
||||
ev_completed("resp-windows-unified-deny-read"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-windows-deny-read", "done"),
|
||||
ev_completed("resp-windows-deny-read-complete"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let permission_profile = harness
|
||||
.test()
|
||||
.config
|
||||
.permissions
|
||||
.effective_permission_profile();
|
||||
harness
|
||||
.submit_with_permission_profile("read the sandbox fixtures", permission_profile)
|
||||
.await?;
|
||||
|
||||
for (tool_name, call_id) in [
|
||||
("shell_command", shell_call_id),
|
||||
("exec_command", unified_call_id),
|
||||
] {
|
||||
let output = harness.function_call_stdout(call_id).await;
|
||||
assert!(
|
||||
output.contains("GLOB-DENIED"),
|
||||
"{tool_name} should reject glob-denied reads: {output:?}"
|
||||
);
|
||||
assert!(
|
||||
output.contains("EXACT-DENIED"),
|
||||
"{tool_name} should reject exact-path-denied reads: {output:?}"
|
||||
);
|
||||
assert!(
|
||||
output.contains("public ok"),
|
||||
"{tool_name} should preserve allowed reads: {output:?}"
|
||||
);
|
||||
assert!(
|
||||
!output.contains("GLOB-READ") && !output.contains("glob secret"),
|
||||
"{tool_name} leaked glob-denied file contents: {output:?}"
|
||||
);
|
||||
assert!(
|
||||
!output.contains("EXACT-READ") && !output.contains("exact secret"),
|
||||
"{tool_name} leaked exact-path-denied file contents: {output:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -52,9 +52,21 @@ pub fn resolve_windows_deny_read_paths(
|
||||
return Ok(paths);
|
||||
};
|
||||
|
||||
for pattern in unreadable_globs {
|
||||
let scan_plans = unreadable_globs
|
||||
.iter()
|
||||
.map(|pattern| {
|
||||
let scan_plan = glob_scan_plan(pattern, file_system_sandbox_policy.glob_scan_max_depth);
|
||||
if scan_plan.max_depth.is_none() && scan_plan.root.parent().is_none() {
|
||||
return Err(format!(
|
||||
"unreadable glob `{pattern}` cannot be safely expanded from a filesystem root without `glob_scan_max_depth`; configure `glob_scan_max_depth` or use a non-root directory prefix"
|
||||
));
|
||||
}
|
||||
Ok(scan_plan)
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
|
||||
for scan_plan in scan_plans {
|
||||
let mut seen_scan_dirs = HashSet::new();
|
||||
let scan_plan = glob_scan_plan(&pattern, file_system_sandbox_policy.glob_scan_max_depth);
|
||||
collect_existing_glob_matches(
|
||||
&scan_plan.root,
|
||||
&matcher,
|
||||
@@ -269,6 +281,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_recursive_globs_without_depth_fail_before_expansion() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path()).expect("absolute cwd");
|
||||
let root = cwd.as_path().ancestors().last().expect("filesystem root");
|
||||
let pattern = root.join("**").join("*.env").display().to_string();
|
||||
let policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![unreadable_glob_entry(pattern.clone())]);
|
||||
|
||||
assert_eq!(
|
||||
resolve_windows_deny_read_paths(&policy, &cwd).expect_err("unbounded root glob"),
|
||||
format!(
|
||||
"unreadable glob `{pattern}` cannot be safely expanded from a filesystem root without `glob_scan_max_depth`; configure `glob_scan_max_depth` or use a non-root directory prefix"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_depth_bounds_root_recursive_glob_scans() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path()).expect("absolute cwd");
|
||||
let root = cwd.as_path().ancestors().last().expect("filesystem root");
|
||||
let pattern = root.join("**").join("*.env").display().to_string();
|
||||
let scan_plan = glob_scan_plan(&pattern, Some(2));
|
||||
|
||||
assert_eq!(scan_plan.root, root);
|
||||
assert_eq!(scan_plan.max_depth, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_missing_paths_are_preserved() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::sync::OnceLock;
|
||||
|
||||
use crate::allow::AllowDenyPaths;
|
||||
use crate::allow::compute_allow_paths_for_permissions;
|
||||
use crate::deny_read_resolver::resolve_windows_deny_read_paths;
|
||||
use crate::helper_materialization::bundled_executable_path_for_exe;
|
||||
use crate::helper_materialization::helper_bin_dir;
|
||||
use crate::identity::sandbox_setup_is_complete;
|
||||
@@ -233,6 +234,8 @@ pub fn run_setup_refresh(
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let deny_read_paths =
|
||||
setup_refresh_deny_read_paths(permission_profile, workspace_roots, command_cwd)?;
|
||||
run_setup_refresh_inner(
|
||||
SandboxSetupRequest {
|
||||
permissions: &permissions,
|
||||
@@ -241,7 +244,10 @@ pub fn run_setup_refresh(
|
||||
codex_home,
|
||||
proxy_enforced,
|
||||
},
|
||||
SetupRootOverrides::default(),
|
||||
SetupRootOverrides {
|
||||
deny_read_paths: Some(deny_read_paths),
|
||||
..SetupRootOverrides::default()
|
||||
},
|
||||
/*offline_proxy_settings_override*/ None,
|
||||
)
|
||||
}
|
||||
@@ -271,6 +277,8 @@ pub fn run_setup_refresh_with_extra_read_roots(
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let deny_read_paths =
|
||||
setup_refresh_deny_read_paths(permission_profile, workspace_roots, command_cwd)?;
|
||||
let mut read_roots = gather_read_roots(command_cwd, &permissions, env_map, codex_home);
|
||||
read_roots.extend(extra_read_roots);
|
||||
run_setup_refresh_inner(
|
||||
@@ -285,13 +293,32 @@ pub fn run_setup_refresh_with_extra_read_roots(
|
||||
read_roots: Some(read_roots),
|
||||
read_roots_include_platform_defaults: false,
|
||||
write_roots: Some(Vec::new()),
|
||||
deny_read_paths: None,
|
||||
deny_read_paths: Some(deny_read_paths),
|
||||
deny_write_paths: None,
|
||||
},
|
||||
/*offline_proxy_settings_override*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_refresh_deny_read_paths(
|
||||
permission_profile: &PermissionProfile,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
command_cwd: &Path,
|
||||
) -> Result<Vec<PathBuf>> {
|
||||
let (mut file_system, _) = permission_profile.to_runtime_permissions();
|
||||
file_system.remove_skip_missing_path_entries();
|
||||
let file_system = file_system.materialize_project_roots_with_workspace_roots(workspace_roots);
|
||||
let command_cwd = AbsolutePathBuf::from_absolute_path(command_cwd)?;
|
||||
resolve_windows_deny_read_paths(&file_system, &command_cwd)
|
||||
.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(AbsolutePathBuf::into_path_buf)
|
||||
.collect()
|
||||
})
|
||||
.map_err(|err| anyhow!(err))
|
||||
}
|
||||
|
||||
fn run_setup_refresh_inner(
|
||||
request: SandboxSetupRequest<'_>,
|
||||
overrides: SetupRootOverrides,
|
||||
@@ -1298,8 +1325,14 @@ mod tests {
|
||||
use crate::setup_error::SetupErrorReport;
|
||||
use crate::setup_error::extract_failure;
|
||||
use crate::setup_error::write_setup_error_report;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::permissions::project_roots_glob_pattern;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
@@ -1556,6 +1589,93 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_refresh_preserves_workspace_scoped_deny_read_paths() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let workspace_root = tmp.path().join("workspace");
|
||||
let command_cwd = tmp.path().join("command-cwd");
|
||||
let denied_glob_match = workspace_root.join("app").join("secret.env");
|
||||
fs::create_dir_all(&command_cwd).expect("create command cwd");
|
||||
fs::create_dir_all(denied_glob_match.parent().expect("glob parent"))
|
||||
.expect("create glob parent");
|
||||
fs::write(&denied_glob_match, "secret").expect("write denied glob match");
|
||||
let permission_profile = PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Restricted {
|
||||
entries: vec![
|
||||
FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
FileSystemAccessMode::Read,
|
||||
),
|
||||
FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(Some(
|
||||
"private".to_string(),
|
||||
)),
|
||||
},
|
||||
FileSystemAccessMode::Deny,
|
||||
),
|
||||
FileSystemSandboxEntry::new(
|
||||
FileSystemPath::GlobPattern {
|
||||
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
|
||||
},
|
||||
FileSystemAccessMode::Deny,
|
||||
),
|
||||
],
|
||||
glob_scan_max_depth: None,
|
||||
},
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
};
|
||||
|
||||
let deny_read_paths = super::setup_refresh_deny_read_paths(
|
||||
&permission_profile,
|
||||
workspace_roots_for(&workspace_root).as_slice(),
|
||||
&command_cwd,
|
||||
)
|
||||
.expect("resolve refresh deny-read paths");
|
||||
|
||||
assert_eq!(
|
||||
deny_read_paths.into_iter().collect::<HashSet<_>>(),
|
||||
[
|
||||
dunce::canonicalize(&workspace_root)
|
||||
.expect("canonicalize workspace root")
|
||||
.join("private"),
|
||||
denied_glob_match,
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_refresh_rejects_invalid_deny_read_globs() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let workspace_root = tmp.path().join("workspace");
|
||||
fs::create_dir_all(&workspace_root).expect("create workspace");
|
||||
let permission_profile = PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Restricted {
|
||||
entries: vec![FileSystemSandboxEntry::new(
|
||||
FileSystemPath::GlobPattern {
|
||||
pattern: project_roots_glob_pattern(Path::new("[z-a]")),
|
||||
},
|
||||
FileSystemAccessMode::Deny,
|
||||
)],
|
||||
glob_scan_max_depth: None,
|
||||
},
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
};
|
||||
|
||||
let err = super::setup_refresh_deny_read_paths(
|
||||
&permission_profile,
|
||||
workspace_roots_for(&workspace_root).as_slice(),
|
||||
&workspace_root,
|
||||
)
|
||||
.expect_err("invalid deny-read glob");
|
||||
|
||||
assert!(err.to_string().contains("invalid deny-read glob pattern"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loopback_proxy_url_parsing_supports_common_forms() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user