Files
codex/codex-rs/utils/approval-presets/src/lib.rs
Michael Bolin bbb5c2811d tui: pass active permission profiles through app commands (#22891)
## Why

This continues the permissions migration by keeping the TUI command
boundary aligned with the app-server protocol direction from #22795:
callers should select a permission profile by id instead of passing a
concrete `PermissionProfile` value around as the turn configuration.

`AppCommand` is internal to the TUI, but it is the path that eventually
becomes `thread/turn/start`, so carrying concrete profile details there
made it too easy for UI code to keep relying on the old whole-profile
replacement model.

## What changed

- `AppCommand::UserTurn` and `AppCommand::OverrideTurnContext` now carry
`Option<ActivePermissionProfile>` instead of `PermissionProfile`.
- Composer submissions copy the active permission profile id from the
current session snapshot; legacy snapshots intentionally submit no
active profile id.
- Permission preset UI events now carry only the active built-in profile
id. The app derives the concrete built-in `PermissionProfile` internally
only when updating its local config/status snapshot.
- Permission presets expose their built-in active profile id, and preset
selection preserves that id in both the immediate turn override and the
local TUI config snapshot.
- Turn routing sends `TurnPermissionsOverride::ActiveProfile` when an
active id is present, and only falls back to the legacy sandbox
projection for the remaining runtime override path.

## How to review

Start with `codex-rs/tui/src/app_command.rs` to verify the command shape
no longer exposes `PermissionProfile`.

Then read `codex-rs/tui/src/app/thread_routing.rs` to verify the
app-server turn-start conversion: active ids go through as ids, while
the legacy sandbox fallback is still constrained to the existing runtime
override case.

Finally, check `codex-rs/tui/src/chatwidget/permission_popups.rs`,
`codex-rs/tui/src/app/event_dispatch.rs`,
`codex-rs/tui/src/app/config_persistence.rs`, and
`codex-rs/utils/approval-presets/src/lib.rs` to see how preset
selections stay id-only across TUI events while the local display/config
mirror still gets a concrete built-in profile.

## Verification

Latest local verification after the id-only `AppEvent` cleanup:

- `cargo check -p codex-tui --tests`
- `cargo test -p codex-tui
permissions_selection_sends_approvals_reviewer_in_override_turn_context`
- `cargo test -p codex-tui update_feature_flags_enabling_guardian`
- `cargo test -p codex-utils-approval-presets`
- `just fmt`
- `just fix -p codex-tui -p codex-utils-approval-presets`

Earlier in the same PR, before the final event-shape cleanup:

- `cargo test -p codex-tui turn_permissions_`
- `cargo test -p codex-tui submission_`
- `cargo test -p codex-tui
session_configured_syncs_widget_config_permissions_and_cwd`
- `RUST_MIN_STACK=16777216 cargo test -p codex-tui`
2026-05-15 22:42:35 +00:00

78 lines
3.3 KiB
Rust

use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
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;
/// A simple preset pairing an approval policy with a permission profile.
#[derive(Debug, Clone)]
pub struct ApprovalPreset {
/// Stable identifier for the preset.
pub id: &'static str,
/// Display label shown in UIs.
pub label: &'static str,
/// Short human description shown next to the label in UIs.
pub description: &'static str,
/// Approval policy to apply.
pub approval: AskForApproval,
/// Built-in permission profile selected by this preset.
pub active_permission_profile: ActivePermissionProfile,
/// Permission profile to apply.
pub permission_profile: PermissionProfile,
}
/// Built-in list of approval presets that pair approval and permissions.
///
/// Keep this UI-agnostic so it can be reused by both TUI and MCP server.
pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
vec![
ApprovalPreset {
id: "read-only",
label: "Read Only",
description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet.",
approval: AskForApproval::OnRequest,
active_permission_profile: ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
),
permission_profile: PermissionProfile::read_only(),
},
ApprovalPreset {
id: "auto",
label: "Default",
description: "Codex can read and edit files in the current workspace, and run commands. Approval is required to access the internet or edit other files. (Identical to Agent mode)",
approval: AskForApproval::OnRequest,
active_permission_profile: ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
),
permission_profile: PermissionProfile::workspace_write(),
},
ApprovalPreset {
id: "full-access",
label: "Full Access",
description: "Codex can edit files outside this workspace and access the internet without asking for approval. Exercise caution when using.",
approval: AskForApproval::Never,
active_permission_profile: ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS,
),
permission_profile: PermissionProfile::Disabled,
},
]
}
/// Return the concrete profile for one of the built-in active profile ids.
pub fn builtin_permission_profile_for_active_permission_profile(
active_permission_profile: &ActivePermissionProfile,
) -> Option<PermissionProfile> {
if active_permission_profile.extends.is_some() {
return None;
}
match active_permission_profile.id.as_str() {
BUILT_IN_PERMISSION_PROFILE_READ_ONLY => Some(PermissionProfile::read_only()),
BUILT_IN_PERMISSION_PROFILE_WORKSPACE => Some(PermissionProfile::workspace_write()),
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS => Some(PermissionProfile::Disabled),
_ => None,
}
}