Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions codex-rs/config/src/config_requirements.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::ShellEnvironmentPolicyFilter;
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
Expand All @@ -10,6 +12,7 @@ use serde::de::Error as _;
use serde::de::value::Error as ValueDeserializerError;
use serde::de::value::StrDeserializer;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt;
use wildmatch::WildMatchPattern;

Expand Down Expand Up @@ -155,6 +158,7 @@ pub struct ConfigRequirements {
pub check_for_update_on_startup: Option<Sourced<bool>>,
pub otel: Option<Sourced<OtelConfigToml>>,
pub allow_login_shell: Option<Sourced<bool>>,
pub shell_environment_policy: Option<Sourced<ShellEnvironmentPolicyRequirementsToml>>,
pub feedback: Option<Sourced<FeedbackConfigToml>>,
pub approval_policy: ConstrainedWithSource<AskForApproval>,
pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
Expand Down Expand Up @@ -193,6 +197,7 @@ impl Default for ConfigRequirements {
check_for_update_on_startup: None,
otel: None,
allow_login_shell: None,
shell_environment_policy: None,
feedback: None,
approval_policy: ConstrainedWithSource::new(
Constrained::allow_any_from_default(),
Expand Down Expand Up @@ -859,6 +864,7 @@ pub struct ConfigRequirementsToml {
pub check_for_update_on_startup: Option<bool>,
pub otel: Option<OtelConfigToml>,
pub allow_login_shell: Option<bool>,
pub shell_environment_policy: Option<ShellEnvironmentPolicyRequirementsToml>,
pub feedback: Option<FeedbackConfigToml>,
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
Expand Down Expand Up @@ -886,6 +892,21 @@ pub struct ConfigRequirementsToml {
pub guardian_policy_config: Option<String>,
}

/// Managed shell environment policy fields accepted by `requirements.toml`.
///
/// Unlike ordinary `config.toml`, requirements intentionally accept only the
/// keyed `filters` form. This makes include/exclude actions compose by pattern,
/// while config arrays remain a migration-only compatibility path that can be
/// deprecated independently later.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ShellEnvironmentPolicyRequirementsToml {
pub inherit: Option<ShellEnvironmentPolicyInherit>,
pub ignore_default_excludes: Option<bool>,
pub r#set: Option<HashMap<String, String>>,
pub filters: Option<BTreeMap<String, ShellEnvironmentPolicyFilter>>,
}

#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct RemoteSandboxConfigToml {
pub hostname_patterns: Vec<String>,
Expand Down Expand Up @@ -926,6 +947,7 @@ pub struct ConfigRequirementsWithSources {
pub check_for_update_on_startup: Option<Sourced<bool>>,
pub otel: Option<Sourced<OtelConfigToml>>,
pub allow_login_shell: Option<Sourced<bool>>,
pub shell_environment_policy: Option<Sourced<ShellEnvironmentPolicyRequirementsToml>>,
pub feedback: Option<Sourced<FeedbackConfigToml>>,
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
Expand Down Expand Up @@ -979,6 +1001,7 @@ impl ConfigRequirementsWithSources {
check_for_update_on_startup: _,
otel: _,
allow_login_shell: _,
shell_environment_policy: _,
feedback: _,
allowed_approval_policies: _,
allowed_approvals_reviewers: _,
Expand Down Expand Up @@ -1027,6 +1050,7 @@ impl ConfigRequirementsWithSources {
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
Expand Down Expand Up @@ -1072,6 +1096,7 @@ impl ConfigRequirementsWithSources {
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
Expand Down Expand Up @@ -1106,6 +1131,7 @@ impl ConfigRequirementsWithSources {
check_for_update_on_startup: check_for_update_on_startup.map(|sourced| sourced.value),
otel: otel.map(|sourced| sourced.value),
allow_login_shell: allow_login_shell.map(|sourced| sourced.value),
shell_environment_policy: shell_environment_policy.map(|sourced| sourced.value),
feedback: feedback.map(|sourced| sourced.value),
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value),
Expand Down Expand Up @@ -1210,6 +1236,10 @@ impl ConfigRequirementsToml {
.as_ref()
.is_none_or(|otel| otel == &OtelConfigToml::default())
&& self.allow_login_shell.is_none()
&& self
.shell_environment_policy
.as_ref()
.is_none_or(|policy| policy == &ShellEnvironmentPolicyRequirementsToml::default())
&& self
.feedback
.as_ref()
Expand Down Expand Up @@ -1278,6 +1308,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
Expand Down Expand Up @@ -1618,6 +1649,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
approval_policy,
approvals_reviewer,
Expand Down Expand Up @@ -1720,6 +1752,7 @@ mod tests {
check_for_update_on_startup,
otel,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
Expand Down Expand Up @@ -1762,6 +1795,8 @@ mod tests {
otel: otel.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allow_login_shell: allow_login_shell
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
shell_environment_policy: shell_environment_policy
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
feedback: feedback.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_approval_policies: allowed_approval_policies
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
Expand Down Expand Up @@ -3779,4 +3814,43 @@ command = "python3 /enterprise/hooks/pre.py"
);
Ok(())
}

#[test]
fn shell_environment_policy_requires_filters_for_list_fields() -> Result<()> {
let requirements: ConfigRequirementsToml = from_str(
r#"
[shell_environment_policy.filters]
"HOME" = "include"
"SECRET_*" = "exclude"
"#,
)?;

assert_eq!(
requirements.shell_environment_policy,
Some(ShellEnvironmentPolicyRequirementsToml {
filters: Some(BTreeMap::from([
("HOME".to_string(), ShellEnvironmentPolicyFilter::Include,),
(
"SECRET_*".to_string(),
ShellEnvironmentPolicyFilter::Exclude,
),
])),
..Default::default()
})
);

for field in ["exclude", "include_only"] {
let error = from_str::<ConfigRequirementsToml>(&format!(
"[shell_environment_policy]\n{field} = [\"LEGACY_*\"]\n"
))
.expect_err("legacy requirements.toml arrays should be rejected");
assert!(error.to_string().contains(field));
}
let error = from_str::<ConfigRequirementsToml>(
"[shell_environment_policy.filters]\n\"PATH\" = \"keep\"\n",
)
.expect_err("unknown rule actions should be rejected");
assert!(error.to_string().contains("keep"));
Ok(())
}
}
1 change: 1 addition & 0 deletions codex-rs/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub use config_requirements::RemoteSandboxConfigToml;
pub use config_requirements::RequirementSource;
pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::ShellEnvironmentPolicyRequirementsToml;
pub use config_requirements::Sourced;
pub use config_requirements::WebSearchModeRequirement;
pub use config_requirements::WindowsRequirementsToml;
Expand Down
44 changes: 41 additions & 3 deletions codex-rs/config/src/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &m
normalize_network_domain_keys(base_table);
normalize_network_domain_keys(&mut overlay_table);
}
if is_shell_environment_filters_path(path) {
if is_shell_environment_filters_path(path)
|| cfg!(target_os = "windows") && is_shell_environment_set_path(path)
{
normalize_case_insensitive_keys(base_table);
normalize_case_insensitive_keys(&mut overlay_table);
}
Expand All @@ -31,12 +33,12 @@ fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &m
if let Some(existing) = base_table.get_mut(&key) {
merge_toml_values_at_path(existing, &value, path);
} else {
base_table.insert(key, normalized_with_key_aliases(&value, path));
base_table.insert(key, normalized_for_merge(&value, path));
}
path.pop();
}
} else {
*base = normalized_with_key_aliases(overlay, path);
*base = normalized_for_merge(overlay, path);
}
}

Expand Down Expand Up @@ -176,6 +178,13 @@ fn is_shell_environment_filters_path(path: &[String]) -> bool {
)
}

fn is_shell_environment_set_path(path: &[String]) -> bool {
matches!(
path,
[policy, set] if policy == "shell_environment_policy" && set == "set"
)
}

fn is_permission_network_domains_path(path: &[String]) -> bool {
matches!(
path,
Expand All @@ -198,6 +207,35 @@ fn normalize_case_insensitive_keys(table: &mut toml::map::Map<String, TomlValue>
}
}

fn normalized_for_merge(value: &TomlValue, path: &[String]) -> TomlValue {
let mut normalized = normalized_with_key_aliases(value, path);
normalize_nested_case_insensitive_keys(&mut normalized, &mut path.to_vec());
normalized
}

fn normalize_nested_case_insensitive_keys(value: &mut TomlValue, path: &mut Vec<String>) {
match value {
TomlValue::Table(table) => {
if is_shell_environment_filters_path(path)
|| cfg!(target_os = "windows") && is_shell_environment_set_path(path)
{
normalize_case_insensitive_keys(table);
}
for (key, value) in table {
path.push(key.clone());
normalize_nested_case_insensitive_keys(value, path);
path.pop();
}
}
TomlValue::Array(items) => {
for item in items {
normalize_nested_case_insensitive_keys(item, path);
}
}
_ => {}
}
}

#[cfg(test)]
#[path = "merge_tests.rs"]
mod tests;
52 changes: 52 additions & 0 deletions codex-rs/config/src/merge_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,58 @@ fn shell_environment_policy_filters_overlay_merges_by_key_case_insensitively() {
);
}

#[test]
fn shell_environment_policy_filters_are_normalized_in_the_first_layer() {
let mut base = parse_toml("");
let overlay = parse_toml(
r#"
[shell_environment_policy.filters]
"UPPER_*" = "exclude"
"#,
);

merge_toml_values(&mut base, &overlay);

assert_eq!(
base,
parse_toml(
r#"
[shell_environment_policy.filters]
"upper_*" = "exclude"
"#,
)
);
}

#[cfg(target_os = "windows")]
#[test]
fn shell_environment_policy_set_keys_merge_case_insensitively_on_windows() {
let mut base = parse_toml(
r#"
[shell_environment_policy.set]
Path = "low"
"#,
);
let overlay = parse_toml(
r#"
[shell_environment_policy.set]
PATH = "high"
"#,
);

merge_toml_values(&mut base, &overlay);

assert_eq!(
base,
parse_toml(
r#"
[shell_environment_policy.set]
path = "high"
"#,
)
);
}

#[test]
fn shell_environment_policy_filters_override_lower_legacy_arrays_by_pattern() {
let mut base = parse_toml(
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/config/src/requirements_layers/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ fn populate_merged_regular_fields_with_sources(
check_for_update_on_startup,
otel: _,
allow_login_shell,
shell_environment_policy,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
Expand Down Expand Up @@ -221,6 +222,7 @@ fn populate_merged_regular_fields_with_sources(
&["check_for_update_on_startup"]
);
set_sourced!(allow_login_shell, &["allow_login_shell"]);
set_sourced!(shell_environment_policy, &["shell_environment_policy"]);
set_sourced!(feedback, &["feedback"]);
set_sourced!(allowed_approval_policies, &["allowed_approval_policies"]);
set_sourced!(
Expand Down
Loading
Loading