mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Honor per-environment shell variable policies (#38902)
## What changed - Carry `ShellEnvironmentPolicy` in each resolved `EnvironmentConfig` and use the selected turn environment's policy for shell commands, user shell tasks, and unified exec. - Infer the policy from the thread configuration when an environment does not provide its own resolved configuration. - Redact the policy from `EnvironmentConfig` debug output because it can contain explicit environment variable values. ## Testing - Verify shell handlers and unified exec filter inherited variables according to the selected environment while preserving its explicit overrides. GitOrigin-RevId: 9f6a52aa4af60e5f17251a0e1b11e1926b779055
This commit is contained in:
@@ -344,6 +344,7 @@ fn resolved_local_environments<const N: usize>(
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
PermissionProfile::read_only(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -786,6 +786,7 @@ mod tests {
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -957,6 +958,7 @@ url = "ws://127.0.0.1:8765"
|
||||
ActivePermissionProfile::read_only(),
|
||||
vec![cwd.join("profile-root")],
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
};
|
||||
let turn_environments = ThreadEnvironments::new(
|
||||
@@ -1158,6 +1160,7 @@ url = "ws://127.0.0.1:8765"
|
||||
ActivePermissionProfile::read_only(),
|
||||
vec![cwd.join("profile-root")],
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
};
|
||||
let cwd = PathUri::from_abs_path(&cwd);
|
||||
@@ -1491,6 +1494,7 @@ url = "ws://127.0.0.1:8765"
|
||||
ActivePermissionProfile::read_only(),
|
||||
vec![cwd.join("child-profile-root")],
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
};
|
||||
let environments = ThreadEnvironments::new(
|
||||
@@ -1534,6 +1538,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let parent_owner_config = EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: vec![root("parent-root")],
|
||||
};
|
||||
let mut owner_selection = selection.clone();
|
||||
|
||||
@@ -1120,6 +1120,7 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul
|
||||
.permissions
|
||||
.permission_profile_state()
|
||||
.snapshot(),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -109,7 +109,7 @@ impl Session {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.services.turn_environments.update_selections(
|
||||
&environments,
|
||||
&state.session_configuration.turn_environment_config(),
|
||||
&state.session_configuration.inferred_environment_config(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -695,6 +695,8 @@ impl Session {
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: session_permission_profile_state_from_config(&config)?,
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -1596,12 +1598,14 @@ impl Session {
|
||||
if mcp_inputs_changed {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
}
|
||||
let environment_config = updated.turn_environment_config();
|
||||
let environment_config = updated.inferred_environment_config();
|
||||
if let Some(environments) = &updates.environments {
|
||||
self.services
|
||||
.turn_environments
|
||||
.update_selections(&environments.environments, &environment_config);
|
||||
} else if state.session_configuration.turn_environment_config() != environment_config {
|
||||
} else if state.session_configuration.inferred_environment_config()
|
||||
!= environment_config
|
||||
{
|
||||
self.services
|
||||
.turn_environments
|
||||
.update_thread_config(&environment_config);
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_protocol::SessionId;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
@@ -90,6 +91,8 @@ pub(crate) struct SessionConfiguration {
|
||||
/// active profile id, and profile-defined workspace roots in sync by using
|
||||
/// the methods below instead of mutating the fields independently.
|
||||
pub(super) permission_profile_state: PermissionProfileState,
|
||||
pub(super) allow_login_shell: bool,
|
||||
pub(super) shell_environment_policy: ShellEnvironmentPolicy,
|
||||
pub(super) windows_sandbox_level: WindowsSandboxLevel,
|
||||
|
||||
/// Legacy thread cwd used when a turn does not select an environment.
|
||||
@@ -136,13 +139,11 @@ impl SessionConfiguration {
|
||||
&self.permission_profile_state
|
||||
}
|
||||
|
||||
pub(super) fn turn_environment_config(&self) -> EnvironmentConfig {
|
||||
pub(super) fn inferred_environment_config(&self) -> EnvironmentConfig {
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: self
|
||||
.original_config_do_not_use
|
||||
.permissions
|
||||
.allow_login_shell,
|
||||
allow_login_shell: self.allow_login_shell,
|
||||
permission_profile: self.permission_profile_state.snapshot(),
|
||||
shell_environment_policy: self.shell_environment_policy.clone(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1080,14 +1081,14 @@ impl Session {
|
||||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||||
environment_manager,
|
||||
default_shell.clone(),
|
||||
session_configuration.turn_environment_config(),
|
||||
session_configuration.inferred_environment_config(),
|
||||
shell_snapshot,
|
||||
inherited_environments.unwrap_or_default(),
|
||||
config.features.enabled(Feature::DeferredExecutor),
|
||||
));
|
||||
turn_environments.update_selections(
|
||||
environment_selections,
|
||||
&session_configuration.turn_environment_config(),
|
||||
&session_configuration.inferred_environment_config(),
|
||||
);
|
||||
let resolved_environments = turn_environments.snapshot().await;
|
||||
let agents_md_manager = Arc::new(AgentsMdManager::new(user_instructions));
|
||||
|
||||
@@ -4189,6 +4189,8 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -4298,6 +4300,8 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -4852,6 +4856,8 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -4963,14 +4969,14 @@ async fn resolved_environments_for_configuration(
|
||||
let turn_environments = ThreadEnvironments::new(
|
||||
Arc::clone(&environment_manager),
|
||||
default_user_shell(),
|
||||
session_configuration.turn_environment_config(),
|
||||
session_configuration.inferred_environment_config(),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
);
|
||||
turn_environments.update_selections(
|
||||
environment_selections,
|
||||
&session_configuration.turn_environment_config(),
|
||||
&session_configuration.inferred_environment_config(),
|
||||
);
|
||||
(environment_manager, turn_environments.snapshot().await)
|
||||
}
|
||||
@@ -5641,6 +5647,8 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -5787,6 +5795,8 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -5820,7 +5830,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||||
environment_manager,
|
||||
default_user_shell(),
|
||||
session_configuration.turn_environment_config(),
|
||||
session_configuration.inferred_environment_config(),
|
||||
ShellSnapshot::disabled(),
|
||||
resolved_environments,
|
||||
/*non_blocking_snapshots*/ false,
|
||||
@@ -6068,6 +6078,8 @@ async fn make_session_with_config_and_rx(
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -6187,6 +6199,8 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -7987,6 +8001,8 @@ where
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
@@ -8019,7 +8035,7 @@ where
|
||||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||||
environment_manager,
|
||||
default_user_shell(),
|
||||
session_configuration.turn_environment_config(),
|
||||
session_configuration.inferred_environment_config(),
|
||||
ShellSnapshot::disabled(),
|
||||
resolved_turn_environments.clone(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
@@ -66,6 +67,10 @@ impl TurnEnvironment {
|
||||
config
|
||||
}
|
||||
|
||||
pub(crate) fn shell_environment_policy(&self) -> &ShellEnvironmentPolicy {
|
||||
&self.config().shell_environment_policy
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn config_mut(&mut self) -> &mut EnvironmentConfig {
|
||||
let EnvironmentConfigState::Ready(config) = &mut self.selection.config else {
|
||||
@@ -712,12 +717,12 @@ impl Session {
|
||||
previous_permission_profile != next_permission_profile;
|
||||
let previous_config = notify_config_contributors
|
||||
.then(|| self.build_effective_session_config(&state.session_configuration));
|
||||
let environment_config = next.turn_environment_config();
|
||||
let environment_config = next.inferred_environment_config();
|
||||
if let Some(environments) = &updates.environments {
|
||||
self.services
|
||||
.turn_environments
|
||||
.update_selections(&environments.environments, &environment_config);
|
||||
} else if state.session_configuration.turn_environment_config()
|
||||
} else if state.session_configuration.inferred_environment_config()
|
||||
!= environment_config
|
||||
{
|
||||
self.services
|
||||
|
||||
@@ -158,10 +158,8 @@ pub(crate) async fn execute_user_shell_command(
|
||||
return;
|
||||
};
|
||||
let shell_snapshot_location = turn_environment.shell_snapshot(&cwd);
|
||||
let mut exec_env_map = create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
);
|
||||
let shell_environment_policy = turn_environment.shell_environment_policy();
|
||||
let mut exec_env_map = create_env(shell_environment_policy, Some(session.thread_id));
|
||||
inject_session_id_env(&mut exec_env_map, session.session_id());
|
||||
inject_apply_patch_env(&mut exec_env_map, &turn_context.config.features);
|
||||
if exec_env_map.contains_key(PROXY_ACTIVE_ENV_KEY) {
|
||||
@@ -171,11 +169,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
&display_command,
|
||||
environment_shell,
|
||||
shell_snapshot_location.as_ref(),
|
||||
&turn_context
|
||||
.config
|
||||
.permissions
|
||||
.shell_environment_policy
|
||||
.r#set,
|
||||
&shell_environment_policy.r#set,
|
||||
&mut exec_env_map,
|
||||
);
|
||||
|
||||
|
||||
@@ -81,12 +81,7 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
|
||||
let mut explicit_env_overrides = turn
|
||||
.config
|
||||
.permissions
|
||||
.shell_environment_policy
|
||||
.r#set
|
||||
.clone();
|
||||
let mut explicit_env_overrides = turn_environment.shell_environment_policy().r#set.clone();
|
||||
let mut env = exec_params.env.clone();
|
||||
strip_output_env(&mut env);
|
||||
strip_output_env(&mut explicit_env_overrides);
|
||||
|
||||
@@ -108,7 +108,7 @@ impl ShellCommandHandler {
|
||||
let command = Self::base_command(shell, ¶ms.command, use_login_shell);
|
||||
|
||||
let mut env = create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
turn_environment.shell_environment_policy(),
|
||||
Some(session.thread_id),
|
||||
);
|
||||
inject_session_id_env(&mut env, session.session_id());
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::config_types::EnvironmentVariablePattern;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ShellCommandToolCallParams;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -82,7 +86,14 @@ fn assert_safe(shell: &Shell, command: &str) {
|
||||
async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
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)
|
||||
let config = Arc::make_mut(&mut turn_context.config);
|
||||
config.permissions.shell_environment_policy.r#set = HashMap::from([
|
||||
("KEEP".to_string(), "from-thread".to_string()),
|
||||
("DROP".to_string(), "from-thread".to_string()),
|
||||
]);
|
||||
config.permissions.shell_environment_policy.include_only =
|
||||
vec![EnvironmentVariablePattern::new_case_insensitive("DROP")];
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
||||
permission_profile.clone(),
|
||||
@@ -105,6 +116,17 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
let selected_cwd = turn_context.config.cwd.join("selected-environment");
|
||||
let expected_cwd = selected_cwd.join("subdir");
|
||||
let active_permission_profile = ActivePermissionProfile::new("selected-profile");
|
||||
let selected_shell_environment_policy = ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::None,
|
||||
include_only: vec![EnvironmentVariablePattern::new_case_insensitive("KEEP")],
|
||||
r#set: turn_context
|
||||
.config
|
||||
.permissions
|
||||
.shell_environment_policy
|
||||
.r#set
|
||||
.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let selected_environment = TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: "selected-environment".to_string(),
|
||||
@@ -116,6 +138,7 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
permission_profile,
|
||||
active_permission_profile.clone(),
|
||||
),
|
||||
shell_environment_policy: selected_shell_environment_policy.clone(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
},
|
||||
@@ -129,10 +152,7 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
),
|
||||
Some(selected_shell),
|
||||
);
|
||||
let mut expected_env = create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
);
|
||||
let mut expected_env = create_env(&selected_shell_environment_policy, Some(session.thread_id));
|
||||
inject_session_id_env(&mut expected_env, session.session_id());
|
||||
inject_permission_profile_env(&mut expected_env, Some(&active_permission_profile));
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
PermissionProfile::read_only(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ async fn approval_key_uses_path_uri_and_includes_environment_id() {
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
PermissionProfile::read_only(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -542,6 +542,7 @@ mod tests {
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
PermissionProfile::read_only(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -916,6 +916,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
|
||||
.permissions
|
||||
.permission_profile_state()
|
||||
.snapshot(),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1217,10 +1217,8 @@ impl UnifiedExecProcessManager {
|
||||
context: &UnifiedExecContext,
|
||||
) -> Result<(UnifiedExecAttempt, Option<DeferredNetworkApproval>), UnifiedExecError> {
|
||||
let turn = &context.step_context.turn;
|
||||
let local_policy_env = create_env(
|
||||
&turn.config.permissions.shell_environment_policy,
|
||||
/*thread_id*/ None,
|
||||
);
|
||||
let shell_environment_policy = request.turn_environment.shell_environment_policy();
|
||||
let local_policy_env = create_env(shell_environment_policy, /*thread_id*/ None);
|
||||
let mut env = local_policy_env.clone();
|
||||
env.insert(
|
||||
CODEX_THREAD_ID_ENV_VAR.to_string(),
|
||||
@@ -1232,17 +1230,10 @@ impl UnifiedExecProcessManager {
|
||||
inject_permission_profile_env(&mut env, active_permission_profile.as_ref());
|
||||
let mut env = apply_unified_exec_env(env);
|
||||
strip_output_env(&mut env);
|
||||
let mut explicit_env_overrides = turn
|
||||
.config
|
||||
.permissions
|
||||
.shell_environment_policy
|
||||
.r#set
|
||||
.clone();
|
||||
let mut explicit_env_overrides = shell_environment_policy.r#set.clone();
|
||||
strip_output_env(&mut explicit_env_overrides);
|
||||
let exec_server_env_config = ExecServerEnvConfig {
|
||||
policy: exec_env_policy_from_shell_policy(
|
||||
&turn.config.permissions.shell_environment_policy,
|
||||
),
|
||||
policy: exec_env_policy_from_shell_policy(shell_environment_policy),
|
||||
local_policy_env,
|
||||
};
|
||||
let mut orchestrator = ToolOrchestrator::new();
|
||||
|
||||
@@ -597,6 +597,7 @@ async fn guardian_session_is_reused_for_consecutive_tool_reviews_without_prewarm
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
test.config.permissions.permission_profile().clone(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
};
|
||||
parent_environments
|
||||
|
||||
@@ -482,6 +482,7 @@ async fn root_reconciliation_reuses_pending_apps_startup() -> Result<()> {
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
test.config.permissions.permission_profile().clone(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: vec![SelectedCapabilityRoot {
|
||||
id: "calendar-root".to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
|
||||
@@ -550,6 +550,7 @@ async fn environment_permissions_follow_configuration_ownership() -> Result<()>
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
PermissionProfile::read_only(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
..selection
|
||||
@@ -1171,6 +1172,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
||||
EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
permission_profile: permission_profile.clone(),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: vec![root("duplicate"), root("duplicate")],
|
||||
}),
|
||||
] {
|
||||
@@ -1205,6 +1207,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
permission_profile: permission_profile.clone(),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: vec![root("startup-root"), root("second-root")],
|
||||
}),
|
||||
..selection.clone()
|
||||
@@ -1223,6 +1226,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
permission_profile: permission_profile.clone(),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: vec![root("first-root")],
|
||||
}),
|
||||
..selection.clone()
|
||||
@@ -1280,6 +1284,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
permission_profile: permission_profile.clone(),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: vec![root("first-updated-root")],
|
||||
}),
|
||||
..selection.clone()
|
||||
@@ -1375,6 +1380,7 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes()
|
||||
let owner_config = |id: &str, allow_login_shell: bool| EnvironmentConfig {
|
||||
allow_login_shell,
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: vec![root(id)],
|
||||
};
|
||||
let start_pending_thread = || {
|
||||
@@ -1690,6 +1696,7 @@ async fn ready_before_selection_exposes_remote_tools_and_capability_context_afte
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
test.config.permissions.permission_profile().clone(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
selected_capability_roots: vec![ready_root],
|
||||
}),
|
||||
}]),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_core::EnvironmentConfig;
|
||||
use codex_core::TurnInputRequest;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
use std::collections::HashMap;
|
||||
@@ -10,9 +11,13 @@ use anyhow::Result;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::EnvironmentVariablePattern;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::PermissionProfileSnapshot;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -296,6 +301,106 @@ async fn exec_command_hides_and_rejects_login_when_disabled() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_command_uses_installed_environment_shell_policy_with_explicit_overrides() -> Result<()>
|
||||
{
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
"basic PowerShell execution through Wine exec is not passing yet"
|
||||
);
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let builder = test_codex().with_model("gpt-5.4").with_config(|config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow feature update");
|
||||
config.permissions.shell_environment_policy = ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::None,
|
||||
include_only: vec![EnvironmentVariablePattern::new_case_insensitive("DROP")],
|
||||
r#set: HashMap::from([
|
||||
("KEEP".to_string(), "preserved".to_string()),
|
||||
("DROP".to_string(), "filtered".to_string()),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
});
|
||||
let harness = TestCodexHarness::with_auto_env_builder(builder).await?;
|
||||
let selection = harness
|
||||
.test()
|
||||
.codex
|
||||
.environment_selections()
|
||||
.await
|
||||
.into_iter()
|
||||
.next()
|
||||
.context("thread should select its executor environment")?;
|
||||
harness
|
||||
.test()
|
||||
.codex
|
||||
.environment_ready(
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
|
||||
shell_environment_policy: ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::None,
|
||||
include_only: vec![EnvironmentVariablePattern::new_case_insensitive("KEEP")],
|
||||
r#set: harness
|
||||
.test()
|
||||
.config
|
||||
.permissions
|
||||
.shell_environment_policy
|
||||
.r#set
|
||||
.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
selected_capability_roots: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let call_id = "exec-command-environment-shell-policy";
|
||||
let command = match core_test_support::test_target_os() {
|
||||
core_test_support::TestTargetOs::Linux | core_test_support::TestTargetOs::MacOs => {
|
||||
r#"printf '%s:%s:%s' "$KEEP" "${DROP:-missing}" "${OWNER_ONLY:-missing}""#
|
||||
}
|
||||
core_test_support::TestTargetOs::Windows => {
|
||||
r#"if (Test-Path Env:DROP) { $drop = $env:DROP } else { $drop = 'missing' }; if (Test-Path Env:OWNER_ONLY) { $owner = $env:OWNER_ONLY } else { $owner = 'missing' }; Write-Output "${env:KEEP}:${drop}:${owner}""#
|
||||
}
|
||||
};
|
||||
let arguments = json!({
|
||||
"cmd": command,
|
||||
"login": false,
|
||||
"yield_time_ms": 5_000,
|
||||
});
|
||||
mount_sse_sequence(
|
||||
harness.server(),
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "exec_command", &serde_json::to_string(&arguments)?),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
harness
|
||||
.submit_with_permission_profile(
|
||||
"inspect the shell environment",
|
||||
PermissionProfile::Disabled,
|
||||
)
|
||||
.await?;
|
||||
let output = parse_unified_exec_output(&harness.function_call_stdout(call_id).await)?;
|
||||
assert_eq!(output.output.trim(), "preserved:missing:missing");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::capabilities::SelectedCapabilityRoot;
|
||||
use crate::config_types::ShellEnvironmentPolicy;
|
||||
use crate::models::PermissionProfileSnapshot;
|
||||
|
||||
/// Configuration supplied for a thread's selected environment.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum EnvironmentConfigState {
|
||||
/// Preserve the existing thread-derived environment configuration.
|
||||
FromThread,
|
||||
@@ -15,12 +17,26 @@ pub enum EnvironmentConfigState {
|
||||
}
|
||||
|
||||
/// Resolved configuration for a thread/environment attachment.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct EnvironmentConfig {
|
||||
/// Whether shell tools may start login shells in this environment.
|
||||
pub allow_login_shell: bool,
|
||||
/// Resolved permissions for this thread's environment attachment.
|
||||
pub permission_profile: PermissionProfileSnapshot,
|
||||
/// Controls which environment variables shell commands may inherit.
|
||||
pub shell_environment_policy: ShellEnvironmentPolicy,
|
||||
/// Capability roots selected for this thread's environment attachment.
|
||||
pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EnvironmentConfig {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter
|
||||
.debug_struct("EnvironmentConfig")
|
||||
.field("allow_login_shell", &self.allow_login_shell)
|
||||
.field("permission_profile", &self.permission_profile)
|
||||
.field("shell_environment_policy", &"<redacted>")
|
||||
.field("selected_capability_roots", &self.selected_capability_roots)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user