mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
## What changed - Add `NODE_REPL_AUTH_TOKEN` to the environment variables that model-reachable child processes cannot inherit. - Remove the variable case-insensitively after shell environment policy overrides and from explicitly configured child command environments. - Extend the environment scrubber tests to cover inherited and configured forms of the token. GitOrigin-RevId: 716c65e0a9e222f705c226b7c86545251d3ea580
303 lines
9.7 KiB
Rust
303 lines
9.7 KiB
Rust
use crate::config_types::EnvironmentVariablePattern;
|
|
use crate::config_types::ShellEnvironmentPolicy;
|
|
use crate::config_types::ShellEnvironmentPolicyInherit;
|
|
use std::collections::HashMap;
|
|
|
|
pub const CODEX_SESSION_ID_ENV_VAR: &str = "CODEX_SESSION_ID";
|
|
pub const CODEX_THREAD_ID_ENV_VAR: &str = "CODEX_THREAD_ID";
|
|
pub const CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR: &str = "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN";
|
|
pub const OPENAI_FEDERATION_RULE_ID_ENV_VAR: &str = "OPENAI_FEDERATION_RULE_ID";
|
|
pub const OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR: &str = "OPENAI_IDENTITY_TOKEN_FILE";
|
|
pub const OPENAI_WORKLOAD_IDENTITY_CONTEXT_ENV_VAR: &str = "OPENAI_WORKLOAD_IDENTITY_CONTEXT";
|
|
|
|
/// Environment variables that model-reachable child processes must not inherit.
|
|
pub const NON_INHERITABLE_ENV_VARS: &[&str] = &[
|
|
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR,
|
|
"NODE_REPL_AUTH_TOKEN",
|
|
OPENAI_FEDERATION_RULE_ID_ENV_VAR,
|
|
OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR,
|
|
OPENAI_WORKLOAD_IDENTITY_CONTEXT_ENV_VAR,
|
|
];
|
|
|
|
pub fn is_non_inheritable_env_var(name: &str) -> bool {
|
|
NON_INHERITABLE_ENV_VARS
|
|
.iter()
|
|
.any(|restricted| restricted.eq_ignore_ascii_case(name))
|
|
}
|
|
|
|
/// Configures a child command to omit non-inheritable variables from the
|
|
/// process environment and explicit command overrides.
|
|
///
|
|
/// This prevents accidental propagation of Codex launch context; it is not a
|
|
/// filesystem security boundary for the referenced identity-token file.
|
|
pub fn scrub_non_inheritable_env_vars(command: &mut std::process::Command) {
|
|
let configured_names = command
|
|
.get_envs()
|
|
.map(|(name, _)| name.to_os_string())
|
|
.collect::<Vec<_>>();
|
|
|
|
for name in NON_INHERITABLE_ENV_VARS {
|
|
command.env_remove(name);
|
|
}
|
|
for name in std::env::vars_os()
|
|
.map(|(name, _)| name)
|
|
.chain(configured_names)
|
|
{
|
|
if name.to_str().is_some_and(is_non_inheritable_env_var) {
|
|
command.env_remove(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Construct a shell environment from the supplied process environment and
|
|
/// shell-environment policy.
|
|
pub fn create_env(
|
|
policy: &ShellEnvironmentPolicy,
|
|
thread_id: Option<&str>,
|
|
) -> HashMap<String, String> {
|
|
create_env_from_vars(std::env::vars(), policy, thread_id)
|
|
}
|
|
|
|
pub fn create_env_from_vars<I>(
|
|
vars: I,
|
|
policy: &ShellEnvironmentPolicy,
|
|
thread_id: Option<&str>,
|
|
) -> HashMap<String, String>
|
|
where
|
|
I: IntoIterator<Item = (String, String)>,
|
|
{
|
|
let mut env_map = populate_env(vars, policy, thread_id);
|
|
|
|
if cfg!(target_os = "windows") {
|
|
// This is a workaround to address the failures we are seeing in the
|
|
// following tests when run via Bazel on Windows:
|
|
//
|
|
// ```
|
|
// suite::shell_command::unicode_output::with_login
|
|
// suite::shell_command::unicode_output::without_login
|
|
// ```
|
|
//
|
|
// Currently, we can only reproduce these failures in CI, which makes
|
|
// iteration times long, so we include this quick fix for now to unblock
|
|
// getting the Windows Bazel build running.
|
|
if !env_map.keys().any(|k| k.eq_ignore_ascii_case("PATHEXT")) {
|
|
env_map.insert("PATHEXT".to_string(), ".COM;.EXE;.BAT;.CMD".to_string());
|
|
}
|
|
}
|
|
env_map
|
|
}
|
|
|
|
pub fn populate_env<I>(
|
|
vars: I,
|
|
policy: &ShellEnvironmentPolicy,
|
|
thread_id: Option<&str>,
|
|
) -> 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 => {
|
|
#[cfg(not(target_os = "windows"))]
|
|
let core_env_vars = UNIX_CORE_ENV_VARS;
|
|
#[cfg(target_os = "windows")]
|
|
let core_env_vars = WINDOWS_CORE_ENV_VARS;
|
|
|
|
vars.into_iter()
|
|
.filter(|(k, _)| {
|
|
core_env_vars
|
|
.iter()
|
|
.any(|allowed| allowed.eq_ignore_ascii_case(k))
|
|
})
|
|
.collect()
|
|
}
|
|
};
|
|
|
|
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));
|
|
}
|
|
|
|
// Step 6 - Populate the thread ID environment variable when provided.
|
|
if let Some(thread_id) = thread_id {
|
|
env_map.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.to_string());
|
|
}
|
|
|
|
// Restricted launch context cannot be restored through user-provided shell
|
|
// environment overrides.
|
|
env_map.retain(|name, _| !is_non_inheritable_env_var(name));
|
|
|
|
env_map
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
const UNIX_CORE_ENV_VARS: &[&str] = &[
|
|
"PATH", "SHELL", "TMPDIR", "TEMP", "TMP", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME",
|
|
"USER",
|
|
];
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub const WINDOWS_CORE_ENV_VARS: &[&str] = &[
|
|
// Core path resolution
|
|
"PATH",
|
|
"PATHEXT",
|
|
// Shell and system roots
|
|
"SHELL",
|
|
"COMSPEC",
|
|
"SYSTEMROOT",
|
|
"SYSTEMDRIVE",
|
|
// User context and profiles
|
|
"USERNAME",
|
|
"USERDOMAIN",
|
|
"USERPROFILE",
|
|
"HOMEDRIVE",
|
|
"HOMEPATH",
|
|
// Program locations
|
|
"PROGRAMFILES",
|
|
"PROGRAMFILES(X86)",
|
|
"PROGRAMW6432",
|
|
"PROGRAMDATA",
|
|
// App data and caches
|
|
"LOCALAPPDATA",
|
|
"APPDATA",
|
|
// Temp locations
|
|
"TEMP",
|
|
"TMP",
|
|
"TMPDIR",
|
|
// Common shells/pwsh hints
|
|
"POWERSHELL",
|
|
"PWSH",
|
|
];
|
|
|
|
#[cfg(test)]
|
|
#[path = "shell_environment_tests.rs"]
|
|
mod tests;
|
|
|
|
#[cfg(all(test, target_os = "windows"))]
|
|
mod windows_tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
|
|
pairs
|
|
.iter()
|
|
.map(|(key, value)| (key.to_string(), value.to_string()))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "windows")]
|
|
fn core_inherit_preserves_windows_startup_vars_case_insensitively() {
|
|
let vars = make_vars(&[
|
|
("Shell", "C:\\Program Files\\Git\\bin\\bash.exe"),
|
|
("SystemRoot", "C:\\Windows"),
|
|
("AppData", "C:\\Users\\codex\\AppData\\Roaming"),
|
|
("TmpDir", "C:\\Temp\\custom"),
|
|
("OPENAI_API_KEY", "secret"),
|
|
]);
|
|
|
|
let policy = ShellEnvironmentPolicy {
|
|
inherit: ShellEnvironmentPolicyInherit::Core,
|
|
ignore_default_excludes: true,
|
|
..Default::default()
|
|
};
|
|
|
|
// Check a few sample vars instead of the full Windows core list.
|
|
let result = populate_env(vars, &policy, /*thread_id*/ None);
|
|
let expected = HashMap::from([
|
|
(
|
|
"Shell".to_string(),
|
|
"C:\\Program Files\\Git\\bin\\bash.exe".to_string(),
|
|
),
|
|
("SystemRoot".to_string(), "C:\\Windows".to_string()),
|
|
(
|
|
"AppData".to_string(),
|
|
"C:\\Users\\codex\\AppData\\Roaming".to_string(),
|
|
),
|
|
("TmpDir".to_string(), "C:\\Temp\\custom".to_string()),
|
|
]);
|
|
|
|
assert_eq!(result, expected);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "windows")]
|
|
fn create_env_inserts_pathext_on_windows_when_missing() {
|
|
let policy = ShellEnvironmentPolicy {
|
|
inherit: ShellEnvironmentPolicyInherit::None,
|
|
ignore_default_excludes: true,
|
|
..Default::default()
|
|
};
|
|
|
|
let result = create_env_from_vars(Vec::new(), &policy, /*thread_id*/ None);
|
|
let expected = HashMap::from([("PATHEXT".to_string(), ".COM;.EXE;.BAT;.CMD".to_string())]);
|
|
|
|
assert_eq!(result, expected);
|
|
}
|
|
}
|
|
|
|
#[cfg(all(test, not(target_os = "windows")))]
|
|
mod non_windows_tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
|
|
pairs
|
|
.iter()
|
|
.map(|(key, value)| (key.to_string(), value.to_string()))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn core_inherit_preserves_non_windows_core_vars_case_insensitively() {
|
|
let vars = make_vars(&[
|
|
("path", "/usr/bin"),
|
|
("home", "/home/codex"),
|
|
("TmpDir", "/tmp/custom"),
|
|
("OPENAI_API_KEY", "secret"),
|
|
]);
|
|
|
|
let policy = ShellEnvironmentPolicy {
|
|
inherit: ShellEnvironmentPolicyInherit::Core,
|
|
ignore_default_excludes: true,
|
|
..Default::default()
|
|
};
|
|
|
|
let result = populate_env(vars, &policy, /*thread_id*/ None);
|
|
let expected = HashMap::from([
|
|
("path".to_string(), "/usr/bin".to_string()),
|
|
("home".to_string(), "/home/codex".to_string()),
|
|
("TmpDir".to_string(), "/tmp/custom".to_string()),
|
|
]);
|
|
|
|
assert_eq!(result, expected);
|
|
}
|
|
}
|