From 03564ba83c664480cde5436b91a119806f1393a0 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 18 May 2026 22:57:31 -0700 Subject: [PATCH 1/7] feat(config): support managed permissions in requirements Co-authored-by: Codex noreply@openai.com --- .../codex_app_server_protocol.schemas.json | 15 ++ .../codex_app_server_protocol.v2.schemas.json | 15 ++ .../v2/ConfigRequirementsReadResponse.json | 15 ++ .../typescript/v2/ConfigRequirements.ts | 2 +- .../src/protocol/v2/config.rs | 2 + .../src/protocol/v2/tests.rs | 2 + codex-rs/app-server/README.md | 2 +- .../request_processors/config_processor.rs | 18 +++ codex-rs/cloud-requirements/src/lib.rs | 32 ++++ codex-rs/config/src/config_requirements.rs | 77 ++++++++++ .../core/src/config/config_loader_tests.rs | 63 ++++++++ codex-rs/core/src/config/config_tests.rs | 4 + codex-rs/core/src/config/mod.rs | 143 +++++++++++++++++- codex-rs/tui/src/debug_config.rs | 4 + 14 files changed, 384 insertions(+), 10 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b0158d0a19bd..4494f0a1b5fd 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -7672,6 +7672,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/v2/SandboxMode" @@ -7700,6 +7709,12 @@ } ] }, + "defaultPermissions": { + "type": [ + "string", + "null" + ] + }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 7e0053dcf2c8..1b51c7b93745 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -4041,6 +4041,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" @@ -4069,6 +4078,12 @@ } ] }, + "defaultPermissions": { + "type": [ + "string", + "null" + ] + }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 3515b5d1d1f4..b470c8b30d35 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -88,6 +88,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" @@ -116,6 +125,12 @@ } ] }, + "defaultPermissions": { + "type": [ + "string", + "null" + ] + }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index 5d2755d5ac5d..fa1fef2f2e8e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -7,4 +7,4 @@ import type { ComputerUseRequirements } from "./ComputerUseRequirements"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, defaultPermissions: string | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index 25d75b56b475..f758c2c08902 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -386,6 +386,8 @@ pub struct ConfigRequirements { #[experimental("configRequirements/read.allowedApprovalsReviewers")] pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, + pub default_permissions: Option, + pub allowed_permissions: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, pub computer_use: Option, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index a7cec1d36da8..5fc8adf6924c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1728,6 +1728,8 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() }]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 8ce55410a1a8..1d5b0714535c 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -226,7 +226,7 @@ Example with notification opt-out: - `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish). - `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. -- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), permission-profile defaults/catalogs (`defaultPermissions`, `allowedPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 860385a1ffb7..01a10e8804c3 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -419,6 +419,8 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .filter_map(map_sandbox_mode_requirement_to_api) .collect() }), + default_permissions: requirements.default_permissions, + allowed_permissions: requirements.allowed_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes .into_iter() @@ -636,10 +638,26 @@ mod tests { #[test] fn requirements_api_includes_allow_managed_hooks_only() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { + default_permissions: Some("managed-standard".to_string()), + allowed_permissions: Some(vec![ + "managed-standard".to_string(), + "managed-build".to_string(), + ]), allow_managed_hooks_only: Some(true), ..ConfigRequirementsToml::default() }); + assert_eq!( + mapped.default_permissions, + Some("managed-standard".to_string()) + ); + assert_eq!( + mapped.allowed_permissions, + Some(vec![ + "managed-standard".to_string(), + "managed-build".to_string(), + ]) + ); assert_eq!(mapped.allow_managed_hooks_only, Some(true)); assert_eq!(mapped.hooks, None); } diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index b9740602dda3..3f501827b8e7 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -1216,6 +1216,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1300,6 +1302,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1335,6 +1339,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1387,6 +1393,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1568,6 +1576,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1650,6 +1660,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1730,6 +1742,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1938,6 +1952,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1980,6 +1996,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2042,6 +2060,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2100,6 +2120,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2160,6 +2182,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2221,6 +2245,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2286,6 +2312,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2377,6 +2405,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -2414,6 +2444,8 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 8e2e21f0c4b2..3f266acc2534 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -19,6 +19,7 @@ use crate::Constrained; use crate::ConstraintError; use crate::ManagedHooksRequirementsToml; use crate::mcp_types::AppToolApproval; +use crate::permissions_toml::PermissionProfileToml; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequirementSource { @@ -440,6 +441,8 @@ pub struct FilesystemRequirementsToml { #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct PermissionsRequirementsToml { pub filesystem: Option, + #[serde(default, flatten)] + pub profiles: BTreeMap, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -701,6 +704,8 @@ pub struct ConfigRequirementsToml { pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, + pub default_permissions: Option, + pub allowed_permissions: Option>, pub remote_sandbox_config: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, @@ -752,6 +757,8 @@ pub struct ConfigRequirementsWithSources { pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, + pub default_permissions: Option>, + pub allowed_permissions: Option>>, pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, pub computer_use: Option>, @@ -789,6 +796,8 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: _, allowed_approvals_reviewers: _, allowed_sandbox_modes: _, + default_permissions: _, + allowed_permissions: _, remote_sandbox_config: _, allowed_web_search_modes: _, allow_managed_hooks_only: _, @@ -821,6 +830,8 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, + default_permissions, + allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, computer_use, @@ -850,6 +861,8 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, + default_permissions, + allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, computer_use, @@ -868,6 +881,8 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), + default_permissions: default_permissions.map(|sourced| sourced.value), + allowed_permissions: allowed_permissions.map(|sourced| sourced.value), remote_sandbox_config: None, allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value), allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value), @@ -953,6 +968,8 @@ impl ConfigRequirementsToml { self.allowed_approval_policies.is_none() && self.allowed_approvals_reviewers.is_none() && self.allowed_sandbox_modes.is_none() + && self.default_permissions.is_none() + && self.allowed_permissions.is_none() && self.remote_sandbox_config.is_none() && self.allowed_web_search_modes.is_none() && self.allow_managed_hooks_only.is_none() @@ -996,6 +1013,8 @@ impl TryFrom for ConfigRequirements { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, + default_permissions: _default_permissions, + allowed_permissions: _allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, computer_use, @@ -1301,6 +1320,8 @@ mod tests { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, + default_permissions, + allowed_permissions, remote_sandbox_config: _, allowed_web_search_modes, allow_managed_hooks_only, @@ -1323,6 +1344,10 @@ mod tests { .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_sandbox_modes: allowed_sandbox_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), + default_permissions: default_permissions + .map(|value| Sourced::new(value, RequirementSource::Unknown)), + allowed_permissions: allowed_permissions + .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_web_search_modes: allowed_web_search_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), allow_managed_hooks_only: allow_managed_hooks_only @@ -1370,6 +1395,47 @@ mod tests { Ok(()) } + #[test] + fn deserialize_managed_permission_profiles() -> Result<()> { + let requirements: ConfigRequirementsToml = from_str( + r#" + default_permissions = "managed-standard" + allowed_permissions = ["managed-standard", "managed-build"] + + [permissions.managed-standard] + + [permissions.managed-build.filesystem] + ":workspace_roots" = "write" + "#, + )?; + + assert_eq!( + requirements.default_permissions.as_deref(), + Some("managed-standard") + ); + assert_eq!( + requirements.allowed_permissions, + Some(vec![ + "managed-standard".to_string(), + "managed-build".to_string(), + ]) + ); + let permissions = requirements + .permissions + .as_ref() + .expect("managed permission profiles"); + assert!(permissions.profiles.contains_key("managed-standard")); + assert!( + permissions + .profiles + .get("managed-build") + .and_then(|profile| profile.filesystem.as_ref()) + .is_some() + ); + assert!(!requirements.is_empty()); + Ok(()) + } + #[test] fn deserialize_computer_use_requirements() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( @@ -1421,6 +1487,8 @@ mod tests { allowed_approval_policies: Some(allowed_approval_policies.clone()), allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()), allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()), + default_permissions: Some("managed".to_string()), + allowed_permissions: Some(vec!["managed".to_string()]), remote_sandbox_config: None, allowed_web_search_modes: Some(allowed_web_search_modes.clone()), allow_managed_hooks_only: Some(true), @@ -1451,6 +1519,11 @@ mod tests { source.clone(), )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), + default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)), + allowed_permissions: Some(Sourced::new( + vec!["managed".to_string()], + source.clone(), + )), allowed_web_search_modes: Some(Sourced::new( allowed_web_search_modes, enforce_source.clone(), @@ -1501,6 +1574,8 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, @@ -1550,6 +1625,8 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 760b1f3fc590..8738749b67ab 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1095,6 +1095,8 @@ allowed_approval_policies = ["on-request"] allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1154,6 +1156,8 @@ allowed_approval_policies = ["on-request"] allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1362,6 +1366,8 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1411,6 +1417,63 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> Ok(()) } +#[tokio::test] +async fn system_requirements_define_managed_permission_profiles() -> anyhow::Result<()> { + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#" +default_permissions = "local" + +[permissions.local.filesystem] +":workspace_roots" = "read" +"#, + ) + .await?; + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +default_permissions = "managed-standard" +allowed_permissions = ["managed-standard"] + +[permissions.managed-standard] + +[permissions.managed-standard.filesystem] +":workspace_roots" = "read" +"#, + ) + .await?; + + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(cwd.to_path_buf())) + .loader_overrides(overrides) + .build() + .await?; + + assert_eq!( + config + .config_layer_stack + .requirements_toml() + .allowed_permissions, + Some(vec!["managed-standard".to_string()]) + ); + assert_eq!( + config + .permissions + .active_permission_profile() + .map(|profile| profile.id), + Some("managed-standard".to_string()) + ); + Ok(()) +} + #[tokio::test] async fn load_config_layers_can_ignore_managed_requirements() -> anyhow::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 19fd762da916..d78b9ffa739e 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8897,6 +8897,8 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]), allow_managed_hooks_only: None, @@ -9681,6 +9683,8 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]), + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 434969e2c5f4..194f91b1fe8d 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -33,6 +33,7 @@ use codex_config::config_toml::ThreadStoreToml; use codex_config::config_toml::validate_model_providers; use codex_config::loader::load_config_layers_state; use codex_config::loader::project_trust_key; +use codex_config::permissions_toml::PermissionsToml; use codex_config::profile_toml::ConfigProfile; use codex_config::sandbox_mode_requirement_for_permission_profile; use codex_config::types::ApprovalsReviewer; @@ -121,6 +122,7 @@ use crate::config::permissions::compile_permission_profile_selection; use crate::config::permissions::compile_permission_profile_workspace_roots; use crate::config::permissions::default_builtin_permission_profile_name; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; +use crate::config::permissions::is_builtin_permission_profile_name; use crate::config::permissions::network_proxy_config_for_profile_selection; use crate::config::permissions::validate_user_permission_profile_names; use crate::config_lock::config_without_lock_controls; @@ -2586,14 +2588,49 @@ impl Config { sandbox_mode, config_profile.sandbox_mode, ); - let has_permission_profiles = cfg - .permissions + let requirements_toml = config_layer_stack.requirements_toml(); + let effective_permissions = + merge_managed_permission_profiles(cfg.permissions.as_ref(), requirements_toml)?; + validate_user_permission_profile_names(effective_permissions.as_ref())?; + validate_required_permission_profile_catalog( + requirements_toml, + effective_permissions.as_ref(), + )?; + + let has_permission_profiles = effective_permissions .as_ref() .is_some_and(|profiles| !profiles.is_empty()); - let default_permissions = default_permissions_override + let allowed_permissions = requirements_toml.allowed_permissions.as_ref(); + let mut default_permissions = default_permissions_override .as_deref() + .or(requirements_toml.default_permissions.as_deref()) .or(cfg.default_permissions.as_deref()); - validate_user_permission_profile_names(cfg.permissions.as_ref())?; + if default_permissions.is_none() { + default_permissions = allowed_permissions + .and_then(|allowed_permissions| allowed_permissions.first()) + .map(String::as_str); + } + if let (Some(selected_permissions), Some(allowed_permissions)) = + (default_permissions, allowed_permissions) + && !allowed_permissions + .iter() + .any(|allowed_permission| allowed_permission == selected_permissions) + { + let Some(fallback_permissions) = requirements_toml + .default_permissions + .as_deref() + .or_else(|| allowed_permissions.first().map(String::as_str)) + else { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "requirements.toml allowed_permissions must include at least one profile", + )); + }; + startup_warnings.push(format!( + "Configured permission profile `{selected_permissions}` is disallowed by requirements; falling back to required value `{fallback_permissions}`." + )); + default_permissions = Some(fallback_permissions); + } if has_permission_profiles && !matches!( permission_config_syntax, @@ -2692,7 +2729,7 @@ impl Config { ) }); network_proxy_config_for_profile_selection( - cfg.permissions.as_ref(), + effective_permissions.as_ref(), default_permissions, )? } else { @@ -2739,19 +2776,19 @@ impl Config { None }; let configured_network_proxy_config = network_proxy_config_for_profile_selection( - cfg.permissions.as_ref(), + effective_permissions.as_ref(), default_permissions, )?; let (mut file_system_sandbox_policy, network_sandbox_policy) = compile_permission_profile_selection( - cfg.permissions.as_ref(), + effective_permissions.as_ref(), default_permissions, builtin_workspace_write_settings, resolved_cwd.as_path(), &mut startup_warnings, )?; let mut configured_workspace_roots = compile_permission_profile_workspace_roots( - cfg.permissions.as_ref(), + effective_permissions.as_ref(), default_permissions, resolved_cwd.as_path(), )?; @@ -3711,6 +3748,96 @@ fn guardian_policy_config_from_requirements( normalize_guardian_policy_config(requirements_toml.guardian_policy_config.as_deref()) } +fn merge_managed_permission_profiles( + configured_permissions: Option<&PermissionsToml>, + requirements_toml: &ConfigRequirementsToml, +) -> std::io::Result> { + let managed_profiles = requirements_toml + .permissions + .as_ref() + .map(|permissions| &permissions.profiles) + .filter(|profiles| !profiles.is_empty()); + let Some(managed_profiles) = managed_profiles else { + return Ok(configured_permissions.cloned()); + }; + + let mut merged_permissions = configured_permissions.cloned().unwrap_or_default(); + for (profile_id, managed_profile) in managed_profiles { + if merged_permissions.entries.contains_key(profile_id) { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml permissions profile `{profile_id}` conflicts with a config-defined profile of the same name" + ), + )); + } + merged_permissions + .entries + .insert(profile_id.clone(), managed_profile.clone()); + } + + Ok(Some(merged_permissions)) +} + +fn validate_required_permission_profile_catalog( + requirements_toml: &ConfigRequirementsToml, + available_permissions: Option<&PermissionsToml>, +) -> std::io::Result<()> { + let is_known_profile = |profile_id: &str| { + is_builtin_permission_profile_name(profile_id) + || available_permissions + .as_ref() + .is_some_and(|permissions| permissions.entries.contains_key(profile_id)) + }; + + if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() + && !is_known_profile(default_permissions) + { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml default_permissions refers to undefined profile `{default_permissions}`" + ), + )); + } + + let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { + return Ok(()); + }; + if allowed_permissions.is_empty() { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "requirements.toml allowed_permissions must include at least one profile", + )); + } + + for profile_id in allowed_permissions { + if !is_known_profile(profile_id) { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml allowed_permissions refers to undefined profile `{profile_id}`" + ), + )); + } + } + + if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() + && !allowed_permissions + .iter() + .any(|profile_id| profile_id == default_permissions) + { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml default_permissions `{default_permissions}` must also be listed in allowed_permissions" + ), + )); + } + + Ok(()) +} + fn normalize_guardian_policy_config(value: Option<&str>) -> Option { value.and_then(|value| { let trimmed = value.trim(); diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 2075636e3d23..7ec1e302c604 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -697,6 +697,8 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]), allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]), allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]), allow_managed_hooks_only: Some(true), @@ -912,6 +914,8 @@ approval_policy = "never" allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + default_permissions: None, + allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(Vec::new()), allow_managed_hooks_only: None, From caef4ec6236201564eac05c045e5f81bf6f92e3f Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 19 May 2026 14:15:42 -0700 Subject: [PATCH 2/7] fix(config): honor managed permission requirements Co-authored-by: Codex noreply@openai.com --- codex-rs/config/src/config_requirements.rs | 7 +- .../core/src/config/config_loader_tests.rs | 143 ++++++++++++++++++ codex-rs/core/src/config/mod.rs | 82 ++++++---- 3 files changed, 198 insertions(+), 34 deletions(-) diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 3f266acc2534..a4dd9188e3c0 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -1009,12 +1009,15 @@ impl TryFrom for ConfigRequirements { type Error = ConstraintError; fn try_from(toml: ConfigRequirementsWithSources) -> Result { + // Profile catalog selection remains on ConfigRequirementsToml for + // config loading and requirements API projection. The normalized + // constraints below only need the compiled PermissionProfile envelope. let ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions: _default_permissions, - allowed_permissions: _allowed_permissions, + default_permissions: _, + allowed_permissions: _, allowed_web_search_modes, allow_managed_hooks_only, computer_use, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 8738749b67ab..5f178b6baa23 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1474,6 +1474,149 @@ allowed_permissions = ["managed-standard"] Ok(()) } +#[tokio::test] +async fn system_requirements_profiles_apply_with_legacy_sandbox_config() -> anyhow::Result<()> { + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#" +sandbox_mode = "danger-full-access" +"#, + ) + .await?; + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +default_permissions = "managed-standard" +allowed_permissions = ["managed-standard"] + +[permissions.managed-standard.filesystem] +":workspace_roots" = "read" +"#, + ) + .await?; + + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(cwd.to_path_buf())) + .loader_overrides(overrides) + .build() + .await?; + + assert_eq!( + config + .permissions + .active_permission_profile() + .map(|profile| profile.id), + Some("managed-standard".to_string()) + ); + Ok(()) +} + +#[tokio::test] +async fn system_requirements_preserve_allowed_configured_permission_default() -> anyhow::Result<()> +{ + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#" +default_permissions = "managed-build" +"#, + ) + .await?; + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +default_permissions = "managed-standard" +allowed_permissions = ["managed-standard", "managed-build"] + +[permissions.managed-standard.filesystem] +":workspace_roots" = "read" + +[permissions.managed-build.filesystem] +":workspace_roots" = "write" +"#, + ) + .await?; + + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(cwd.to_path_buf())) + .loader_overrides(overrides) + .build() + .await?; + + assert_eq!( + config + .permissions + .active_permission_profile() + .map(|profile| profile.id), + Some("managed-build".to_string()) + ); + Ok(()) +} + +#[tokio::test] +async fn system_requirements_warn_for_disallowed_explicit_permission_override() -> anyhow::Result<()> +{ + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +default_permissions = "managed-standard" +allowed_permissions = ["managed-standard"] + +[permissions.managed-standard.filesystem] +":workspace_roots" = "read" +"#, + ) + .await?; + + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(cwd.to_path_buf())) + .harness_overrides(ConfigOverrides { + default_permissions: Some("managed-build".to_string()), + ..ConfigOverrides::default() + }) + .loader_overrides(overrides) + .build() + .await?; + + assert_eq!( + config + .permissions + .active_permission_profile() + .map(|profile| profile.id), + Some("managed-standard".to_string()) + ); + assert!( + config.startup_warnings.iter().any(|warning| warning + .contains("Configured value for `permission_profile` is disallowed by requirements")), + "{:?}", + config.startup_warnings + ); + Ok(()) +} + #[tokio::test] async fn load_config_layers_can_ignore_managed_requirements() -> anyhow::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 194f91b1fe8d..6f16e1284fff 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2600,37 +2600,12 @@ impl Config { let has_permission_profiles = effective_permissions .as_ref() .is_some_and(|profiles| !profiles.is_empty()); - let allowed_permissions = requirements_toml.allowed_permissions.as_ref(); - let mut default_permissions = default_permissions_override - .as_deref() - .or(requirements_toml.default_permissions.as_deref()) - .or(cfg.default_permissions.as_deref()); - if default_permissions.is_none() { - default_permissions = allowed_permissions - .and_then(|allowed_permissions| allowed_permissions.first()) - .map(String::as_str); - } - if let (Some(selected_permissions), Some(allowed_permissions)) = - (default_permissions, allowed_permissions) - && !allowed_permissions - .iter() - .any(|allowed_permission| allowed_permission == selected_permissions) - { - let Some(fallback_permissions) = requirements_toml - .default_permissions - .as_deref() - .or_else(|| allowed_permissions.first().map(String::as_str)) - else { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - "requirements.toml allowed_permissions must include at least one profile", - )); - }; - startup_warnings.push(format!( - "Configured permission profile `{selected_permissions}` is disallowed by requirements; falling back to required value `{fallback_permissions}`." - )); - default_permissions = Some(fallback_permissions); - } + let default_permissions = resolve_default_permissions( + default_permissions_override.as_deref(), + cfg.default_permissions.as_deref(), + requirements_toml, + &mut startup_warnings, + )?; if has_permission_profiles && !matches!( permission_config_syntax, @@ -2653,7 +2628,10 @@ impl Config { std::fs::create_dir_all(&memories_root)?; let internal_writable_roots = vec![memories_root]; - let profiles_are_active = default_permissions_override.is_some() + let requirements_select_permissions = requirements_toml.default_permissions.is_some() + || requirements_toml.allowed_permissions.is_some(); + let profiles_are_active = requirements_select_permissions + || default_permissions_override.is_some() || matches!( permission_config_syntax, Some(PermissionConfigSyntax::Profiles) @@ -3779,6 +3757,46 @@ fn merge_managed_permission_profiles( Ok(Some(merged_permissions)) } +fn resolve_default_permissions<'a>( + default_permissions_override: Option<&'a str>, + configured_default_permissions: Option<&'a str>, + requirements_toml: &'a ConfigRequirementsToml, + startup_warnings: &mut Vec, +) -> std::io::Result> { + let allowed_permissions = requirements_toml.allowed_permissions.as_ref(); + let mut default_permissions = default_permissions_override + .or(configured_default_permissions) + .or(requirements_toml.default_permissions.as_deref()); + if default_permissions.is_none() { + default_permissions = allowed_permissions + .and_then(|allowed_permissions| allowed_permissions.first()) + .map(String::as_str); + } + if let (Some(selected_permissions), Some(allowed_permissions)) = + (default_permissions, allowed_permissions) + && !allowed_permissions + .iter() + .any(|allowed_permission| allowed_permission == selected_permissions) + { + let Some(fallback_permissions) = requirements_toml + .default_permissions + .as_deref() + .or_else(|| allowed_permissions.first().map(String::as_str)) + else { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "requirements.toml allowed_permissions must include at least one profile", + )); + }; + startup_warnings.push(format!( + "Configured value for `permission_profile` is disallowed by requirements; falling back from `{selected_permissions}` to required value `{fallback_permissions}`." + )); + default_permissions = Some(fallback_permissions); + } + + Ok(default_permissions) +} + fn validate_required_permission_profile_catalog( requirements_toml: &ConfigRequirementsToml, available_permissions: Option<&PermissionsToml>, From 0f46a331c329c691de1574aa029aaf899da67d99 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 19 May 2026 18:53:51 -0700 Subject: [PATCH 3/7] refactor(config): centralize effective permission selection Co-authored-by: Codex noreply@openai.com --- codex-rs/core/src/config/mod.rs | 126 ++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 39 deletions(-) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 6f16e1284fff..ba2761f90504 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1964,6 +1964,38 @@ struct PermissionSelectionToml { sandbox_mode: Option, } +// Resolve the named-profile catalog and selected profile id together. Runtime +// profile constraints are applied later after this selection compiles into a +// concrete `PermissionProfile`. +#[derive(Debug)] +struct EffectivePermissionSelection<'a> { + profiles: Option, + selected_profile_id: Option<&'a str>, + requirements_force_profile_selection: bool, +} + +impl EffectivePermissionSelection<'_> { + fn has_profiles(&self) -> bool { + self.profiles + .as_ref() + .is_some_and(|profiles| !profiles.is_empty()) + } + + fn profiles_are_active( + &self, + default_permissions_override: Option<&str>, + permission_config_syntax: Option, + ) -> bool { + self.requirements_force_profile_selection + || default_permissions_override.is_some() + || matches!( + permission_config_syntax, + Some(PermissionConfigSyntax::Profiles) + ) + || permission_config_syntax.is_none() + } +} + fn resolve_permission_config_syntax( config_layer_stack: &ConfigLayerStack, cfg: &ConfigToml, @@ -2589,29 +2621,19 @@ impl Config { config_profile.sandbox_mode, ); let requirements_toml = config_layer_stack.requirements_toml(); - let effective_permissions = - merge_managed_permission_profiles(cfg.permissions.as_ref(), requirements_toml)?; - validate_user_permission_profile_names(effective_permissions.as_ref())?; - validate_required_permission_profile_catalog( - requirements_toml, - effective_permissions.as_ref(), - )?; - - let has_permission_profiles = effective_permissions - .as_ref() - .is_some_and(|profiles| !profiles.is_empty()); - let default_permissions = resolve_default_permissions( + let effective_permission_selection = resolve_effective_permission_selection( + cfg.permissions.as_ref(), default_permissions_override.as_deref(), cfg.default_permissions.as_deref(), requirements_toml, &mut startup_warnings, )?; - if has_permission_profiles + if effective_permission_selection.has_profiles() && !matches!( permission_config_syntax, Some(PermissionConfigSyntax::Legacy) ) - && default_permissions.is_none() + && effective_permission_selection.selected_profile_id.is_none() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -2628,15 +2650,10 @@ impl Config { std::fs::create_dir_all(&memories_root)?; let internal_writable_roots = vec![memories_root]; - let requirements_select_permissions = requirements_toml.default_permissions.is_some() - || requirements_toml.allowed_permissions.is_some(); - let profiles_are_active = requirements_select_permissions - || default_permissions_override.is_some() - || matches!( - permission_config_syntax, - Some(PermissionConfigSyntax::Profiles) - ) - || permission_config_syntax.is_none(); + let profiles_are_active = effective_permission_selection.profiles_are_active( + default_permissions_override.as_deref(), + permission_config_syntax, + ); let explicit_permission_profile_mode = default_permissions_override.is_some() || matches!( permission_config_syntax, @@ -2648,9 +2665,11 @@ impl Config { .map_or_else(Vec::new, |permissions| { permissions.entries.keys().cloned().collect() }); - let using_implicit_builtin_profile = - permission_config_syntax.is_none() && default_permissions.is_none(); - let should_seed_legacy_workspace_roots = default_permissions.is_none() + let using_implicit_builtin_profile = permission_config_syntax.is_none() + && effective_permission_selection.selected_profile_id.is_none(); + let should_seed_legacy_workspace_roots = effective_permission_selection + .selected_profile_id + .is_none() && matches!( permission_config_syntax, None | Some(PermissionConfigSyntax::Legacy) @@ -2700,14 +2719,16 @@ impl Config { // PermissionProfile carries the active network sandbox bit, not the configured // proxy/allowlist policy. Keep that config so active profiles can round-trip // without broadening network behavior. - let default_permissions = default_permissions.unwrap_or_else(|| { - default_builtin_permission_profile_name( - &active_project, - windows_sandbox_level, - ) - }); + let default_permissions = effective_permission_selection + .selected_profile_id + .unwrap_or_else(|| { + default_builtin_permission_profile_name( + &active_project, + windows_sandbox_level, + ) + }); network_proxy_config_for_profile_selection( - effective_permissions.as_ref(), + effective_permission_selection.profiles.as_ref(), default_permissions, )? } else { @@ -2745,28 +2766,30 @@ impl Config { Vec::new(), ) } else if profiles_are_active { - let default_permissions = default_permissions.unwrap_or_else(|| { - default_builtin_permission_profile_name(&active_project, windows_sandbox_level) - }); + let default_permissions = effective_permission_selection + .selected_profile_id + .unwrap_or_else(|| { + default_builtin_permission_profile_name(&active_project, windows_sandbox_level) + }); let builtin_workspace_write_settings = if using_implicit_builtin_profile { cfg.sandbox_workspace_write.as_ref() } else { None }; let configured_network_proxy_config = network_proxy_config_for_profile_selection( - effective_permissions.as_ref(), + effective_permission_selection.profiles.as_ref(), default_permissions, )?; let (mut file_system_sandbox_policy, network_sandbox_policy) = compile_permission_profile_selection( - effective_permissions.as_ref(), + effective_permission_selection.profiles.as_ref(), default_permissions, builtin_workspace_write_settings, resolved_cwd.as_path(), &mut startup_warnings, )?; let mut configured_workspace_roots = compile_permission_profile_workspace_roots( - effective_permissions.as_ref(), + effective_permission_selection.profiles.as_ref(), default_permissions, resolved_cwd.as_path(), )?; @@ -3757,6 +3780,31 @@ fn merge_managed_permission_profiles( Ok(Some(merged_permissions)) } +fn resolve_effective_permission_selection<'a>( + configured_permissions: Option<&PermissionsToml>, + default_permissions_override: Option<&'a str>, + configured_default_permissions: Option<&'a str>, + requirements_toml: &'a ConfigRequirementsToml, + startup_warnings: &mut Vec, +) -> std::io::Result> { + let profiles = merge_managed_permission_profiles(configured_permissions, requirements_toml)?; + validate_user_permission_profile_names(profiles.as_ref())?; + validate_required_permission_profile_catalog(requirements_toml, profiles.as_ref())?; + let selected_profile_id = resolve_default_permissions( + default_permissions_override, + configured_default_permissions, + requirements_toml, + startup_warnings, + )?; + + Ok(EffectivePermissionSelection { + profiles, + selected_profile_id, + requirements_force_profile_selection: requirements_toml.default_permissions.is_some() + || requirements_toml.allowed_permissions.is_some(), + }) +} + fn resolve_default_permissions<'a>( default_permissions_override: Option<&'a str>, configured_default_permissions: Option<&'a str>, From bfc13d357ecd9a30f7a8108452bc1da4a2dc2e9e Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 20 May 2026 08:01:15 -0700 Subject: [PATCH 4/7] fix(config): preserve builtin permission fallbacks Co-authored-by: Codex noreply@openai.com --- .../core/src/config/config_loader_tests.rs | 113 ++++++++++++++++++ codex-rs/core/src/config/mod.rs | 8 +- 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 5f178b6baa23..8b3a044c2700 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -27,6 +27,8 @@ use codex_config::loader::load_requirements_toml; use codex_exec_server::LOCAL_FS; use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_utils_absolute_path::AbsolutePathBuf; @@ -1474,6 +1476,117 @@ allowed_permissions = ["managed-standard"] Ok(()) } +#[tokio::test] +async fn system_allowed_permissions_keep_builtin_permission_fallbacks() -> anyhow::Result<()> { + for (trust_level, expected_profile) in [ + ( + Some(TrustLevel::Trusted), + if cfg!(target_os = "windows") { + BUILT_IN_PERMISSION_PROFILE_READ_ONLY + } else { + BUILT_IN_PERMISSION_PROFILE_WORKSPACE + }, + ), + ( + Some(TrustLevel::Untrusted), + if cfg!(target_os = "windows") { + BUILT_IN_PERMISSION_PROFILE_READ_ONLY + } else { + BUILT_IN_PERMISSION_PROFILE_WORKSPACE + }, + ), + (None, BUILT_IN_PERMISSION_PROFILE_READ_ONLY), + ] { + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + if let Some(trust_level) = trust_level { + make_config_for_test( + &codex_home, + tmp.path(), + trust_level, + /*project_root_markers*/ None, + ) + .await?; + } + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +allowed_permissions = ["managed-standard"] + +[permissions.managed-standard.filesystem] +":workspace_roots" = "read" +"#, + ) + .await?; + + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(cwd.to_path_buf())) + .loader_overrides(overrides) + .build() + .await?; + + assert_eq!( + config + .permissions + .active_permission_profile() + .map(|profile| profile.id), + Some(expected_profile.to_string()), + "trust level {trust_level:?}", + ); + } + Ok(()) +} + +#[tokio::test] +async fn system_allowed_permissions_keep_explicit_builtin_defaults() -> anyhow::Result<()> { + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#" +default_permissions = ":workspace" +"#, + ) + .await?; + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +allowed_permissions = ["managed-standard"] + +[permissions.managed-standard.filesystem] +":workspace_roots" = "read" +"#, + ) + .await?; + + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(cwd.to_path_buf())) + .loader_overrides(overrides) + .build() + .await?; + + assert_eq!( + config + .permissions + .active_permission_profile() + .map(|profile| profile.id), + Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()) + ); + Ok(()) +} + #[tokio::test] async fn system_requirements_profiles_apply_with_legacy_sandbox_config() -> anyhow::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index ba2761f90504..937828692f0b 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2634,6 +2634,7 @@ impl Config { Some(PermissionConfigSyntax::Legacy) ) && effective_permission_selection.selected_profile_id.is_none() + && !effective_permission_selection.requirements_force_profile_selection { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -3815,13 +3816,9 @@ fn resolve_default_permissions<'a>( let mut default_permissions = default_permissions_override .or(configured_default_permissions) .or(requirements_toml.default_permissions.as_deref()); - if default_permissions.is_none() { - default_permissions = allowed_permissions - .and_then(|allowed_permissions| allowed_permissions.first()) - .map(String::as_str); - } if let (Some(selected_permissions), Some(allowed_permissions)) = (default_permissions, allowed_permissions) + && !is_builtin_permission_profile_name(selected_permissions) && !allowed_permissions .iter() .any(|allowed_permission| allowed_permission == selected_permissions) @@ -3889,6 +3886,7 @@ fn validate_required_permission_profile_catalog( } if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() + && !is_builtin_permission_profile_name(default_permissions) && !allowed_permissions .iter() .any(|profile_id| profile_id == default_permissions) From e0073d2b6f7215eebad7089fbb9cfd32bbbb32e8 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 20 May 2026 14:58:30 -0700 Subject: [PATCH 5/7] refactor(config): keep permission defaults out of requirements Co-authored-by: Codex noreply@openai.com --- .../codex_app_server_protocol.schemas.json | 6 -- .../codex_app_server_protocol.v2.schemas.json | 6 -- .../v2/ConfigRequirementsReadResponse.json | 6 -- .../typescript/v2/ConfigRequirements.ts | 2 +- .../src/protocol/v2/config.rs | 1 - .../src/protocol/v2/tests.rs | 1 - codex-rs/app-server/README.md | 2 +- .../request_processors/config_processor.rs | 6 -- codex-rs/cloud-requirements/src/lib.rs | 16 ----- codex-rs/config/src/config_requirements.rs | 28 ++------ .../core/src/config/config_loader_tests.rs | 72 +++---------------- codex-rs/core/src/config/config_tests.rs | 2 - codex-rs/core/src/config/mod.rs | 39 +--------- codex-rs/tui/src/debug_config.rs | 2 - 14 files changed, 18 insertions(+), 171 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 4494f0a1b5fd..a8662e6853a7 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -7709,12 +7709,6 @@ } ] }, - "defaultPermissions": { - "type": [ - "string", - "null" - ] - }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 1b51c7b93745..8340c171a565 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -4078,12 +4078,6 @@ } ] }, - "defaultPermissions": { - "type": [ - "string", - "null" - ] - }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index b470c8b30d35..73a482ae1ab0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -125,12 +125,6 @@ } ] }, - "defaultPermissions": { - "type": [ - "string", - "null" - ] - }, "enforceResidency": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index fa1fef2f2e8e..8653da78cded 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -7,4 +7,4 @@ import type { ComputerUseRequirements } from "./ComputerUseRequirements"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, defaultPermissions: string | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index f758c2c08902..258844cf089d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -386,7 +386,6 @@ pub struct ConfigRequirements { #[experimental("configRequirements/read.allowedApprovalsReviewers")] pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, - pub default_permissions: Option, pub allowed_permissions: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 5fc8adf6924c..b693679f4121 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1728,7 +1728,6 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() }]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 1d5b0714535c..5de40e88ad43 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -226,7 +226,7 @@ Example with notification opt-out: - `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish). - `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. -- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), permission-profile defaults/catalogs (`defaultPermissions`, `allowedPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`, `allowedPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 01a10e8804c3..338282679ba1 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -419,7 +419,6 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .filter_map(map_sandbox_mode_requirement_to_api) .collect() }), - default_permissions: requirements.default_permissions, allowed_permissions: requirements.allowed_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes @@ -638,7 +637,6 @@ mod tests { #[test] fn requirements_api_includes_allow_managed_hooks_only() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { - default_permissions: Some("managed-standard".to_string()), allowed_permissions: Some(vec![ "managed-standard".to_string(), "managed-build".to_string(), @@ -647,10 +645,6 @@ mod tests { ..ConfigRequirementsToml::default() }); - assert_eq!( - mapped.default_permissions, - Some("managed-standard".to_string()) - ); assert_eq!( mapped.allowed_permissions, Some(vec![ diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 3f501827b8e7..27649ad4c1b4 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -1216,7 +1216,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1302,7 +1301,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1339,7 +1337,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1393,7 +1390,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1576,7 +1572,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1660,7 +1655,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1742,7 +1736,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1952,7 +1945,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1996,7 +1988,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -2060,7 +2051,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -2120,7 +2110,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -2182,7 +2171,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -2245,7 +2233,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -2312,7 +2299,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -2405,7 +2391,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -2444,7 +2429,6 @@ command = "sample-mcp" allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index a4dd9188e3c0..4518609ec343 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -441,6 +441,7 @@ pub struct FilesystemRequirementsToml { #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct PermissionsRequirementsToml { pub filesystem: Option, + // `filesystem` stays reserved for requirements-level filesystem constraints. #[serde(default, flatten)] pub profiles: BTreeMap, } @@ -704,7 +705,6 @@ pub struct ConfigRequirementsToml { pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, - pub default_permissions: Option, pub allowed_permissions: Option>, pub remote_sandbox_config: Option>, pub allowed_web_search_modes: Option>, @@ -757,7 +757,6 @@ pub struct ConfigRequirementsWithSources { pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, - pub default_permissions: Option>, pub allowed_permissions: Option>>, pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, @@ -796,7 +795,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: _, allowed_approvals_reviewers: _, allowed_sandbox_modes: _, - default_permissions: _, allowed_permissions: _, remote_sandbox_config: _, allowed_web_search_modes: _, @@ -830,7 +828,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions, allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, @@ -861,7 +858,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions, allowed_permissions, allowed_web_search_modes, allow_managed_hooks_only, @@ -881,7 +877,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), - default_permissions: default_permissions.map(|sourced| sourced.value), allowed_permissions: allowed_permissions.map(|sourced| sourced.value), remote_sandbox_config: None, allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value), @@ -968,7 +963,6 @@ impl ConfigRequirementsToml { self.allowed_approval_policies.is_none() && self.allowed_approvals_reviewers.is_none() && self.allowed_sandbox_modes.is_none() - && self.default_permissions.is_none() && self.allowed_permissions.is_none() && self.remote_sandbox_config.is_none() && self.allowed_web_search_modes.is_none() @@ -1016,7 +1010,6 @@ impl TryFrom for ConfigRequirements { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions: _, allowed_permissions: _, allowed_web_search_modes, allow_managed_hooks_only, @@ -1323,7 +1316,6 @@ mod tests { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - default_permissions, allowed_permissions, remote_sandbox_config: _, allowed_web_search_modes, @@ -1347,8 +1339,6 @@ mod tests { .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_sandbox_modes: allowed_sandbox_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), - default_permissions: default_permissions - .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_permissions: allowed_permissions .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_web_search_modes: allowed_web_search_modes @@ -1402,20 +1392,16 @@ mod tests { fn deserialize_managed_permission_profiles() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" - default_permissions = "managed-standard" allowed_permissions = ["managed-standard", "managed-build"] [permissions.managed-standard] + extends = ":workspace" - [permissions.managed-build.filesystem] - ":workspace_roots" = "write" + [permissions.managed-build] + extends = "managed-standard" "#, )?; - assert_eq!( - requirements.default_permissions.as_deref(), - Some("managed-standard") - ); assert_eq!( requirements.allowed_permissions, Some(vec![ @@ -1432,7 +1418,7 @@ mod tests { permissions .profiles .get("managed-build") - .and_then(|profile| profile.filesystem.as_ref()) + .and_then(|profile| profile.extends.as_deref()) .is_some() ); assert!(!requirements.is_empty()); @@ -1490,7 +1476,6 @@ mod tests { allowed_approval_policies: Some(allowed_approval_policies.clone()), allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()), allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()), - default_permissions: Some("managed".to_string()), allowed_permissions: Some(vec!["managed".to_string()]), remote_sandbox_config: None, allowed_web_search_modes: Some(allowed_web_search_modes.clone()), @@ -1522,7 +1507,6 @@ mod tests { source.clone(), )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), - default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)), allowed_permissions: Some(Sourced::new( vec!["managed".to_string()], source.clone(), @@ -1577,7 +1561,6 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1628,7 +1611,6 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 8b3a044c2700..db671e1d3a94 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1097,7 +1097,6 @@ allowed_approval_policies = ["on-request"] allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1158,7 +1157,6 @@ allowed_approval_policies = ["on-request"] allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1368,7 +1366,6 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, @@ -1427,10 +1424,7 @@ async fn system_requirements_define_managed_permission_profiles() -> anyhow::Res tokio::fs::write( codex_home.join(CONFIG_TOML_FILE), r#" -default_permissions = "local" - -[permissions.local.filesystem] -":workspace_roots" = "read" +default_permissions = "managed-standard" "#, ) .await?; @@ -1438,13 +1432,10 @@ default_permissions = "local" tokio::fs::write( &requirements_path, r#" -default_permissions = "managed-standard" allowed_permissions = ["managed-standard"] [permissions.managed-standard] - -[permissions.managed-standard.filesystem] -":workspace_roots" = "read" +extends = ":workspace" "#, ) .await?; @@ -1587,51 +1578,6 @@ allowed_permissions = ["managed-standard"] Ok(()) } -#[tokio::test] -async fn system_requirements_profiles_apply_with_legacy_sandbox_config() -> anyhow::Result<()> { - let tmp = tempdir()?; - let codex_home = tmp.path().join("home"); - tokio::fs::create_dir_all(&codex_home).await?; - tokio::fs::write( - codex_home.join(CONFIG_TOML_FILE), - r#" -sandbox_mode = "danger-full-access" -"#, - ) - .await?; - let requirements_path = tmp.path().join("requirements.toml"); - tokio::fs::write( - &requirements_path, - r#" -default_permissions = "managed-standard" -allowed_permissions = ["managed-standard"] - -[permissions.managed-standard.filesystem] -":workspace_roots" = "read" -"#, - ) - .await?; - - let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; - let mut overrides = LoaderOverrides::without_managed_config_for_tests(); - overrides.system_requirements_path = Some(requirements_path); - let config = ConfigBuilder::default() - .codex_home(codex_home) - .fallback_cwd(Some(cwd.to_path_buf())) - .loader_overrides(overrides) - .build() - .await?; - - assert_eq!( - config - .permissions - .active_permission_profile() - .map(|profile| profile.id), - Some("managed-standard".to_string()) - ); - Ok(()) -} - #[tokio::test] async fn system_requirements_preserve_allowed_configured_permission_default() -> anyhow::Result<()> { @@ -1649,14 +1595,13 @@ default_permissions = "managed-build" tokio::fs::write( &requirements_path, r#" -default_permissions = "managed-standard" allowed_permissions = ["managed-standard", "managed-build"] -[permissions.managed-standard.filesystem] -":workspace_roots" = "read" +[permissions.managed-standard] +extends = ":read-only" -[permissions.managed-build.filesystem] -":workspace_roots" = "write" +[permissions.managed-build] +extends = ":workspace" "#, ) .await?; @@ -1691,11 +1636,10 @@ async fn system_requirements_warn_for_disallowed_explicit_permission_override() tokio::fs::write( &requirements_path, r#" -default_permissions = "managed-standard" allowed_permissions = ["managed-standard"] -[permissions.managed-standard.filesystem] -":workspace_roots" = "read" +[permissions.managed-standard] +extends = ":workspace" "#, ) .await?; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index d78b9ffa739e..7284d36efec5 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8897,7 +8897,6 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]), @@ -9683,7 +9682,6 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]), - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: None, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 937828692f0b..3a6d9336af86 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -122,7 +122,6 @@ use crate::config::permissions::compile_permission_profile_selection; use crate::config::permissions::compile_permission_profile_workspace_roots; use crate::config::permissions::default_builtin_permission_profile_name; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; -use crate::config::permissions::is_builtin_permission_profile_name; use crate::config::permissions::network_proxy_config_for_profile_selection; use crate::config::permissions::validate_user_permission_profile_names; use crate::config_lock::config_without_lock_controls; @@ -3801,8 +3800,7 @@ fn resolve_effective_permission_selection<'a>( Ok(EffectivePermissionSelection { profiles, selected_profile_id, - requirements_force_profile_selection: requirements_toml.default_permissions.is_some() - || requirements_toml.allowed_permissions.is_some(), + requirements_force_profile_selection: requirements_toml.allowed_permissions.is_some(), }) } @@ -3813,9 +3811,7 @@ fn resolve_default_permissions<'a>( startup_warnings: &mut Vec, ) -> std::io::Result> { let allowed_permissions = requirements_toml.allowed_permissions.as_ref(); - let mut default_permissions = default_permissions_override - .or(configured_default_permissions) - .or(requirements_toml.default_permissions.as_deref()); + let mut default_permissions = default_permissions_override.or(configured_default_permissions); if let (Some(selected_permissions), Some(allowed_permissions)) = (default_permissions, allowed_permissions) && !is_builtin_permission_profile_name(selected_permissions) @@ -3823,11 +3819,7 @@ fn resolve_default_permissions<'a>( .iter() .any(|allowed_permission| allowed_permission == selected_permissions) { - let Some(fallback_permissions) = requirements_toml - .default_permissions - .as_deref() - .or_else(|| allowed_permissions.first().map(String::as_str)) - else { + let Some(fallback_permissions) = allowed_permissions.first().map(String::as_str) else { return Err(std::io::Error::new( ErrorKind::InvalidInput, "requirements.toml allowed_permissions must include at least one profile", @@ -3853,17 +3845,6 @@ fn validate_required_permission_profile_catalog( .is_some_and(|permissions| permissions.entries.contains_key(profile_id)) }; - if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() - && !is_known_profile(default_permissions) - { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - format!( - "requirements.toml default_permissions refers to undefined profile `{default_permissions}`" - ), - )); - } - let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { return Ok(()); }; @@ -3885,20 +3866,6 @@ fn validate_required_permission_profile_catalog( } } - if let Some(default_permissions) = requirements_toml.default_permissions.as_deref() - && !is_builtin_permission_profile_name(default_permissions) - && !allowed_permissions - .iter() - .any(|profile_id| profile_id == default_permissions) - { - return Err(std::io::Error::new( - ErrorKind::InvalidInput, - format!( - "requirements.toml default_permissions `{default_permissions}` must also be listed in allowed_permissions" - ), - )); - } - Ok(()) } diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 7ec1e302c604..e89b97ed5417 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -697,7 +697,6 @@ mod tests { allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]), allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]), allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]), @@ -914,7 +913,6 @@ approval_policy = "never" allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - default_permissions: None, allowed_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(Vec::new()), From 1f9889bed2ba9c95f87b477cc1455f5213963962 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 20 May 2026 15:47:09 -0700 Subject: [PATCH 6/7] docs(config): note filesystem profile reservation Co-authored-by: Codex noreply@openai.com --- codex-rs/config/src/config_requirements.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 4518609ec343..00034faafc34 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -441,7 +441,8 @@ pub struct FilesystemRequirementsToml { #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct PermissionsRequirementsToml { pub filesystem: Option, - // `filesystem` stays reserved for requirements-level filesystem constraints. + // For legacy reasons, `filesystem` stays reserved for requirements-level + // filesystem constraints and cannot name a profile. #[serde(default, flatten)] pub profiles: BTreeMap, } From 554fd87c2b0d1c860021383088edc8ecb4211f5e Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 20 May 2026 15:59:02 -0700 Subject: [PATCH 7/7] fix(config): reject filesystem requirement profiles Co-authored-by: Codex noreply@openai.com --- codex-rs/config/src/config_requirements.rs | 60 +++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 00034faafc34..d209c161999e 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -433,11 +433,51 @@ impl From for NetworkConstraints { } } -#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct FilesystemRequirementsToml { pub deny_read: Option>, } +#[derive(Deserialize)] +struct RawFilesystemRequirementsToml { + deny_read: Option>, + description: Option, + extends: Option, + workspace_roots: Option, + filesystem: Option, + network: Option, +} + +impl<'de> Deserialize<'de> for FilesystemRequirementsToml { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw = RawFilesystemRequirementsToml::deserialize(deserializer)?; + let RawFilesystemRequirementsToml { + deny_read, + description, + extends, + workspace_roots, + filesystem, + network, + } = raw; + + if description.is_some() + || extends.is_some() + || workspace_roots.is_some() + || filesystem.is_some() + || network.is_some() + { + return Err(D::Error::custom( + "`permissions.filesystem` is reserved for requirements-level filesystem constraints and cannot define a profile", + )); + } + + Ok(Self { deny_read }) + } +} + #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct PermissionsRequirementsToml { pub filesystem: Option, @@ -1426,6 +1466,24 @@ mod tests { Ok(()) } + #[test] + fn filesystem_requirements_table_cannot_define_a_permission_profile() { + let err = from_str::( + r#" + [permissions.filesystem] + extends = ":workspace" + "#, + ) + .expect_err("filesystem requirements cannot define a permission profile"); + + assert!( + err.to_string().contains( + "`permissions.filesystem` is reserved for requirements-level filesystem constraints and cannot define a profile" + ), + "unexpected error: {err:#}" + ); + } + #[test] fn deserialize_computer_use_requirements() -> Result<()> { let requirements: ConfigRequirementsToml = from_str(