Build remote exec env from exec-server policy (#17216)

## Summary
- add an exec-server `envPolicy` field; when present, the server starts
from its own process env and applies the shell environment policy there
- keep `env` as the exact environment for local/embedded starts, but
make it an overlay for remote unified-exec starts
- move the shell-environment-policy builder into `codex-config` so Core
and exec-server share the inherit/filter/set/include behavior
- overlay only runtime/sandbox/network deltas from Core onto the
exec-server-derived env

## Why
Remote unified exec was materializing the shell env inside Core and
forwarding the whole map to exec-server, so remote processes could
inherit the orchestrator machine's `HOME`, `PATH`, etc. This keeps the
base env on the executor while preserving Core-owned runtime additions
like `CODEX_THREAD_ID`, unified-exec defaults, network proxy env, and
sandbox marker env.

## Validation
- `just fmt`
- `git diff --check`
- `cargo test -p codex-exec-server --lib`
- `cargo test -p codex-core --lib unified_exec::process_manager::tests`
- `cargo test -p codex-core --lib exec_env::tests`
- `cargo test -p codex-core --lib exec_env_tests` (compile-only; filter
matched 0 tests)
- `cargo test -p codex-config --lib shell_environment` (compile-only;
filter matched 0 tests)
- `just bazel-lock-update`

## Known local validation issue
- `just bazel-lock-check` is not runnable in this checkout: it invokes
`./scripts/check-module-bazel-lock.sh`, which is missing.

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: pakrym-oai <pakrym@openai.com>
This commit is contained in:
jif-oai
2026-04-13 09:59:08 +01:00
committed by GitHub
parent 4ffe6c2ce6
commit bacb92b1d7
22 changed files with 455 additions and 125 deletions

View File

@@ -343,6 +343,7 @@ mod tests {
process_id: ProcessId::from("default-env-proc"),
argv: vec!["true".to_string()],
cwd: std::env::current_dir().expect("read current dir"),
env_policy: None,
env: Default::default(),
tty: false,
arg0: None,

View File

@@ -42,6 +42,7 @@ pub use process::ExecProcess;
pub use process::StartedExecProcess;
pub use process_id::ProcessId;
pub use protocol::ExecClosedNotification;
pub use protocol::ExecEnvPolicy;
pub use protocol::ExecExitedNotification;
pub use protocol::ExecOutputDeltaNotification;
pub use protocol::ExecOutputStream;

View File

@@ -5,6 +5,9 @@ use std::time::Duration;
use async_trait::async_trait;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_config::shell_environment;
use codex_config::types::EnvironmentVariablePattern;
use codex_config::types::ShellEnvironmentPolicy;
use codex_utils_pty::ExecCommandSession;
use codex_utils_pty::TerminalSize;
use tokio::sync::Mutex;
@@ -19,6 +22,7 @@ use crate::ProcessId;
use crate::StartedExecProcess;
use crate::protocol::EXEC_CLOSED_METHOD;
use crate::protocol::ExecClosedNotification;
use crate::protocol::ExecEnvPolicy;
use crate::protocol::ExecExitedNotification;
use crate::protocol::ExecOutputDeltaNotification;
use crate::protocol::ExecOutputStream;
@@ -150,12 +154,13 @@ impl LocalProcess {
process_map.insert(process_id.clone(), ProcessEntry::Starting);
}
let env = child_env(&params);
let spawned_result = if params.tty {
codex_utils_pty::spawn_pty_process(
program,
args,
params.cwd.as_path(),
&params.env,
&env,
&params.arg0,
TerminalSize::default(),
)
@@ -165,7 +170,7 @@ impl LocalProcess {
program,
args,
params.cwd.as_path(),
&params.env,
&env,
&params.arg0,
)
.await
@@ -375,6 +380,36 @@ impl LocalProcess {
}
}
fn child_env(params: &ExecParams) -> HashMap<String, String> {
let Some(env_policy) = &params.env_policy else {
return params.env.clone();
};
let policy = shell_environment_policy(env_policy);
let mut env = shell_environment::create_env(&policy, /*thread_id*/ None);
env.extend(params.env.clone());
env
}
fn shell_environment_policy(env_policy: &ExecEnvPolicy) -> ShellEnvironmentPolicy {
ShellEnvironmentPolicy {
inherit: env_policy.inherit.clone(),
ignore_default_excludes: env_policy.ignore_default_excludes,
exclude: env_policy
.exclude
.iter()
.map(|pattern| EnvironmentVariablePattern::new_case_insensitive(pattern))
.collect(),
r#set: env_policy.r#set.clone(),
include_only: env_policy
.include_only
.iter()
.map(|pattern| EnvironmentVariablePattern::new_case_insensitive(pattern))
.collect(),
use_profile: false,
}
}
#[async_trait]
impl ExecBackend for LocalProcess {
async fn start(&self, params: ExecParams) -> Result<StartedExecProcess, ExecServerError> {
@@ -618,3 +653,56 @@ fn notification_sender(inner: &Inner) -> Option<RpcNotificationSender> {
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
#[cfg(test)]
mod tests {
use super::*;
use codex_config::types::ShellEnvironmentPolicyInherit;
fn test_exec_params(env: HashMap<String, String>) -> ExecParams {
ExecParams {
process_id: ProcessId::from("env-test"),
argv: vec!["true".to_string()],
cwd: std::path::PathBuf::from("/tmp"),
env_policy: None,
env,
tty: false,
arg0: None,
}
}
#[test]
fn child_env_defaults_to_exact_env() {
let params = test_exec_params(HashMap::from([("ONLY_THIS".to_string(), "1".to_string())]));
assert_eq!(
child_env(&params),
HashMap::from([("ONLY_THIS".to_string(), "1".to_string())])
);
}
#[test]
fn child_env_applies_policy_then_overlay() {
let mut params = test_exec_params(HashMap::from([
("OVERLAY".to_string(), "overlay".to_string()),
("POLICY_SET".to_string(), "overlay-wins".to_string()),
]));
params.env_policy = Some(ExecEnvPolicy {
inherit: ShellEnvironmentPolicyInherit::None,
ignore_default_excludes: true,
exclude: Vec::new(),
r#set: HashMap::from([("POLICY_SET".to_string(), "policy".to_string())]),
include_only: Vec::new(),
});
let mut expected = HashMap::from([
("OVERLAY".to_string(), "overlay".to_string()),
("POLICY_SET".to_string(), "overlay-wins".to_string()),
]);
if cfg!(target_os = "windows") {
expected.insert("PATHEXT".to_string(), ".COM;.EXE;.BAT;.CMD".to_string());
}
assert_eq!(child_env(&params), expected);
}
}

View File

@@ -3,6 +3,7 @@ use std::path::PathBuf;
use crate::FileSystemSandboxContext;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_config::types::ShellEnvironmentPolicyInherit;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
@@ -64,11 +65,23 @@ pub struct ExecParams {
pub process_id: ProcessId,
pub argv: Vec<String>,
pub cwd: PathBuf,
#[serde(default)]
pub env_policy: Option<ExecEnvPolicy>,
pub env: HashMap<String, String>,
pub tty: bool,
pub arg0: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecEnvPolicy {
pub inherit: ShellEnvironmentPolicyInherit,
pub ignore_default_excludes: bool,
pub exclude: Vec<String>,
pub r#set: HashMap<String, String>,
pub include_only: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecResponse {

View File

@@ -27,6 +27,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams {
process_id: ProcessId::from(process_id),
argv,
cwd: std::env::current_dir().expect("cwd"),
env_policy: None,
env: inherited_path_env(),
tty: false,
arg0: None,

View File

@@ -390,6 +390,7 @@ mod tests {
process_id,
argv: sleep_then_print_argv(),
cwd: std::env::current_dir().expect("cwd"),
env_policy: None,
env,
tty: false,
arg0: None,