core: expose permission profile picker metadata (#22928)

## Why

The `/permissions` picker needs a config-level way to distinguish legacy
anonymous presets from named permission-profile mode. That signal cannot
be inferred reliably in the TUI, especially for the edge case where
`default_permissions = ":workspace"` is present without a
`[permissions]` table.

## What changed

- Expose whether the merged config is explicitly in permission-profile
mode.
- Expose the configured custom permission profile IDs alongside the
built-in profile semantics.
- Add regression coverage for profile mode detection and custom profile
metadata, including the `default_permissions = ":workspace"` case.
- Update the thread-manager sample config literal to match the expanded
config shape.

## Stack

1. **This PR**: config metadata needed by downstream permission-profile
consumers.
2. [#22931](https://github.com/openai/codex/pull/22931): refresh active
permission profiles through runtime/session/network state.
3. [#21559](https://github.com/openai/codex/pull/21559): switch
`/permissions` to the profile-aware TUI picker.

## Verification

- `cargo check -p codex-thread-manager-sample`
- `cargo test -p codex-core
default_permissions_can_select_builtin_profile_without_permissions_table`
- `cargo test -p codex-core
permissions_profiles_allow_direct_write_roots_outside_workspace_root`
This commit is contained in:
viyatb-oai
2026-05-18 23:26:17 -07:00
committed by GitHub
Unverified
parent 1dd9bf9a74
commit 3009e23644
3 changed files with 36 additions and 0 deletions
+14
View File
@@ -1822,6 +1822,8 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
)
.await?;
assert!(config.explicit_permission_profile_mode);
assert!(config.custom_permission_profile_ids.is_empty());
let policy = config.permissions.file_system_sandbox_policy();
assert_eq!(
config
@@ -2377,6 +2379,10 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
)
.await?;
assert_eq!(
config.custom_permission_profile_ids,
vec!["workspace".to_string()]
);
let memories_root = AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(
codex_home.path().join("memories"),
)?)?;
@@ -7721,6 +7727,8 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
},
explicit_permission_profile_mode: false,
custom_permission_profile_ids: Vec::new(),
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
user_instructions: None,
@@ -8172,6 +8180,8 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
},
explicit_permission_profile_mode: false,
custom_permission_profile_ids: Vec::new(),
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
user_instructions: None,
@@ -8337,6 +8347,8 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
},
explicit_permission_profile_mode: false,
custom_permission_profile_ids: Vec::new(),
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
user_instructions: None,
@@ -8487,6 +8499,8 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
},
explicit_permission_profile_mode: false,
custom_permission_profile_ids: Vec::new(),
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
user_instructions: None,
+20
View File
@@ -539,6 +539,13 @@ pub struct Config {
/// Effective permission configuration for shell tool execution.
pub permissions: Permissions,
/// Whether config explicitly selected named permissions profiles instead
/// of the legacy `sandbox_mode` syntax.
pub explicit_permission_profile_mode: bool,
/// User-defined permission profile IDs available from effective config.
pub custom_permission_profile_ids: Vec<String>,
/// Configures who approval requests are routed to for review once they have
/// been escalated. This does not disable separate safety checks such as
/// ARC.
@@ -2604,6 +2611,17 @@ impl Config {
Some(PermissionConfigSyntax::Profiles)
)
|| permission_config_syntax.is_none();
let explicit_permission_profile_mode = default_permissions_override.is_some()
|| matches!(
permission_config_syntax,
Some(PermissionConfigSyntax::Profiles)
);
let custom_permission_profile_ids = cfg
.permissions
.as_ref()
.map_or_else(Vec::new, |permissions| {
permissions.entries.keys().cloned().collect()
});
let using_implicit_builtin_profile =
permission_config_syntax.is_none() && default_permissions.is_none();
let should_seed_legacy_workspace_roots = default_permissions.is_none()
@@ -3371,6 +3389,8 @@ impl Config {
windows_sandbox_mode,
windows_sandbox_private_desktop,
},
explicit_permission_profile_mode,
custom_permission_profile_ids,
approvals_reviewer: constrained_approvals_reviewer.value(),
enforce_residency: enforce_residency.value,
notify: cfg.notify,
@@ -175,6 +175,8 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
Constrained::allow_any(AskForApproval::Never),
Constrained::allow_any(PermissionProfile::read_only()),
)?,
explicit_permission_profile_mode: false,
custom_permission_profile_ids: Vec::new(),
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
hide_agent_reasoning: false,