mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
feat: introduce support for shell_environment_policy in config.toml
This commit is contained in:
8
codex-rs/Cargo.lock
generated
8
codex-rs/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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 = ["AWS_*", "AZURE_*"]
|
||||
# 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:<br>`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`, `SECRET`, or `TOKEN` (case-insensitive) before other rules run. |
|
||||
| `exclude` | array<string> | `[]` | Case-insensitive glob patterns to drop after the default filter.<br>Examples: `"AWS_*"`, `"AZURE_*"`. |
|
||||
| `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. (Generally used with `inherit = "all"`.) |
|
||||
|
||||
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.:
|
||||
|
||||
@@ -3,27 +3,29 @@
|
||||
//! On Linux the command is executed inside a Landlock + seccomp sandbox by
|
||||
//! calling the low-level `exec_linux` helper from `codex_core::linux`.
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_core::exec::StdioPolicy;
|
||||
use codex_core::exec::spawn_child_sync;
|
||||
use codex_core::exec_linux::apply_sandbox_policy_to_current_thread;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use std::process::ExitStatus;
|
||||
|
||||
use crate::exit_status::handle_exit_status;
|
||||
|
||||
/// Execute `command` in a Linux sandbox (Landlock + seccomp) the way Codex
|
||||
/// would.
|
||||
pub fn run_landlock(command: Vec<String>, sandbox_policy: SandboxPolicy) -> anyhow::Result<()> {
|
||||
pub fn run_landlock(command: Vec<String>, config: &Config) -> anyhow::Result<()> {
|
||||
if command.is_empty() {
|
||||
anyhow::bail!("command args are empty");
|
||||
}
|
||||
|
||||
// Spawn a new thread and apply the sandbox policies there.
|
||||
let env = codex_core::exec_env::create_env(&config.shell_environment_policy);
|
||||
let sandbox_policy = config.sandbox_policy.clone();
|
||||
let handle = std::thread::spawn(move || -> anyhow::Result<ExitStatus> {
|
||||
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 mut child = spawn_child_sync(command, cwd, &sandbox_policy, StdioPolicy::Inherit, env)?;
|
||||
let status = child.wait()?;
|
||||
Ok(status)
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ use codex_cli::SeatbeltCommand;
|
||||
use codex_cli::create_sandbox_policy;
|
||||
use codex_cli::proto;
|
||||
use codex_cli::seatbelt;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_exec::Cli as ExecCli;
|
||||
use codex_tui::Cli as TuiCli;
|
||||
|
||||
@@ -86,7 +88,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
full_auto,
|
||||
}) => {
|
||||
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
|
||||
seatbelt::run_seatbelt(command, sandbox_policy).await?;
|
||||
let config = Config::load_with_overrides(ConfigOverrides {
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
..Default::default()
|
||||
})?;
|
||||
seatbelt::run_seatbelt(command, &config).await?;
|
||||
}
|
||||
#[cfg(unix)]
|
||||
DebugCommand::Landlock(LandlockCommand {
|
||||
@@ -95,7 +101,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
full_auto,
|
||||
}) => {
|
||||
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
|
||||
codex_cli::landlock::run_landlock(command, sandbox_policy)?;
|
||||
let config = Config::load_with_overrides(ConfigOverrides {
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
..Default::default()
|
||||
})?;
|
||||
codex_cli::landlock::run_landlock(command, &config)?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
DebugCommand::Landlock(_) => {
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
use codex_core::config::Config;
|
||||
use codex_core::exec::StdioPolicy;
|
||||
use codex_core::exec::spawn_command_under_seatbelt;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::exec_env::create_env;
|
||||
|
||||
use crate::exit_status::handle_exit_status;
|
||||
|
||||
pub async fn run_seatbelt(
|
||||
command: Vec<String>,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
) -> anyhow::Result<()> {
|
||||
pub async fn run_seatbelt(command: Vec<String>, config: &Config) -> anyhow::Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let mut child =
|
||||
spawn_command_under_seatbelt(command, &sandbox_policy, cwd, StdioPolicy::Inherit).await?;
|
||||
let env = create_env(&config.shell_environment_policy);
|
||||
let mut child = spawn_command_under_seatbelt(
|
||||
command,
|
||||
&config.sandbox_policy,
|
||||
cwd,
|
||||
StdioPolicy::Inherit,
|
||||
env,
|
||||
)
|
||||
.await?;
|
||||
let status = child.wait().await?;
|
||||
handle_exit_status(status);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<String>,
|
||||
approval_policy: AskForApproval,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
shell_environment_policy: ShellEnvironmentPolicy,
|
||||
writable_roots: Mutex<Vec<PathBuf>>,
|
||||
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AskForApproval>,
|
||||
|
||||
#[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,
|
||||
|
||||
@@ -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<ShellEnvironmentPolicyInherit>,
|
||||
|
||||
pub ignore_default_excludes: Option<bool>,
|
||||
|
||||
/// List of regular expressions.
|
||||
pub exclude: Option<Vec<String>>,
|
||||
|
||||
pub r#set: Option<HashMap<String, String>>,
|
||||
|
||||
/// List of regular expressions.
|
||||
pub include_only: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
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<EnvironmentVariablePattern>,
|
||||
|
||||
/// (key, value) pairs to insert in the environment.
|
||||
pub r#set: HashMap<String, String>,
|
||||
|
||||
/// Environment variable names to retain in the environment.
|
||||
pub include_only: Vec<EnvironmentVariablePattern>,
|
||||
}
|
||||
|
||||
impl From<ShellEnvironmentPolicyToml> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -59,6 +60,7 @@ pub struct ExecParams {
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub timeout_ms: Option<u64>,
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -87,12 +89,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 +149,10 @@ pub async fn spawn_command_under_seatbelt(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cwd: PathBuf,
|
||||
stdio_policy: StdioPolicy,
|
||||
env: HashMap<String, String>,
|
||||
) -> std::io::Result<Child> {
|
||||
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 +238,7 @@ async fn exec(
|
||||
command,
|
||||
cwd,
|
||||
timeout_ms,
|
||||
env,
|
||||
}: ExecParams,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
ctrl_c: Arc<Notify>,
|
||||
@@ -242,6 +248,7 @@ async fn exec(
|
||||
cwd,
|
||||
sandbox_policy,
|
||||
StdioPolicy::RedirectForShellTool,
|
||||
env,
|
||||
)
|
||||
.await?;
|
||||
consume_truncated_output(child, ctrl_c, timeout_ms).await
|
||||
@@ -259,7 +266,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 +287,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 +327,9 @@ pub(crate) async fn spawn_child_async(
|
||||
cwd: PathBuf,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
stdio_policy: StdioPolicy,
|
||||
env: HashMap<String, String>,
|
||||
) -> std::io::Result<Child> {
|
||||
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 +341,15 @@ pub fn spawn_child_sync(
|
||||
cwd: PathBuf,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
stdio_policy: StdioPolicy,
|
||||
env: HashMap<String, String>,
|
||||
) -> std::io::Result<std::process::Child> {
|
||||
let mut cmd = configure_command!(
|
||||
std::process::Command,
|
||||
command,
|
||||
cwd,
|
||||
sandbox_policy,
|
||||
stdio_policy
|
||||
stdio_policy,
|
||||
env
|
||||
)?;
|
||||
cmd.spawn()
|
||||
}
|
||||
|
||||
196
codex-rs/core/src/exec_env.rs
Normal file
196
codex-rs/core/src/exec_env.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
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 fn create_env(policy: &ShellEnvironmentPolicy) -> HashMap<String, String> {
|
||||
populate_env(std::env::vars(), policy)
|
||||
}
|
||||
|
||||
fn populate_env<I>(vars: I, policy: &ShellEnvironmentPolicy) -> HashMap<String, String>
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
// Step 1 – determine the starting set of variables based on the
|
||||
// `inherit` strategy.
|
||||
let mut env_map: HashMap<String, String> = 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("*SECRET*"),
|
||||
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<String, String> = 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 policy = ShellEnvironmentPolicy {
|
||||
// skip default excludes so nothing is removed prematurely
|
||||
ignore_default_excludes: true,
|
||||
include_only: vec![EnvironmentVariablePattern::new_case_insensitive("*PATH")],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = populate_env(vars, &policy);
|
||||
|
||||
let expected: HashMap<String, String> = 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 {
|
||||
ignore_default_excludes: true,
|
||||
..Default::default()
|
||||
};
|
||||
policy.r#set.insert("NEW_VAR".to_string(), "42".to_string());
|
||||
|
||||
let result = populate_env(vars, &policy);
|
||||
|
||||
let expected: HashMap<String, String> = 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 policy = ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::All,
|
||||
ignore_default_excludes: true, // keep everything
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = populate_env(vars.clone(), &policy);
|
||||
let expected: HashMap<String, String> = 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 policy = ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::All,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = populate_env(vars, &policy);
|
||||
let expected: HashMap<String, String> = 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 {
|
||||
inherit: ShellEnvironmentPolicyInherit::None,
|
||||
ignore_default_excludes: true,
|
||||
..Default::default()
|
||||
};
|
||||
policy
|
||||
.r#set
|
||||
.insert("ONLY_VAR".to_string(), "yes".to_string());
|
||||
|
||||
let result = populate_env(vars, &policy);
|
||||
let expected: HashMap<String, String> = hashmap! {
|
||||
"ONLY_VAR".to_string() => "yes".to_string(),
|
||||
};
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -147,6 +147,7 @@ mod tests {
|
||||
use crate::exec::SandboxType;
|
||||
use crate::exec::process_exec_tool_call;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::sync::Notify;
|
||||
@@ -157,6 +158,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: HashMap::new(),
|
||||
};
|
||||
|
||||
let sandbox_policy =
|
||||
@@ -236,9 +238,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: HashMap::new(),
|
||||
};
|
||||
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
|
||||
@@ -17,6 +17,7 @@ pub mod config_types;
|
||||
mod conversation_history;
|
||||
pub mod error;
|
||||
pub mod exec;
|
||||
pub mod exec_env;
|
||||
pub mod exec_linux;
|
||||
mod flags;
|
||||
mod is_safe_command;
|
||||
|
||||
Reference in New Issue
Block a user