From 7230d8aeeb841aa1d6d81e2263a1248cd4f30de8 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 20 May 2025 16:17:52 -0700 Subject: [PATCH] feat: introduce support for shell_environment_policy in config.toml --- codex-rs/Cargo.lock | 8 ++ codex-rs/README.md | 43 +++++++ codex-rs/cli/src/landlock.rs | 3 +- codex-rs/cli/src/seatbelt.rs | 5 +- codex-rs/core/Cargo.toml | 2 + codex-rs/core/src/codex.rs | 5 + codex-rs/core/src/config.rs | 13 +++ codex-rs/core/src/config_types.rs | 89 ++++++++++++++ codex-rs/core/src/exec.rs | 24 +++- codex-rs/core/src/exec_env.rs | 185 ++++++++++++++++++++++++++++++ codex-rs/core/src/exec_linux.rs | 2 + codex-rs/core/src/landlock.rs | 4 +- codex-rs/core/src/lib.rs | 1 + 13 files changed, 377 insertions(+), 7 deletions(-) create mode 100644 codex-rs/core/src/exec_env.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 5358065cd5..6408e8de6f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -526,6 +526,7 @@ dependencies = [ "futures", "landlock", "libc", + "maplit", "mcp-types", "mime_guess", "openssl-sys", @@ -548,6 +549,7 @@ dependencies = [ "tree-sitter", "tree-sitter-bash", "uuid", + "wildmatch", "wiremock", ] @@ -4309,6 +4311,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wildmatch" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" + [[package]] name = "winapi" version = "0.3.9" diff --git a/codex-rs/README.md b/codex-rs/README.md index bedce9f22d..556cfd5155 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -222,6 +222,49 @@ Currently, customers whose accounts are set to use Zero Data Retention (ZDR) mus disable_response_storage = true ``` +### shell_environment_policy + +Codex spawns subprocesses (e.g. when executing a `local_shell` tool-call suggested by the assistant). By default it passes **only a minimal core subset** of your environment to those subprocesses to avoid leaking credentials. You can tune this behavior via the **`shell_environment_policy`** block in +`config.toml`: + +```toml +[shell_environment_policy] +# inherit can be "core" (default), "all", or "none" +inherit = "core" +# set to true to *skip* the filter for `"*KEY*"` and `"*TOKEN*"` +ignore_default_excludes = false +# exclude patterns (case-insensitive globs) +exclude = ["*_SECRET", "AWS_*"] +# force-set / override values +set = { CI = "1" } +# if provided, *only* vars matching these patterns are kept +include_only = ["PATH", "HOME"] +``` + +| Field | Type | Default | Description | +| ------------------------- | -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `inherit` | string | `core` | Starting template for the environment:
`core` (HOME, PATH, USER, …), `all` (clone full parent env), or `none` (start empty). | +| `ignore_default_excludes` | boolean | `false` | When `false`, Codex removes any var whose **name** contains `KEY` or `TOKEN` (case-insensitive) before other rules run. | +| `exclude` | array<string> | `[]` | Case-insensitive glob patterns to drop after the default filter.
Examples: `"*_SECRET"`, `"AWS_*"`. | +| `set` | table<string,string> | `{}` | Explicit key/value overrides or additions – always win over inherited values. | +| `include_only` | array<string> | `[]` | If non-empty, a whitelist of patterns; only variables that match _one_ pattern survive the final step. | + +The patterns are **glob style**, not full regular expressions: `*` matches any +number of characters, `?` matches exactly one, and character classes like +`[A-Z]`/`[^0-9]` are supported. Matching is always **case-insensitive**. This +syntax is documented in code as `EnvironmentVariablePattern` (see +`core/src/config_types.rs`). + +If you just need a clean slate with a few custom entries you can write: + +```toml +[shell_environment_policy] +inherit = "none" +set = { PATH = "/usr/bin", MY_FLAG = "1" } +``` + +Currently, `CODEX_SANDBOX_NETWORK_DISABLED=1` is also added to the environment, assuming network is disabled. This is not configurable. + ### notify Specify a program that will be executed to get notified about events generated by Codex. Note that the program will receive the notification argument as a string of JSON, e.g.: diff --git a/codex-rs/cli/src/landlock.rs b/codex-rs/cli/src/landlock.rs index 998072c5ad..3674e6a127 100644 --- a/codex-rs/cli/src/landlock.rs +++ b/codex-rs/cli/src/landlock.rs @@ -23,7 +23,8 @@ pub fn run_landlock(command: Vec, sandbox_policy: SandboxPolicy) -> anyh let cwd = std::env::current_dir()?; apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd)?; - let mut child = spawn_child_sync(command, cwd, &sandbox_policy, StdioPolicy::Inherit)?; + let env: std::collections::HashMap = std::env::vars().collect(); + let mut child = spawn_child_sync(command, cwd, &sandbox_policy, StdioPolicy::Inherit, env)?; let status = child.wait()?; Ok(status) }); diff --git a/codex-rs/cli/src/seatbelt.rs b/codex-rs/cli/src/seatbelt.rs index e40848ca0f..f5201ff7f6 100644 --- a/codex-rs/cli/src/seatbelt.rs +++ b/codex-rs/cli/src/seatbelt.rs @@ -9,8 +9,11 @@ pub async fn run_seatbelt( sandbox_policy: SandboxPolicy, ) -> anyhow::Result<()> { let cwd = std::env::current_dir()?; + // For the debug CLI we currently inherit the full parent environment. + let env: std::collections::HashMap = std::env::vars().collect(); let mut child = - spawn_command_under_seatbelt(command, &sandbox_policy, cwd, StdioPolicy::Inherit).await?; + spawn_command_under_seatbelt(command, &sandbox_policy, cwd, StdioPolicy::Inherit, env) + .await?; let status = child.wait().await?; handle_exit_status(status); } diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index e2979497d8..2d4ed8f36a 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -46,6 +46,7 @@ tracing = { version = "0.1.41", features = ["log"] } tree-sitter = "0.25.3" tree-sitter-bash = "0.23.3" uuid = { version = "1", features = ["serde", "v4"] } +wildmatch = "2.4.0" [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2.172" @@ -58,6 +59,7 @@ openssl-sys = { version = "*", features = ["vendored"] } [dev-dependencies] assert_cmd = "2" +maplit = "1.0.2" predicates = "3" pretty_assertions = "1.4.1" tempfile = "3" diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 0f91472768..69e504781f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -37,6 +37,7 @@ use crate::client::ModelClient; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::config::Config; +use crate::config_types::ShellEnvironmentPolicy; use crate::conversation_history::ConversationHistory; use crate::error::CodexErr; use crate::error::Result as CodexResult; @@ -45,6 +46,7 @@ use crate::exec::ExecParams; use crate::exec::ExecToolCallOutput; use crate::exec::SandboxType; use crate::exec::process_exec_tool_call; +use crate::exec_env::create_env; use crate::flags::OPENAI_STREAM_MAX_RETRIES; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_connection_manager::try_parse_fully_qualified_tool_name; @@ -171,6 +173,7 @@ pub(crate) struct Session { instructions: Option, approval_policy: AskForApproval, sandbox_policy: SandboxPolicy, + shell_environment_policy: ShellEnvironmentPolicy, writable_roots: Mutex>, /// Manager for external MCP servers/tools. @@ -634,6 +637,7 @@ async fn submission_loop( instructions, approval_policy, sandbox_policy, + shell_environment_policy: config.shell_environment_policy.clone(), cwd, writable_roots, mcp_connection_manager, @@ -1124,6 +1128,7 @@ fn to_exec_params(params: ShellToolCallParams, sess: &Session) -> ExecParams { command: params.command, cwd: sess.resolve_path(params.workdir.clone()), timeout_ms: params.timeout_ms, + env: create_env(&sess.shell_environment_policy), } } diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index de97b36e88..2a3f454342 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -1,6 +1,8 @@ use crate::config_profile::ConfigProfile; use crate::config_types::History; use crate::config_types::McpServerConfig; +use crate::config_types::ShellEnvironmentPolicy; +use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::Tui; use crate::config_types::UriBasedFileOpener; use crate::flags::OPENAI_DEFAULT_MODEL; @@ -37,6 +39,8 @@ pub struct Config { pub sandbox_policy: SandboxPolicy, + pub shell_environment_policy: ShellEnvironmentPolicy, + /// Disable server-side response storage (sends the full conversation /// context with every request). Currently necessary for OpenAI customers /// who have opted into Zero Data Retention (ZDR). @@ -108,6 +112,9 @@ pub struct ConfigToml { /// Default approval policy for executing commands. pub approval_policy: Option, + #[serde(default)] + pub shell_environment_policy: ShellEnvironmentPolicyToml, + // The `default` attribute ensures that the field is treated as `None` when // the key is omitted from the TOML. Without it, Serde treats the field as // required because we supply a custom deserializer. @@ -302,6 +309,8 @@ impl Config { })? .clone(); + let shell_environment_policy = cfg.shell_environment_policy.into(); + let resolved_cwd = { use std::env; @@ -336,6 +345,7 @@ impl Config { .or(cfg.approval_policy) .unwrap_or_else(AskForApproval::default), sandbox_policy, + shell_environment_policy, disable_response_storage: disable_response_storage .or(config_profile.disable_response_storage) .or(cfg.disable_response_storage) @@ -677,6 +687,7 @@ disable_response_storage = true model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::Never, sandbox_policy: SandboxPolicy::new_read_only_policy(), + shell_environment_policy: ShellEnvironmentPolicy::default(), disable_response_storage: false, instructions: None, notify: None, @@ -714,6 +725,7 @@ disable_response_storage = true model_provider: fixture.openai_chat_completions_provider.clone(), approval_policy: AskForApproval::UnlessAllowListed, sandbox_policy: SandboxPolicy::new_read_only_policy(), + shell_environment_policy: ShellEnvironmentPolicy::default(), disable_response_storage: false, instructions: None, notify: None, @@ -766,6 +778,7 @@ disable_response_storage = true model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::OnFailure, sandbox_policy: SandboxPolicy::new_read_only_policy(), + shell_environment_policy: ShellEnvironmentPolicy::default(), disable_response_storage: true, instructions: None, notify: None, diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 22c3e8565f..6696f76f0b 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 wildmatch::WildMatchPattern; use serde::Deserialize; @@ -86,3 +87,91 @@ pub struct Tui { /// using the mouse without needing to hold down a modifier key. pub disable_mouse_capture: bool, } + +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] + +pub enum ShellEnvironmentPolicyInherit { + /// "Core" environment variables for the platform. On UNIX, this would + /// include HOME, LOGNAME, PATH, SHELL, and USER, among others. + #[default] + Core, + + /// Inherits the full environment from the parent process. + All, + + /// Do not inherit any environment variables from the parent process. + None, +} + +/// Policy for building the `env` when spawning a process via either the +/// `shell` or `local_shell` tool. +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] +pub struct ShellEnvironmentPolicyToml { + pub inherit: Option, + + pub ignore_default_excludes: Option, + + /// List of regular expressions. + pub exclude: Option>, + + pub r#set: Option>, + + /// List of regular expressions. + pub include_only: Option>, +} + +pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>; + +/// Deriving the `env` based on this policy works as follows: +/// 1. Create an initial map based on the `inherit` policy. +/// 2. If `ignore_default_excludes` is false, filter the map using the default +/// exclude pattern(s), which are: `"*KEY*"` and `"*TOKEN*"`. +/// 3. If `exclude` is not empty, filter the map using the provided patterns. +/// 4. Insert any entries from `r#set` into the map. +/// 5. If non-empty, filter the map using the `include_only` patterns. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ShellEnvironmentPolicy { + /// Starting point when building the environment. + pub inherit: ShellEnvironmentPolicyInherit, + + /// True to skip the check to exclude default environment variables that + /// contain "KEY" or "TOKEN" in their name. + pub ignore_default_excludes: bool, + + /// Environment variable names to exclude from the environment. + pub exclude: Vec, + + /// (key, value) pairs to insert in the environment. + pub r#set: HashMap, + + /// Environment variable names to retain in the environment. + pub include_only: Vec, +} + +impl From for ShellEnvironmentPolicy { + fn from(toml: ShellEnvironmentPolicyToml) -> Self { + let inherit = toml.inherit.unwrap_or(ShellEnvironmentPolicyInherit::Core); + let ignore_default_excludes = toml.ignore_default_excludes.unwrap_or(false); + let exclude = toml + .exclude + .unwrap_or_default() + .into_iter() + .map(|s| EnvironmentVariablePattern::new_case_insensitive(&s)) + .collect(); + let r#set = toml.r#set.unwrap_or_default(); + let include_only = toml + .include_only + .unwrap_or_default() + .into_iter() + .map(|s| EnvironmentVariablePattern::new_case_insensitive(&s)) + .collect(); + + Self { + inherit, + ignore_default_excludes, + exclude, + r#set, + include_only, + } + } +} diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 158a0da9b4..5c822b1363 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -59,6 +59,7 @@ pub struct ExecParams { pub command: Vec, pub cwd: PathBuf, pub timeout_ms: Option, + pub env: std::collections::HashMap, } #[derive(Clone, Copy, Debug, PartialEq)] @@ -87,12 +88,14 @@ pub async fn process_exec_tool_call( command, cwd, timeout_ms, + env, } = params; let child = spawn_command_under_seatbelt( command, sandbox_policy, cwd, StdioPolicy::RedirectForShellTool, + env, ) .await?; consume_truncated_output(child, ctrl_c, timeout_ms).await @@ -145,9 +148,10 @@ pub async fn spawn_command_under_seatbelt( sandbox_policy: &SandboxPolicy, cwd: PathBuf, stdio_policy: StdioPolicy, + env: std::collections::HashMap, ) -> std::io::Result { let seatbelt_command = create_seatbelt_command(command, sandbox_policy, &cwd); - spawn_child_async(seatbelt_command, cwd, sandbox_policy, stdio_policy).await + spawn_child_async(seatbelt_command, cwd, sandbox_policy, stdio_policy, env).await } fn create_seatbelt_command( @@ -233,6 +237,7 @@ async fn exec( command, cwd, timeout_ms, + env, }: ExecParams, sandbox_policy: &SandboxPolicy, ctrl_c: Arc, @@ -242,6 +247,7 @@ async fn exec( cwd, sandbox_policy, StdioPolicy::RedirectForShellTool, + env, ) .await?; consume_truncated_output(child, ctrl_c, timeout_ms).await @@ -259,7 +265,8 @@ macro_rules! configure_command { $command: expr, $cwd: expr, $sandbox_policy: expr, - $stdio_policy: expr + $stdio_policy: expr, + $env_map: expr ) => {{ // For now, we take `SandboxPolicy` as a parameter to spawn_child() because // we need to determine whether to set the @@ -279,6 +286,12 @@ macro_rules! configure_command { cmd.args(&$command[1..]); cmd.current_dir($cwd); + // Clear the inherited environment to avoid leaking unexpected + // variables to the child process. Afterwards, populate the + // environment map passed in from the caller. + cmd.env_clear(); + cmd.envs(&$env_map); + if !$sandbox_policy.has_full_network_access() { cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1"); } @@ -313,8 +326,9 @@ pub(crate) async fn spawn_child_async( cwd: PathBuf, sandbox_policy: &SandboxPolicy, stdio_policy: StdioPolicy, + env: std::collections::HashMap, ) -> std::io::Result { - let mut cmd = configure_command!(Command, command, cwd, sandbox_policy, stdio_policy)?; + let mut cmd = configure_command!(Command, command, cwd, sandbox_policy, stdio_policy, env)?; cmd.kill_on_drop(true).spawn() } @@ -326,13 +340,15 @@ pub fn spawn_child_sync( cwd: PathBuf, sandbox_policy: &SandboxPolicy, stdio_policy: StdioPolicy, + env: std::collections::HashMap, ) -> std::io::Result { let mut cmd = configure_command!( std::process::Command, command, cwd, sandbox_policy, - stdio_policy + stdio_policy, + env )?; cmd.spawn() } diff --git a/codex-rs/core/src/exec_env.rs b/codex-rs/core/src/exec_env.rs new file mode 100644 index 0000000000..5c310a03d0 --- /dev/null +++ b/codex-rs/core/src/exec_env.rs @@ -0,0 +1,185 @@ +use crate::config_types::EnvironmentVariablePattern; +use crate::config_types::ShellEnvironmentPolicy; +use crate::config_types::ShellEnvironmentPolicyInherit; +use std::collections::HashMap; +use std::collections::HashSet; + +/// Construct an environment map based on the rules in the specified policy. The +/// resulting map can be passed directly to `Command::envs()` after calling +/// `env_clear()` to ensure no unintended variables are leaked to the spawned +/// process. +/// +/// The derivation follows the algorithm documented in the struct-level comment +/// for [`ShellEnvironmentPolicy`]. +pub(crate) fn create_env(policy: &ShellEnvironmentPolicy) -> HashMap { + populate_env(std::env::vars(), policy) +} + +fn populate_env(vars: I, policy: &ShellEnvironmentPolicy) -> HashMap +where + I: IntoIterator, +{ + // Step 1 – determine the starting set of variables based on the + // `inherit` strategy. + let mut env_map: HashMap = match policy.inherit { + ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(), + ShellEnvironmentPolicyInherit::None => HashMap::new(), + ShellEnvironmentPolicyInherit::Core => { + const CORE_VARS: &[&str] = &[ + "HOME", "LOGNAME", "PATH", "SHELL", "USER", "USERNAME", "TMPDIR", "TEMP", "TMP", + ]; + let allow: HashSet<&str> = CORE_VARS.iter().copied().collect(); + vars.into_iter() + .filter(|(k, _)| allow.contains(k.as_str())) + .collect() + } + }; + + // Internal helper – does `name` match **any** pattern in `patterns`? + let matches_any = |name: &str, patterns: &[EnvironmentVariablePattern]| -> bool { + patterns.iter().any(|pattern| pattern.matches(name)) + }; + + // Step 2 – Apply the default exclude if not disabled. + if !policy.ignore_default_excludes { + let default_excludes = vec![ + EnvironmentVariablePattern::new_case_insensitive("*KEY*"), + EnvironmentVariablePattern::new_case_insensitive("*TOKEN*"), + ]; + env_map.retain(|k, _| !matches_any(k, &default_excludes)); + } + + // Step 3 – Apply custom excludes. + if !policy.exclude.is_empty() { + env_map.retain(|k, _| !matches_any(k, &policy.exclude)); + } + + // Step 4 – Apply user-provided overrides. + for (key, val) in &policy.r#set { + env_map.insert(key.clone(), val.clone()); + } + + // Step 5 – If include_only is non-empty, keep *only* the matching vars. + if !policy.include_only.is_empty() { + env_map.retain(|k, _| matches_any(k, &policy.include_only)); + } + + env_map +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used, clippy::expect_used)] + + use super::*; + use crate::config_types::ShellEnvironmentPolicyInherit; + use maplit::hashmap; + + fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> { + pairs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } + + #[test] + fn test_core_inherit_and_default_excludes() { + let vars = make_vars(&[ + ("PATH", "/usr/bin"), + ("HOME", "/home/user"), + ("API_KEY", "secret"), + ("SECRET_TOKEN", "t"), + ]); + + let policy = ShellEnvironmentPolicy::default(); // inherit Core, default excludes on + let result = populate_env(vars, &policy); + + let expected: HashMap = hashmap! { + "PATH".to_string() => "/usr/bin".to_string(), + "HOME".to_string() => "/home/user".to_string(), + }; + + assert_eq!(result, expected); + } + + #[test] + fn test_include_only() { + let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]); + + let mut policy = ShellEnvironmentPolicy::default(); + // skip default excludes so nothing is removed prematurely + policy.ignore_default_excludes = true; + policy.include_only = vec![EnvironmentVariablePattern::new_case_insensitive("*PATH")]; + + let result = populate_env(vars, &policy); + + let expected: HashMap = hashmap! { + "PATH".to_string() => "/usr/bin".to_string(), + }; + + assert_eq!(result, expected); + } + + #[test] + fn test_set_overrides() { + let vars = make_vars(&[("PATH", "/usr/bin")]); + + let mut policy = ShellEnvironmentPolicy::default(); + policy.ignore_default_excludes = true; + policy.r#set.insert("NEW_VAR".to_string(), "42".to_string()); + + let result = populate_env(vars, &policy); + + let expected: HashMap = hashmap! { + "PATH".to_string() => "/usr/bin".to_string(), + "NEW_VAR".to_string() => "42".to_string(), + }; + + assert_eq!(result, expected); + } + + #[test] + fn test_inherit_all() { + let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]); + + let mut policy = ShellEnvironmentPolicy::default(); + policy.inherit = ShellEnvironmentPolicyInherit::All; + policy.ignore_default_excludes = true; // keep everything + + let result = populate_env(vars.clone(), &policy); + let expected: HashMap = vars.into_iter().collect(); + assert_eq!(result, expected); + } + + #[test] + fn test_inherit_all_with_default_excludes() { + let vars = make_vars(&[("PATH", "/usr/bin"), ("API_KEY", "secret")]); + + let mut policy = ShellEnvironmentPolicy::default(); + policy.inherit = ShellEnvironmentPolicyInherit::All; // default ignore_default_excludes = false + + let result = populate_env(vars, &policy); + let expected: HashMap = hashmap! { + "PATH".to_string() => "/usr/bin".to_string(), + }; + assert_eq!(result, expected); + } + + #[test] + fn test_inherit_none() { + let vars = make_vars(&[("PATH", "/usr/bin"), ("HOME", "/home")]); + + let mut policy = ShellEnvironmentPolicy::default(); + policy.inherit = ShellEnvironmentPolicyInherit::None; + policy.ignore_default_excludes = true; + policy + .r#set + .insert("ONLY_VAR".to_string(), "yes".to_string()); + + let result = populate_env(vars, &policy); + let expected: HashMap = hashmap! { + "ONLY_VAR".to_string() => "yes".to_string(), + }; + assert_eq!(result, expected); + } +} diff --git a/codex-rs/core/src/exec_linux.rs b/codex-rs/core/src/exec_linux.rs index e74c56219c..76bd428a7f 100644 --- a/codex-rs/core/src/exec_linux.rs +++ b/codex-rs/core/src/exec_linux.rs @@ -34,6 +34,7 @@ pub fn exec_linux( command, cwd, timeout_ms, + env, } = params; apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd)?; let child = spawn_child_async( @@ -41,6 +42,7 @@ pub fn exec_linux( cwd, &sandbox_policy, StdioPolicy::RedirectForShellTool, + env, ) .await?; consume_truncated_output(child, ctrl_c_copy, timeout_ms).await diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 6e9b8de7c6..00bf2d4d2f 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -157,6 +157,7 @@ mod tests { command: cmd.iter().map(|elm| elm.to_string()).collect(), cwd: std::env::current_dir().expect("cwd should exist"), timeout_ms: Some(timeout_ms), + env: std::collections::HashMap::new(), }; let sandbox_policy = @@ -236,9 +237,10 @@ mod tests { let params = ExecParams { command: cmd.iter().map(|s| s.to_string()).collect(), cwd: std::env::current_dir().expect("cwd should exist"), - // Give the tool a generous 2‑second timeout so even slow DNS timeouts + // Give the tool a generous 2-second timeout so even slow DNS timeouts // do not stall the suite. timeout_ms: Some(2_000), + env: std::collections::HashMap::new(), }; let sandbox_policy = SandboxPolicy::new_read_only_policy(); diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 759f10291b..cee2392265 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -17,6 +17,7 @@ pub mod config_types; mod conversation_history; pub mod error; pub mod exec; +mod exec_env; pub mod exec_linux; mod flags; mod is_safe_command;