Skip to content

Commit d0a99a9

Browse files
committed
fix: honor active permission profiles in sandbox debug
1 parent e791559 commit d0a99a9

File tree

3 files changed

+253
-23
lines changed

3 files changed

+253
-23
lines changed

codex-rs/cli/src/debug_sandbox.rs

Lines changed: 249 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@ mod pid_tracker;
44
mod seatbelt;
55

66
use std::path::PathBuf;
7+
use std::process::Stdio;
78

89
use codex_core::config::Config;
10+
use codex_core::config::ConfigBuilder;
911
use codex_core::config::ConfigOverrides;
1012
use codex_core::config::NetworkProxyAuditMetadata;
1113
use codex_core::exec_env::create_env;
12-
use codex_core::landlock::spawn_command_under_linux_sandbox;
14+
use codex_core::landlock::create_linux_sandbox_command_args_for_policies;
1315
#[cfg(target_os = "macos")]
14-
use codex_core::seatbelt::spawn_command_under_seatbelt;
15-
use codex_core::spawn::StdioPolicy;
16+
use codex_core::seatbelt::create_seatbelt_command_args_for_policies_with_extensions;
17+
#[cfg(target_os = "macos")]
18+
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
19+
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
1620
use codex_protocol::config_types::SandboxMode;
21+
use codex_protocol::permissions::NetworkSandboxPolicy;
1722
use codex_utils_cli::CliConfigOverrides;
23+
use tokio::process::Child;
24+
use tokio::process::Command as TokioCommand;
25+
use toml::Value as TomlValue;
1826

