diff --git a/codex-rs/core/src/exec_env.rs b/codex-rs/core/src/exec_env.rs index 938667b12e..f337d61620 100644 --- a/codex-rs/core/src/exec_env.rs +++ b/codex-rs/core/src/exec_env.rs @@ -2,11 +2,16 @@ use codex_protocol::ThreadId; #[cfg(test)] use codex_protocol::config_types::EnvironmentVariablePattern; use codex_protocol::config_types::ShellEnvironmentPolicy; +use codex_protocol::models::ActivePermissionProfile; use codex_protocol::shell_environment; use std::collections::HashMap; pub use codex_protocol::shell_environment::CODEX_THREAD_ID_ENV_VAR; +/// Informational name of the active permission profile. Child processes can +/// overwrite this value, so it must not be treated as proof of enforcement. +pub const CODEX_PERMISSION_PROFILE_ENV_VAR: &str = "CODEX_PERMISSION_PROFILE"; + /// 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 @@ -25,6 +30,24 @@ pub fn create_env( shell_environment::create_env(policy, thread_id.as_deref()) } +/// Injects the selected named permission profile into a shell tool's environment. +/// +/// This is applied after the shell environment policy so the runtime-selected +/// profile wins over inherited or configured values. +pub(crate) fn inject_permission_profile_env( + env: &mut HashMap, + active_permission_profile: Option<&ActivePermissionProfile>, +) { + if let Some(active_permission_profile) = active_permission_profile { + env.insert( + CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(), + active_permission_profile.id.clone(), + ); + } else { + env.remove(CODEX_PERMISSION_PROFILE_ENV_VAR); + } +} + #[cfg(all(test, target_os = "windows"))] fn create_env_from_vars( vars: I, diff --git a/codex-rs/core/src/exec_env_tests.rs b/codex-rs/core/src/exec_env_tests.rs index 725edd8cc5..d7605ac8df 100644 --- a/codex-rs/core/src/exec_env_tests.rs +++ b/codex-rs/core/src/exec_env_tests.rs @@ -10,6 +10,37 @@ fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> { .collect() } +#[test] +fn inject_permission_profile_env_overrides_policy_value() { + let mut env = HashMap::from([( + CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(), + "stale-profile".to_string(), + )]); + + inject_permission_profile_env( + &mut env, + Some(&ActivePermissionProfile::new("current-profile")), + ); + + assert_eq!( + env.get(CODEX_PERMISSION_PROFILE_ENV_VAR) + .map(String::as_str), + Some("current-profile") + ); +} + +#[test] +fn inject_permission_profile_env_removes_stale_value_without_active_profile() { + let mut env = HashMap::from([( + CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(), + "stale-profile".to_string(), + )]); + + inject_permission_profile_env(&mut env, None); + + assert_eq!(env.get(CODEX_PERMISSION_PROFILE_ENV_VAR), None); +} + #[test] fn test_core_inherit_defaults_keep_sensitive_vars() { let vars = make_vars(&[ diff --git a/codex-rs/core/src/tools/handlers/shell/shell_command.rs b/codex-rs/core/src/tools/handlers/shell/shell_command.rs index 1faa89b31f..62bcee8bdf 100644 --- a/codex-rs/core/src/tools/handlers/shell/shell_command.rs +++ b/codex-rs/core/src/tools/handlers/shell/shell_command.rs @@ -6,6 +6,7 @@ use codex_utils_absolute_path::AbsolutePathBuf; use crate::exec::ExecCapturePolicy; use crate::exec::ExecParams; use crate::exec_env::create_env; +use crate::exec_env::inject_permission_profile_env; use crate::function_tool::FunctionCallError; use crate::maybe_emit_implicit_skill_invocation; use crate::session::turn_context::TurnContext; @@ -99,15 +100,19 @@ impl ShellCommandHandler { let use_login_shell = Self::resolve_use_login_shell(params.login, allow_login_shell)?; let command = Self::base_command(shell, ¶ms.command, use_login_shell); + let mut env = create_env( + &turn_context.config.permissions.shell_environment_policy, + Some(session.thread_id), + ); + let active_permission_profile = turn_context.config.permissions.active_permission_profile(); + inject_permission_profile_env(&mut env, active_permission_profile.as_ref()); + Ok(ExecParams { command, cwd, expiration: params.timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, - env: create_env( - &turn_context.config.permissions.shell_environment_policy, - Some(session.thread_id), - ), + env, network: turn_context.network.clone(), network_environment_id: Some(turn_environment.environment_id.clone()), sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), diff --git a/codex-rs/core/src/tools/handlers/shell_tests.rs b/codex-rs/core/src/tools/handlers/shell_tests.rs index ded7733022..ebfbf9127c 100644 --- a/codex-rs/core/src/tools/handlers/shell_tests.rs +++ b/codex-rs/core/src/tools/handlers/shell_tests.rs @@ -1,10 +1,14 @@ use std::path::PathBuf; use std::sync::Arc; +use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ShellCommandToolCallParams; use pretty_assertions::assert_eq; +use crate::config::PermissionProfileSnapshot; +use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR; use crate::exec_env::create_env; +use crate::exec_env::inject_permission_profile_env; use crate::sandboxing::SandboxPermissions; use crate::session::step_context::StepContext; use crate::session::tests::make_session_and_context; @@ -71,7 +75,15 @@ fn assert_safe(shell: &Shell, command: &str) { #[tokio::test] async fn shell_command_handler_to_exec_params_uses_selected_environment() { - let (session, turn_context) = make_session_and_context().await; + let (session, mut turn_context) = make_session_and_context().await; + let permission_profile = turn_context.config.permissions.permission_profile().clone(); + Arc::make_mut(&mut turn_context.config) + .permissions + .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active( + permission_profile, + ActivePermissionProfile::new("test-profile"), + )) + .expect("set active permission profile"); let command = "echo hello".to_string(); let workdir = Some("subdir".to_string()); @@ -99,10 +111,12 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() { PathUri::from_abs_path(&selected_cwd), Some(selected_shell), ); - let expected_env = create_env( + let mut expected_env = create_env( &turn_context.config.permissions.shell_environment_policy, Some(session.thread_id), ); + let active_permission_profile = turn_context.config.permissions.active_permission_profile(); + inject_permission_profile_env(&mut expected_env, active_permission_profile.as_ref()); let params = ShellCommandToolCallParams { command, @@ -129,6 +143,12 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() { assert_eq!(exec_params.command, expected_command); assert_eq!(exec_params.cwd, expected_cwd); assert_eq!(exec_params.env, expected_env); + assert_eq!( + exec_params.env.get(CODEX_PERMISSION_PROFILE_ENV_VAR), + active_permission_profile + .as_ref() + .map(|profile| &profile.id) + ); assert_eq!(exec_params.network, turn_context.network); assert_eq!( exec_params.network_environment_id.as_deref(), diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index db55cc2bbc..729a9c55b1 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -4,6 +4,7 @@ Module: runtimes Concrete ToolRuntime implementations for specific tools. Each runtime stays small and focused and reuses the orchestrator for approvals + sandbox + retry. */ +use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR; use crate::exec_env::CODEX_THREAD_ID_ENV_VAR; use crate::sandboxing::SandboxPermissions; use crate::shell::Shell; @@ -287,8 +288,10 @@ pub(crate) fn maybe_wrap_shell_lc_with_snapshot( .map(|arg| format!(" '{}'", shell_single_quote(arg))) .collect::(); let mut override_env = explicit_env_overrides.clone(); - if let Some(thread_id) = env.get(CODEX_THREAD_ID_ENV_VAR) { - override_env.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.clone()); + for key in [CODEX_THREAD_ID_ENV_VAR, CODEX_PERMISSION_PROFILE_ENV_VAR] { + if let Some(value) = env.get(key) { + override_env.insert(key.to_string(), value.clone()); + } } let (override_captures, override_exports) = build_override_exports(&override_env); let (proxy_captures, proxy_exports) = build_proxy_env_exports(); diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index 9b485dbe58..7384551d91 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -534,6 +534,44 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_codex_thread_id_from_env() { assert_eq!(String::from_utf8_lossy(&output.stdout), "nested-thread"); } +#[test] +fn maybe_wrap_shell_lc_with_snapshot_restores_permission_profile_from_env() { + let dir = tempdir().expect("create temp dir"); + let snapshot_path = dir.path().join("snapshot.sh"); + std::fs::write( + &snapshot_path, + "# Snapshot file\nexport CODEX_PERMISSION_PROFILE='parent-profile'\n", + ) + .expect("write snapshot"); + let (session_shell, shell_snapshot) = + shell_with_snapshot(ShellType::Bash, "/bin/bash", snapshot_path.abs()); + let command = vec![ + "/bin/bash".to_string(), + "-lc".to_string(), + "printenv CODEX_PERMISSION_PROFILE".to_string(), + ]; + let env = HashMap::from([( + CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(), + "current-profile".to_string(), + )]); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + Some(&shell_snapshot), + &HashMap::new(), + &env, + &RuntimePathPrepends::default(), + ); + let output = Command::new(&rewritten[0]) + .args(&rewritten[1..]) + .env(CODEX_PERMISSION_PROFILE_ENV_VAR, "current-profile") + .output() + .expect("run rewritten command"); + + assert!(output.status.success(), "command failed: {output:?}"); + assert_eq!(String::from_utf8_lossy(&output.stdout), "current-profile\n"); +} + #[test] fn maybe_wrap_shell_lc_with_snapshot_restores_proxy_env_from_process_env() { let dir = tempdir().expect("create temp dir"); diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 5e89445765..9910dc913c 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -15,6 +15,7 @@ use uuid::Uuid; use crate::codex_thread::BackgroundTerminalInfo; use crate::exec_env::CODEX_THREAD_ID_ENV_VAR; use crate::exec_env::create_env; +use crate::exec_env::inject_permission_profile_env; use crate::exec_policy::ExecApprovalRequest; use crate::sandboxing::ExecOptions; use crate::sandboxing::ExecRequest; @@ -1110,6 +1111,8 @@ impl UnifiedExecProcessManager { CODEX_THREAD_ID_ENV_VAR.to_string(), context.session.thread_id.to_string(), ); + let active_permission_profile = context.turn.config.permissions.active_permission_profile(); + inject_permission_profile_env(&mut env, active_permission_profile.as_ref()); let env = apply_unified_exec_env(env); let exec_server_env_config = ExecServerEnvConfig { policy: exec_env_policy_from_shell_policy(