diff --git a/README.md b/README.md index 54c5f233..60e44298 100644 --- a/README.md +++ b/README.md @@ -202,12 +202,12 @@ Codex lets you decide _how much autonomy_ you want to grant the agent. The follo - [`approval_policy`](./codex-rs/config.md#approval_policy) determines when you should be prompted to approve whether Codex can execute a command - [`sandbox`](./codex-rs/config.md#sandbox) determines the _sandbox policy_ that Codex uses to execute untrusted commands -By default, Codex runs with `approval_policy = "untrusted"` and `sandbox.mode = "read-only"`, which means that: +By default, Codex runs with `--ask-for-approval untrusted` and `--sandbox read-only`, which means that: - The user is prompted to approve every command not on the set of "trusted" commands built into Codex (`cat`, `ls`, etc.) - Approved commands are run outside of a sandbox because user approval implies "trust," in this case. -Though running Codex with the `--full-auto` option changes the configuration to `approval_policy = "on-failure"` and `sandbox.mode = "workspace-write"`, which means that: +Running Codex with the `--full-auto` convenience flag changes the configuration to `--ask-for-approval on-failure` and `--sandbox workspace-write`, which means that: - Codex does not initially ask for user approval before running an individual command. - Though when it runs a command, it is run under a sandbox in which: @@ -216,16 +216,16 @@ Though running Codex with the `--full-auto` option changes the configuration to - Network requests are completely disabled. - Only if the command exits with a non-zero exit code will it ask the user for approval. If granted, it will re-attempt the command outside of the sandbox. (A common case is when Codex cannot `npm install` a dependency because that requires network access.) -Again, these two options can be configured independently. For example, if you want Codex to perform an "exploration" where you are happy for it to read anything it wants but you never want to be prompted, you could run Codex with `approval_policy = "never"` and `sandbox.mode = "read-only"`. +Again, these two options can be configured independently. For example, if you want Codex to perform an "exploration" where you are happy for it to read anything it wants but you never want to be prompted, you could run Codex with `--ask-for-approval never` and `--sandbox read-only`. ### Platform sandboxing details The mechanism Codex uses to implement the sandbox policy depends on your OS: -- **macOS 12+** uses **Apple Seatbelt** and runs commands using `sandbox-exec` with a profile (`-p`) that corresponds to the `sandbox.mode` that was specified. +- **macOS 12+** uses **Apple Seatbelt** and runs commands using `sandbox-exec` with a profile (`-p`) that corresponds to the `--sandbox` that was specified. - **Linux** uses a combination of Landlock/seccomp APIs to enforce the `sandbox` configuration. -Note that when running Linux in a containerized environment such as Docker, sandboxing may not work if the host/container configuration does not support the necessary Landlock/seccomp APIs. In such cases, we recommend configuring your Docker container so that it provides the sandbox guarantees you are looking for and then running `codex` with `sandbox.mode = "danger-full-access"` (or more simply, the `--dangerously-bypass-approvals-and-sandbox` flag) within your container. +Note that when running Linux in a containerized environment such as Docker, sandboxing may not work if the host/container configuration does not support the necessary Landlock/seccomp APIs. In such cases, we recommend configuring your Docker container so that it provides the sandbox guarantees you are looking for and then running `codex` with `--sandbox danger-full-access` (or, more simply, the `--dangerously-bypass-approvals-and-sandbox` flag) within your container. --- diff --git a/codex-rs/README.md b/codex-rs/README.md index caa21639..9cd03a7a 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -39,6 +39,10 @@ You can enable notifications by configuring a script that is run whenever the ag To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the prompt via `stdin`) and Codex will work on your task until it decides that it is done and exits. Output is printed to the terminal directly. You can set the `RUST_LOG` environment variable to see more about what's going on. +### Use `@` for file search + +Typing `@` triggers a fuzzy-filename search over the workspace root. Use up/down to select among the results and Tab or Enter to replace the `@` with the selected path. You can use Esc to cancel the search. + ### `--cd`/`-C` flag Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session. @@ -49,15 +53,28 @@ To test to see what happens when a command is run under the sandbox provided by ``` # macOS -codex debug seatbelt [-s SANDBOX_PERMISSION]... [COMMAND]... +codex debug seatbelt [--full-auto] [COMMAND]... # Linux -codex debug landlock [-s SANDBOX_PERMISSION]... [COMMAND]... +codex debug landlock [--full-auto] [COMMAND]... ``` -You can experiment with different values of `-s` to see what permissions the `COMMAND` needs to execute successfully. +### Selecting a sandbox policy via `--sandbox` -Note that the exact API for the `-s` flag is currently in flux. See https://github.com/openai/codex/issues/1248 for details. +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: + +```shell +# Run Codex with the default, read-only sandbox +codex --sandbox read-only + +# Allow the agent to write within the current workspace while still blocking network access +codex --sandbox workspace-write + +# Danger! Disable sandboxing entirely (only do this if you are already running in a container or other isolated env) +codex --sandbox danger-full-access +``` + +The same setting can be persisted in `~/.codex/config.toml` via the top-level `sandbox_mode = "MODE"` key, e.g. `sandbox_mode = "workspace-write"`. ## Code Organization diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index a21cd4e7..905b7461 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -3,11 +3,11 @@ use std::path::PathBuf; use codex_common::CliConfigOverrides; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config_types::SandboxMode; use codex_core::exec::StdioPolicy; use codex_core::exec::spawn_command_under_linux_sandbox; use codex_core::exec::spawn_command_under_seatbelt; use codex_core::exec_env::create_env; -use codex_core::protocol::SandboxPolicy; use crate::LandlockCommand; use crate::SeatbeltCommand; @@ -63,14 +63,14 @@ async fn run_command_under_sandbox( codex_linux_sandbox_exe: Option, sandbox_type: SandboxType, ) -> anyhow::Result<()> { - let sandbox_policy = create_sandbox_policy(full_auto); + let sandbox_mode = create_sandbox_mode(full_auto); let cwd = std::env::current_dir()?; let config = Config::load_with_cli_overrides( config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?, ConfigOverrides { - sandbox_policy: Some(sandbox_policy), + sandbox_mode: Some(sandbox_mode), codex_linux_sandbox_exe, ..Default::default() }, @@ -104,10 +104,10 @@ async fn run_command_under_sandbox( handle_exit_status(status); } -pub fn create_sandbox_policy(full_auto: bool) -> SandboxPolicy { +pub fn create_sandbox_mode(full_auto: bool) -> SandboxMode { if full_auto { - SandboxPolicy::new_workspace_write_policy() + SandboxMode::WorkspaceWrite } else { - SandboxPolicy::new_read_only_policy() + SandboxMode::ReadOnly } } diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs index 18ed49e5..3d498a8e 100644 --- a/codex-rs/common/src/lib.rs +++ b/codex-rs/common/src/lib.rs @@ -7,6 +7,12 @@ pub mod elapsed; #[cfg(feature = "cli")] pub use approval_mode_cli_arg::ApprovalModeCliArg; +#[cfg(feature = "cli")] +mod sandbox_mode_cli_arg; + +#[cfg(feature = "cli")] +pub use sandbox_mode_cli_arg::SandboxModeCliArg; + #[cfg(any(feature = "cli", test))] mod config_override; diff --git a/codex-rs/common/src/sandbox_mode_cli_arg.rs b/codex-rs/common/src/sandbox_mode_cli_arg.rs new file mode 100644 index 00000000..588637ae --- /dev/null +++ b/codex-rs/common/src/sandbox_mode_cli_arg.rs @@ -0,0 +1,28 @@ +//! Standard type to use with the `--sandbox` (`-s`) CLI option. +//! +//! This mirrors the variants of [`codex_core::protocol::SandboxPolicy`], but +//! without any of the associated data so it can be expressed as a simple flag +//! on the command-line. Users that need to tweak the advanced options for +//! `workspace-write` can continue to do so via `-c` overrides or their +//! `config.toml`. + +use clap::ValueEnum; +use codex_core::config_types::SandboxMode; + +#[derive(Clone, Copy, Debug, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum SandboxModeCliArg { + ReadOnly, + WorkspaceWrite, + DangerFullAccess, +} + +impl From for SandboxMode { + fn from(value: SandboxModeCliArg) -> Self { + match value { + SandboxModeCliArg::ReadOnly => SandboxMode::ReadOnly, + SandboxModeCliArg::WorkspaceWrite => SandboxMode::WorkspaceWrite, + SandboxModeCliArg::DangerFullAccess => SandboxMode::DangerFullAccess, + } + } +} diff --git a/codex-rs/config.md b/codex-rs/config.md index 2eaae760..59cf4204 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -204,36 +204,41 @@ To disable reasoning summaries, set `model_reasoning_summary` to `"none"` in you model_reasoning_summary = "none" # disable reasoning summaries ``` -## sandbox +## sandbox_mode -The `sandbox` configuration determines the _sandbox policy_ that Codex uses to execute untrusted commands. The `mode` determines the "base policy." Currently, only `workspace-write` supports additional configuration options, but this may change in the future. +Codex executes model-generated shell commands inside an OS-level sandbox. -The default policy is `read-only`, which means commands can read any file on disk, but attempts to write a file or access the network will be blocked. +In most cases you can pick the desired behaviour with a single option: ```toml -[sandbox] -mode = "read-only" +# same as `--sandbox read-only` +sandbox_mode = "read-only" ``` -A more relaxed policy is `workspace-write`. When specified, the current working directory for the Codex task will be writable (as well as `$TMPDIR` on macOS). Note that the CLI defaults to using `cwd` where it was spawned, though this can be overridden using `--cwd/-C`. +The default policy is `read-only`, which means commands can read any file on +disk, but attempts to write a file or access the network will be blocked. + +A more relaxed policy is `workspace-write`. When specified, the current working directory for the Codex task will be writable (as well as `$TMPDIR` on macOS). Note that the CLI defaults to using the directory where it was spawned as `cwd`, though this can be overridden using `--cwd/-C`. ```toml -[sandbox] -mode = "workspace-write" +# same as `--sandbox workspace-write` +sandbox_mode = "workspace-write" -# By default, only the cwd for the Codex session will be writable (and $TMPDIR on macOS), -# but you can specify additional writable folders in this array. -writable_roots = [ - "/tmp", -] -network_access = false # Like read-only, this also defaults to false and can be omitted. +# Extra settings that only apply when `sandbox = "workspace-write"`. +[sandbox_workspace_write] +# By default, only the cwd for the Codex session will be writable (and $TMPDIR +# on macOS), but you can specify additional writable folders in this array. +writable_roots = ["/tmp"] +# Allow the command being run inside the sandbox to make outbound network +# requests. Disabled by default. +network_access = false ``` To disable sandboxing altogether, specify `danger-full-access` like so: ```toml -[sandbox] -mode = "danger-full-access" +# same as `--sandbox danger-full-access` +sandbox_mode = "danger-full-access" ``` This is reasonable to use if Codex is running in an environment that provides its own sandboxing (such as a Docker container) such that further sandboxing is unnecessary. diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 18c4ec23..26f84be6 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -3,6 +3,8 @@ use crate::config_types::History; use crate::config_types::McpServerConfig; use crate::config_types::ReasoningEffort; use crate::config_types::ReasoningSummary; +use crate::config_types::SandboxMode; +use crate::config_types::SandboxWorkplaceWrite; use crate::config_types::ShellEnvironmentPolicy; use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::Tui; @@ -253,8 +255,11 @@ pub struct ConfigToml { #[serde(default)] pub shell_environment_policy: ShellEnvironmentPolicyToml, - /// If omitted, Codex defaults to the restrictive `read-only` policy. - pub sandbox: Option, + /// Sandbox mode to use. + pub sandbox_mode: Option, + + /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. + pub sandbox_workspace_write: Option, /// Disable server-side response storage (sends the full conversation /// context with every request). Currently necessary for OpenAI customers @@ -305,13 +310,33 @@ pub struct ConfigToml { pub model_reasoning_summary: Option, } +impl ConfigToml { + /// Derive the effective sandbox policy from the configuration. + fn derive_sandbox_policy(&self, sandbox_mode_override: Option) -> SandboxPolicy { + let resolved_sandbox_mode = sandbox_mode_override + .or(self.sandbox_mode) + .unwrap_or_default(); + match resolved_sandbox_mode { + SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(), + SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { + Some(s) => SandboxPolicy::WorkspaceWrite { + writable_roots: s.writable_roots.clone(), + network_access: s.network_access, + }, + None => SandboxPolicy::new_workspace_write_policy(), + }, + SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess, + } + } +} + /// Optional overrides for user configuration (e.g., from CLI flags). #[derive(Default, Debug, Clone)] pub struct ConfigOverrides { pub model: Option, pub cwd: Option, pub approval_policy: Option, - pub sandbox_policy: Option, + pub sandbox_mode: Option, pub model_provider: Option, pub config_profile: Option, pub codex_linux_sandbox_exe: Option, @@ -332,16 +357,16 @@ impl Config { model, cwd, approval_policy, - sandbox_policy, + sandbox_mode, model_provider, config_profile: config_profile_key, codex_linux_sandbox_exe, } = overrides; - let config_profile = match config_profile_key.or(cfg.profile) { + let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) { Some(key) => cfg .profiles - .get(&key) + .get(key) .ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::NotFound, @@ -352,10 +377,7 @@ impl Config { None => ConfigProfile::default(), }; - let sandbox_policy = sandbox_policy.unwrap_or_else(|| { - cfg.sandbox - .unwrap_or_else(SandboxPolicy::new_read_only_policy) - }); + let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode); let mut model_providers = built_in_model_providers(); // Merge user-defined providers into the built-in list. @@ -549,30 +571,38 @@ persistence = "none" #[test] fn test_sandbox_config_parsing() { let sandbox_full_access = r#" -[sandbox] -mode = "danger-full-access" +sandbox_mode = "danger-full-access" + +[sandbox_workspace_write] network_access = false # This should be ignored. "#; let sandbox_full_access_cfg = toml::from_str::(sandbox_full_access) .expect("TOML deserialization should succeed"); + let sandbox_mode_override = None; assert_eq!( - Some(SandboxPolicy::DangerFullAccess), - sandbox_full_access_cfg.sandbox + SandboxPolicy::DangerFullAccess, + sandbox_full_access_cfg.derive_sandbox_policy(sandbox_mode_override) ); let sandbox_read_only = r#" -[sandbox] -mode = "read-only" +sandbox_mode = "read-only" + +[sandbox_workspace_write] network_access = true # This should be ignored. "#; let sandbox_read_only_cfg = toml::from_str::(sandbox_read_only) .expect("TOML deserialization should succeed"); - assert_eq!(Some(SandboxPolicy::ReadOnly), sandbox_read_only_cfg.sandbox); + let sandbox_mode_override = None; + assert_eq!( + SandboxPolicy::ReadOnly, + sandbox_read_only_cfg.derive_sandbox_policy(sandbox_mode_override) + ); let sandbox_workspace_write = r#" -[sandbox] -mode = "workspace-write" +sandbox_mode = "workspace-write" + +[sandbox_workspace_write] writable_roots = [ "/tmp", ] @@ -580,12 +610,13 @@ writable_roots = [ let sandbox_workspace_write_cfg = toml::from_str::(sandbox_workspace_write) .expect("TOML deserialization should succeed"); + let sandbox_mode_override = None; assert_eq!( - Some(SandboxPolicy::WorkspaceWrite { + SandboxPolicy::WorkspaceWrite { writable_roots: vec![PathBuf::from("/tmp")], - network_access: false - }), - sandbox_workspace_write_cfg.sandbox + network_access: false, + }, + sandbox_workspace_write_cfg.derive_sandbox_policy(sandbox_mode_override) ); } diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index a7152d14..83fe613c 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -4,6 +4,7 @@ // definitions that do not contain business logic. use std::collections::HashMap; +use std::path::PathBuf; use strum_macros::Display; use wildmatch::WildMatchPattern; @@ -90,6 +91,28 @@ pub struct Tui { pub disable_mouse_capture: bool, } +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default)] +#[serde(rename_all = "kebab-case")] +pub enum SandboxMode { + #[serde(rename = "read-only")] + #[default] + ReadOnly, + + #[serde(rename = "workspace-write")] + WorkspaceWrite, + + #[serde(rename = "danger-full-access")] + DangerFullAccess, +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] +pub struct SandboxWorkplaceWrite { + #[serde(default)] + pub writable_roots: Vec, + #[serde(default)] + pub network_access: bool, +} + #[derive(Deserialize, Debug, Clone, PartialEq, Default)] #[serde(rename_all = "kebab-case")] pub enum ShellEnvironmentPolicyInherit { diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index d9d577eb..613fedf0 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -14,11 +14,16 @@ pub struct Cli { #[arg(long, short = 'm')] pub model: Option, + /// Select the sandbox policy to use when executing model-generated shell + /// commands. + #[arg(long = "sandbox", short = 's')] + pub sandbox_mode: Option, + /// Configuration profile from config.toml to specify default options. #[arg(long = "profile", short = 'p')] pub config_profile: Option, - /// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, -c sandbox.mode=workspace-write). + /// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, --sandbox workspace-write). #[arg(long = "full-auto", default_value_t = false)] pub full_auto: bool, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 8603a753..44dddd4d 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -11,12 +11,12 @@ pub use cli::Cli; use codex_core::codex_wrapper; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; -use codex_core::protocol::SandboxPolicy; use codex_core::protocol::TaskCompleteEvent; use codex_core::util::is_inside_git_repo; use event_processor::EventProcessor; @@ -36,6 +36,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any skip_git_repo_check, color, last_message_file, + sandbox_mode: sandbox_mode_cli_arg, prompt, config_overrides, } = cli; @@ -84,12 +85,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any ), }; - let sandbox_policy = if full_auto { - Some(SandboxPolicy::new_workspace_write_policy()) + let sandbox_mode = if full_auto { + Some(SandboxMode::WorkspaceWrite) } else if dangerously_bypass_approvals_and_sandbox { - Some(SandboxPolicy::DangerFullAccess) + Some(SandboxMode::DangerFullAccess) } else { - None + sandbox_mode_cli_arg.map(Into::::into) }; // Load configuration and determine approval policy @@ -99,7 +100,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // This CLI is intended to be headless and has no affordances for asking // the user for approval. approval_policy: Some(AskForApproval::Never), - sandbox_policy, + sandbox_mode, cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)), model_provider: None, codex_linux_sandbox_exe, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 86541a0b..85555249 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -1,5 +1,6 @@ //! Configuration object accepted by the `codex` MCP tool-call. +use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; use mcp_types::Tool; use mcp_types::ToolInputSchema; @@ -31,19 +32,23 @@ pub(crate) struct CodexToolCallParam { #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option, - /// Execution approval policy expressed as the kebab-case variant name - /// (`unless-allow-listed`, `auto-edit`, `on-failure`, `never`). + /// Approval policy for shell commands generated by the model: + /// `untrusted`, `on-failure`, `never`. #[serde(default, skip_serializing_if = "Option::is_none")] pub approval_policy: Option, + /// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sandbox: Option, + /// Individual config settings that will override what is in /// CODEX_HOME/config.toml. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option>, } -// Custom enum mirroring `AskForApproval`, but constrained to the subset we -// expose via the tool-call schema. +/// Custom enum mirroring [`AskForApproval`], but has an extra dependency on +/// [`JsonSchema`]. #[derive(Debug, Clone, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub(crate) enum CodexToolCallApprovalPolicy { @@ -62,6 +67,26 @@ impl From for AskForApproval { } } +/// Custom enum mirroring [`SandboxMode`] from config_types.rs, but with +/// `JsonSchema` support. +#[derive(Debug, Clone, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum CodexToolCallSandboxMode { + ReadOnly, + WorkspaceWrite, + DangerFullAccess, +} + +impl From for SandboxMode { + fn from(value: CodexToolCallSandboxMode) -> Self { + match value { + CodexToolCallSandboxMode::ReadOnly => SandboxMode::ReadOnly, + CodexToolCallSandboxMode::WorkspaceWrite => SandboxMode::WorkspaceWrite, + CodexToolCallSandboxMode::DangerFullAccess => SandboxMode::DangerFullAccess, + } + } +} + /// Builds a `Tool` definition (JSON schema etc.) for the Codex tool-call. pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool { let schema = SchemaSettings::draft2019_09() @@ -104,6 +129,7 @@ impl CodexToolCallParam { profile, cwd, approval_policy, + sandbox, config: cli_overrides, } = self; @@ -113,9 +139,7 @@ impl CodexToolCallParam { config_profile: profile, cwd: cwd.map(PathBuf::from), approval_policy: approval_policy.map(Into::into), - // Note we may want to expose a field on CodexToolCallParam to - // facilitate configuring the sandbox policy. - sandbox_policy: None, + sandbox_mode: sandbox.map(Into::into), model_provider: None, codex_linux_sandbox_exe, }; @@ -160,7 +184,7 @@ mod tests { "type": "object", "properties": { "approval-policy": { - "description": "Execution approval policy expressed as the kebab-case variant name (`unless-allow-listed`, `auto-edit`, `on-failure`, `never`).", + "description": "Approval policy for shell commands generated by the model: `untrusted`, `on-failure`, `never`.", "enum": [ "untrusted", "on-failure", @@ -168,6 +192,15 @@ mod tests { ], "type": "string" }, + "sandbox": { + "description": "Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.", + "enum": [ + "read-only", + "workspace-write", + "danger-full-access" + ], + "type": "string" + }, "config": { "description": "Individual config settings that will override what is in CODEX_HOME/config.toml.", "additionalProperties": true, diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index cb6bb923..cb1b725a 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -21,11 +21,16 @@ pub struct Cli { #[arg(long = "profile", short = 'p')] pub config_profile: Option, + /// Select the sandbox policy to use when executing model-generated shell + /// commands. + #[arg(long = "sandbox", short = 's')] + pub sandbox_mode: Option, + /// Configure when the model requires human approval before executing a command. #[arg(long = "ask-for-approval", short = 'a')] pub approval_policy: Option, - /// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, -c sandbox.mode=workspace-write). + /// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, --sandbox workspace-write). #[arg(long = "full-auto", default_value_t = false)] pub full_auto: bool, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 317cd57f..07ddbc41 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -5,11 +5,11 @@ use app::App; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config_types::SandboxMode; use codex_core::openai_api_key::OPENAI_API_KEY_ENV_VAR; use codex_core::openai_api_key::get_openai_api_key; use codex_core::openai_api_key::set_openai_api_key; use codex_core::protocol::AskForApproval; -use codex_core::protocol::SandboxPolicy; use codex_core::util::is_inside_git_repo; use codex_login::try_read_openai_api_key; use log_layer::TuiLogLayer; @@ -48,19 +48,21 @@ mod user_approval_widget; pub use cli::Cli; pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> std::io::Result<()> { - let (sandbox_policy, approval_policy) = if cli.full_auto { + let (sandbox_mode, approval_policy) = if cli.full_auto { ( - Some(SandboxPolicy::new_workspace_write_policy()), + Some(SandboxMode::WorkspaceWrite), Some(AskForApproval::OnFailure), ) } else if cli.dangerously_bypass_approvals_and_sandbox { ( - Some(SandboxPolicy::DangerFullAccess), + Some(SandboxMode::DangerFullAccess), Some(AskForApproval::Never), ) } else { - let sandbox_policy = None; - (sandbox_policy, cli.approval_policy.map(Into::into)) + ( + cli.sandbox_mode.map(Into::::into), + cli.approval_policy.map(Into::into), + ) }; let config = { @@ -68,7 +70,7 @@ pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> std::io:: let overrides = ConfigOverrides { model: cli.model.clone(), approval_policy, - sandbox_policy, + sandbox_mode, cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)), model_provider: None, config_profile: cli.config_profile.clone(),