1927
use crate::LandlockCommand;
2028
use crate::SeatbeltCommand;
@@ -109,16 +117,12 @@ async fn run_command_under_sandbox(
109117
sandbox_type: SandboxType,
110118
log_denials: bool,
111119
) -> anyhow::Result<()> {
112-
let sandbox_mode = create_sandbox_mode(full_auto);
113-
let config = Config::load_with_cli_overrides_and_harness_overrides(
120+
let config = load_debug_sandbox_config(
114121
config_overrides
115122
.parse_overrides()
116123
.map_err(anyhow::Error::msg)?,
117-
ConfigOverrides {
118-
sandbox_mode: Some(sandbox_mode),
119-
codex_linux_sandbox_exe,
120-
..Default::default()
121-
},
124+
codex_linux_sandbox_exe,
125+
full_auto,
122126
)
123127
.await?;
124128

@@ -130,7 +134,6 @@ async fn run_command_under_sandbox(
130134
// separately.
131135
let sandbox_policy_cwd = cwd.clone();
132136

133-
let stdio_policy = StdioPolicy::Inherit;
134137
let env = create_env(&config.permissions.shell_environment_policy, None);
135138

136139
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
@@ -238,14 +241,29 @@ async fn run_command_under_sandbox(
238241
let mut child = match sandbox_type {
239242
#[cfg(target_os = "macos")]
240243
SandboxType::Seatbelt => {
241-
spawn_command_under_seatbelt(
244+
let args = create_seatbelt_command_args_for_policies_with_extensions(
242245
command,
243-
cwd,
244-
config.permissions.sandbox_policy.get(),
246+
&config.permissions.file_system_sandbox_policy,
247+
config.permissions.network_sandbox_policy,
245248
sandbox_policy_cwd.as_path(),
246-
stdio_policy,
249+
false,
247250
network.as_ref(),
251+
None,
252+
);
253+
let network_policy = config.permissions.network_sandbox_policy;
254+
spawn_debug_sandbox_child(
255+
PathBuf::from("/usr/bin/sandbox-exec"),
256+
args,
257+
None,
258+
cwd,
259+
network_policy,
248260
env,
261+
|env_map| {
262+
env_map.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
263+
if let Some(network) = network.as_ref() {
264+
network.apply_to_env(env_map);
265+
}
266+
},
249267
)
250268
.await?
251269
}
@@ -256,16 +274,28 @@ async fn run_command_under_sandbox(
256274
.codex_linux_sandbox_exe
257275
.expect("codex-linux-sandbox executable not found");
258276
let use_bwrap_sandbox = config.features.enabled(Feature::UseLinuxSandboxBwrap);
259-
spawn_command_under_linux_sandbox(
260-
codex_linux_sandbox_exe,
277+
let args = create_linux_sandbox_command_args_for_policies(
261278
command,
262-
cwd,
263279
config.permissions.sandbox_policy.get(),
280+
&config.permissions.file_system_sandbox_policy,
281+
config.permissions.network_sandbox_policy,
264282
sandbox_policy_cwd.as_path(),
265283
use_bwrap_sandbox,
266-
stdio_policy,
267-
network.as_ref(),
284+
false,
285+
);
286+
let network_policy = config.permissions.network_sandbox_policy;
287+
spawn_debug_sandbox_child(
288+
codex_linux_sandbox_exe,
289+
args,
290+
Some("codex-linux-sandbox"),
291+
cwd,
292+
network_policy,
268293
env,
294+
|env_map| {
295+
if let Some(network) = network.as_ref() {
296+
network.apply_to_env(env_map);
297+
}
298+
},
269299
)
270300
.await?
271301
}
@@ -304,3 +334,202 @@ pub fn create_sandbox_mode(full_auto: bool) -> SandboxMode {
304334
SandboxMode::ReadOnly
305335
}
306336
}
337+
338+
async fn spawn_debug_sandbox_child(
339+
program: PathBuf,
340+
args: Vec<String>,
341+
arg0: Option<&str>,
342+
cwd: PathBuf,
343+
network_sandbox_policy: NetworkSandboxPolicy,
344+
mut env: std::collections::HashMap<String, String>,
345+
apply_env: impl FnOnce(&mut std::collections::HashMap<String, String>),
346+
) -> std::io::Result<Child> {
347+
let mut cmd = TokioCommand::new(&program);
348+
#[cfg(unix)]
349+
cmd.arg0(arg0.map_or_else(|| program.to_string_lossy().to_string(), String::from));
350+
#[cfg(not(unix))]
351+
let _ = arg0;
352+
cmd.args(args);
353+
cmd.current_dir(cwd);
354+
apply_env(&mut env);
355+
cmd.env_clear();
356+
cmd.envs(env);
357+
358+
if !network_sandbox_policy.is_enabled() {
359+
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
360+
}
361+
362+
cmd.stdin(Stdio::inherit())
363+
.stdout(Stdio::inherit())
364+
.stderr(Stdio::inherit())
365+
.kill_on_drop(true)
366+
.spawn()
367+
}
368+
369+
async fn load_debug_sandbox_config(
370+
cli_overrides: Vec<(String, TomlValue)>,
371+
codex_linux_sandbox_exe: Option<PathBuf>,
372+
full_auto: bool,
373+
) -> anyhow::Result<Config> {
374+
load_debug_sandbox_config_with_codex_home(
375+
cli_overrides,
376+
codex_linux_sandbox_exe,
377+
full_auto,
378+
None,
379+
)
380+
.await
381+
}
382+
383+
async fn load_debug_sandbox_config_with_codex_home(
384+
cli_overrides: Vec<(String, TomlValue)>,
385+
codex_linux_sandbox_exe: Option<PathBuf>,
386+
full_auto: bool,
387+
codex_home: Option<PathBuf>,
388+
) -> anyhow::Result<Config> {
389+
let config = build_debug_sandbox_config(
390+
cli_overrides.clone(),
391+
ConfigOverrides {
392+
codex_linux_sandbox_exe: codex_linux_sandbox_exe.clone(),
393+
..Default::default()
394+
},
395+
codex_home.clone(),
396+
)
397+
.await?;
398+
399+
if config_uses_permission_profiles(&config) {
400+
if full_auto {
401+
anyhow::bail!(
402+
"`codex sandbox --full-auto` is only supported for legacy `sandbox_mode` configs; choose a writable `[permissions]` profile instead"
403+
);
404+
}
405+
return Ok(config);
406+
}
407+
408+
build_debug_sandbox_config(
409+
cli_overrides,
410+
ConfigOverrides {
411+
sandbox_mode: Some(create_sandbox_mode(full_auto)),
412+
codex_linux_sandbox_exe,
413+
..Default::default()
414+
},
415+
codex_home,
416+
)
417+
.await
418+
.map_err(Into::into)
419+
}
420+
421+
async fn build_debug_sandbox_config(
422+
cli_overrides: Vec<(String, TomlValue)>,
423+
harness_overrides: ConfigOverrides,
424+
codex_home: Option<PathBuf>,
425+
) -> std::io::Result<Config> {
426+
let mut builder = ConfigBuilder::default()
427+
.cli_overrides(cli_overrides)
428+
.harness_overrides(harness_overrides);
429+
if let Some(codex_home) = codex_home {
430+
builder = builder
431+
.codex_home(codex_home.clone())
432+
.fallback_cwd(Some(codex_home));
433+
}
434+
builder.build().await
435+
}
436+
437+
fn config_uses_permission_profiles(config: &Config) -> bool {
438+
config
439+
.config_layer_stack
440+
.effective_config()
441+
.get("default_permissions")
442+
.is_some()
443+
}
444+
445+
#[cfg(test)]
446+
mod tests {
447+
use super::*;
448+
use tempfile::TempDir;
449+
450+
fn escape_toml_path(path: &std::path::Path) -> String {
451+
path.display().to_string().replace('\\', "\\\\")
452+
}
453+
454+
fn write_permissions_profile_config(
455+
codex_home: &TempDir,
456+
) -> std::io::Result<(PathBuf, PathBuf)> {
457+
let docs = codex_home.path().join("docs");
458+
let private = docs.join("private");
459+
std::fs::create_dir_all(&private)?;
460+
let config = format!(
461+
"default_permissions = \"limited-read-test\"\n\
462+
[permissions.limited-read-test.filesystem]\n\
463+
\":minimal\" = \"read\"\n\
464+
\"{}\" = \"read\"\n\
465+
\"{}\" = \"none\"\n\
466+
\n\
467+
[permissions.limited-read-test.network]\n\
468+
enabled = true\n",
469+
escape_toml_path(&docs),
470+
escape_toml_path(&private),
471+
);
472+
std::fs::write(codex_home.path().join("config.toml"), config)?;
473+
Ok((docs, private))
474+
}
475+
476+
#[tokio::test]
477+
async fn debug_sandbox_honors_active_permission_profiles() -> anyhow::Result<()> {
478+
let codex_home = TempDir::new()?;
479+
let (docs, private) = write_permissions_profile_config(&codex_home)?;
480+
481+
let config = load_debug_sandbox_config_with_codex_home(
482+
Vec::new(),
483+
None,
484+
false,
485+
Some(codex_home.path().to_path_buf()),
486+
)
487+
.await?;
488+
489+
assert!(config_uses_permission_profiles(&config));
490+
let readable_roots = config
491+
.permissions
492+
.file_system_sandbox_policy
493+
.get_readable_roots_with_cwd(config.cwd.as_path());
494+
assert!(
495+
readable_roots
496+
.iter()
497+
.any(|path| path.as_path() == docs.as_path()),
498+
"expected {docs:?} in readable roots, got {readable_roots:?}"
499+
);
500+
let unreadable_roots = config
501+
.permissions
502+
.file_system_sandbox_policy
503+
.get_unreadable_roots_with_cwd(config.cwd.as_path());
504+
assert!(
505+
unreadable_roots
506+
.iter()
507+
.any(|path| path.as_path() == private.as_path()),
508+
"expected {private:?} in unreadable roots, got {unreadable_roots:?}"
509+
);
510+
511+
Ok(())
512+
}
513+
514+
#[tokio::test]
515+
async fn debug_sandbox_rejects_full_auto_for_permission_profiles() -> anyhow::Result<()> {
516+
let codex_home = TempDir::new()?;
517+
let _ = write_permissions_profile_config(&codex_home)?;
518+
519+
let err = load_debug_sandbox_config_with_codex_home(
520+
Vec::new(),
521+
None,
522+
true,
523+
Some(codex_home.path().to_path_buf()),
524+
)
525+
.await
526+
.expect_err("full-auto should be rejected for active permission profiles");
527+
528+
assert!(
529+
err.to_string().contains("--full-auto"),
530+
"unexpected error: {err}"
531+
);
532+
533+
Ok(())
534+
}
535+
}

codex-rs/core/src/landlock.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub(crate) fn allow_network_for_proxy(enforce_managed_network: bool) -> bool {
7474
/// flags so the argv order matches the helper's CLI shape. See
7575
/// `docs/linux_sandbox.md` for the Linux semantics.
7676
#[allow(clippy::too_many_arguments)]
77-
pub(crate) fn create_linux_sandbox_command_args_for_policies(
77+
pub fn create_linux_sandbox_command_args_for_policies(
7878
command: Vec<String>,
7979
sandbox_policy: &SandboxPolicy,
8080
file_system_sandbox_policy: &FileSystemSandboxPolicy,

codex-rs/core/src/seatbelt.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const MACOS_SEATBELT_PLATFORM_DEFAULTS: &str = include_str!("seatbelt_platform_d
3333
/// to defend against an attacker trying to inject a malicious version on the
3434
/// PATH. If /usr/bin/sandbox-exec has been tampered with, then the attacker
3535
/// already has root access.
36-
pub(crate) const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";
36+
pub const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";
3737

3838
pub async fn spawn_command_under_seatbelt(
3939
command: Vec<String>,
@@ -324,6 +324,7 @@ fn dynamic_network_policy_for_network(
324324
}
325325
}
326326

327+
#[cfg_attr(not(test), allow(dead_code))]
327328
pub(crate) fn create_seatbelt_command_args(
328329
command: Vec<String>,
329330
sandbox_policy: &SandboxPolicy,
@@ -418,7 +419,7 @@ pub(crate) fn create_seatbelt_command_args_with_extensions(
418419
)
419420
}
420421

421-
pub(crate) fn create_seatbelt_command_args_for_policies_with_extensions(
422+
pub fn create_seatbelt_command_args_for_policies_with_extensions(
422423
command: Vec<String>,
423424
file_system_sandbox_policy: &FileSystemSandboxPolicy,
424425
network_sandbox_policy: NetworkSandboxPolicy,

0 commit comments

Comments
 (0)