feat(cli): add sandbox profile config controls (#20118)

## Why

The explicit profile path from #20117 is meant for standalone testing,
but it still inherited the
shell cwd and all managed requirements implicitly. The pre-existing
launcher path even called out
that it did not support a separate cwd yet in

[`debug_sandbox.rs`](https://github.com/openai/codex/blob/509453f688a30929432be866402d1ea46aa12169/codex-rs/cli/src/debug_sandbox.rs#L174-L179).

For a standalone command, the useful default is to let the caller choose
the project directory being
tested and to avoid administrator-provided constraints unless the caller
explicitly wants to test
those too.

## What changed

- Add explicit-profile-only `-C/--cd DIR`, and use that cwd for both
profile resolution and command
  execution.
- Add explicit-profile-only `--include-managed-config`.
- Make explicit profile mode skip managed requirement sources by
default, including cloud
requirements, MDM requirements, `/etc/codex/requirements.toml`, and the
legacy managed-config
  requirements projection.
- Preserve all existing invocations outside the explicit-profile path.

## Stack

1. #20117 `sandbox-ui-profile`
2. #20118 `sandbox-ui-config` --> this PR

Both PRs are additive. Replay JSON is intentionally deferred to a
follow-up design pass.

## Tests ran

- `cargo test -p codex-cli debug_sandbox`
- `cargo test -p codex-cli sandbox_macos_`
- `cargo test -p codex-core
load_config_layers_can_ignore_managed_requirements`
- `cargo test -p codex-core
load_config_layers_includes_cloud_requirements`
- macOS branch-binary smoke on the rebased top of stack: `-C` changed
execution cwd, explicit
profile mode omitted managed proxy env under `env -i`, and
`--include-managed-config` restored it.
- Linux devbox branch-binary smoke on the rebased top of stack: `-C`
changed execution cwd for
  built-in and user-defined explicit profiles.
This commit is contained in:
viyatb-oai
2026-04-29 06:55:51 +00:00
committed by GitHub
parent 857146b328
commit 5597925155
6 changed files with 284 additions and 37 deletions
+140 -14
View File
@@ -6,6 +6,7 @@ mod seatbelt;
use std::path::PathBuf;
use std::process::Stdio;
use codex_config::LoaderOverrides;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
@@ -43,13 +44,23 @@ pub async fn run_command_under_seatbelt(
) -> anyhow::Result<()> {
let SeatbeltCommand {
permissions_profile,
cwd,
include_managed_config,
allow_unix_sockets,
log_denials,
config_overrides,
command,
} = command;
let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation(
&permissions_profile,
include_managed_config,
);
run_command_under_sandbox(
permissions_profile,
DebugSandboxConfigOptions {
permissions_profile,
cwd,
managed_requirements_mode,
},
command,
config_overrides,
codex_linux_sandbox_exe,
@@ -74,11 +85,21 @@ pub async fn run_command_under_landlock(
) -> anyhow::Result<()> {
let LandlockCommand {
permissions_profile,
cwd,
include_managed_config,
config_overrides,
command,
} = command;
let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation(
&permissions_profile,
include_managed_config,
);
run_command_under_sandbox(
permissions_profile,
DebugSandboxConfigOptions {
permissions_profile,
cwd,
managed_requirements_mode,
},
command,
config_overrides,
codex_linux_sandbox_exe,
@@ -95,11 +116,21 @@ pub async fn run_command_under_windows(
) -> anyhow::Result<()> {
let WindowsCommand {
permissions_profile,
cwd,
include_managed_config,
config_overrides,
command,
} = command;
let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation(
&permissions_profile,
include_managed_config,
);
run_command_under_sandbox(
permissions_profile,
DebugSandboxConfigOptions {
permissions_profile,
cwd,
managed_requirements_mode,
},
command,
config_overrides,
codex_linux_sandbox_exe,
@@ -117,8 +148,34 @@ enum SandboxType {
Windows,
}
async fn run_command_under_sandbox(
#[derive(Debug)]
struct DebugSandboxConfigOptions {
permissions_profile: Option<String>,
cwd: Option<PathBuf>,
managed_requirements_mode: ManagedRequirementsMode,
}
#[derive(Debug, Clone, Copy)]
enum ManagedRequirementsMode {
Include,
Ignore,
}
impl ManagedRequirementsMode {
fn for_profile_invocation(
permissions_profile: &Option<String>,
include_managed_config: bool,
) -> Self {
if permissions_profile.is_some() && !include_managed_config {
Self::Ignore
} else {
Self::Include
}
}
}
async fn run_command_under_sandbox(
config_options: DebugSandboxConfigOptions,
command: Vec<String>,
config_overrides: CliConfigOverrides,
codex_linux_sandbox_exe: Option<PathBuf>,
@@ -132,7 +189,7 @@ async fn run_command_under_sandbox(
.parse_overrides()
.map_err(anyhow::Error::msg)?,
codex_linux_sandbox_exe,
permissions_profile,
config_options,
)
.await?;
@@ -571,12 +628,12 @@ mod windows_stdio_bridge {
async fn load_debug_sandbox_config(
cli_overrides: Vec<(String, TomlValue)>,
codex_linux_sandbox_exe: Option<PathBuf>,
permissions_profile: Option<String>,
options: DebugSandboxConfigOptions,
) -> anyhow::Result<Config> {
load_debug_sandbox_config_with_codex_home(
cli_overrides,
codex_linux_sandbox_exe,
permissions_profile,
options,
/*codex_home*/ None,
)
.await
@@ -585,9 +642,15 @@ async fn load_debug_sandbox_config(
async fn load_debug_sandbox_config_with_codex_home(
mut cli_overrides: Vec<(String, TomlValue)>,
codex_linux_sandbox_exe: Option<PathBuf>,
permissions_profile: Option<String>,
options: DebugSandboxConfigOptions,
codex_home: Option<PathBuf>,
) -> anyhow::Result<Config> {
let DebugSandboxConfigOptions {
permissions_profile,
cwd,
managed_requirements_mode,
} = options;
if let Some(permissions_profile) = permissions_profile {
cli_overrides.push((
"default_permissions".to_string(),
@@ -604,10 +667,12 @@ async fn load_debug_sandbox_config_with_codex_home(
let config = build_debug_sandbox_config(
cli_overrides.clone(),
ConfigOverrides {
cwd: cwd.clone(),
codex_linux_sandbox_exe: codex_linux_sandbox_exe.clone(),
..Default::default()
},
codex_home.clone(),
managed_requirements_mode,
)
.await?;
@@ -619,10 +684,12 @@ async fn load_debug_sandbox_config_with_codex_home(
cli_overrides,
ConfigOverrides {
sandbox_mode: Some(SandboxMode::ReadOnly),
cwd,
codex_linux_sandbox_exe,
..Default::default()
},
codex_home,
managed_requirements_mode,
)
.await
.map_err(Into::into)
@@ -632,10 +699,17 @@ async fn build_debug_sandbox_config(
cli_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
codex_home: Option<PathBuf>,
managed_requirements_mode: ManagedRequirementsMode,
) -> std::io::Result<Config> {
let mut builder = ConfigBuilder::default()
.cli_overrides(cli_overrides)
.harness_overrides(harness_overrides);
if let ManagedRequirementsMode::Ignore = managed_requirements_mode {
builder = builder.loader_overrides(LoaderOverrides {
ignore_managed_requirements: true,
..Default::default()
});
}
if let Some(codex_home) = codex_home {
builder = builder
.codex_home(codex_home.clone())
@@ -701,6 +775,7 @@ mod tests {
Vec::new(),
ConfigOverrides::default(),
Some(codex_home_path.clone()),
ManagedRequirementsMode::Include,
)
.await?;
let legacy_config = build_debug_sandbox_config(
@@ -710,13 +785,18 @@ mod tests {
..Default::default()
},
Some(codex_home_path.clone()),
ManagedRequirementsMode::Include,
)
.await?;
let config = load_debug_sandbox_config_with_codex_home(
Vec::new(),
/*codex_linux_sandbox_exe*/ None,
/*permissions_profile*/ None,
DebugSandboxConfigOptions {
permissions_profile: None,
cwd: None,
managed_requirements_mode: ManagedRequirementsMode::Include,
},
Some(codex_home_path),
)
.await?;
@@ -752,6 +832,7 @@ mod tests {
cli_overrides.clone(),
ConfigOverrides::default(),
Some(codex_home_path.clone()),
ManagedRequirementsMode::Include,
)
.await?;
let read_only_config = build_debug_sandbox_config(
@@ -761,13 +842,18 @@ mod tests {
..Default::default()
},
Some(codex_home_path.clone()),
ManagedRequirementsMode::Include,
)
.await?;
let config = load_debug_sandbox_config_with_codex_home(
cli_overrides,
/*codex_linux_sandbox_exe*/ None,
/*permissions_profile*/ None,
DebugSandboxConfigOptions {
permissions_profile: None,
cwd: None,
managed_requirements_mode: ManagedRequirementsMode::Include,
},
Some(codex_home_path),
)
.await?;
@@ -811,13 +897,18 @@ mod tests {
..Default::default()
},
Some(codex_home_path.clone()),
ManagedRequirementsMode::Include,
)
.await?;
let config = load_debug_sandbox_config_with_codex_home(
Vec::new(),
/*codex_linux_sandbox_exe*/ None,
/*permissions_profile*/ None,
DebugSandboxConfigOptions {
permissions_profile: None,
cwd: None,
managed_requirements_mode: ManagedRequirementsMode::Include,
},
Some(codex_home_path),
)
.await?;
@@ -838,7 +929,11 @@ mod tests {
let config = load_debug_sandbox_config_with_codex_home(
Vec::new(),
/*codex_linux_sandbox_exe*/ None,
Some(":workspace".to_string()),
DebugSandboxConfigOptions {
permissions_profile: Some(":workspace".to_string()),
cwd: None,
managed_requirements_mode: ManagedRequirementsMode::Ignore,
},
Some(codex_home.path().to_path_buf()),
)
.await?;
@@ -867,7 +962,11 @@ mod tests {
let config = load_debug_sandbox_config_with_codex_home(
Vec::new(),
/*codex_linux_sandbox_exe*/ None,
Some(":workspace".to_string()),
DebugSandboxConfigOptions {
permissions_profile: Some(":workspace".to_string()),
cwd: None,
managed_requirements_mode: ManagedRequirementsMode::Ignore,
},
Some(codex_home.path().to_path_buf()),
)
.await?;
@@ -892,7 +991,11 @@ mod tests {
let config = load_debug_sandbox_config_with_codex_home(
Vec::new(),
/*codex_linux_sandbox_exe*/ None,
Some("limited-read-test".to_string()),
DebugSandboxConfigOptions {
permissions_profile: Some("limited-read-test".to_string()),
cwd: None,
managed_requirements_mode: ManagedRequirementsMode::Ignore,
},
Some(codex_home.path().to_path_buf()),
)
.await?;
@@ -904,6 +1007,7 @@ mod tests {
)],
ConfigOverrides::default(),
Some(codex_home.path().to_path_buf()),
ManagedRequirementsMode::Include,
)
.await?;
@@ -914,4 +1018,26 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn debug_sandbox_uses_explicit_profile_cwd() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let config = load_debug_sandbox_config_with_codex_home(
Vec::new(),
/*codex_linux_sandbox_exe*/ None,
DebugSandboxConfigOptions {
permissions_profile: Some(":workspace".to_string()),
cwd: Some(cwd.path().to_path_buf()),
managed_requirements_mode: ManagedRequirementsMode::Ignore,
},
Some(codex_home.path().to_path_buf()),
)
.await?;
assert_eq!(config.cwd.as_path(), cwd.path());
Ok(())
}
}