mirror of
https://github.com/openai/codex.git
synced 2026-05-02 02:17:22 +00:00
Compare commits
9 Commits
dev/jm/dev
...
codex/viya
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ed259b7dc | ||
|
|
45cd4ae0ec | ||
|
|
42537ceb2c | ||
|
|
b4382c4525 | ||
|
|
2ffb87137d | ||
|
|
e898e3c10a | ||
|
|
dcc592e216 | ||
|
|
8fcc1c3827 | ||
|
|
9a8b11e0a7 |
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -2123,6 +2123,7 @@ dependencies = [
|
||||
"codex-mcp-server",
|
||||
"codex-memories-write",
|
||||
"codex-models-manager",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-responses-api-proxy",
|
||||
"codex-rmcp-client",
|
||||
@@ -2142,6 +2143,7 @@ dependencies = [
|
||||
"predicates",
|
||||
"pretty_assertions",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"supports-color 3.0.2",
|
||||
|
||||
@@ -72,6 +72,21 @@ codex debug seatbelt [--full-auto] [--log-denials] [COMMAND]...
|
||||
codex debug landlock [--full-auto] [COMMAND]...
|
||||
```
|
||||
|
||||
To select a named permissions profile explicitly, pass it by name:
|
||||
|
||||
```shell
|
||||
codex sandbox macos --permissions-profile :workspace -- echo hello
|
||||
codex sandbox linux --permissions-profile my-test-profile -C /tmp/project -- echo hello
|
||||
```
|
||||
|
||||
To replay an already-resolved sandbox state without reading ambient config, pass the JSON payload
|
||||
inline or from a file:
|
||||
|
||||
```shell
|
||||
codex sandbox macos --permissions-json-file replay.json -- echo hello
|
||||
codex sandbox linux --permissions-json '{"permissionProfile":{"type":"disabled"},"networkProxy":null,"managedNetworkRequirementsEnabled":false,"sandboxCwd":"/tmp/project","codexHome":"/tmp/codex-home","env":{},"codexLinuxSandboxExe":"/usr/local/bin/codex-linux-sandbox","useLegacyLandlock":false,"windowsSandboxLevel":"disabled","windowsSandboxPrivateDesktop":true}' -- echo hello
|
||||
```
|
||||
|
||||
### Selecting a sandbox policy via `--sandbox`
|
||||
|
||||
The Rust CLI exposes a dedicated `--sandbox` (`-s`) flag that lets you pick the sandbox policy **without** having to reach for the generic `-c/--config` option:
|
||||
|
||||
@@ -39,6 +39,7 @@ codex-memories-write = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
codex-mcp-server = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-responses-api-proxy = { workspace = true }
|
||||
codex-rmcp-client = { workspace = true }
|
||||
@@ -53,6 +54,7 @@ codex-utils-path = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
regex-lite = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
supports-color = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
mod pid_tracker;
|
||||
mod replay;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod seatbelt;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::Context;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::NetworkProxyAuditMetadata;
|
||||
use codex_core::config::NetworkProxySpec;
|
||||
use codex_core::exec_env::create_env;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::landlock::allow_network_for_proxy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
@@ -32,6 +39,8 @@ use crate::LandlockCommand;
|
||||
use crate::SeatbeltCommand;
|
||||
use crate::WindowsCommand;
|
||||
use crate::exit_status::handle_exit_status;
|
||||
use replay::SandboxReplayPayload;
|
||||
use replay::parse_sandbox_replay_payload;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use seatbelt::DenialLogger;
|
||||
@@ -43,19 +52,34 @@ pub async fn run_command_under_seatbelt(
|
||||
) -> anyhow::Result<()> {
|
||||
let SeatbeltCommand {
|
||||
full_auto,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
permissions_json,
|
||||
permissions_json_file,
|
||||
allow_unix_sockets,
|
||||
log_denials,
|
||||
config_overrides,
|
||||
command,
|
||||
} = command;
|
||||
run_command_under_sandbox(
|
||||
full_auto,
|
||||
DebugSandboxConfigSource::from_flags(
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
},
|
||||
permissions_json,
|
||||
permissions_json_file,
|
||||
config_overrides,
|
||||
)?,
|
||||
command,
|
||||
config_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
SandboxType::Seatbelt,
|
||||
log_denials,
|
||||
&allow_unix_sockets,
|
||||
SandboxType::Seatbelt {
|
||||
allow_unix_sockets,
|
||||
log_denials,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -74,17 +98,29 @@ pub async fn run_command_under_landlock(
|
||||
) -> anyhow::Result<()> {
|
||||
let LandlockCommand {
|
||||
full_auto,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
permissions_json,
|
||||
permissions_json_file,
|
||||
config_overrides,
|
||||
command,
|
||||
} = command;
|
||||
run_command_under_sandbox(
|
||||
full_auto,
|
||||
DebugSandboxConfigSource::from_flags(
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
},
|
||||
permissions_json,
|
||||
permissions_json_file,
|
||||
config_overrides,
|
||||
)?,
|
||||
command,
|
||||
config_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
SandboxType::Landlock,
|
||||
/*log_denials*/ false,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -95,65 +131,230 @@ pub async fn run_command_under_windows(
|
||||
) -> anyhow::Result<()> {
|
||||
let WindowsCommand {
|
||||
full_auto,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
permissions_json,
|
||||
permissions_json_file,
|
||||
config_overrides,
|
||||
command,
|
||||
} = command;
|
||||
run_command_under_sandbox(
|
||||
full_auto,
|
||||
DebugSandboxConfigSource::from_flags(
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
},
|
||||
permissions_json,
|
||||
permissions_json_file,
|
||||
config_overrides,
|
||||
)?,
|
||||
command,
|
||||
config_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
SandboxType::Windows,
|
||||
/*log_denials*/ false,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
enum SandboxType {
|
||||
#[cfg(target_os = "macos")]
|
||||
Seatbelt,
|
||||
Seatbelt {
|
||||
allow_unix_sockets: Vec<AbsolutePathBuf>,
|
||||
log_denials: bool,
|
||||
},
|
||||
Landlock,
|
||||
Windows,
|
||||
}
|
||||
|
||||
async fn run_command_under_sandbox(
|
||||
#[derive(Debug)]
|
||||
struct DebugSandboxConfigOptions {
|
||||
full_auto: bool,
|
||||
permissions_profile: Option<String>,
|
||||
cwd: Option<PathBuf>,
|
||||
include_managed_config: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DebugSandboxConfigSource {
|
||||
Config {
|
||||
options: DebugSandboxConfigOptions,
|
||||
overrides: CliConfigOverrides,
|
||||
},
|
||||
Replay(Box<SandboxReplayPayload>),
|
||||
}
|
||||
|
||||
impl DebugSandboxConfigSource {
|
||||
fn from_flags(
|
||||
options: DebugSandboxConfigOptions,
|
||||
permissions_json: Option<String>,
|
||||
permissions_json_file: Option<PathBuf>,
|
||||
overrides: CliConfigOverrides,
|
||||
) -> anyhow::Result<Self> {
|
||||
let replay = match (permissions_json, permissions_json_file) {
|
||||
(Some(json), None) => Some(parse_sandbox_replay_payload(&json)?),
|
||||
(None, Some(path)) => {
|
||||
let json = std::fs::read_to_string(&path).with_context(|| {
|
||||
format!("failed to read sandbox replay file {}", path.display())
|
||||
})?;
|
||||
Some(parse_sandbox_replay_payload(&json)?)
|
||||
}
|
||||
(None, None) => None,
|
||||
(Some(_), Some(_)) => {
|
||||
anyhow::bail!(
|
||||
"--permissions-json and --permissions-json-file are mutually exclusive"
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(replay) = replay {
|
||||
if !overrides.raw_overrides.is_empty() {
|
||||
anyhow::bail!("sandbox replay JSON cannot be combined with -c/--config overrides");
|
||||
}
|
||||
return Ok(Self::Replay(Box::new(replay)));
|
||||
}
|
||||
|
||||
Ok(Self::Config { options, overrides })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ManagedRequirementsMode {
|
||||
Include,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
struct SandboxRuntimeConfig {
|
||||
permission_profile: PermissionProfile,
|
||||
network: Option<NetworkProxySpec>,
|
||||
managed_network_requirements_enabled: bool,
|
||||
cwd: AbsolutePathBuf,
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
codex_home: AbsolutePathBuf,
|
||||
env: HashMap<String, String>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
use_legacy_landlock: bool,
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
windows_sandbox_private_desktop: bool,
|
||||
}
|
||||
|
||||
impl SandboxRuntimeConfig {
|
||||
fn from_config(config: &Config) -> Self {
|
||||
Self {
|
||||
permission_profile: config.permissions.permission_profile(),
|
||||
network: config.permissions.network.clone(),
|
||||
managed_network_requirements_enabled: config.managed_network_requirements_enabled(),
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
env: create_env(
|
||||
&config.permissions.shell_environment_policy,
|
||||
/*thread_id*/ None,
|
||||
),
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: config.features.use_legacy_landlock(),
|
||||
windows_sandbox_level: codex_core::windows_sandbox::windows_sandbox_level_from_config(
|
||||
config,
|
||||
),
|
||||
windows_sandbox_private_desktop: config.permissions.windows_sandbox_private_desktop,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_replay(payload: SandboxReplayPayload) -> anyhow::Result<Self> {
|
||||
let permission_profile = payload.permission_profile;
|
||||
let network = payload
|
||||
.network_proxy
|
||||
.map(|network| {
|
||||
NetworkProxySpec::from_config_and_constraints(
|
||||
network.config,
|
||||
network.requirements,
|
||||
&permission_profile,
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(Self {
|
||||
permission_profile,
|
||||
network,
|
||||
managed_network_requirements_enabled: payload.managed_network_requirements_enabled,
|
||||
cwd: payload.sandbox_cwd,
|
||||
codex_home: payload.codex_home,
|
||||
env: payload.env,
|
||||
codex_linux_sandbox_exe: payload.codex_linux_sandbox_exe,
|
||||
use_legacy_landlock: payload.use_legacy_landlock,
|
||||
windows_sandbox_level: payload.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: payload.windows_sandbox_private_desktop,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn file_system_sandbox_policy(&self) -> codex_protocol::permissions::FileSystemSandboxPolicy {
|
||||
self.permission_profile.file_system_sandbox_policy()
|
||||
}
|
||||
|
||||
fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
|
||||
self.permission_profile.network_sandbox_policy()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn legacy_sandbox_policy(
|
||||
&self,
|
||||
sandbox_policy_cwd: &AbsolutePathBuf,
|
||||
) -> anyhow::Result<codex_protocol::protocol::SandboxPolicy> {
|
||||
self.permission_profile
|
||||
.to_legacy_sandbox_policy(sandbox_policy_cwd.as_path())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_sandbox_runtime_config(
|
||||
source: DebugSandboxConfigSource,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
) -> anyhow::Result<SandboxRuntimeConfig> {
|
||||
match source {
|
||||
DebugSandboxConfigSource::Config { options, overrides } => {
|
||||
let config = load_debug_sandbox_config(
|
||||
overrides.parse_overrides().map_err(anyhow::Error::msg)?,
|
||||
codex_linux_sandbox_exe,
|
||||
options,
|
||||
)
|
||||
.await?;
|
||||
Ok(SandboxRuntimeConfig::from_config(&config))
|
||||
}
|
||||
DebugSandboxConfigSource::Replay(payload) => SandboxRuntimeConfig::from_replay(*payload),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_command_under_sandbox(
|
||||
config_source: DebugSandboxConfigSource,
|
||||
command: Vec<String>,
|
||||
config_overrides: CliConfigOverrides,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
sandbox_type: SandboxType,
|
||||
log_denials: bool,
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
|
||||
allow_unix_sockets: &[AbsolutePathBuf],
|
||||
) -> anyhow::Result<()> {
|
||||
let config = load_debug_sandbox_config(
|
||||
config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?,
|
||||
codex_linux_sandbox_exe,
|
||||
full_auto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// In practice, this should be `std::env::current_dir()` because this CLI
|
||||
// does not support `--cwd`, but let's use the config value for consistency.
|
||||
let cwd = config.cwd.clone();
|
||||
let runtime_config =
|
||||
load_sandbox_runtime_config(config_source, codex_linux_sandbox_exe).await?;
|
||||
let cwd = runtime_config.cwd.clone();
|
||||
// For now, we always use the same cwd for both the command and the
|
||||
// sandbox policy. In the future, we could add a CLI option to set them
|
||||
// separately.
|
||||
let sandbox_policy_cwd = cwd.clone();
|
||||
|
||||
let env = create_env(
|
||||
&config.permissions.shell_environment_policy,
|
||||
/*thread_id*/ None,
|
||||
);
|
||||
let env = runtime_config.env.clone();
|
||||
|
||||
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
|
||||
if let SandboxType::Windows = sandbox_type {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
run_command_under_windows_session(&config, command, cwd, sandbox_policy_cwd, env).await;
|
||||
run_command_under_windows_session(
|
||||
&runtime_config,
|
||||
command,
|
||||
cwd,
|
||||
sandbox_policy_cwd,
|
||||
env,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
@@ -162,17 +363,18 @@ async fn run_command_under_sandbox(
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut denial_logger = log_denials.then(DenialLogger::new).flatten();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = log_denials;
|
||||
let mut denial_logger = match &sandbox_type {
|
||||
SandboxType::Seatbelt { log_denials, .. } => log_denials.then(DenialLogger::new).flatten(),
|
||||
SandboxType::Landlock | SandboxType::Windows => None,
|
||||
};
|
||||
|
||||
let managed_network_requirements_enabled = config.managed_network_requirements_enabled();
|
||||
let managed_network_requirements_enabled = runtime_config.managed_network_requirements_enabled;
|
||||
|
||||
// This proxy should only live for the lifetime of the child process.
|
||||
let network_proxy = match config.permissions.network.as_ref() {
|
||||
let network_proxy = match runtime_config.network.as_ref() {
|
||||
Some(spec) => Some(
|
||||
spec.start_proxy(
|
||||
config.permissions.permission_profile.get(),
|
||||
&runtime_config.permission_profile,
|
||||
/*policy_decider*/ None,
|
||||
/*blocked_request_observer*/ None,
|
||||
managed_network_requirements_enabled,
|
||||
@@ -189,9 +391,11 @@ async fn run_command_under_sandbox(
|
||||
|
||||
let mut child = match sandbox_type {
|
||||
#[cfg(target_os = "macos")]
|
||||
SandboxType::Seatbelt => {
|
||||
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
|
||||
let network_sandbox_policy = config.permissions.network_sandbox_policy();
|
||||
SandboxType::Seatbelt {
|
||||
allow_unix_sockets, ..
|
||||
} => {
|
||||
let file_system_sandbox_policy = runtime_config.file_system_sandbox_policy();
|
||||
let network_sandbox_policy = runtime_config.network_sandbox_policy();
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
@@ -199,7 +403,7 @@ async fn run_command_under_sandbox(
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
|
||||
enforce_managed_network: false,
|
||||
network: network.as_ref(),
|
||||
extra_allow_unix_sockets: allow_unix_sockets,
|
||||
extra_allow_unix_sockets: &allow_unix_sockets,
|
||||
});
|
||||
spawn_debug_sandbox_child(
|
||||
PathBuf::from("/usr/bin/sandbox-exec"),
|
||||
@@ -218,16 +422,16 @@ async fn run_command_under_sandbox(
|
||||
.await?
|
||||
}
|
||||
SandboxType::Landlock => {
|
||||
#[expect(clippy::expect_used)]
|
||||
let codex_linux_sandbox_exe = config
|
||||
let codex_linux_sandbox_exe = runtime_config
|
||||
.codex_linux_sandbox_exe
|
||||
.expect("codex-linux-sandbox executable not found");
|
||||
let use_legacy_landlock = config.features.use_legacy_landlock();
|
||||
let network_sandbox_policy = config.permissions.network_sandbox_policy();
|
||||
.clone()
|
||||
.context("codex-linux-sandbox executable not found")?;
|
||||
let use_legacy_landlock = runtime_config.use_legacy_landlock;
|
||||
let network_sandbox_policy = runtime_config.network_sandbox_policy();
|
||||
let args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
command,
|
||||
cwd.as_path(),
|
||||
&config.permissions.permission_profile(),
|
||||
&runtime_config.permission_profile,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
use_legacy_landlock,
|
||||
allow_network_for_proxy(managed_network_requirements_enabled),
|
||||
@@ -277,20 +481,22 @@ async fn run_command_under_sandbox(
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn run_command_under_windows_session(
|
||||
config: &Config,
|
||||
runtime_config: &SandboxRuntimeConfig,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
sandbox_policy_cwd: AbsolutePathBuf,
|
||||
env: std::collections::HashMap<String, String>,
|
||||
) -> ! {
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_windows_sandbox::spawn_windows_sandbox_session_elevated;
|
||||
use codex_windows_sandbox::spawn_windows_sandbox_session_legacy;
|
||||
|
||||
let sandbox_policy = config
|
||||
.permissions
|
||||
.legacy_sandbox_policy(sandbox_policy_cwd.as_path());
|
||||
let sandbox_policy = match runtime_config.legacy_sandbox_policy(&sandbox_policy_cwd) {
|
||||
Ok(sandbox_policy) => sandbox_policy,
|
||||
Err(err) => {
|
||||
eprintln!("windows sandbox failed to project policy: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let policy_str = match serde_json::to_string(&sandbox_policy) {
|
||||
Ok(policy_str) => policy_str,
|
||||
Err(err) => {
|
||||
@@ -300,7 +506,7 @@ async fn run_command_under_windows_session(
|
||||
};
|
||||
|
||||
let use_elevated = matches!(
|
||||
WindowsSandboxLevel::from_config(config),
|
||||
runtime_config.windows_sandbox_level,
|
||||
WindowsSandboxLevel::Elevated
|
||||
);
|
||||
|
||||
@@ -308,28 +514,28 @@ async fn run_command_under_windows_session(
|
||||
spawn_windows_sandbox_session_elevated(
|
||||
policy_str.as_str(),
|
||||
sandbox_policy_cwd.as_path(),
|
||||
config.codex_home.as_path(),
|
||||
runtime_config.codex_home.as_path(),
|
||||
command,
|
||||
cwd.as_path(),
|
||||
env,
|
||||
None,
|
||||
/*tty*/ false,
|
||||
/*stdin_open*/ true,
|
||||
config.permissions.windows_sandbox_private_desktop,
|
||||
runtime_config.windows_sandbox_private_desktop,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
spawn_windows_sandbox_session_legacy(
|
||||
policy_str.as_str(),
|
||||
sandbox_policy_cwd.as_path(),
|
||||
config.codex_home.as_path(),
|
||||
runtime_config.codex_home.as_path(),
|
||||
command,
|
||||
cwd.as_path(),
|
||||
env,
|
||||
None,
|
||||
/*tty*/ false,
|
||||
/*stdin_open*/ true,
|
||||
config.permissions.windows_sandbox_private_desktop,
|
||||
runtime_config.windows_sandbox_private_desktop,
|
||||
)
|
||||
.await
|
||||
};
|
||||
@@ -579,30 +785,52 @@ mod windows_stdio_bridge {
|
||||
async fn load_debug_sandbox_config(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
full_auto: bool,
|
||||
options: DebugSandboxConfigOptions,
|
||||
) -> anyhow::Result<Config> {
|
||||
load_debug_sandbox_config_with_codex_home(
|
||||
cli_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
full_auto,
|
||||
options,
|
||||
/*codex_home*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_debug_sandbox_config_with_codex_home(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
mut cli_overrides: Vec<(String, TomlValue)>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
full_auto: bool,
|
||||
options: DebugSandboxConfigOptions,
|
||||
codex_home: Option<PathBuf>,
|
||||
) -> anyhow::Result<Config> {
|
||||
let DebugSandboxConfigOptions {
|
||||
full_auto,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
include_managed_config,
|
||||
} = options;
|
||||
|
||||
let managed_requirements_mode = if permissions_profile.is_some() && !include_managed_config {
|
||||
ManagedRequirementsMode::Ignore
|
||||
} else {
|
||||
ManagedRequirementsMode::Include
|
||||
};
|
||||
|
||||
if let Some(permissions_profile) = permissions_profile {
|
||||
cli_overrides.push((
|
||||
"default_permissions".to_string(),
|
||||
TomlValue::String(permissions_profile),
|
||||
));
|
||||
}
|
||||
|
||||
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 +847,12 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
cli_overrides,
|
||||
ConfigOverrides {
|
||||
sandbox_mode: Some(create_sandbox_mode(full_auto)),
|
||||
cwd,
|
||||
codex_linux_sandbox_exe,
|
||||
..Default::default()
|
||||
},
|
||||
codex_home,
|
||||
managed_requirements_mode,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
@@ -632,10 +862,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())
|
||||
@@ -655,6 +892,7 @@ fn config_uses_permission_profiles(config: &Config) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_config::NetworkConstraints;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn escape_toml_path(path: &std::path::Path) -> String {
|
||||
@@ -683,6 +921,24 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sample_replay_payload(
|
||||
codex_home: &TempDir,
|
||||
cwd: &TempDir,
|
||||
) -> anyhow::Result<SandboxReplayPayload> {
|
||||
Ok(SandboxReplayPayload {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
network_proxy: None,
|
||||
managed_network_requirements_enabled: false,
|
||||
sandbox_cwd: AbsolutePathBuf::from_absolute_path(cwd.path())?,
|
||||
codex_home: AbsolutePathBuf::from_absolute_path(codex_home.path())?,
|
||||
env: HashMap::from([("PATH".to_string(), "/usr/bin".to_string())]),
|
||||
codex_linux_sandbox_exe: Some(PathBuf::from("/tmp/codex-linux-sandbox")),
|
||||
use_legacy_landlock: true,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_honors_active_permission_profiles() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -696,6 +952,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
ConfigOverrides::default(),
|
||||
Some(codex_home_path.clone()),
|
||||
ManagedRequirementsMode::Include,
|
||||
)
|
||||
.await?;
|
||||
let legacy_config = build_debug_sandbox_config(
|
||||
@@ -705,13 +962,19 @@ 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,
|
||||
/*full_auto*/ false,
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto: false,
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
include_managed_config: false,
|
||||
},
|
||||
Some(codex_home_path),
|
||||
)
|
||||
.await?;
|
||||
@@ -745,7 +1008,12 @@ mod tests {
|
||||
let err = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
/*full_auto*/ true,
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto: true,
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
include_managed_config: false,
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
)
|
||||
.await
|
||||
@@ -758,4 +1026,250 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_honors_explicit_builtin_permission_profile() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let config = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto: false,
|
||||
permissions_profile: Some(":workspace".to_string()),
|
||||
cwd: None,
|
||||
include_managed_config: false,
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
codex_protocol::models::PermissionProfile::workspace_write()
|
||||
.file_system_sandbox_policy()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_honors_explicit_named_permission_profile() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let sandbox_paths = TempDir::new()?;
|
||||
let docs = sandbox_paths.path().join("docs");
|
||||
let private = docs.join("private");
|
||||
write_permissions_profile_config(&codex_home, &docs, &private)?;
|
||||
|
||||
let config = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto: false,
|
||||
permissions_profile: Some("limited-read-test".to_string()),
|
||||
cwd: None,
|
||||
include_managed_config: false,
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let expected = build_debug_sandbox_config(
|
||||
vec![(
|
||||
"default_permissions".to_string(),
|
||||
TomlValue::String("limited-read-test".to_string()),
|
||||
)],
|
||||
ConfigOverrides::default(),
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
ManagedRequirementsMode::Include,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
expected.permissions.file_system_sandbox_policy()
|
||||
);
|
||||
|
||||
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 {
|
||||
full_auto: false,
|
||||
permissions_profile: Some(":workspace".to_string()),
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
include_managed_config: false,
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.cwd.as_path(), cwd.path());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_replay_payload_round_trips() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let payload = sample_replay_payload(&codex_home, &cwd)?;
|
||||
|
||||
let json = serde_json::to_string(&payload)?;
|
||||
let reparsed = parse_sandbox_replay_payload(&json)?;
|
||||
|
||||
assert_eq!(reparsed, payload);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_sandbox_loads_replay_json_file() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let payload = sample_replay_payload(&codex_home, &cwd)?;
|
||||
let replay_file = codex_home.path().join("replay.json");
|
||||
std::fs::write(&replay_file, serde_json::to_vec(&payload)?)?;
|
||||
|
||||
let source = DebugSandboxConfigSource::from_flags(
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto: false,
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
include_managed_config: false,
|
||||
},
|
||||
/*permissions_json*/ None,
|
||||
Some(replay_file),
|
||||
CliConfigOverrides::default(),
|
||||
)?;
|
||||
|
||||
let DebugSandboxConfigSource::Replay(parsed) = source else {
|
||||
panic!("expected replay config source");
|
||||
};
|
||||
assert_eq!(*parsed, payload);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_sandbox_replay_rejects_config_overrides() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let payload = sample_replay_payload(&codex_home, &cwd)?;
|
||||
|
||||
let err = DebugSandboxConfigSource::from_flags(
|
||||
DebugSandboxConfigOptions {
|
||||
full_auto: false,
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
include_managed_config: false,
|
||||
},
|
||||
Some(serde_json::to_string(&payload)?),
|
||||
/*permissions_json_file*/ None,
|
||||
CliConfigOverrides {
|
||||
raw_overrides: vec!["model=o3".to_string()],
|
||||
},
|
||||
)
|
||||
.expect_err("config overrides should be rejected");
|
||||
|
||||
assert!(
|
||||
err.to_string().contains("-c/--config"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_replay_bypasses_ambient_config() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join("config.toml"), "invalid = [")?;
|
||||
let payload = sample_replay_payload(&codex_home, &cwd)?;
|
||||
|
||||
let runtime = load_sandbox_runtime_config(
|
||||
DebugSandboxConfigSource::Replay(Box::new(payload.clone())),
|
||||
Some(PathBuf::from("/ignored/from/ambient/config")),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(runtime.permission_profile, payload.permission_profile);
|
||||
assert_eq!(runtime.cwd.as_path(), cwd.path());
|
||||
assert_eq!(
|
||||
runtime.codex_linux_sandbox_exe,
|
||||
payload.codex_linux_sandbox_exe
|
||||
);
|
||||
assert_eq!(runtime.env, payload.env);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_replay_payload_rejects_relative_paths() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let payload = sample_replay_payload(&codex_home, &cwd)?;
|
||||
let mut json = serde_json::to_value(payload)?;
|
||||
json["sandboxCwd"] = serde_json::Value::String("relative".to_string());
|
||||
|
||||
parse_sandbox_replay_payload(&serde_json::to_string(&json)?)
|
||||
.expect_err("relative sandboxCwd should be rejected");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_replay_payload_preserves_managed_network_state() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let mut payload = sample_replay_payload(&codex_home, &cwd)?;
|
||||
let network_config = codex_network_proxy::NetworkProxyConfig::default();
|
||||
let network_requirements = NetworkConstraints::default();
|
||||
payload.network_proxy = Some(replay::SandboxReplayNetworkProxy {
|
||||
config: network_config.clone(),
|
||||
requirements: Some(network_requirements.clone()),
|
||||
});
|
||||
payload.managed_network_requirements_enabled = true;
|
||||
|
||||
let expected_network = NetworkProxySpec::from_config_and_constraints(
|
||||
network_config,
|
||||
Some(network_requirements),
|
||||
&payload.permission_profile,
|
||||
)?;
|
||||
let runtime = SandboxRuntimeConfig::from_replay(payload)?;
|
||||
|
||||
assert_eq!(runtime.network, Some(expected_network));
|
||||
assert!(runtime.managed_network_requirements_enabled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_replay_requires_linux_sandbox_executable() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let mut payload = sample_replay_payload(&codex_home, &cwd)?;
|
||||
payload.codex_linux_sandbox_exe = None;
|
||||
|
||||
let err = run_command_under_sandbox(
|
||||
DebugSandboxConfigSource::Replay(Box::new(payload)),
|
||||
vec!["true".to_string()],
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
SandboxType::Landlock,
|
||||
)
|
||||
.await
|
||||
.expect_err("missing codex-linux-sandbox should return an error");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("codex-linux-sandbox executable not found"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
37
codex-rs/cli/src/debug_sandbox/replay.rs
Normal file
37
codex-rs/cli/src/debug_sandbox/replay.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use codex_config::NetworkConstraints;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(super) struct SandboxReplayPayload {
|
||||
pub(super) permission_profile: PermissionProfile,
|
||||
pub(super) network_proxy: Option<SandboxReplayNetworkProxy>,
|
||||
pub(super) managed_network_requirements_enabled: bool,
|
||||
pub(super) sandbox_cwd: AbsolutePathBuf,
|
||||
pub(super) codex_home: AbsolutePathBuf,
|
||||
pub(super) env: HashMap<String, String>,
|
||||
pub(super) codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub(super) use_legacy_landlock: bool,
|
||||
pub(super) windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub(super) windows_sandbox_private_desktop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(super) struct SandboxReplayNetworkProxy {
|
||||
pub(super) config: NetworkProxyConfig,
|
||||
pub(super) requirements: Option<NetworkConstraints>,
|
||||
}
|
||||
|
||||
pub(super) fn parse_sandbox_replay_payload(json: &str) -> anyhow::Result<SandboxReplayPayload> {
|
||||
serde_json::from_str(json).context("failed to parse sandbox replay JSON")
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub(crate) mod login;
|
||||
use clap::Parser;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use debug_sandbox::run_command_under_landlock;
|
||||
pub use debug_sandbox::run_command_under_seatbelt;
|
||||
@@ -25,6 +26,55 @@ pub struct SeatbeltCommand {
|
||||
#[arg(long = "full-auto", default_value_t = false)]
|
||||
pub full_auto: bool,
|
||||
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Working directory used for profile resolution and command execution.
|
||||
#[arg(
|
||||
short = 'C',
|
||||
long = "cd",
|
||||
value_name = "DIR",
|
||||
requires = "permissions_profile"
|
||||
)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
|
||||
/// Include managed requirements while resolving an explicit permissions profile.
|
||||
#[arg(
|
||||
long = "include-managed-config",
|
||||
default_value_t = false,
|
||||
requires = "permissions_profile"
|
||||
)]
|
||||
pub include_managed_config: bool,
|
||||
|
||||
/// Replay a fully resolved sandbox state from inline JSON.
|
||||
#[arg(
|
||||
long = "permissions-json",
|
||||
value_name = "JSON",
|
||||
conflicts_with_all = [
|
||||
"full_auto",
|
||||
"permissions_profile",
|
||||
"cwd",
|
||||
"include_managed_config",
|
||||
"permissions_json_file"
|
||||
]
|
||||
)]
|
||||
pub permissions_json: Option<String>,
|
||||
|
||||
/// Replay a fully resolved sandbox state from a JSON file.
|
||||
#[arg(
|
||||
long = "permissions-json-file",
|
||||
value_name = "FILE",
|
||||
conflicts_with_all = [
|
||||
"full_auto",
|
||||
"permissions_profile",
|
||||
"cwd",
|
||||
"include_managed_config",
|
||||
"permissions_json"
|
||||
]
|
||||
)]
|
||||
pub permissions_json_file: Option<PathBuf>,
|
||||
|
||||
/// Allow the sandboxed command to bind/connect AF_UNIX sockets rooted at this path. Relative paths are resolved against the current directory. Repeat to allow multiple paths.
|
||||
#[arg(long = "allow-unix-socket", value_parser = parse_allow_unix_socket_path)]
|
||||
pub allow_unix_sockets: Vec<AbsolutePathBuf>,
|
||||
@@ -52,6 +102,55 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "full-auto", default_value_t = false)]
|
||||
pub full_auto: bool,
|
||||
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Working directory used for profile resolution and command execution.
|
||||
#[arg(
|
||||
short = 'C',
|
||||
long = "cd",
|
||||
value_name = "DIR",
|
||||
requires = "permissions_profile"
|
||||
)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
|
||||
/// Include managed requirements while resolving an explicit permissions profile.
|
||||
#[arg(
|
||||
long = "include-managed-config",
|
||||
default_value_t = false,
|
||||
requires = "permissions_profile"
|
||||
)]
|
||||
pub include_managed_config: bool,
|
||||
|
||||
/// Replay a fully resolved sandbox state from inline JSON.
|
||||
#[arg(
|
||||
long = "permissions-json",
|
||||
value_name = "JSON",
|
||||
conflicts_with_all = [
|
||||
"full_auto",
|
||||
"permissions_profile",
|
||||
"cwd",
|
||||
"include_managed_config",
|
||||
"permissions_json_file"
|
||||
]
|
||||
)]
|
||||
pub permissions_json: Option<String>,
|
||||
|
||||
/// Replay a fully resolved sandbox state from a JSON file.
|
||||
#[arg(
|
||||
long = "permissions-json-file",
|
||||
value_name = "FILE",
|
||||
conflicts_with_all = [
|
||||
"full_auto",
|
||||
"permissions_profile",
|
||||
"cwd",
|
||||
"include_managed_config",
|
||||
"permissions_json"
|
||||
]
|
||||
)]
|
||||
pub permissions_json_file: Option<PathBuf>,
|
||||
|
||||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
@@ -66,6 +165,55 @@ pub struct WindowsCommand {
|
||||
#[arg(long = "full-auto", default_value_t = false)]
|
||||
pub full_auto: bool,
|
||||
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
/// Working directory used for profile resolution and command execution.
|
||||
#[arg(
|
||||
short = 'C',
|
||||
long = "cd",
|
||||
value_name = "DIR",
|
||||
requires = "permissions_profile"
|
||||
)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
|
||||
/// Include managed requirements while resolving an explicit permissions profile.
|
||||
#[arg(
|
||||
long = "include-managed-config",
|
||||
default_value_t = false,
|
||||
requires = "permissions_profile"
|
||||
)]
|
||||
pub include_managed_config: bool,
|
||||
|
||||
/// Replay a fully resolved sandbox state from inline JSON.
|
||||
#[arg(
|
||||
long = "permissions-json",
|
||||
value_name = "JSON",
|
||||
conflicts_with_all = [
|
||||
"full_auto",
|
||||
"permissions_profile",
|
||||
"cwd",
|
||||
"include_managed_config",
|
||||
"permissions_json_file"
|
||||
]
|
||||
)]
|
||||
pub permissions_json: Option<String>,
|
||||
|
||||
/// Replay a fully resolved sandbox state from a JSON file.
|
||||
#[arg(
|
||||
long = "permissions-json-file",
|
||||
value_name = "FILE",
|
||||
conflicts_with_all = [
|
||||
"full_auto",
|
||||
"permissions_profile",
|
||||
"cwd",
|
||||
"include_managed_config",
|
||||
"permissions_json"
|
||||
]
|
||||
)]
|
||||
pub permissions_json_file: Option<PathBuf>,
|
||||
|
||||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
|
||||
@@ -1926,6 +1926,146 @@ mod tests {
|
||||
assert!(matches!(cli.subcommand, Some(Subcommand::Update)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_macos_parses_permissions_profile() {
|
||||
let cli = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"sandbox",
|
||||
"macos",
|
||||
"--permissions-profile",
|
||||
":workspace",
|
||||
"--",
|
||||
"echo",
|
||||
])
|
||||
.expect("parse");
|
||||
|
||||
let Some(Subcommand::Sandbox(SandboxArgs {
|
||||
cmd: SandboxCommand::Macos(command),
|
||||
})) = cli.subcommand
|
||||
else {
|
||||
panic!("expected sandbox macos command");
|
||||
};
|
||||
|
||||
assert_eq!(command.permissions_profile.as_deref(), Some(":workspace"));
|
||||
assert_eq!(command.command, vec!["echo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_macos_parses_explicit_profile_controls() {
|
||||
let cli = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"sandbox",
|
||||
"macos",
|
||||
"--permissions-profile",
|
||||
":workspace",
|
||||
"-C",
|
||||
"/tmp",
|
||||
"--include-managed-config",
|
||||
"--",
|
||||
"echo",
|
||||
])
|
||||
.expect("parse");
|
||||
|
||||
let Some(Subcommand::Sandbox(SandboxArgs {
|
||||
cmd: SandboxCommand::Macos(command),
|
||||
})) = cli.subcommand
|
||||
else {
|
||||
panic!("expected sandbox macos command");
|
||||
};
|
||||
|
||||
assert_eq!(command.cwd.as_deref(), Some(std::path::Path::new("/tmp")));
|
||||
assert!(command.include_managed_config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_macos_rejects_explicit_profile_controls_without_profile() {
|
||||
let err = MultitoolCli::try_parse_from(["codex", "sandbox", "macos", "-C", "/tmp"])
|
||||
.expect_err("parse should fail");
|
||||
|
||||
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_macos_parses_replay_json_file() {
|
||||
let cli = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"sandbox",
|
||||
"macos",
|
||||
"--permissions-json-file",
|
||||
"/tmp/replay.json",
|
||||
"--",
|
||||
"echo",
|
||||
])
|
||||
.expect("parse");
|
||||
|
||||
let Some(Subcommand::Sandbox(SandboxArgs {
|
||||
cmd: SandboxCommand::Macos(command),
|
||||
})) = cli.subcommand
|
||||
else {
|
||||
panic!("expected sandbox macos command");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
command.permissions_json_file.as_deref(),
|
||||
Some(std::path::Path::new("/tmp/replay.json"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_macos_parses_replay_json() {
|
||||
let cli = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"sandbox",
|
||||
"macos",
|
||||
"--permissions-json",
|
||||
"{}",
|
||||
"--",
|
||||
"echo",
|
||||
])
|
||||
.expect("parse");
|
||||
|
||||
let Some(Subcommand::Sandbox(SandboxArgs {
|
||||
cmd: SandboxCommand::Macos(command),
|
||||
})) = cli.subcommand
|
||||
else {
|
||||
panic!("expected sandbox macos command");
|
||||
};
|
||||
|
||||
assert_eq!(command.permissions_json.as_deref(), Some("{}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_macos_rejects_multiple_replay_sources() {
|
||||
let err = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"sandbox",
|
||||
"macos",
|
||||
"--permissions-json",
|
||||
"{}",
|
||||
"--permissions-json-file",
|
||||
"/tmp/replay.json",
|
||||
])
|
||||
.expect_err("parse should fail");
|
||||
|
||||
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_macos_rejects_replay_json_with_profile_flags() {
|
||||
let err = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"sandbox",
|
||||
"macos",
|
||||
"--permissions-json",
|
||||
"{}",
|
||||
"--permissions-profile",
|
||||
":workspace",
|
||||
])
|
||||
.expect_err("parse should fail");
|
||||
|
||||
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_marketplace_remove_parses_under_plugin() {
|
||||
let cli =
|
||||
|
||||
@@ -92,41 +92,46 @@ pub async fn load_config_layers_state(
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
thread_config_loader: &dyn ThreadConfigLoader,
|
||||
) -> io::Result<ConfigLayerStack> {
|
||||
let ignore_managed_requirements = overrides.ignore_managed_requirements;
|
||||
let ignore_user_config = overrides.ignore_user_config;
|
||||
let ignore_user_and_project_exec_policy_rules =
|
||||
overrides.ignore_user_and_project_exec_policy_rules;
|
||||
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||
|
||||
if let Some(requirements) = cloud_requirements.get().await.map_err(io::Error::other)? {
|
||||
merge_requirements_with_remote_sandbox_config(
|
||||
if !ignore_managed_requirements {
|
||||
if let Some(requirements) = cloud_requirements.get().await.map_err(io::Error::other)? {
|
||||
merge_requirements_with_remote_sandbox_config(
|
||||
&mut config_requirements_toml,
|
||||
RequirementSource::CloudRequirements,
|
||||
requirements,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::load_managed_admin_requirements_toml(
|
||||
&mut config_requirements_toml,
|
||||
RequirementSource::CloudRequirements,
|
||||
requirements,
|
||||
);
|
||||
overrides
|
||||
.macos_managed_config_requirements_base64
|
||||
.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Honor the system requirements.toml location.
|
||||
let requirements_toml_file = system_requirements_toml_file_with_overrides(&overrides)?;
|
||||
load_requirements_toml(fs, &mut config_requirements_toml, &requirements_toml_file).await?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::load_managed_admin_requirements_toml(
|
||||
&mut config_requirements_toml,
|
||||
overrides
|
||||
.macos_managed_config_requirements_base64
|
||||
.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Honor the system requirements.toml location.
|
||||
let requirements_toml_file = system_requirements_toml_file_with_overrides(&overrides)?;
|
||||
load_requirements_toml(fs, &mut config_requirements_toml, &requirements_toml_file).await?;
|
||||
|
||||
// Make a best-effort to support the legacy `managed_config.toml` as a
|
||||
// requirements specification.
|
||||
let loaded_config_layers =
|
||||
layer_io::load_config_layers_internal(fs, codex_home, overrides.clone()).await?;
|
||||
load_requirements_from_legacy_scheme(
|
||||
&mut config_requirements_toml,
|
||||
loaded_config_layers.clone(),
|
||||
)
|
||||
.await?;
|
||||
if !ignore_managed_requirements {
|
||||
load_requirements_from_legacy_scheme(
|
||||
&mut config_requirements_toml,
|
||||
loaded_config_layers.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let thread_config_context = ThreadConfigContext {
|
||||
thread_id: None,
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct LoaderOverrides {
|
||||
pub managed_config_path: Option<PathBuf>,
|
||||
pub system_config_path: Option<PathBuf>,
|
||||
pub system_requirements_path: Option<PathBuf>,
|
||||
pub ignore_managed_requirements: bool,
|
||||
pub ignore_user_config: bool,
|
||||
pub ignore_user_and_project_exec_policy_rules: bool,
|
||||
//TODO(gt): Add a macos_ prefix to this field and remove the target_os check.
|
||||
@@ -38,6 +39,7 @@ impl LoaderOverrides {
|
||||
managed_config_path: Some(base.join("managed_config.toml")),
|
||||
system_config_path: Some(base.join("config.toml")),
|
||||
system_requirements_path: Some(base.join("requirements.toml")),
|
||||
ignore_managed_requirements: false,
|
||||
ignore_user_config: false,
|
||||
ignore_user_and_project_exec_policy_rules: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -1084,6 +1084,52 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_can_ignore_managed_requirements() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?;
|
||||
|
||||
let managed_config_path = tmp.path().join("managed_config.toml");
|
||||
tokio::fs::write(&managed_config_path, "approval_policy = \"never\"\n").await?;
|
||||
let system_requirements_path = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&system_requirements_path,
|
||||
"allowed_sandbox_modes = [\"read-only\"]\n",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_config_path);
|
||||
overrides.system_requirements_path = Some(system_requirements_path);
|
||||
overrides.ignore_managed_requirements = true;
|
||||
|
||||
let cloud_requirements = CloudRequirementsLoader::new(async {
|
||||
Ok(Some(ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
..Default::default()
|
||||
}))
|
||||
});
|
||||
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(cwd),
|
||||
&[] as &[(String, TomlValue)],
|
||||
overrides,
|
||||
cloud_requirements,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
layers.requirements_toml(),
|
||||
&ConfigRequirementsToml::default()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
|
||||
@@ -86,7 +86,8 @@ impl NetworkProxySpec {
|
||||
self.config.network.enable_socks5
|
||||
}
|
||||
|
||||
pub(crate) fn from_config_and_constraints(
|
||||
/// Build a runtime proxy spec from effective config plus optional managed constraints.
|
||||
pub fn from_config_and_constraints(
|
||||
config: NetworkProxyConfig,
|
||||
requirements: Option<NetworkConstraints>,
|
||||
permission_profile: &PermissionProfile,
|
||||
|
||||
Reference in New Issue
Block a user