Enforce environment-specific command policies (#38942)

## What changed

- Add an optional restrictive execution policy to `EnvironmentConfig` and merge it over the active command policy for shell execution and approval decisions.
- Reject environment policies that contain allow rules, so environment configuration can only tighten command access.
- Include the environment policy fingerprint in cached approval keys so policy changes require fresh session approvals.

## Testing

- Cover restrictions overriding saved prefix approvals, policy changes invalidating session approvals, and environment overlays remaining effective when model-specific prefix filtering is active.

GitOrigin-RevId: e94f877549ed5e0f577b8cb52f3c1872c58f029d
This commit is contained in:
sayan-oai
2026-08-17 04:37:50 +00:00
committed by copyberry
parent 89e297729e
commit e38290846c
27 changed files with 364 additions and 17 deletions
+1
View File
@@ -345,6 +345,7 @@ fn resolved_local_environments<const N: usize>(
PermissionProfile::read_only(),
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}),
},
@@ -787,6 +787,7 @@ mod tests {
allow_login_shell: true,
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}
}
@@ -959,6 +960,7 @@ url = "ws://127.0.0.1:8765"
vec![cwd.join("profile-root")],
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
};
let turn_environments = ThreadEnvironments::new(
@@ -1161,6 +1163,7 @@ url = "ws://127.0.0.1:8765"
vec![cwd.join("profile-root")],
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
};
let cwd = PathUri::from_abs_path(&cwd);
@@ -1495,6 +1498,7 @@ url = "ws://127.0.0.1:8765"
vec![cwd.join("child-profile-root")],
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
};
let environments = ThreadEnvironments::new(
@@ -1539,6 +1543,7 @@ url = "ws://127.0.0.1:8765"
allow_login_shell: false,
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: vec![root("parent-root")],
};
let mut owner_selection = selection.clone();
+4 -1
View File
@@ -15,6 +15,7 @@ use codex_execpolicy::MatchOptions;
use codex_execpolicy::NetworkRuleProtocol;
use codex_execpolicy::Policy;
use codex_execpolicy::PolicyParser;
use codex_execpolicy::RequirementsExecPolicy;
use codex_execpolicy::RuleMatch;
use codex_execpolicy::blocking_append_allow_prefix_rule;
use codex_execpolicy::blocking_append_network_rule;
@@ -282,6 +283,7 @@ pub(crate) struct ExecApprovalRequest<'a> {
pub(crate) command: &'a [String],
pub(crate) approval_policy: AskForApproval,
pub(crate) permission_profile: PermissionProfile,
pub(crate) environment_policy: Option<&'a RequirementsExecPolicy>,
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) sandbox_permissions: SandboxPermissions,
pub(crate) prefix_rule: Option<Vec<String>>,
@@ -317,12 +319,13 @@ impl ExecPolicyManager {
command,
approval_policy,
permission_profile,
environment_policy,
windows_sandbox_level,
sandbox_permissions,
prefix_rule,
allow_prefix_rules,
} = req;
let exec_policy = self.current_for_prefix_rules(allow_prefix_rules);
let exec_policy = self.current_for_environment(environment_policy, allow_prefix_rules);
let ExecPolicyCommands {
commands,
used_complex_parsing,
@@ -2,6 +2,7 @@ use super::ExecPolicyManager;
use codex_execpolicy::Decision;
use codex_execpolicy::Policy;
use codex_execpolicy::PrefixRule;
use codex_execpolicy::RequirementsExecPolicy;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -11,6 +12,18 @@ pub(crate) enum AllowPrefixRules {
}
impl ExecPolicyManager {
pub(crate) fn current_for_environment(
&self,
environment_policy: Option<&RequirementsExecPolicy>,
allow_prefix_rules: AllowPrefixRules,
) -> Arc<Policy> {
let policy = self.current_for_prefix_rules(allow_prefix_rules);
match environment_policy {
Some(environment_policy) => Arc::new(policy.merge_overlay(environment_policy.as_ref())),
None => policy,
}
}
pub(crate) fn current_for_prefix_rules(
&self,
allow_prefix_rules: AllowPrefixRules,
@@ -7,6 +7,7 @@ use codex_execpolicy::Decision;
use codex_execpolicy::MatchOptions;
use codex_execpolicy::Policy;
use codex_execpolicy::PolicyParser;
use codex_execpolicy::RequirementsExecPolicy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -86,6 +87,35 @@ fn cyber_policy_filters_allow_prefixes_but_preserves_restrictive_and_network_rul
);
}
#[test]
fn environment_restrictions_apply_after_model_prefix_filtering() {
let (thread_policy, _) = policy_with_broad_allow_prefix();
let manager = ExecPolicyManager::new(thread_policy);
let mut environment_policy = Policy::empty();
environment_policy
.add_prefix_rule(
&["cargo".to_string(), "install".to_string()],
Decision::Forbidden,
)
.expect("add environment restriction");
let environment_policy = RequirementsExecPolicy::new(environment_policy);
let command = vec!["cargo".to_string(), "install".to_string()];
for allow_prefix_rules in [
AllowPrefixRules::Honor,
AllowPrefixRules::IgnoreForCyberModel,
] {
let policy = manager.current_for_environment(Some(&environment_policy), allow_prefix_rules);
assert_eq!(
policy.check(&command, &|_| Decision::Allow).decision,
Decision::Forbidden,
);
if allow_prefix_rules == AllowPrefixRules::IgnoreForCyberModel {
assert!(policy.get_allowed_prefixes().is_empty());
}
}
}
#[tokio::test]
async fn cyber_policy_requires_approval_for_broad_wrapped_and_resolved_prefixes() {
let (policy, program_path) = policy_with_broad_allow_prefix();
@@ -110,6 +140,7 @@ async fn cyber_policy_requires_approval_for_broad_wrapped_and_resolved_prefixes(
command: &command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
@@ -142,6 +173,7 @@ async fn cyber_policy_keeps_heuristically_safe_commands_inside_the_sandbox() {
command: &command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
@@ -161,6 +193,7 @@ async fn cyber_policy_keeps_heuristically_safe_commands_inside_the_sandbox() {
command: &command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
+12
View File
@@ -1389,6 +1389,7 @@ async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision()
mcp_elicitations: true,
}),
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: None,
@@ -1427,6 +1428,7 @@ async fn forced_rm_preserves_rule_rejection_when_granular_rules_are_disabled() {
mcp_elicitations: true,
}),
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: None,
@@ -1452,6 +1454,7 @@ async fn exec_approval_requirement_falls_back_to_heuristics() {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
@@ -1478,6 +1481,7 @@ async fn empty_bash_lc_script_falls_back_to_original_command() {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
@@ -1508,6 +1512,7 @@ async fn whitespace_bash_lc_script_falls_back_to_original_command() {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
@@ -1538,6 +1543,7 @@ async fn request_rule_uses_prefix_rule() {
command: &command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
@@ -1571,6 +1577,7 @@ async fn request_rule_falls_back_when_prefix_rule_does_not_approve_all_commands(
command: &command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::Disabled,
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
@@ -1611,6 +1618,7 @@ async fn heuristics_apply_when_other_commands_match_policy() {
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
permission_profile: PermissionProfile::Disabled,
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
@@ -2090,6 +2098,7 @@ async fn forced_rm_requires_approval_or_specific_rejection_on_all_platforms() {
command: &dangerous_command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: permissions,
prefix_rule: None,
@@ -2112,6 +2121,7 @@ async fn forced_rm_requires_approval_or_specific_rejection_on_all_platforms() {
command: &dangerous_command,
approval_policy: AskForApproval::Never,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: permissions,
prefix_rule: None,
@@ -2171,6 +2181,7 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
command: &sneaky_command,
approval_policy: AskForApproval::OnRequest,
permission_profile: PermissionProfile::read_only(),
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: permissions,
prefix_rule: None,
@@ -2264,6 +2275,7 @@ async fn exec_approval_requirement_for_command(
command: &command,
approval_policy,
permission_profile,
environment_policy: None,
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
sandbox_permissions,
prefix_rule,
+1
View File
@@ -1121,6 +1121,7 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul
.permission_profile_state()
.snapshot(),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}),
},
+9
View File
@@ -39,6 +39,15 @@ fn validate_environment_config(
"environment readiness contains more than {MAX_SELECTED_CAPABILITY_ROOTS} selected capability roots"
)));
}
if config
.exec_policy
.as_ref()
.is_some_and(|policy| !policy.as_ref().get_allowed_prefixes().is_empty())
{
return Err(CodexErr::InvalidRequest(
"environment command policy cannot contain allow rules".to_string(),
));
}
let mut root_ids = HashSet::with_capacity(config.selected_capability_roots.len());
for root in &config.selected_capability_roots {
+1
View File
@@ -144,6 +144,7 @@ impl SessionConfiguration {
allow_login_shell: self.allow_login_shell,
permission_profile: self.permission_profile_state.snapshot(),
shell_environment_policy: self.shell_environment_policy.clone(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}
}
+1
View File
@@ -11328,6 +11328,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
command: &command,
approval_policy: turn_context.approval_policy(),
permission_profile: turn_context.permission_profile(),
environment_policy: None,
windows_sandbox_level: turn_context.windows_sandbox_level,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
+13 -1
View File
@@ -703,7 +703,19 @@ impl Session {
.clone()
.or_else(|| ctx.approval_reason.clone())
.or_else(|| justification.clone());
with_cached_approval(&self.services, tool_name, action.cache_keys(), || async {
let policy_fingerprint = ctx
.review_context
.environments()
.turn_environments()
.find(|environment| environment.selection.environment_id == *environment_id)
.and_then(|environment| environment.config().exec_policy.as_ref())
.map(codex_execpolicy::RequirementsExecPolicy::fingerprint);
let cache_keys = action
.cache_keys()
.into_iter()
.map(|key| (key, &policy_fingerprint))
.collect();
with_cached_approval(&self.services, tool_name, cache_keys, || async {
self.request_command_approval(
ctx.review_context.turn(),
ctx.call_id.clone(),
@@ -179,6 +179,7 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
command: &exec_params.command,
approval_policy: turn.approval_policy(),
permission_profile: turn_environment.permission_profile().clone(),
environment_policy: turn_environment.config().exec_policy.as_ref(),
windows_sandbox_level: turn.windows_sandbox_level,
sandbox_permissions: if effective_additional_permissions.permissions_preapproved {
codex_protocol::models::SandboxPermissions::UseDefault
@@ -139,6 +139,7 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
active_permission_profile.clone(),
),
shell_environment_policy: selected_shell_environment_policy.clone(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}),
},
@@ -31,6 +31,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
PermissionProfile::read_only(),
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}),
},
@@ -183,7 +183,10 @@ pub(super) async fn try_run_zsh_fork(
ctx.session
.services
.exec_policy
.current_for_prefix_rules(ctx.step_context.turn.allow_prefix_rules())
.current_for_environment(
req.turn_environment.config().exec_policy.as_ref(),
ctx.step_context.turn.allow_prefix_rules(),
)
.as_ref()
.clone(),
));
@@ -297,7 +300,10 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
ctx.session
.services
.exec_policy
.current_for_prefix_rules(ctx.step_context.turn.allow_prefix_rules())
.current_for_environment(
req.turn_environment.config().exec_policy.as_ref(),
ctx.step_context.turn.allow_prefix_rules(),
)
.as_ref()
.clone(),
));
@@ -28,6 +28,7 @@ async fn approval_key_uses_path_uri_and_includes_environment_id() {
PermissionProfile::read_only(),
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}),
},
@@ -543,6 +543,7 @@ mod tests {
PermissionProfile::read_only(),
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}),
},
@@ -917,6 +917,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
.permission_profile_state()
.snapshot(),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
},
),
@@ -1246,6 +1246,7 @@ impl UnifiedExecProcessManager {
command: &request.command,
approval_policy: turn.approval_policy(),
permission_profile: request.turn_environment.permission_profile().clone(),
environment_policy: request.turn_environment.config().exec_policy.as_ref(),
windows_sandbox_level: turn.windows_sandbox_level,
sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault
+229
View File
@@ -2,12 +2,20 @@
use anyhow::Result;
use codex_config::test_support::CloudConfigBundleFixture;
use codex_core::EnvironmentConfig;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained;
use codex_execpolicy::Decision;
use codex_execpolicy::Policy;
use codex_execpolicy::RequirementsExecPolicy;
use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::error::CodexErrorDetails;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::PermissionProfileSnapshot;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GranularApprovalConfig;
@@ -524,6 +532,227 @@ prefix_rules = [
Ok(())
}
#[tokio::test]
async fn environment_command_restrictions_override_saved_prefix_approvals() -> Result<()> {
skip_if_target_windows!(
Ok(()),
"managed prefix fixture uses POSIX executable semantics"
);
let mut builder = test_codex().with_config(|config| {
config
.features
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
let policy_path = config.codex_home.join("rules").join("approved.rules");
fs::create_dir_all(policy_path.parent().expect("rules directory"))
.expect("create rules directory");
fs::write(
policy_path,
r#"prefix_rule(pattern=["echo"], decision="allow")"#,
)
.expect("write approved command prefix");
});
let server = start_mock_server().await;
let test = builder.build_with_auto_env(&server).await?;
let selection = test
.codex
.environment_selections()
.await
.into_iter()
.next()
.expect("thread should select its executor environment");
let mut invalid_policy = Policy::empty();
invalid_policy.add_prefix_rule(&["echo".to_string()], Decision::Allow)?;
let error = test
.codex
.environment_ready(
&selection,
EnvironmentConfig {
allow_login_shell: true,
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
shell_environment_policy: Default::default(),
exec_policy: Some(RequirementsExecPolicy::new(invalid_policy)),
selected_capability_roots: Vec::new(),
},
)
.await
.expect_err("environment policies must not introduce command allowances");
assert!(matches!(
error.details(),
CodexErrorDetails::InvalidRequest(message)
if message == "environment command policy cannot contain allow rules"
));
let mut environment_policy = Policy::empty();
environment_policy.add_prefix_rule(&["echo".to_string()], Decision::Forbidden)?;
test.codex
.environment_ready(
&selection,
EnvironmentConfig {
allow_login_shell: true,
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
shell_environment_policy: Default::default(),
exec_policy: Some(RequirementsExecPolicy::new(environment_policy)),
selected_capability_roots: Vec::new(),
},
)
.await?;
let call_id = "environment-managed-command";
let args = json!({
"cmd": "echo blocked",
"yield_time_ms": 1_000,
});
mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-environment-managed-1"),
ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?),
ev_completed("resp-environment-managed-1"),
]),
)
.await;
let results_mock = mount_sse_once(
&server,
sse(vec![
ev_assistant_message("msg-environment-managed", "done"),
ev_completed("resp-environment-managed-2"),
]),
)
.await;
test.submit_turn_with_approval_and_permission_profile(
"run shell command",
AskForApproval::Never,
PermissionProfile::Disabled,
)
.await?;
let output_item = results_mock.single_request().function_call_output(call_id);
let output = output_item
.get("output")
.and_then(Value::as_str)
.expect("function call output should include a string output payload");
assert!(
output.contains("policy forbids commands starting with `echo`"),
"unexpected output: {output}"
);
Ok(())
}
#[tokio::test]
async fn environment_command_policy_changes_invalidate_session_approvals() -> Result<()> {
skip_if_target_windows!(
Ok(()),
"managed prefix fixture uses POSIX executable semantics"
);
let mut builder = test_codex().with_config(|config| {
config
.features
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config
.permissions
.set_permission_profile(PermissionProfile::Disabled)
.expect("test config should allow unrestricted permissions");
config.approvals_reviewer = ApprovalsReviewer::User;
let policy_path = config.codex_home.join("rules").join("prompt.rules");
fs::create_dir_all(policy_path.parent().expect("rules directory"))
.expect("create rules directory");
fs::write(
policy_path,
r#"prefix_rule(pattern=["echo"], decision="prompt")"#,
)
.expect("write prompt command prefix");
});
let server = start_mock_server().await;
let test = builder.build_with_auto_env(&server).await?;
let selection = test
.codex
.environment_selections()
.await
.into_iter()
.next()
.expect("thread should select its executor environment");
for (attempt, decision) in [
("before-owner-policy", ReviewDecision::ApprovedForSession),
("after-owner-policy", ReviewDecision::Approved),
] {
if attempt == "after-owner-policy" {
let mut policy = Policy::empty();
policy.add_prefix_rule(&["echo".to_string()], Decision::Prompt)?;
test.codex
.environment_ready(
&selection,
EnvironmentConfig {
allow_login_shell: true,
permission_profile: PermissionProfileSnapshot::legacy(
PermissionProfile::Disabled,
),
shell_environment_policy: Default::default(),
exec_policy: Some(RequirementsExecPolicy::new(policy)),
selected_capability_roots: Vec::new(),
},
)
.await?;
}
let args = json!({ "cmd": "echo approval", "yield_time_ms": 1_000 });
mount_sse_once(
&server,
sse(vec![
ev_response_created(attempt),
ev_function_call(attempt, "exec_command", &serde_json::to_string(&args)?),
ev_completed(attempt),
]),
)
.await;
mount_sse_once(
&server,
sse(vec![
ev_assistant_message(attempt, "done"),
ev_completed(attempt),
]),
)
.await;
test.codex
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
text: attempt.into(),
text_elements: Vec::new(),
}]))
.await?;
let event = wait_for_event(&test.codex, |event| {
matches!(
event,
EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_)
)
})
.await;
let EventMsg::ExecApprovalRequest(approval) = event else {
panic!("expected a fresh command approval {attempt}");
};
test.codex
.submit(Op::ExecApproval {
id: approval.effective_approval_id(),
turn_id: None,
decision,
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_command_empty_script_with_collaboration_mode_does_not_panic() -> Result<()> {
let server = start_mock_server().await;
@@ -598,6 +598,7 @@ async fn guardian_session_is_reused_for_consecutive_tool_reviews_without_prewarm
test.config.permissions.permission_profile().clone(),
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
};
parent_environments
@@ -483,6 +483,7 @@ async fn root_reconciliation_reuses_pending_apps_startup() -> Result<()> {
test.config.permissions.permission_profile().clone(),
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: vec![SelectedCapabilityRoot {
id: "calendar-root".to_string(),
location: CapabilityRootLocation::Environment {
+7
View File
@@ -551,6 +551,7 @@ async fn environment_permissions_follow_configuration_ownership() -> Result<()>
PermissionProfile::read_only(),
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: Vec::new(),
}),
..selection
@@ -1173,6 +1174,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
allow_login_shell: false,
permission_profile: permission_profile.clone(),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: vec![root("duplicate"), root("duplicate")],
}),
] {
@@ -1208,6 +1210,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
allow_login_shell: true,
permission_profile: permission_profile.clone(),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: vec![root("startup-root"), root("second-root")],
}),
..selection.clone()
@@ -1227,6 +1230,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
allow_login_shell: false,
permission_profile: permission_profile.clone(),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: vec![root("first-root")],
}),
..selection.clone()
@@ -1285,6 +1289,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
allow_login_shell: false,
permission_profile: permission_profile.clone(),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: vec![root("first-updated-root")],
}),
..selection.clone()
@@ -1381,6 +1386,7 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes()
allow_login_shell,
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: vec![root(id)],
};
let start_pending_thread = || {
@@ -1697,6 +1703,7 @@ async fn ready_before_selection_exposes_remote_tools_and_capability_context_afte
test.config.permissions.permission_profile().clone(),
),
shell_environment_policy: Default::default(),
exec_policy: None,
selected_capability_roots: vec![ready_root],
}),
}]),
@@ -355,6 +355,7 @@ async fn exec_command_uses_installed_environment_shell_policy_with_explicit_over
.clone(),
..Default::default()
},
exec_policy: None,
selected_capability_roots: Vec::new(),
},
)
+12 -12
View File
@@ -41,11 +41,22 @@ impl RequirementsExecPolicy {
pub fn new(policy: Policy) -> Self {
Self { policy }
}
pub fn fingerprint(&self) -> Vec<String> {
let mut entries = Vec::new();
for (program, rules) in self.policy.rules().iter_all() {
for rule in rules {
entries.push(format!("{program}:{rule:?}"));
}
}
entries.sort();
entries
}
}
impl PartialEq for RequirementsExecPolicy {
fn eq(&self, other: &Self) -> bool {
policy_fingerprint(&self.policy) == policy_fingerprint(&other.policy)
self.fingerprint() == other.fingerprint()
}
}
@@ -57,17 +68,6 @@ impl AsRef<Policy> for RequirementsExecPolicy {
}
}
fn policy_fingerprint(policy: &Policy) -> Vec<String> {
let mut entries = Vec::new();
for (program, rules) in policy.rules().iter_all() {
for rule in rules {
entries.push(format!("{program}:{rule:?}"));
}
}
entries.sort();
entries
}
impl Policy {
pub fn new(rules_by_program: MultiMap<String, RuleRef>) -> Self {
Self::from_parts(rules_by_program, Vec::new(), HashMap::new())
+1 -1
View File
@@ -16,6 +16,7 @@ workspace = true
chardetng = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
codex-async-utils = { workspace = true }
codex-execpolicy = { workspace = true }
codex-extension-items = { workspace = true }
codex-http-client = { workspace = true }
codex-network-proxy = { workspace = true }
@@ -54,7 +55,6 @@ seccompiler = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
codex-execpolicy = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
+4
View File
@@ -1,6 +1,7 @@
use crate::capabilities::SelectedCapabilityRoot;
use crate::config_types::ShellEnvironmentPolicy;
use crate::models::PermissionProfileSnapshot;
use codex_execpolicy::RequirementsExecPolicy;
/// Configuration supplied for a thread's selected environment.
#[allow(clippy::large_enum_variant)]
@@ -25,6 +26,8 @@ pub struct EnvironmentConfig {
pub permission_profile: PermissionProfileSnapshot,
/// Controls which environment variables shell commands may inherit.
pub shell_environment_policy: ShellEnvironmentPolicy,
/// Additional managed command restrictions for this environment attachment.
pub exec_policy: Option<RequirementsExecPolicy>,
/// Capability roots selected for this thread's environment attachment.
pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
}
@@ -36,6 +39,7 @@ impl std::fmt::Debug for EnvironmentConfig {
.field("allow_login_shell", &self.allow_login_shell)
.field("permission_profile", &self.permission_profile)
.field("shell_environment_policy", &"<redacted>")
.field("exec_policy", &self.exec_policy)
.field("selected_capability_roots", &self.selected_capability_roots)
.finish()
}