From d4bf7bafbaf7a1ae72427606319afa6b3465c80b Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 27 May 2026 19:18:08 -0700 Subject: [PATCH 1/8] Enforce permission profile defaults and allowlists --- .../core/src/config/config_loader_tests.rs | 85 ++++++++++++++----- codex-rs/core/src/config/config_tests.rs | 38 ++------- codex-rs/core/src/config/mod.rs | 45 +++++----- codex-rs/core/src/config/permissions.rs | 2 +- 4 files changed, 95 insertions(+), 75 deletions(-) diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 5edecebec03..b128eeb79a5 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -31,7 +31,6 @@ use codex_config::test_support::CloudConfigBundleFixture; 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; @@ -1411,26 +1410,9 @@ extends = ":workspace" } #[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), - ] { +async fn system_allowed_permissions_select_first_profile_without_explicit_default() +-> anyhow::Result<()> { + for trust_level in [Some(TrustLevel::Trusted), Some(TrustLevel::Untrusted), None] { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); tokio::fs::create_dir_all(&codex_home).await?; @@ -1470,15 +1452,22 @@ allowed_permissions = ["managed-standard"] .permissions .active_permission_profile() .map(|profile| profile.id), - Some(expected_profile.to_string()), + Some("managed-standard".to_string()), "trust level {trust_level:?}", ); + assert!( + !config.startup_warnings.iter().any(|warning| warning + .contains("Configured value for `permission_profile` is disallowed")), + "{:?}", + config.startup_warnings + ); } Ok(()) } #[tokio::test] -async fn system_allowed_permissions_keep_explicit_builtin_defaults() -> anyhow::Result<()> { +async fn system_allowed_permissions_fall_back_from_disallowed_explicit_builtin_default() +-> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); tokio::fs::create_dir_all(&codex_home).await?; @@ -1495,6 +1484,56 @@ default_permissions = ":workspace" 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("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 system_allowed_permissions_keep_allowed_explicit_builtin_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 = ":workspace" +"#, + ) + .await?; + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +allowed_permissions = [":workspace", "managed-standard"] + [permissions.managed-standard.filesystem] ":workspace_roots" = "read" "#, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 588eb7ac1b4..4cbcfad48f6 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8626,7 +8626,7 @@ async fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error } #[tokio::test] -async fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> { +async fn test_untrusted_project_gets_read_only_sandbox() -> anyhow::Result<()> { let config_with_untrusted = r#" [projects."/tmp/test"] trust_level = "untrusted" @@ -8647,18 +8647,7 @@ trust_level = "untrusted" ) .await; - // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) - if cfg!(target_os = "windows") { - assert!( - matches!(resolution, SandboxPolicy::ReadOnly { .. }), - "Expected ReadOnly on Windows, got {resolution:?}" - ); - } else { - assert!( - matches!(resolution, SandboxPolicy::WorkspaceWrite { .. }), - "Expected WorkspaceWrite for untrusted project, got {resolution:?}" - ); - } + assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); Ok(()) } @@ -9015,24 +9004,11 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow: "Expected UnlessTrusted approval policy for untrusted project" ); - // Verify that untrusted projects still get WorkspaceWrite sandbox (or ReadOnly on Windows) - if cfg!(target_os = "windows") { - assert!( - matches!( - &config.legacy_sandbox_policy(), - SandboxPolicy::ReadOnly { .. } - ), - "Expected ReadOnly on Windows" - ); - } else { - assert!( - matches!( - &config.legacy_sandbox_policy(), - SandboxPolicy::WorkspaceWrite { .. } - ), - "Expected WorkspaceWrite sandbox for untrusted project" - ); - } + assert_eq!( + config.legacy_sandbox_policy(), + SandboxPolicy::new_read_only_policy(), + "Expected ReadOnly sandbox for untrusted project" + ); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index e319f3b3962..bc5dfdc4bbc 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -3845,28 +3845,33 @@ fn resolve_default_permissions<'a>( 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); - 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) - { - 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", - )); - }; - startup_warnings.push(format!( - "Configured value for `permission_profile` is disallowed by requirements; falling back from `{selected_permissions}` to required value `{fallback_permissions}`." + let selected_permissions = default_permissions_override.or(configured_default_permissions); + let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { + return Ok(selected_permissions); + }; + 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", )); - default_permissions = Some(fallback_permissions); - } + }; - Ok(default_permissions) + match selected_permissions { + None => Ok(Some(fallback_permissions)), + Some(selected_permissions) + if allowed_permissions + .iter() + .any(|allowed_permission| allowed_permission == selected_permissions) => + { + Ok(Some(selected_permissions)) + } + Some(selected_permissions) => { + startup_warnings.push(format!( + "Configured value for `permission_profile` is disallowed by requirements; falling back from `{selected_permissions}` to required value `{fallback_permissions}`." + )); + Ok(Some(fallback_permissions)) + } + } } fn validate_required_permission_profile_catalog( diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index f683d9c7eb3..f338d2d61dd 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -49,7 +49,7 @@ pub(crate) fn default_builtin_permission_profile_name( active_project: &ProjectConfig, windows_sandbox_level: WindowsSandboxLevel, ) -> &'static str { - if (active_project.is_trusted() || active_project.is_untrusted()) + if active_project.is_trusted() && !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled) { BUILT_IN_WORKSPACE_PROFILE From 26b0a07d3a66589dbd3595d1eb9cfa1f43dce1cb Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 27 May 2026 19:33:10 -0700 Subject: [PATCH 2/8] fix(permissions): preserve implicit sandbox defaults Co-authored-by: Codex noreply@openai.com --- codex-rs/core/src/config/config_tests.rs | 38 +++++++++++++++++++----- codex-rs/core/src/config/permissions.rs | 2 +- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 4cbcfad48f6..588eb7ac1b4 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8626,7 +8626,7 @@ async fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error } #[tokio::test] -async fn test_untrusted_project_gets_read_only_sandbox() -> anyhow::Result<()> { +async fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> { let config_with_untrusted = r#" [projects."/tmp/test"] trust_level = "untrusted" @@ -8647,7 +8647,18 @@ trust_level = "untrusted" ) .await; - assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); + // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) + if cfg!(target_os = "windows") { + assert!( + matches!(resolution, SandboxPolicy::ReadOnly { .. }), + "Expected ReadOnly on Windows, got {resolution:?}" + ); + } else { + assert!( + matches!(resolution, SandboxPolicy::WorkspaceWrite { .. }), + "Expected WorkspaceWrite for untrusted project, got {resolution:?}" + ); + } Ok(()) } @@ -9004,11 +9015,24 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow: "Expected UnlessTrusted approval policy for untrusted project" ); - assert_eq!( - config.legacy_sandbox_policy(), - SandboxPolicy::new_read_only_policy(), - "Expected ReadOnly sandbox for untrusted project" - ); + // Verify that untrusted projects still get WorkspaceWrite sandbox (or ReadOnly on Windows) + if cfg!(target_os = "windows") { + assert!( + matches!( + &config.legacy_sandbox_policy(), + SandboxPolicy::ReadOnly { .. } + ), + "Expected ReadOnly on Windows" + ); + } else { + assert!( + matches!( + &config.legacy_sandbox_policy(), + SandboxPolicy::WorkspaceWrite { .. } + ), + "Expected WorkspaceWrite sandbox for untrusted project" + ); + } Ok(()) } diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index f338d2d61dd..f683d9c7eb3 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -49,7 +49,7 @@ pub(crate) fn default_builtin_permission_profile_name( active_project: &ProjectConfig, windows_sandbox_level: WindowsSandboxLevel, ) -> &'static str { - if active_project.is_trusted() + if (active_project.is_trusted() || active_project.is_untrusted()) && !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled) { BUILT_IN_WORKSPACE_PROFILE From ec6d5a352a5c71b3eb47bf9dfaa3474a7cd45c38 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 3 Jun 2026 11:25:38 -0700 Subject: [PATCH 3/8] fix(permissions): require managed default profile Co-authored-by: Codex --- .../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 | 20 ++- codex-rs/config/src/config_requirements.rs | 20 +++ .../core/src/config/config_loader_tests.rs | 144 ++++++++++++++++-- codex-rs/core/src/config/config_tests.rs | 1 + codex-rs/core/src/config/mod.rs | 28 +++- codex-rs/tui/src/debug_config.rs | 2 + 13 files changed, 222 insertions(+), 17 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 a5578e00726..b2b43a7c089 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 @@ -8059,6 +8059,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 36b95b4f641..79a83ea25ae 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 @@ -4401,6 +4401,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 0b8170d659c..1ec7cdb8e1d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -140,6 +140,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 3e60e78da53..779a9314715 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -8,4 +8,4 @@ import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: Array | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: 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 f40192ddc66..9491429b0e8 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -376,6 +376,7 @@ pub struct ConfigRequirements { pub allowed_sandbox_modes: Option>, pub allowed_windows_sandbox_implementations: Option>, pub allowed_permissions: Option>, + pub default_permissions: Option, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, pub allow_appshots: 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 1c1e92b36f3..2d8011b1a60 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1675,6 +1675,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() allowed_sandbox_modes: None, allowed_windows_sandbox_implementations: None, allowed_permissions: None, + default_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, allow_appshots: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a12144238aa..d4b9e8c3bfd 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -231,7 +231,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`, `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`), the managed permission-profile default (`defaultPermissions`), 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 37535509252..c3a9b9e9a42 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -351,6 +351,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR }) }), allowed_permissions: requirements.allowed_permissions, + default_permissions: requirements.default_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes .into_iter() @@ -572,12 +573,23 @@ mod tests { #[test] fn requirements_api_includes_allow_managed_hooks_only() { + let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { + allow_managed_hooks_only: Some(true), + ..ConfigRequirementsToml::default() + }); + + assert_eq!(mapped.allow_managed_hooks_only, Some(true)); + assert_eq!(mapped.hooks, None); + } + + #[test] + fn requirements_api_includes_permission_default_and_allowlist() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { allowed_permissions: Some(vec![ "managed-standard".to_string(), "managed-build".to_string(), ]), - allow_managed_hooks_only: Some(true), + default_permissions: Some("managed-standard".to_string()), ..ConfigRequirementsToml::default() }); @@ -588,8 +600,10 @@ mod tests { "managed-build".to_string(), ]) ); - assert_eq!(mapped.allow_managed_hooks_only, Some(true)); - assert_eq!(mapped.hooks, None); + assert_eq!( + mapped.default_permissions, + Some("managed-standard".to_string()) + ); } #[test] diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index dcaefb8dbea..20ec6676204 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -823,6 +823,7 @@ pub struct ConfigRequirementsToml { pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, pub allowed_permissions: Option>, + pub default_permissions: Option, pub remote_sandbox_config: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, @@ -877,6 +878,7 @@ pub struct ConfigRequirementsWithSources { pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, pub allowed_permissions: Option>>, + pub default_permissions: Option>, pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, pub allow_appshots: Option>, @@ -917,6 +919,7 @@ impl ConfigRequirementsWithSources { allowed_approvals_reviewers: _, allowed_sandbox_modes: _, allowed_permissions: _, + default_permissions: _, remote_sandbox_config: _, allowed_web_search_modes: _, allow_managed_hooks_only: _, @@ -952,6 +955,7 @@ impl ConfigRequirementsWithSources { allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions, + default_permissions, allowed_web_search_modes, allow_managed_hooks_only, allow_appshots, @@ -984,6 +988,7 @@ impl ConfigRequirementsWithSources { allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions, + default_permissions, allowed_web_search_modes, allow_managed_hooks_only, allow_appshots, @@ -1005,6 +1010,7 @@ impl ConfigRequirementsWithSources { allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), allowed_permissions: allowed_permissions.map(|sourced| sourced.value), + default_permissions: default_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), @@ -1093,6 +1099,7 @@ impl ConfigRequirementsToml { && self.allowed_approvals_reviewers.is_none() && self.allowed_sandbox_modes.is_none() && self.allowed_permissions.is_none() + && self.default_permissions.is_none() && self.remote_sandbox_config.is_none() && self.allowed_web_search_modes.is_none() && self.allow_managed_hooks_only.is_none() @@ -1145,6 +1152,7 @@ impl TryFrom for ConfigRequirements { allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions: _, + default_permissions: _, allowed_web_search_modes, allow_managed_hooks_only, allow_appshots, @@ -1512,6 +1520,7 @@ mod tests { allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions, + default_permissions, remote_sandbox_config: _, allowed_web_search_modes, allow_managed_hooks_only, @@ -1538,6 +1547,8 @@ mod tests { .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_permissions: allowed_permissions .map(|value| Sourced::new(value, RequirementSource::Unknown)), + default_permissions: default_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 @@ -1593,6 +1604,7 @@ mod tests { let requirements: ConfigRequirementsToml = from_str( r#" allowed_permissions = ["managed-standard", "managed-build"] + default_permissions = "managed-standard" [permissions.managed-standard] extends = ":workspace" @@ -1609,6 +1621,10 @@ mod tests { "managed-build".to_string(), ]) ); + assert_eq!( + requirements.default_permissions, + Some("managed-standard".to_string()) + ); let permissions = requirements .permissions .as_ref() @@ -1721,6 +1737,7 @@ mod tests { allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()), allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()), allowed_permissions: Some(vec!["managed".to_string()]), + default_permissions: Some("managed".to_string()), remote_sandbox_config: None, allowed_web_search_modes: Some(allowed_web_search_modes.clone()), allow_managed_hooks_only: Some(true), @@ -1757,6 +1774,7 @@ mod tests { vec!["managed".to_string()], source.clone(), )), + default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)), allowed_web_search_modes: Some(Sourced::new( allowed_web_search_modes, enforce_source.clone(), @@ -1810,6 +1828,7 @@ mod tests { allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permissions: None, + default_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, allow_appshots: None, @@ -1862,6 +1881,7 @@ mod tests { allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permissions: None, + default_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, allow_appshots: None, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index b128eeb79a5..9bdd747289c 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -31,7 +31,7 @@ use codex_config::test_support::CloudConfigBundleFixture; 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_WORKSPACE; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_utils_absolute_path::AbsolutePathBuf; @@ -1375,6 +1375,7 @@ default_permissions = "managed-standard" &requirements_path, r#" allowed_permissions = ["managed-standard"] +default_permissions = "managed-standard" [permissions.managed-standard] extends = ":workspace" @@ -1410,7 +1411,7 @@ extends = ":workspace" } #[tokio::test] -async fn system_allowed_permissions_select_first_profile_without_explicit_default() +async fn system_allowed_permissions_select_managed_default_without_explicit_local_default() -> anyhow::Result<()> { for trust_level in [Some(TrustLevel::Trusted), Some(TrustLevel::Untrusted), None] { let tmp = tempdir()?; @@ -1429,10 +1430,14 @@ async fn system_allowed_permissions_select_first_profile_without_explicit_defaul tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-standard"] +allowed_permissions = ["managed-build", "managed-standard"] +default_permissions = "managed-standard" [permissions.managed-standard.filesystem] ":workspace_roots" = "read" + +[permissions.managed-build] +extends = ":workspace" "#, ) .await?; @@ -1466,23 +1471,133 @@ allowed_permissions = ["managed-standard"] } #[tokio::test] -async fn system_allowed_permissions_fall_back_from_disallowed_explicit_builtin_default() --> anyhow::Result<()> { +async fn system_allowed_permissions_require_managed_default() -> 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( - codex_home.join(CONFIG_TOML_FILE), + &requirements_path, r#" -default_permissions = ":workspace" +allowed_permissions = ["managed-standard"] + +[permissions.managed-standard] +extends = ":read-only" +"#, + ) + .await?; + + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let err = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(tmp.path().to_path_buf())) + .loader_overrides(overrides) + .build() + .await + .expect_err("allowed_permissions without default_permissions should fail"); + + assert!( + err.to_string() + .contains("default_permissions must be set when allowed_permissions is configured"), + "{err}" + ); + Ok(()) +} + +#[tokio::test] +async fn system_managed_default_must_be_allowed() -> 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#" +allowed_permissions = ["managed-standard"] +default_permissions = "managed-build" + +[permissions.managed-standard] +extends = ":read-only" + +[permissions.managed-build] +extends = ":workspace" +"#, + ) + .await?; + + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let err = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(tmp.path().to_path_buf())) + .loader_overrides(overrides) + .build() + .await + .expect_err("managed default outside allowed_permissions should fail"); + + assert!( + err.to_string().contains( + "default_permissions `managed-build` must be included in allowed_permissions" + ), + "{err}" + ); + Ok(()) +} + +#[tokio::test] +async fn system_managed_default_requires_allowed_permissions() -> 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 = ":read-only" "#, ) .await?; + + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let err = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(tmp.path().to_path_buf())) + .loader_overrides(overrides) + .build() + .await + .expect_err("managed default without allowed_permissions should fail"); + + assert!( + err.to_string() + .contains("default_permissions requires allowed_permissions"), + "{err}" + ); + Ok(()) +} + +#[tokio::test] +async fn system_allowed_permissions_fall_back_from_disallowed_danger_full_access() +-> 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), + format!( + r#" +default_permissions = "{BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS}" +"# + ), + ) + .await?; let requirements_path = tmp.path().join("requirements.toml"); tokio::fs::write( &requirements_path, r#" allowed_permissions = ["managed-standard"] +default_permissions = "managed-standard" [permissions.managed-standard.filesystem] ":workspace_roots" = "read" @@ -1517,7 +1632,7 @@ allowed_permissions = ["managed-standard"] } #[tokio::test] -async fn system_allowed_permissions_keep_allowed_explicit_builtin_default() -> anyhow::Result<()> { +async fn system_allowed_permissions_fall_back_from_disallowed_workspace() -> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); tokio::fs::create_dir_all(&codex_home).await?; @@ -1532,7 +1647,8 @@ default_permissions = ":workspace" tokio::fs::write( &requirements_path, r#" -allowed_permissions = [":workspace", "managed-standard"] +allowed_permissions = ["managed-standard"] +default_permissions = "managed-standard" [permissions.managed-standard.filesystem] ":workspace_roots" = "read" @@ -1555,7 +1671,13 @@ allowed_permissions = [":workspace", "managed-standard"] .permissions .active_permission_profile() .map(|profile| profile.id), - Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()) + 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(()) } @@ -1578,6 +1700,7 @@ default_permissions = "managed-build" &requirements_path, r#" allowed_permissions = ["managed-standard", "managed-build"] +default_permissions = "managed-standard" [permissions.managed-standard] extends = ":read-only" @@ -1619,6 +1742,7 @@ async fn system_requirements_warn_for_disallowed_explicit_permission_override() &requirements_path, r#" allowed_permissions = ["managed-standard"] +default_permissions = "managed-standard" [permissions.managed-standard] extends = ":workspace" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 588eb7ac1b4..20e0cc02af3 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8352,6 +8352,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permissions: None, + default_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]), allow_managed_hooks_only: None, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index bc5dfdc4bbc..08b3496d0c8 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -3849,10 +3849,10 @@ fn resolve_default_permissions<'a>( let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { return Ok(selected_permissions); }; - let Some(fallback_permissions) = allowed_permissions.first().map(String::as_str) else { + let Some(fallback_permissions) = requirements_toml.default_permissions.as_deref() else { return Err(std::io::Error::new( ErrorKind::InvalidInput, - "requirements.toml allowed_permissions must include at least one profile", + "requirements.toml default_permissions must be set when allowed_permissions is configured", )); }; @@ -3886,6 +3886,12 @@ fn validate_required_permission_profile_catalog( }; let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { + if requirements_toml.default_permissions.is_some() { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "requirements.toml default_permissions requires allowed_permissions", + )); + } return Ok(()); }; if allowed_permissions.is_empty() { @@ -3906,6 +3912,24 @@ fn validate_required_permission_profile_catalog( } } + let Some(default_permissions) = requirements_toml.default_permissions.as_deref() else { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "requirements.toml default_permissions must be set when allowed_permissions is configured", + )); + }; + if !allowed_permissions + .iter() + .any(|allowed_permission| allowed_permission == default_permissions) + { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!( + "requirements.toml default_permissions `{default_permissions}` must be included in allowed_permissions" + ), + )); + } + Ok(()) } diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 562df95989a..d58fb8c23c6 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -703,6 +703,7 @@ mod tests { allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]), allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), allowed_permissions: None, + default_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]), allow_managed_hooks_only: Some(true), @@ -974,6 +975,7 @@ approval_policy = "never" allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permissions: None, + default_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(Vec::new()), allow_managed_hooks_only: None, From d70eda07414e169b9e93e7787b55bb637168533c Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 3 Jun 2026 12:03:35 -0700 Subject: [PATCH 4/8] fix(permissions): allow implicit standard default --- .../core/src/config/config_loader_tests.rs | 37 ++++++++++++++++++- codex-rs/core/src/config/mod.rs | 28 ++++++++++++-- 2 files changed, 60 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 9bdd747289c..0362653bfad 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -32,6 +32,7 @@ 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_DANGER_FULL_ACCESS; +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; @@ -1499,12 +1500,46 @@ extends = ":read-only" assert!( err.to_string() - .contains("default_permissions must be set when allowed_permissions is configured"), + .contains("default_permissions must be set unless allowed_permissions includes both"), "{err}" ); Ok(()) } +#[tokio::test] +async fn system_allowed_permissions_standard_pair_implicitly_defaults_to_workspace() +-> 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#" +allowed_permissions = [":read-only", ":workspace"] +"#, + ) + .await?; + + 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(tmp.path().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_managed_default_must_be_allowed() -> anyhow::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 08b3496d0c8..8c024c34cdd 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -119,6 +119,7 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use crate::config::permissions::BUILT_IN_READ_ONLY_PROFILE; use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE; use crate::config::permissions::apply_network_proxy_feature_config; use crate::config::permissions::builtin_permission_profile; @@ -3849,10 +3850,14 @@ fn resolve_default_permissions<'a>( let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { return Ok(selected_permissions); }; - let Some(fallback_permissions) = requirements_toml.default_permissions.as_deref() else { + let Some(fallback_permissions) = requirements_toml + .default_permissions + .as_deref() + .or_else(|| implicit_default_permissions(allowed_permissions)) + else { return Err(std::io::Error::new( ErrorKind::InvalidInput, - "requirements.toml default_permissions must be set when allowed_permissions is configured", + "requirements.toml default_permissions must be set unless allowed_permissions includes both `:workspace` and `:read-only`", )); }; @@ -3912,10 +3917,14 @@ fn validate_required_permission_profile_catalog( } } - let Some(default_permissions) = requirements_toml.default_permissions.as_deref() else { + let Some(default_permissions) = requirements_toml + .default_permissions + .as_deref() + .or_else(|| implicit_default_permissions(allowed_permissions)) + else { return Err(std::io::Error::new( ErrorKind::InvalidInput, - "requirements.toml default_permissions must be set when allowed_permissions is configured", + "requirements.toml default_permissions must be set unless allowed_permissions includes both `:workspace` and `:read-only`", )); }; if !allowed_permissions @@ -3933,6 +3942,17 @@ fn validate_required_permission_profile_catalog( Ok(()) } +fn implicit_default_permissions(allowed_permissions: &[String]) -> Option<&'static str> { + let allows_workspace = allowed_permissions + .iter() + .any(|profile_id| profile_id == BUILT_IN_WORKSPACE_PROFILE); + let allows_read_only = allowed_permissions + .iter() + .any(|profile_id| profile_id == BUILT_IN_READ_ONLY_PROFILE); + + (allows_workspace && allows_read_only).then_some(BUILT_IN_WORKSPACE_PROFILE) +} + fn normalize_guardian_policy_config(value: Option<&str>) -> Option { value.and_then(|value| { let trimmed = value.trim(); From 4379a32b69fc770f27e0c843ca391e377eb6c198 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 3 Jun 2026 17:30:32 -0700 Subject: [PATCH 5/8] fix(permissions): make managed allowlist mergeable 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 | 2 +- codex-rs/app-server/README.md | 2 +- .../request_processors/config_processor.rs | 17 +++--- codex-rs/config/src/config_requirements.rs | 21 ++++--- .../src/requirements_layers/stack_tests.rs | 16 ++++++ .../core/src/config/config_loader_tests.rs | 56 +++++++++++++------ codex-rs/core/src/config/mod.rs | 51 +++++++++-------- 11 files changed, 113 insertions(+), 72 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 b2b43a7c089..682e37e202a 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 @@ -8014,11 +8014,11 @@ ] }, "allowedPermissions": { - "items": { - "type": "string" + "additionalProperties": { + "type": "boolean" }, "type": [ - "array", + "object", "null" ] }, 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 79a83ea25ae..46aa15b1300 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 @@ -4356,11 +4356,11 @@ ] }, "allowedPermissions": { - "items": { - "type": "string" + "additionalProperties": { + "type": "boolean" }, "type": [ - "array", + "object", "null" ] }, 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 1ec7cdb8e1d..27b31bbeac3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -95,11 +95,11 @@ ] }, "allowedPermissions": { - "items": { - "type": "string" + "additionalProperties": { + "type": "boolean" }, "type": [ - "array", + "object", "null" ] }, 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 779a9314715..96e333ed8dc 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -8,4 +8,4 @@ import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: Array | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: 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 9491429b0e8..ba8a7e17d67 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -375,7 +375,7 @@ pub struct ConfigRequirements { pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, pub allowed_windows_sandbox_implementations: Option>, - pub allowed_permissions: Option>, + pub allowed_permissions: Option>, pub default_permissions: Option, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index d4b9e8c3bfd..809748cdfe7 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -231,7 +231,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`, `allowedPermissions`), the managed permission-profile default (`defaultPermissions`), 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`), the layered permission-profile allow map (`allowedPermissions`), the managed permission-profile default (`defaultPermissions`), 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 c3a9b9e9a42..acc95ad30f6 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -570,6 +570,7 @@ mod tests { use codex_config::ConfigRequirementsToml; use codex_config::WindowsRequirementsToml; use pretty_assertions::assert_eq; + use std::collections::BTreeMap; #[test] fn requirements_api_includes_allow_managed_hooks_only() { @@ -585,20 +586,20 @@ mod tests { #[test] fn requirements_api_includes_permission_default_and_allowlist() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { - allowed_permissions: Some(vec![ - "managed-standard".to_string(), - "managed-build".to_string(), - ]), + allowed_permissions: Some(BTreeMap::from([ + ("managed-build".to_string(), false), + ("managed-standard".to_string(), true), + ])), default_permissions: Some("managed-standard".to_string()), ..ConfigRequirementsToml::default() }); assert_eq!( mapped.allowed_permissions, - Some(vec![ - "managed-standard".to_string(), - "managed-build".to_string(), - ]) + Some(BTreeMap::from([ + ("managed-build".to_string(), false), + ("managed-standard".to_string(), true), + ])) ); assert_eq!( mapped.default_permissions, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 20ec6676204..28d708b55f7 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -822,7 +822,7 @@ pub struct ConfigRequirementsToml { pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, - pub allowed_permissions: Option>, + pub allowed_permissions: Option>, pub default_permissions: Option, pub remote_sandbox_config: Option>, pub allowed_web_search_modes: Option>, @@ -877,7 +877,7 @@ pub struct ConfigRequirementsWithSources { pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, - pub allowed_permissions: Option>>, + pub allowed_permissions: Option>>, pub default_permissions: Option>, pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, @@ -1603,9 +1603,12 @@ mod tests { fn deserialize_managed_permission_profiles() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" - allowed_permissions = ["managed-standard", "managed-build"] default_permissions = "managed-standard" + [allowed_permissions] + managed-standard = true + managed-build = true + [permissions.managed-standard] extends = ":workspace" @@ -1616,10 +1619,10 @@ mod tests { assert_eq!( requirements.allowed_permissions, - Some(vec![ - "managed-standard".to_string(), - "managed-build".to_string(), - ]) + Some(BTreeMap::from([ + ("managed-build".to_string(), true), + ("managed-standard".to_string(), true), + ])) ); assert_eq!( requirements.default_permissions, @@ -1736,7 +1739,7 @@ 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()), - allowed_permissions: Some(vec!["managed".to_string()]), + allowed_permissions: Some(BTreeMap::from([("managed".to_string(), true)])), default_permissions: Some("managed".to_string()), remote_sandbox_config: None, allowed_web_search_modes: Some(allowed_web_search_modes.clone()), @@ -1771,7 +1774,7 @@ mod tests { )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), allowed_permissions: Some(Sourced::new( - vec!["managed".to_string()], + BTreeMap::from([("managed".to_string(), true)]), source.clone(), )), default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)), diff --git a/codex-rs/config/src/requirements_layers/stack_tests.rs b/codex-rs/config/src/requirements_layers/stack_tests.rs index 8acfd5f0d5b..60d9d1cd3ca 100644 --- a/codex-rs/config/src/requirements_layers/stack_tests.rs +++ b/codex-rs/config/src/requirements_layers/stack_tests.rs @@ -62,6 +62,11 @@ fn top_level_values_use_toml_priority() { r#" allowed_approval_policies = ["on-request"] allowed_sandbox_modes = ["workspace-write"] +default_permissions = ":read-only" + +[allowed_permissions] +":read-only" = true +":workspace" = false "#, ), layer( @@ -70,6 +75,11 @@ allowed_sandbox_modes = ["workspace-write"] r#" allowed_approval_policies = ["never"] allowed_sandbox_modes = ["read-only"] +default_permissions = ":workspace" + +[allowed_permissions] +":danger-full-access" = false +":workspace" = true "#, ), ]) @@ -82,6 +92,12 @@ allowed_sandbox_modes = ["read-only"] r#" allowed_approval_policies = ["never"] allowed_sandbox_modes = ["read-only"] +default_permissions = ":workspace" + +[allowed_permissions] +":danger-full-access" = false +":read-only" = true +":workspace" = true "# ) ); diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 0362653bfad..e5d95cd441e 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1375,9 +1375,11 @@ default_permissions = "managed-standard" tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-standard"] default_permissions = "managed-standard" +[allowed_permissions] +managed-standard = true + [permissions.managed-standard] extends = ":workspace" "#, @@ -1399,7 +1401,7 @@ extends = ":workspace" .config_layer_stack .requirements_toml() .allowed_permissions, - Some(vec!["managed-standard".to_string()]) + Some(BTreeMap::from([("managed-standard".to_string(), true)])) ); assert_eq!( config @@ -1431,9 +1433,12 @@ async fn system_allowed_permissions_select_managed_default_without_explicit_loca tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-build", "managed-standard"] default_permissions = "managed-standard" +[allowed_permissions] +managed-build = true +managed-standard = true + [permissions.managed-standard.filesystem] ":workspace_roots" = "read" @@ -1480,10 +1485,13 @@ async fn system_allowed_permissions_require_managed_default() -> anyhow::Result< tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-standard"] - [permissions.managed-standard] extends = ":read-only" + +[allowed_permissions] +":read-only" = false +":workspace" = false +managed-standard = true "#, ) .await?; @@ -1500,15 +1508,14 @@ extends = ":read-only" assert!( err.to_string() - .contains("default_permissions must be set unless allowed_permissions includes both"), + .contains("default_permissions must be set unless allowed_permissions allows both"), "{err}" ); Ok(()) } #[tokio::test] -async fn system_allowed_permissions_standard_pair_implicitly_defaults_to_workspace() --> anyhow::Result<()> { +async fn system_allowed_permissions_default_standard_pair_to_allowed() -> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); tokio::fs::create_dir_all(&codex_home).await?; @@ -1516,7 +1523,11 @@ async fn system_allowed_permissions_standard_pair_implicitly_defaults_to_workspa tokio::fs::write( &requirements_path, r#" -allowed_permissions = [":read-only", ":workspace"] +[allowed_permissions] +review-only = true + +[permissions.review-only] +extends = ":read-only" "#, ) .await?; @@ -1549,9 +1560,11 @@ async fn system_managed_default_must_be_allowed() -> anyhow::Result<()> { tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-standard"] default_permissions = "managed-build" +[allowed_permissions] +managed-standard = true + [permissions.managed-standard] extends = ":read-only" @@ -1572,9 +1585,8 @@ extends = ":workspace" .expect_err("managed default outside allowed_permissions should fail"); assert!( - err.to_string().contains( - "default_permissions `managed-build` must be included in allowed_permissions" - ), + err.to_string() + .contains("default_permissions `managed-build` must be allowed by allowed_permissions"), "{err}" ); Ok(()) @@ -1631,9 +1643,11 @@ default_permissions = "{BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS}" tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-standard"] default_permissions = "managed-standard" +[allowed_permissions] +managed-standard = true + [permissions.managed-standard.filesystem] ":workspace_roots" = "read" "#, @@ -1682,9 +1696,12 @@ default_permissions = ":workspace" tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-standard"] default_permissions = "managed-standard" +[allowed_permissions] +":workspace" = false +managed-standard = true + [permissions.managed-standard.filesystem] ":workspace_roots" = "read" "#, @@ -1734,9 +1751,12 @@ default_permissions = "managed-build" tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-standard", "managed-build"] default_permissions = "managed-standard" +[allowed_permissions] +managed-build = true +managed-standard = true + [permissions.managed-standard] extends = ":read-only" @@ -1776,9 +1796,11 @@ async fn system_requirements_warn_for_disallowed_explicit_permission_override() tokio::fs::write( &requirements_path, r#" -allowed_permissions = ["managed-standard"] default_permissions = "managed-standard" +[allowed_permissions] +managed-standard = true + [permissions.managed-standard] extends = ":workspace" "#, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 8c024c34cdd..84ea9bb2ae7 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -3857,16 +3857,14 @@ fn resolve_default_permissions<'a>( else { return Err(std::io::Error::new( ErrorKind::InvalidInput, - "requirements.toml default_permissions must be set unless allowed_permissions includes both `:workspace` and `:read-only`", + "requirements.toml default_permissions must be set unless allowed_permissions allows both `:workspace` and `:read-only`", )); }; match selected_permissions { None => Ok(Some(fallback_permissions)), Some(selected_permissions) - if allowed_permissions - .iter() - .any(|allowed_permission| allowed_permission == selected_permissions) => + if is_permission_effectively_allowed(allowed_permissions, selected_permissions) => { Ok(Some(selected_permissions)) } @@ -3899,14 +3897,7 @@ fn validate_required_permission_profile_catalog( } 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 { + for profile_id in allowed_permissions.keys() { if !is_known_profile(profile_id) { return Err(std::io::Error::new( ErrorKind::InvalidInput, @@ -3924,17 +3915,14 @@ fn validate_required_permission_profile_catalog( else { return Err(std::io::Error::new( ErrorKind::InvalidInput, - "requirements.toml default_permissions must be set unless allowed_permissions includes both `:workspace` and `:read-only`", + "requirements.toml default_permissions must be set unless allowed_permissions allows both `:workspace` and `:read-only`", )); }; - if !allowed_permissions - .iter() - .any(|allowed_permission| allowed_permission == default_permissions) - { + if !is_permission_effectively_allowed(allowed_permissions, default_permissions) { return Err(std::io::Error::new( ErrorKind::InvalidInput, format!( - "requirements.toml default_permissions `{default_permissions}` must be included in allowed_permissions" + "requirements.toml default_permissions `{default_permissions}` must be allowed by allowed_permissions" ), )); } @@ -3942,15 +3930,26 @@ fn validate_required_permission_profile_catalog( Ok(()) } -fn implicit_default_permissions(allowed_permissions: &[String]) -> Option<&'static str> { - let allows_workspace = allowed_permissions - .iter() - .any(|profile_id| profile_id == BUILT_IN_WORKSPACE_PROFILE); - let allows_read_only = allowed_permissions - .iter() - .any(|profile_id| profile_id == BUILT_IN_READ_ONLY_PROFILE); +fn implicit_default_permissions( + allowed_permissions: &BTreeMap, +) -> Option<&'static str> { + (is_permission_effectively_allowed(allowed_permissions, BUILT_IN_WORKSPACE_PROFILE) + && is_permission_effectively_allowed(allowed_permissions, BUILT_IN_READ_ONLY_PROFILE)) + .then_some(BUILT_IN_WORKSPACE_PROFILE) +} - (allows_workspace && allows_read_only).then_some(BUILT_IN_WORKSPACE_PROFILE) +/// Applies the managed allow map, including the default-allowed standard profiles. +fn is_permission_effectively_allowed( + allowed_permissions: &BTreeMap, + profile_id: &str, +) -> bool { + allowed_permissions + .get(profile_id) + .copied() + .unwrap_or(matches!( + profile_id, + BUILT_IN_WORKSPACE_PROFILE | BUILT_IN_READ_ONLY_PROFILE + )) } fn normalize_guardian_policy_config(value: Option<&str>) -> Option { From 511bc0ec1f015e95f4c282815a16d2aad75e6b29 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 3 Jun 2026 18:07:59 -0700 Subject: [PATCH 6/8] fix(permissions): make managed map a strict allowlist Co-authored-by: Codex noreply@openai.com --- .../src/requirements_layers/stack_tests.rs | 12 ++++++------ .../core/src/config/config_loader_tests.rs | 12 ++++-------- codex-rs/core/src/config/mod.rs | 19 ++++++------------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/codex-rs/config/src/requirements_layers/stack_tests.rs b/codex-rs/config/src/requirements_layers/stack_tests.rs index 60d9d1cd3ca..3f0df2b5893 100644 --- a/codex-rs/config/src/requirements_layers/stack_tests.rs +++ b/codex-rs/config/src/requirements_layers/stack_tests.rs @@ -62,11 +62,11 @@ fn top_level_values_use_toml_priority() { r#" allowed_approval_policies = ["on-request"] allowed_sandbox_modes = ["workspace-write"] -default_permissions = ":read-only" +default_permissions = ":workspace" [allowed_permissions] ":read-only" = true -":workspace" = false +":workspace" = true "#, ), layer( @@ -75,11 +75,11 @@ default_permissions = ":read-only" r#" allowed_approval_policies = ["never"] allowed_sandbox_modes = ["read-only"] -default_permissions = ":workspace" +default_permissions = ":read-only" [allowed_permissions] ":danger-full-access" = false -":workspace" = true +":workspace" = false "#, ), ]) @@ -92,12 +92,12 @@ default_permissions = ":workspace" r#" allowed_approval_policies = ["never"] allowed_sandbox_modes = ["read-only"] -default_permissions = ":workspace" +default_permissions = ":read-only" [allowed_permissions] ":danger-full-access" = false ":read-only" = true -":workspace" = true +":workspace" = false "# ) ); diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index e5d95cd441e..9c672dcd512 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1489,8 +1489,6 @@ async fn system_allowed_permissions_require_managed_default() -> anyhow::Result< extends = ":read-only" [allowed_permissions] -":read-only" = false -":workspace" = false managed-standard = true "#, ) @@ -1515,7 +1513,8 @@ managed-standard = true } #[tokio::test] -async fn system_allowed_permissions_default_standard_pair_to_allowed() -> anyhow::Result<()> { +async fn system_allowed_permissions_standard_pair_implicitly_defaults_to_workspace() +-> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); tokio::fs::create_dir_all(&codex_home).await?; @@ -1524,10 +1523,8 @@ async fn system_allowed_permissions_default_standard_pair_to_allowed() -> anyhow &requirements_path, r#" [allowed_permissions] -review-only = true - -[permissions.review-only] -extends = ":read-only" +":read-only" = true +":workspace" = true "#, ) .await?; @@ -1699,7 +1696,6 @@ default_permissions = ":workspace" default_permissions = "managed-standard" [allowed_permissions] -":workspace" = false managed-standard = true [permissions.managed-standard.filesystem] diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 84ea9bb2ae7..ec09be8316d 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -3864,7 +3864,7 @@ fn resolve_default_permissions<'a>( match selected_permissions { None => Ok(Some(fallback_permissions)), Some(selected_permissions) - if is_permission_effectively_allowed(allowed_permissions, selected_permissions) => + if is_permission_allowed(allowed_permissions, selected_permissions) => { Ok(Some(selected_permissions)) } @@ -3918,7 +3918,7 @@ fn validate_required_permission_profile_catalog( "requirements.toml default_permissions must be set unless allowed_permissions allows both `:workspace` and `:read-only`", )); }; - if !is_permission_effectively_allowed(allowed_permissions, default_permissions) { + if !is_permission_allowed(allowed_permissions, default_permissions) { return Err(std::io::Error::new( ErrorKind::InvalidInput, format!( @@ -3933,23 +3933,16 @@ fn validate_required_permission_profile_catalog( fn implicit_default_permissions( allowed_permissions: &BTreeMap, ) -> Option<&'static str> { - (is_permission_effectively_allowed(allowed_permissions, BUILT_IN_WORKSPACE_PROFILE) - && is_permission_effectively_allowed(allowed_permissions, BUILT_IN_READ_ONLY_PROFILE)) + (is_permission_allowed(allowed_permissions, BUILT_IN_WORKSPACE_PROFILE) + && is_permission_allowed(allowed_permissions, BUILT_IN_READ_ONLY_PROFILE)) .then_some(BUILT_IN_WORKSPACE_PROFILE) } -/// Applies the managed allow map, including the default-allowed standard profiles. -fn is_permission_effectively_allowed( - allowed_permissions: &BTreeMap, - profile_id: &str, -) -> bool { +fn is_permission_allowed(allowed_permissions: &BTreeMap, profile_id: &str) -> bool { allowed_permissions .get(profile_id) .copied() - .unwrap_or(matches!( - profile_id, - BUILT_IN_WORKSPACE_PROFILE | BUILT_IN_READ_ONLY_PROFILE - )) + .unwrap_or(false) } fn normalize_guardian_policy_config(value: Option<&str>) -> Option { From c330349b3c98b701d7fa60fe1b6c612115ee9db4 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Fri, 5 Jun 2026 10:57:18 -0700 Subject: [PATCH 7/8] fix(permissions): preserve legacy permission allowlists Co-authored-by: Codex noreply@openai.com --- .../codex_app_server_protocol.schemas.json | 11 ++- .../codex_app_server_protocol.v2.schemas.json | 11 ++- .../v2/ConfigRequirementsReadResponse.json | 11 ++- .../typescript/v2/ConfigRequirements.ts | 2 +- .../src/protocol/v2/config.rs | 3 +- .../src/protocol/v2/tests.rs | 1 + codex-rs/app-server/README.md | 2 +- .../request_processors/config_processor.rs | 28 +++++- codex-rs/config/src/config_requirements.rs | 67 ++++++++++++- .../config/src/requirements_layers/layer.rs | 31 +++++++ .../config/src/requirements_layers/stack.rs | 7 ++ .../src/requirements_layers/stack_tests.rs | 84 ++++++++++++++++- .../core/src/config/config_loader_tests.rs | 93 +++++++++++++++---- codex-rs/core/src/config/config_tests.rs | 1 + codex-rs/core/src/config/mod.rs | 43 +++++---- codex-rs/tui/src/debug_config.rs | 2 + 16 files changed, 344 insertions(+), 53 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 682e37e202a..c13bc7a2b75 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 @@ -8013,7 +8013,7 @@ "null" ] }, - "allowedPermissions": { + "allowedPermissionProfiles": { "additionalProperties": { "type": "boolean" }, @@ -8022,6 +8022,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/v2/SandboxMode" 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 46aa15b1300..4942f55bad6 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 @@ -4355,7 +4355,7 @@ "null" ] }, - "allowedPermissions": { + "allowedPermissionProfiles": { "additionalProperties": { "type": "boolean" }, @@ -4364,6 +4364,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" 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 27b31bbeac3..a5524c0881c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -94,7 +94,7 @@ "null" ] }, - "allowedPermissions": { + "allowedPermissionProfiles": { "additionalProperties": { "type": "boolean" }, @@ -103,6 +103,15 @@ "null" ] }, + "allowedPermissions": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" 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 96e333ed8dc..09225a34e07 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -8,4 +8,4 @@ import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: 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 ba8a7e17d67..e339599eea8 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -375,7 +375,8 @@ pub struct ConfigRequirements { pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, pub allowed_windows_sandbox_implementations: Option>, - pub allowed_permissions: Option>, + pub allowed_permissions: Option>, + pub allowed_permission_profiles: Option>, pub default_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 2d8011b1a60..c2e0a7e80a5 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1675,6 +1675,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() allowed_sandbox_modes: None, allowed_windows_sandbox_implementations: None, allowed_permissions: None, + allowed_permission_profiles: None, default_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 809748cdfe7..61b76060c83 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -231,7 +231,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`), the layered permission-profile allow map (`allowedPermissions`), the managed permission-profile default (`defaultPermissions`), 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`), the legacy permission-profile list (`allowedPermissions`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), 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 acc95ad30f6..dddb47ac2cc 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -314,6 +314,25 @@ impl ConfigRequestProcessor { } fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigRequirements { + let allowed_permission_profiles = requirements.allowed_permission_profiles.or_else(|| { + requirements.allowed_permissions.as_ref().map(|profiles| { + profiles + .iter() + .cloned() + .map(|profile| (profile, true)) + .collect() + }) + }); + let allowed_permissions = requirements.allowed_permissions.or_else(|| { + allowed_permission_profiles.as_ref().map(|profiles| { + profiles + .iter() + .filter(|(_, allowed)| **allowed) + .map(|(profile, _)| profile.clone()) + .collect() + }) + }); + ConfigRequirements { allowed_approval_policies: requirements.allowed_approval_policies.map(|policies| { policies @@ -350,7 +369,8 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .collect() }) }), - allowed_permissions: requirements.allowed_permissions, + allowed_permissions, + allowed_permission_profiles, default_permissions: requirements.default_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes @@ -586,7 +606,7 @@ mod tests { #[test] fn requirements_api_includes_permission_default_and_allowlist() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { - allowed_permissions: Some(BTreeMap::from([ + allowed_permission_profiles: Some(BTreeMap::from([ ("managed-build".to_string(), false), ("managed-standard".to_string(), true), ])), @@ -596,6 +616,10 @@ mod tests { assert_eq!( mapped.allowed_permissions, + Some(vec!["managed-standard".to_string()]) + ); + assert_eq!( + mapped.allowed_permission_profiles, Some(BTreeMap::from([ ("managed-build".to_string(), false), ("managed-standard".to_string(), true), diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 28d708b55f7..68a41cce6ff 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -822,7 +822,8 @@ pub struct ConfigRequirementsToml { pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, - pub allowed_permissions: Option>, + pub allowed_permissions: Option>, + pub allowed_permission_profiles: Option>, pub default_permissions: Option, pub remote_sandbox_config: Option>, pub allowed_web_search_modes: Option>, @@ -877,7 +878,8 @@ pub struct ConfigRequirementsWithSources { pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, - pub allowed_permissions: Option>>, + pub allowed_permissions: Option>>, + pub allowed_permission_profiles: Option>>, pub default_permissions: Option>, pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, @@ -919,6 +921,7 @@ impl ConfigRequirementsWithSources { allowed_approvals_reviewers: _, allowed_sandbox_modes: _, allowed_permissions: _, + allowed_permission_profiles: _, default_permissions: _, remote_sandbox_config: _, allowed_web_search_modes: _, @@ -955,6 +958,7 @@ impl ConfigRequirementsWithSources { allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions, + allowed_permission_profiles, default_permissions, allowed_web_search_modes, allow_managed_hooks_only, @@ -988,6 +992,7 @@ impl ConfigRequirementsWithSources { allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions, + allowed_permission_profiles, default_permissions, allowed_web_search_modes, allow_managed_hooks_only, @@ -1010,6 +1015,7 @@ impl ConfigRequirementsWithSources { allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), allowed_permissions: allowed_permissions.map(|sourced| sourced.value), + allowed_permission_profiles: allowed_permission_profiles.map(|sourced| sourced.value), default_permissions: default_permissions.map(|sourced| sourced.value), remote_sandbox_config: None, allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value), @@ -1078,6 +1084,45 @@ pub enum ResidencyRequirement { } impl ConfigRequirementsToml { + pub(crate) fn normalize_permission_allowlist(&mut self) -> Result<(), String> { + let legacy_profiles = self.allowed_permissions.as_ref().map(|profiles| { + profiles + .iter() + .cloned() + .map(|profile| (profile, true)) + .collect::>() + }); + + if let (Some(legacy_profiles), Some(profile_map)) = + (&legacy_profiles, &self.allowed_permission_profiles) + { + let allowed_profiles = profile_map + .iter() + .filter(|(_, allowed)| **allowed) + .map(|(profile, _)| (profile.clone(), true)) + .collect::>(); + if legacy_profiles != &allowed_profiles { + return Err( + "allowed_permissions must match the profiles set to true in allowed_permission_profiles" + .to_string(), + ); + } + } + + if self.allowed_permission_profiles.is_none() { + self.allowed_permission_profiles = legacy_profiles; + } + if self.default_permissions.is_none() { + self.default_permissions = self + .allowed_permissions + .as_ref() + .and_then(|profiles| profiles.first()) + .cloned(); + } + + Ok(()) + } + pub fn apply_remote_sandbox_config(&mut self, hostname: Option<&str>) { let Some(remote_sandbox_config) = self.remote_sandbox_config.as_ref() else { return; @@ -1099,6 +1144,7 @@ impl ConfigRequirementsToml { && self.allowed_approvals_reviewers.is_none() && self.allowed_sandbox_modes.is_none() && self.allowed_permissions.is_none() + && self.allowed_permission_profiles.is_none() && self.default_permissions.is_none() && self.remote_sandbox_config.is_none() && self.allowed_web_search_modes.is_none() @@ -1152,6 +1198,7 @@ impl TryFrom for ConfigRequirements { allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions: _, + allowed_permission_profiles: _, default_permissions: _, allowed_web_search_modes, allow_managed_hooks_only, @@ -1520,6 +1567,7 @@ mod tests { allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions, + allowed_permission_profiles, default_permissions, remote_sandbox_config: _, allowed_web_search_modes, @@ -1547,6 +1595,8 @@ mod tests { .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_permissions: allowed_permissions .map(|value| Sourced::new(value, RequirementSource::Unknown)), + allowed_permission_profiles: allowed_permission_profiles + .map(|value| Sourced::new(value, RequirementSource::Unknown)), default_permissions: default_permissions .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_web_search_modes: allowed_web_search_modes @@ -1605,7 +1655,7 @@ mod tests { r#" default_permissions = "managed-standard" - [allowed_permissions] + [allowed_permission_profiles] managed-standard = true managed-build = true @@ -1618,7 +1668,7 @@ mod tests { )?; assert_eq!( - requirements.allowed_permissions, + requirements.allowed_permission_profiles, Some(BTreeMap::from([ ("managed-build".to_string(), true), ("managed-standard".to_string(), true), @@ -1739,7 +1789,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()), - allowed_permissions: Some(BTreeMap::from([("managed".to_string(), true)])), + allowed_permissions: Some(vec!["managed".to_string()]), + allowed_permission_profiles: Some(BTreeMap::from([("managed".to_string(), true)])), default_permissions: Some("managed".to_string()), remote_sandbox_config: None, allowed_web_search_modes: Some(allowed_web_search_modes.clone()), @@ -1774,6 +1825,10 @@ mod tests { )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), allowed_permissions: Some(Sourced::new( + vec!["managed".to_string()], + source.clone(), + )), + allowed_permission_profiles: Some(Sourced::new( BTreeMap::from([("managed".to_string(), true)]), source.clone(), )), @@ -1831,6 +1886,7 @@ mod tests { allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permissions: None, + allowed_permission_profiles: None, default_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1884,6 +1940,7 @@ mod tests { allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permissions: None, + allowed_permission_profiles: None, default_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/config/src/requirements_layers/layer.rs b/codex-rs/config/src/requirements_layers/layer.rs index d92024e1170..841f81a7882 100644 --- a/codex-rs/config/src/requirements_layers/layer.rs +++ b/codex-rs/config/src/requirements_layers/layer.rs @@ -70,6 +70,13 @@ impl ComposableRequirementsLayer { (regular_toml, requirements) }; + requirements + .normalize_permission_allowlist() + .map_err(|message| RequirementsCompositionError::Parse { + layer_source: source.clone(), + message, + })?; + materialize_permission_allowlist(&mut regular_toml, &requirements)?; requirements.apply_remote_sandbox_config(hostname); materialize_remote_sandbox_config(&mut regular_toml, &requirements)?; strip_special_fields(&mut regular_toml); @@ -134,6 +141,30 @@ fn parse_layer_requirements( } } +fn materialize_permission_allowlist( + layer_toml: &mut TomlValue, + requirements: &ConfigRequirementsToml, +) -> Result<(), RequirementsCompositionError> { + remove_top_level_field(layer_toml, "allowed_permissions"); + remove_top_level_field(layer_toml, "allowed_permission_profiles"); + let Some(table) = layer_toml.as_table_mut() else { + return Ok(()); + }; + if let Some(allowed_permission_profiles) = requirements.allowed_permission_profiles.as_ref() { + table.insert( + "allowed_permission_profiles".to_string(), + toml_value_from_serializable(allowed_permission_profiles)?, + ); + } + if let Some(default_permissions) = requirements.default_permissions.as_ref() { + table.insert( + "default_permissions".to_string(), + TomlValue::String(default_permissions.clone()), + ); + } + Ok(()) +} + fn materialize_remote_sandbox_config( layer_toml: &mut TomlValue, requirements: &ConfigRequirementsToml, diff --git a/codex-rs/config/src/requirements_layers/stack.rs b/codex-rs/config/src/requirements_layers/stack.rs index e93dd1c1da4..0719b60e856 100644 --- a/codex-rs/config/src/requirements_layers/stack.rs +++ b/codex-rs/config/src/requirements_layers/stack.rs @@ -177,6 +177,8 @@ fn populate_merged_regular_fields_with_sources( allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permissions, + allowed_permission_profiles, + default_permissions, remote_sandbox_config: _, allowed_web_search_modes, allow_managed_hooks_only, @@ -202,6 +204,11 @@ fn populate_merged_regular_fields_with_sources( ); set_sourced!(allowed_sandbox_modes, &["allowed_sandbox_modes"]); set_sourced!(allowed_permissions, &["allowed_permissions"]); + set_sourced!( + allowed_permission_profiles, + &["allowed_permission_profiles"] + ); + set_sourced!(default_permissions, &["default_permissions"]); set_sourced!(allowed_web_search_modes, &["allowed_web_search_modes"]); set_sourced!(allow_managed_hooks_only, &["allow_managed_hooks_only"]); set_sourced!(allow_appshots, &["allow_appshots"]); diff --git a/codex-rs/config/src/requirements_layers/stack_tests.rs b/codex-rs/config/src/requirements_layers/stack_tests.rs index 3f0df2b5893..439e0751601 100644 --- a/codex-rs/config/src/requirements_layers/stack_tests.rs +++ b/codex-rs/config/src/requirements_layers/stack_tests.rs @@ -64,7 +64,7 @@ allowed_approval_policies = ["on-request"] allowed_sandbox_modes = ["workspace-write"] default_permissions = ":workspace" -[allowed_permissions] +[allowed_permission_profiles] ":read-only" = true ":workspace" = true "#, @@ -77,7 +77,7 @@ allowed_approval_policies = ["never"] allowed_sandbox_modes = ["read-only"] default_permissions = ":read-only" -[allowed_permissions] +[allowed_permission_profiles] ":danger-full-access" = false ":workspace" = false "#, @@ -94,7 +94,7 @@ allowed_approval_policies = ["never"] allowed_sandbox_modes = ["read-only"] default_permissions = ":read-only" -[allowed_permissions] +[allowed_permission_profiles] ":danger-full-access" = false ":read-only" = true ":workspace" = false @@ -103,6 +103,84 @@ default_permissions = ":read-only" ); } +#[test] +fn legacy_allowed_permissions_normalize_to_profile_map_and_default() { + let composed = compose(vec![layer( + "req_legacy", + "Legacy", + r#" +allowed_permissions = [":read-only", ":workspace"] +"#, + )]) + .expect("compose legacy requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +default_permissions = ":read-only" + +[allowed_permission_profiles] +":read-only" = true +":workspace" = true +"# + ) + ); +} + +#[test] +fn matching_legacy_and_map_allowlists_compose() { + let composed = compose(vec![layer( + "req_dual", + "Dual", + r#" +allowed_permissions = [":workspace"] + +[allowed_permission_profiles] +":read-only" = false +":workspace" = true +"#, + )]) + .expect("compose dual requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +default_permissions = ":workspace" + +[allowed_permission_profiles] +":read-only" = false +":workspace" = true +"# + ) + ); +} + +#[test] +fn conflicting_legacy_and_map_allowlists_fail() { + let err = compose(vec![layer( + "req_conflict", + "Conflict", + r#" +allowed_permissions = [":read-only"] + +[allowed_permission_profiles] +":workspace" = true +"#, + )]) + .expect_err("conflicting allowlists should fail"); + + assert!( + err.to_string().contains( + "allowed_permissions must match the profiles set to true in allowed_permission_profiles" + ), + "{err}" + ); +} + #[test] fn composition_strategy_applies_to_non_cloud_layers() { let mdm_source = RequirementSource::MdmManagedPreferences { diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 9c672dcd512..397db0287c9 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1377,7 +1377,7 @@ default_permissions = "managed-standard" r#" default_permissions = "managed-standard" -[allowed_permissions] +[allowed_permission_profiles] managed-standard = true [permissions.managed-standard] @@ -1400,7 +1400,7 @@ extends = ":workspace" config .config_layer_stack .requirements_toml() - .allowed_permissions, + .allowed_permission_profiles, Some(BTreeMap::from([("managed-standard".to_string(), true)])) ); assert_eq!( @@ -1435,7 +1435,7 @@ async fn system_allowed_permissions_select_managed_default_without_explicit_loca r#" default_permissions = "managed-standard" -[allowed_permissions] +[allowed_permission_profiles] managed-build = true managed-standard = true @@ -1488,7 +1488,7 @@ async fn system_allowed_permissions_require_managed_default() -> anyhow::Result< [permissions.managed-standard] extends = ":read-only" -[allowed_permissions] +[allowed_permission_profiles] managed-standard = true "#, ) @@ -1502,11 +1502,12 @@ managed-standard = true .loader_overrides(overrides) .build() .await - .expect_err("allowed_permissions without default_permissions should fail"); + .expect_err("allowed_permission_profiles without default_permissions should fail"); assert!( - err.to_string() - .contains("default_permissions must be set unless allowed_permissions allows both"), + err.to_string().contains( + "default_permissions must be set unless allowed_permission_profiles allows both" + ), "{err}" ); Ok(()) @@ -1522,7 +1523,7 @@ async fn system_allowed_permissions_standard_pair_implicitly_defaults_to_workspa tokio::fs::write( &requirements_path, r#" -[allowed_permissions] +[allowed_permission_profiles] ":read-only" = true ":workspace" = true "#, @@ -1548,6 +1549,59 @@ async fn system_allowed_permissions_standard_pair_implicitly_defaults_to_workspa Ok(()) } +#[tokio::test] +async fn system_legacy_allowed_permissions_use_first_profile_as_default() -> 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#" +allowed_permissions = ["managed-standard", ":workspace"] + +[permissions.managed-standard] +extends = ":read-only" +"#, + ) + .await?; + + 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(tmp.path().to_path_buf())) + .loader_overrides(overrides) + .build() + .await?; + + assert_eq!( + config + .permissions + .active_permission_profile() + .map(|profile| profile.id), + Some("managed-standard".to_string()) + ); + assert_eq!( + config + .config_layer_stack + .requirements_toml() + .allowed_permission_profiles, + Some(BTreeMap::from([ + (":workspace".to_string(), true), + ("managed-standard".to_string(), true), + ])) + ); + assert_eq!( + config + .config_layer_stack + .requirements_toml() + .default_permissions, + Some("managed-standard".to_string()) + ); + Ok(()) +} + #[tokio::test] async fn system_managed_default_must_be_allowed() -> anyhow::Result<()> { let tmp = tempdir()?; @@ -1559,7 +1613,7 @@ async fn system_managed_default_must_be_allowed() -> anyhow::Result<()> { r#" default_permissions = "managed-build" -[allowed_permissions] +[allowed_permission_profiles] managed-standard = true [permissions.managed-standard] @@ -1579,18 +1633,19 @@ extends = ":workspace" .loader_overrides(overrides) .build() .await - .expect_err("managed default outside allowed_permissions should fail"); + .expect_err("managed default outside allowed_permission_profiles should fail"); assert!( - err.to_string() - .contains("default_permissions `managed-build` must be allowed by allowed_permissions"), + err.to_string().contains( + "default_permissions `managed-build` must be allowed by allowed_permission_profiles" + ), "{err}" ); Ok(()) } #[tokio::test] -async fn system_managed_default_requires_allowed_permissions() -> anyhow::Result<()> { +async fn system_managed_default_requires_allowed_permission_profiles() -> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); tokio::fs::create_dir_all(&codex_home).await?; @@ -1611,11 +1666,11 @@ default_permissions = ":read-only" .loader_overrides(overrides) .build() .await - .expect_err("managed default without allowed_permissions should fail"); + .expect_err("managed default without allowed_permission_profiles should fail"); assert!( err.to_string() - .contains("default_permissions requires allowed_permissions"), + .contains("default_permissions requires allowed_permission_profiles"), "{err}" ); Ok(()) @@ -1642,7 +1697,7 @@ default_permissions = "{BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS}" r#" default_permissions = "managed-standard" -[allowed_permissions] +[allowed_permission_profiles] managed-standard = true [permissions.managed-standard.filesystem] @@ -1695,7 +1750,7 @@ default_permissions = ":workspace" r#" default_permissions = "managed-standard" -[allowed_permissions] +[allowed_permission_profiles] managed-standard = true [permissions.managed-standard.filesystem] @@ -1749,7 +1804,7 @@ default_permissions = "managed-build" r#" default_permissions = "managed-standard" -[allowed_permissions] +[allowed_permission_profiles] managed-build = true managed-standard = true @@ -1794,7 +1849,7 @@ async fn system_requirements_warn_for_disallowed_explicit_permission_override() r#" default_permissions = "managed-standard" -[allowed_permissions] +[allowed_permission_profiles] managed-standard = true [permissions.managed-standard] diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 20e0cc02af3..c479e4be9ed 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8352,6 +8352,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permissions: None, + allowed_permission_profiles: None, default_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index ec09be8316d..fd74fcf2d25 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -3836,7 +3836,9 @@ fn resolve_effective_permission_selection<'a>( Ok(EffectivePermissionSelection { profiles, selected_profile_id, - requirements_force_profile_selection: requirements_toml.allowed_permissions.is_some(), + requirements_force_profile_selection: requirements_toml + .allowed_permission_profiles + .is_some(), }) } @@ -3847,24 +3849,25 @@ fn resolve_default_permissions<'a>( startup_warnings: &mut Vec, ) -> std::io::Result> { let selected_permissions = default_permissions_override.or(configured_default_permissions); - let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { + let Some(allowed_permission_profiles) = requirements_toml.allowed_permission_profiles.as_ref() + else { return Ok(selected_permissions); }; let Some(fallback_permissions) = requirements_toml .default_permissions .as_deref() - .or_else(|| implicit_default_permissions(allowed_permissions)) + .or_else(|| implicit_default_permissions(allowed_permission_profiles)) else { return Err(std::io::Error::new( ErrorKind::InvalidInput, - "requirements.toml default_permissions must be set unless allowed_permissions allows both `:workspace` and `:read-only`", + "requirements.toml default_permissions must be set unless allowed_permission_profiles allows both `:workspace` and `:read-only`", )); }; match selected_permissions { None => Ok(Some(fallback_permissions)), Some(selected_permissions) - if is_permission_allowed(allowed_permissions, selected_permissions) => + if is_permission_allowed(allowed_permission_profiles, selected_permissions) => { Ok(Some(selected_permissions)) } @@ -3888,21 +3891,22 @@ fn validate_required_permission_profile_catalog( .is_some_and(|permissions| permissions.entries.contains_key(profile_id)) }; - let Some(allowed_permissions) = requirements_toml.allowed_permissions.as_ref() else { + let Some(allowed_permission_profiles) = requirements_toml.allowed_permission_profiles.as_ref() + else { if requirements_toml.default_permissions.is_some() { return Err(std::io::Error::new( ErrorKind::InvalidInput, - "requirements.toml default_permissions requires allowed_permissions", + "requirements.toml default_permissions requires allowed_permission_profiles", )); } return Ok(()); }; - for profile_id in allowed_permissions.keys() { + for profile_id in allowed_permission_profiles.keys() { 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}`" + "requirements.toml allowed_permission_profiles refers to undefined profile `{profile_id}`" ), )); } @@ -3911,18 +3915,18 @@ fn validate_required_permission_profile_catalog( let Some(default_permissions) = requirements_toml .default_permissions .as_deref() - .or_else(|| implicit_default_permissions(allowed_permissions)) + .or_else(|| implicit_default_permissions(allowed_permission_profiles)) else { return Err(std::io::Error::new( ErrorKind::InvalidInput, - "requirements.toml default_permissions must be set unless allowed_permissions allows both `:workspace` and `:read-only`", + "requirements.toml default_permissions must be set unless allowed_permission_profiles allows both `:workspace` and `:read-only`", )); }; - if !is_permission_allowed(allowed_permissions, default_permissions) { + if !is_permission_allowed(allowed_permission_profiles, default_permissions) { return Err(std::io::Error::new( ErrorKind::InvalidInput, format!( - "requirements.toml default_permissions `{default_permissions}` must be allowed by allowed_permissions" + "requirements.toml default_permissions `{default_permissions}` must be allowed by allowed_permission_profiles" ), )); } @@ -3931,15 +3935,18 @@ fn validate_required_permission_profile_catalog( } fn implicit_default_permissions( - allowed_permissions: &BTreeMap, + allowed_permission_profiles: &BTreeMap, ) -> Option<&'static str> { - (is_permission_allowed(allowed_permissions, BUILT_IN_WORKSPACE_PROFILE) - && is_permission_allowed(allowed_permissions, BUILT_IN_READ_ONLY_PROFILE)) + (is_permission_allowed(allowed_permission_profiles, BUILT_IN_WORKSPACE_PROFILE) + && is_permission_allowed(allowed_permission_profiles, BUILT_IN_READ_ONLY_PROFILE)) .then_some(BUILT_IN_WORKSPACE_PROFILE) } -fn is_permission_allowed(allowed_permissions: &BTreeMap, profile_id: &str) -> bool { - allowed_permissions +fn is_permission_allowed( + allowed_permission_profiles: &BTreeMap, + profile_id: &str, +) -> bool { + allowed_permission_profiles .get(profile_id) .copied() .unwrap_or(false) diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index d58fb8c23c6..135ad3cd76b 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -703,6 +703,7 @@ mod tests { allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]), allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), allowed_permissions: None, + allowed_permission_profiles: None, default_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]), @@ -975,6 +976,7 @@ approval_policy = "never" allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permissions: None, + allowed_permission_profiles: None, default_permissions: None, remote_sandbox_config: None, allowed_web_search_modes: Some(Vec::new()), From bc249ef5b32f3883950892d1ec03fe165a746130 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Fri, 5 Jun 2026 15:11:56 -0700 Subject: [PATCH 8/8] fix(permissions): remove legacy permission allowlist Co-authored-by: Codex noreply@openai.com --- .../codex_app_server_protocol.schemas.json | 9 --- .../codex_app_server_protocol.v2.schemas.json | 9 --- .../v2/ConfigRequirementsReadResponse.json | 9 --- .../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 | 26 +------ codex-rs/config/src/config_requirements.rs | 57 -------------- .../config/src/requirements_layers/layer.rs | 31 -------- .../config/src/requirements_layers/stack.rs | 2 - .../src/requirements_layers/stack_tests.rs | 78 ------------------- .../core/src/config/config_loader_tests.rs | 64 ++------------- codex-rs/core/src/config/config_tests.rs | 1 - codex-rs/tui/src/debug_config.rs | 2 - 15 files changed, 9 insertions(+), 285 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 c13bc7a2b75..ce0c75d349d 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 @@ -8022,15 +8022,6 @@ "null" ] }, - "allowedPermissions": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/v2/SandboxMode" 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 4942f55bad6..981a70faff0 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 @@ -4364,15 +4364,6 @@ "null" ] }, - "allowedPermissions": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" 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 a5524c0881c..def89d64e36 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -103,15 +103,6 @@ "null" ] }, - "allowedPermissions": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, "allowedSandboxModes": { "items": { "$ref": "#/definitions/SandboxMode" 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 09225a34e07..29704982ff5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -8,4 +8,4 @@ import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: 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 e339599eea8..227adffc30b 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -375,7 +375,6 @@ pub struct ConfigRequirements { pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, pub allowed_windows_sandbox_implementations: Option>, - pub allowed_permissions: Option>, pub allowed_permission_profiles: Option>, pub default_permissions: Option, pub allowed_web_search_modes: 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 c2e0a7e80a5..4a683be9904 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1674,7 +1674,6 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_windows_sandbox_implementations: None, - allowed_permissions: None, allowed_permission_profiles: None, default_permissions: None, allowed_web_search_modes: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 61b76060c83..8863b279e73 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -231,7 +231,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`), the legacy permission-profile list (`allowedPermissions`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), 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`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), 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 dddb47ac2cc..bdc80139509 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -314,25 +314,6 @@ impl ConfigRequestProcessor { } fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigRequirements { - let allowed_permission_profiles = requirements.allowed_permission_profiles.or_else(|| { - requirements.allowed_permissions.as_ref().map(|profiles| { - profiles - .iter() - .cloned() - .map(|profile| (profile, true)) - .collect() - }) - }); - let allowed_permissions = requirements.allowed_permissions.or_else(|| { - allowed_permission_profiles.as_ref().map(|profiles| { - profiles - .iter() - .filter(|(_, allowed)| **allowed) - .map(|(profile, _)| profile.clone()) - .collect() - }) - }); - ConfigRequirements { allowed_approval_policies: requirements.allowed_approval_policies.map(|policies| { policies @@ -369,8 +350,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .collect() }) }), - allowed_permissions, - allowed_permission_profiles, + allowed_permission_profiles: requirements.allowed_permission_profiles, default_permissions: requirements.default_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes @@ -614,10 +594,6 @@ mod tests { ..ConfigRequirementsToml::default() }); - assert_eq!( - mapped.allowed_permissions, - Some(vec!["managed-standard".to_string()]) - ); assert_eq!( mapped.allowed_permission_profiles, Some(BTreeMap::from([ diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 68a41cce6ff..1fbdebbd0a3 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -822,7 +822,6 @@ pub struct ConfigRequirementsToml { pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, - pub allowed_permissions: Option>, pub allowed_permission_profiles: Option>, pub default_permissions: Option, pub remote_sandbox_config: Option>, @@ -878,7 +877,6 @@ pub struct ConfigRequirementsWithSources { pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, - pub allowed_permissions: Option>>, pub allowed_permission_profiles: Option>>, pub default_permissions: Option>, pub allowed_web_search_modes: Option>>, @@ -920,7 +918,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies: _, allowed_approvals_reviewers: _, allowed_sandbox_modes: _, - allowed_permissions: _, allowed_permission_profiles: _, default_permissions: _, remote_sandbox_config: _, @@ -957,7 +954,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - allowed_permissions, allowed_permission_profiles, default_permissions, allowed_web_search_modes, @@ -991,7 +987,6 @@ impl ConfigRequirementsWithSources { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - allowed_permissions, allowed_permission_profiles, default_permissions, allowed_web_search_modes, @@ -1014,7 +1009,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), - allowed_permissions: allowed_permissions.map(|sourced| sourced.value), allowed_permission_profiles: allowed_permission_profiles.map(|sourced| sourced.value), default_permissions: default_permissions.map(|sourced| sourced.value), remote_sandbox_config: None, @@ -1084,45 +1078,6 @@ pub enum ResidencyRequirement { } impl ConfigRequirementsToml { - pub(crate) fn normalize_permission_allowlist(&mut self) -> Result<(), String> { - let legacy_profiles = self.allowed_permissions.as_ref().map(|profiles| { - profiles - .iter() - .cloned() - .map(|profile| (profile, true)) - .collect::>() - }); - - if let (Some(legacy_profiles), Some(profile_map)) = - (&legacy_profiles, &self.allowed_permission_profiles) - { - let allowed_profiles = profile_map - .iter() - .filter(|(_, allowed)| **allowed) - .map(|(profile, _)| (profile.clone(), true)) - .collect::>(); - if legacy_profiles != &allowed_profiles { - return Err( - "allowed_permissions must match the profiles set to true in allowed_permission_profiles" - .to_string(), - ); - } - } - - if self.allowed_permission_profiles.is_none() { - self.allowed_permission_profiles = legacy_profiles; - } - if self.default_permissions.is_none() { - self.default_permissions = self - .allowed_permissions - .as_ref() - .and_then(|profiles| profiles.first()) - .cloned(); - } - - Ok(()) - } - pub fn apply_remote_sandbox_config(&mut self, hostname: Option<&str>) { let Some(remote_sandbox_config) = self.remote_sandbox_config.as_ref() else { return; @@ -1143,7 +1098,6 @@ impl ConfigRequirementsToml { self.allowed_approval_policies.is_none() && self.allowed_approvals_reviewers.is_none() && self.allowed_sandbox_modes.is_none() - && self.allowed_permissions.is_none() && self.allowed_permission_profiles.is_none() && self.default_permissions.is_none() && self.remote_sandbox_config.is_none() @@ -1197,7 +1151,6 @@ impl TryFrom for ConfigRequirements { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - allowed_permissions: _, allowed_permission_profiles: _, default_permissions: _, allowed_web_search_modes, @@ -1566,7 +1519,6 @@ mod tests { allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - allowed_permissions, allowed_permission_profiles, default_permissions, remote_sandbox_config: _, @@ -1593,8 +1545,6 @@ mod tests { .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_sandbox_modes: allowed_sandbox_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), - allowed_permissions: allowed_permissions - .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_permission_profiles: allowed_permission_profiles .map(|value| Sourced::new(value, RequirementSource::Unknown)), default_permissions: default_permissions @@ -1789,7 +1739,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()), - allowed_permissions: Some(vec!["managed".to_string()]), allowed_permission_profiles: Some(BTreeMap::from([("managed".to_string(), true)])), default_permissions: Some("managed".to_string()), remote_sandbox_config: None, @@ -1824,10 +1773,6 @@ mod tests { source.clone(), )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), - allowed_permissions: Some(Sourced::new( - vec!["managed".to_string()], - source.clone(), - )), allowed_permission_profiles: Some(Sourced::new( BTreeMap::from([("managed".to_string(), true)]), source.clone(), @@ -1885,7 +1830,6 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - allowed_permissions: None, allowed_permission_profiles: None, default_permissions: None, allowed_web_search_modes: None, @@ -1939,7 +1883,6 @@ mod tests { )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - allowed_permissions: None, allowed_permission_profiles: None, default_permissions: None, allowed_web_search_modes: None, diff --git a/codex-rs/config/src/requirements_layers/layer.rs b/codex-rs/config/src/requirements_layers/layer.rs index 841f81a7882..d92024e1170 100644 --- a/codex-rs/config/src/requirements_layers/layer.rs +++ b/codex-rs/config/src/requirements_layers/layer.rs @@ -70,13 +70,6 @@ impl ComposableRequirementsLayer { (regular_toml, requirements) }; - requirements - .normalize_permission_allowlist() - .map_err(|message| RequirementsCompositionError::Parse { - layer_source: source.clone(), - message, - })?; - materialize_permission_allowlist(&mut regular_toml, &requirements)?; requirements.apply_remote_sandbox_config(hostname); materialize_remote_sandbox_config(&mut regular_toml, &requirements)?; strip_special_fields(&mut regular_toml); @@ -141,30 +134,6 @@ fn parse_layer_requirements( } } -fn materialize_permission_allowlist( - layer_toml: &mut TomlValue, - requirements: &ConfigRequirementsToml, -) -> Result<(), RequirementsCompositionError> { - remove_top_level_field(layer_toml, "allowed_permissions"); - remove_top_level_field(layer_toml, "allowed_permission_profiles"); - let Some(table) = layer_toml.as_table_mut() else { - return Ok(()); - }; - if let Some(allowed_permission_profiles) = requirements.allowed_permission_profiles.as_ref() { - table.insert( - "allowed_permission_profiles".to_string(), - toml_value_from_serializable(allowed_permission_profiles)?, - ); - } - if let Some(default_permissions) = requirements.default_permissions.as_ref() { - table.insert( - "default_permissions".to_string(), - TomlValue::String(default_permissions.clone()), - ); - } - Ok(()) -} - fn materialize_remote_sandbox_config( layer_toml: &mut TomlValue, requirements: &ConfigRequirementsToml, diff --git a/codex-rs/config/src/requirements_layers/stack.rs b/codex-rs/config/src/requirements_layers/stack.rs index 0719b60e856..0396cda237a 100644 --- a/codex-rs/config/src/requirements_layers/stack.rs +++ b/codex-rs/config/src/requirements_layers/stack.rs @@ -176,7 +176,6 @@ fn populate_merged_regular_fields_with_sources( allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, - allowed_permissions, allowed_permission_profiles, default_permissions, remote_sandbox_config: _, @@ -203,7 +202,6 @@ fn populate_merged_regular_fields_with_sources( &["allowed_approvals_reviewers"] ); set_sourced!(allowed_sandbox_modes, &["allowed_sandbox_modes"]); - set_sourced!(allowed_permissions, &["allowed_permissions"]); set_sourced!( allowed_permission_profiles, &["allowed_permission_profiles"] diff --git a/codex-rs/config/src/requirements_layers/stack_tests.rs b/codex-rs/config/src/requirements_layers/stack_tests.rs index 439e0751601..82a99a7f0ac 100644 --- a/codex-rs/config/src/requirements_layers/stack_tests.rs +++ b/codex-rs/config/src/requirements_layers/stack_tests.rs @@ -103,84 +103,6 @@ default_permissions = ":read-only" ); } -#[test] -fn legacy_allowed_permissions_normalize_to_profile_map_and_default() { - let composed = compose(vec![layer( - "req_legacy", - "Legacy", - r#" -allowed_permissions = [":read-only", ":workspace"] -"#, - )]) - .expect("compose legacy requirements") - .expect("requirements present"); - - assert_eq!( - composed, - expected_requirements( - r#" -default_permissions = ":read-only" - -[allowed_permission_profiles] -":read-only" = true -":workspace" = true -"# - ) - ); -} - -#[test] -fn matching_legacy_and_map_allowlists_compose() { - let composed = compose(vec![layer( - "req_dual", - "Dual", - r#" -allowed_permissions = [":workspace"] - -[allowed_permission_profiles] -":read-only" = false -":workspace" = true -"#, - )]) - .expect("compose dual requirements") - .expect("requirements present"); - - assert_eq!( - composed, - expected_requirements( - r#" -default_permissions = ":workspace" - -[allowed_permission_profiles] -":read-only" = false -":workspace" = true -"# - ) - ); -} - -#[test] -fn conflicting_legacy_and_map_allowlists_fail() { - let err = compose(vec![layer( - "req_conflict", - "Conflict", - r#" -allowed_permissions = [":read-only"] - -[allowed_permission_profiles] -":workspace" = true -"#, - )]) - .expect_err("conflicting allowlists should fail"); - - assert!( - err.to_string().contains( - "allowed_permissions must match the profiles set to true in allowed_permission_profiles" - ), - "{err}" - ); -} - #[test] fn composition_strategy_applies_to_non_cloud_layers() { let mdm_source = RequirementSource::MdmManagedPreferences { diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 397db0287c9..87a46fd14a5 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1414,7 +1414,7 @@ extends = ":workspace" } #[tokio::test] -async fn system_allowed_permissions_select_managed_default_without_explicit_local_default() +async fn system_allowed_permission_profiles_select_managed_default_without_local_default() -> anyhow::Result<()> { for trust_level in [Some(TrustLevel::Trusted), Some(TrustLevel::Untrusted), None] { let tmp = tempdir()?; @@ -1477,7 +1477,7 @@ extends = ":workspace" } #[tokio::test] -async fn system_allowed_permissions_require_managed_default() -> anyhow::Result<()> { +async fn system_allowed_permission_profiles_require_managed_default() -> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); tokio::fs::create_dir_all(&codex_home).await?; @@ -1514,7 +1514,7 @@ managed-standard = true } #[tokio::test] -async fn system_allowed_permissions_standard_pair_implicitly_defaults_to_workspace() +async fn system_allowed_permission_profiles_standard_pair_defaults_to_workspace() -> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); @@ -1549,59 +1549,6 @@ async fn system_allowed_permissions_standard_pair_implicitly_defaults_to_workspa Ok(()) } -#[tokio::test] -async fn system_legacy_allowed_permissions_use_first_profile_as_default() -> 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#" -allowed_permissions = ["managed-standard", ":workspace"] - -[permissions.managed-standard] -extends = ":read-only" -"#, - ) - .await?; - - 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(tmp.path().to_path_buf())) - .loader_overrides(overrides) - .build() - .await?; - - assert_eq!( - config - .permissions - .active_permission_profile() - .map(|profile| profile.id), - Some("managed-standard".to_string()) - ); - assert_eq!( - config - .config_layer_stack - .requirements_toml() - .allowed_permission_profiles, - Some(BTreeMap::from([ - (":workspace".to_string(), true), - ("managed-standard".to_string(), true), - ])) - ); - assert_eq!( - config - .config_layer_stack - .requirements_toml() - .default_permissions, - Some("managed-standard".to_string()) - ); - Ok(()) -} - #[tokio::test] async fn system_managed_default_must_be_allowed() -> anyhow::Result<()> { let tmp = tempdir()?; @@ -1677,7 +1624,7 @@ default_permissions = ":read-only" } #[tokio::test] -async fn system_allowed_permissions_fall_back_from_disallowed_danger_full_access() +async fn system_allowed_permission_profiles_fall_back_from_disallowed_danger_full_access() -> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); @@ -1733,7 +1680,8 @@ managed-standard = true } #[tokio::test] -async fn system_allowed_permissions_fall_back_from_disallowed_workspace() -> anyhow::Result<()> { +async fn system_allowed_permission_profiles_fall_back_from_disallowed_workspace() +-> anyhow::Result<()> { let tmp = tempdir()?; let codex_home = tmp.path().join("home"); tokio::fs::create_dir_all(&codex_home).await?; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index c479e4be9ed..3a3b490d697 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8351,7 +8351,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, - allowed_permissions: None, allowed_permission_profiles: None, default_permissions: None, remote_sandbox_config: None, diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 135ad3cd76b..de5f79a6767 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -702,7 +702,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]), - allowed_permissions: None, allowed_permission_profiles: None, default_permissions: None, remote_sandbox_config: None, @@ -975,7 +974,6 @@ approval_policy = "never" allowed_approval_policies: None, allowed_approvals_reviewers: None, allowed_sandbox_modes: None, - allowed_permissions: None, allowed_permission_profiles: None, default_permissions: None, remote_sandbox_config: None,