mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
bbb5c2811d
## 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`
285 lines
8.4 KiB
Rust
285 lines
8.4 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use codex_app_server_protocol::AskForApproval;
|
|
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
|
use codex_app_server_protocol::FileChangeApprovalDecision;
|
|
use codex_app_server_protocol::McpServerElicitationAction;
|
|
use codex_app_server_protocol::RequestId as AppServerRequestId;
|
|
use codex_app_server_protocol::ReviewTarget;
|
|
use codex_app_server_protocol::ThreadRealtimeAudioChunk;
|
|
use codex_app_server_protocol::ThreadRealtimeStartTransport;
|
|
use codex_app_server_protocol::ToolRequestUserInputResponse;
|
|
use codex_app_server_protocol::UserInput;
|
|
use codex_config::types::ApprovalsReviewer;
|
|
use codex_protocol::approvals::GuardianAssessmentEvent;
|
|
use codex_protocol::config_types::CollaborationMode;
|
|
use codex_protocol::config_types::Personality;
|
|
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::models::ActivePermissionProfile;
|
|
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
|
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
#[allow(clippy::large_enum_variant)]
|
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
|
pub(crate) enum AppCommand {
|
|
Interrupt,
|
|
CleanBackgroundTerminals,
|
|
RealtimeConversationStart {
|
|
transport: Option<ThreadRealtimeStartTransport>,
|
|
voice: Option<Value>,
|
|
},
|
|
RealtimeConversationAudio(ThreadRealtimeAudioChunk),
|
|
RealtimeConversationClose,
|
|
RunUserShellCommand {
|
|
command: String,
|
|
},
|
|
UserTurn {
|
|
items: Vec<UserInput>,
|
|
cwd: PathBuf,
|
|
approval_policy: AskForApproval,
|
|
approvals_reviewer: Option<ApprovalsReviewer>,
|
|
active_permission_profile: Option<ActivePermissionProfile>,
|
|
model: String,
|
|
effort: Option<ReasoningEffortConfig>,
|
|
summary: Option<ReasoningSummaryConfig>,
|
|
service_tier: Option<Option<String>>,
|
|
final_output_json_schema: Option<Value>,
|
|
collaboration_mode: Option<CollaborationMode>,
|
|
personality: Option<Personality>,
|
|
},
|
|
OverrideTurnContext {
|
|
cwd: Option<PathBuf>,
|
|
approval_policy: Option<AskForApproval>,
|
|
approvals_reviewer: Option<ApprovalsReviewer>,
|
|
active_permission_profile: Option<ActivePermissionProfile>,
|
|
windows_sandbox_level: Option<WindowsSandboxLevel>,
|
|
model: Option<String>,
|
|
effort: Option<Option<ReasoningEffortConfig>>,
|
|
summary: Option<ReasoningSummaryConfig>,
|
|
service_tier: Option<Option<String>>,
|
|
collaboration_mode: Option<CollaborationMode>,
|
|
personality: Option<Personality>,
|
|
},
|
|
ExecApproval {
|
|
id: String,
|
|
turn_id: Option<String>,
|
|
decision: CommandExecutionApprovalDecision,
|
|
},
|
|
PatchApproval {
|
|
id: String,
|
|
decision: FileChangeApprovalDecision,
|
|
},
|
|
ResolveElicitation {
|
|
server_name: String,
|
|
request_id: AppServerRequestId,
|
|
decision: McpServerElicitationAction,
|
|
content: Option<Value>,
|
|
meta: Option<Value>,
|
|
},
|
|
UserInputAnswer {
|
|
id: String,
|
|
response: ToolRequestUserInputResponse,
|
|
},
|
|
RequestPermissionsResponse {
|
|
id: String,
|
|
response: RequestPermissionsResponse,
|
|
},
|
|
ReloadUserConfig,
|
|
ListSkills {
|
|
cwds: Vec<PathBuf>,
|
|
force_reload: bool,
|
|
},
|
|
Compact,
|
|
SetThreadName {
|
|
name: String,
|
|
},
|
|
Shutdown,
|
|
ThreadRollback {
|
|
num_turns: u32,
|
|
},
|
|
Review {
|
|
target: ReviewTarget,
|
|
},
|
|
ApproveGuardianDeniedAction {
|
|
event: GuardianAssessmentEvent,
|
|
},
|
|
}
|
|
|
|
impl AppCommand {
|
|
pub(crate) fn interrupt() -> Self {
|
|
Self::Interrupt
|
|
}
|
|
|
|
pub(crate) fn clean_background_terminals() -> Self {
|
|
Self::CleanBackgroundTerminals
|
|
}
|
|
|
|
pub(crate) fn realtime_conversation_start(
|
|
transport: Option<ThreadRealtimeStartTransport>,
|
|
voice: Option<Value>,
|
|
) -> Self {
|
|
Self::RealtimeConversationStart { transport, voice }
|
|
}
|
|
|
|
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
|
pub(crate) fn realtime_conversation_audio(frame: ThreadRealtimeAudioChunk) -> Self {
|
|
Self::RealtimeConversationAudio(frame)
|
|
}
|
|
|
|
pub(crate) fn realtime_conversation_close() -> Self {
|
|
Self::RealtimeConversationClose
|
|
}
|
|
|
|
pub(crate) fn run_user_shell_command(command: String) -> Self {
|
|
Self::RunUserShellCommand { command }
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn user_turn(
|
|
items: Vec<UserInput>,
|
|
cwd: PathBuf,
|
|
approval_policy: AskForApproval,
|
|
active_permission_profile: Option<ActivePermissionProfile>,
|
|
model: String,
|
|
effort: Option<ReasoningEffortConfig>,
|
|
summary: Option<ReasoningSummaryConfig>,
|
|
service_tier: Option<Option<String>>,
|
|
final_output_json_schema: Option<Value>,
|
|
collaboration_mode: Option<CollaborationMode>,
|
|
personality: Option<Personality>,
|
|
) -> Self {
|
|
Self::UserTurn {
|
|
items,
|
|
cwd,
|
|
approval_policy,
|
|
approvals_reviewer: None,
|
|
active_permission_profile,
|
|
model,
|
|
effort,
|
|
summary,
|
|
service_tier,
|
|
final_output_json_schema,
|
|
collaboration_mode,
|
|
personality,
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn override_turn_context(
|
|
cwd: Option<PathBuf>,
|
|
approval_policy: Option<AskForApproval>,
|
|
approvals_reviewer: Option<ApprovalsReviewer>,
|
|
active_permission_profile: Option<ActivePermissionProfile>,
|
|
windows_sandbox_level: Option<WindowsSandboxLevel>,
|
|
model: Option<String>,
|
|
effort: Option<Option<ReasoningEffortConfig>>,
|
|
summary: Option<ReasoningSummaryConfig>,
|
|
service_tier: Option<Option<String>>,
|
|
collaboration_mode: Option<CollaborationMode>,
|
|
personality: Option<Personality>,
|
|
) -> Self {
|
|
Self::OverrideTurnContext {
|
|
cwd,
|
|
approval_policy,
|
|
approvals_reviewer,
|
|
active_permission_profile,
|
|
windows_sandbox_level,
|
|
model,
|
|
effort,
|
|
summary,
|
|
service_tier,
|
|
collaboration_mode,
|
|
personality,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn exec_approval(
|
|
id: String,
|
|
turn_id: Option<String>,
|
|
decision: CommandExecutionApprovalDecision,
|
|
) -> Self {
|
|
Self::ExecApproval {
|
|
id,
|
|
turn_id,
|
|
decision,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn patch_approval(id: String, decision: FileChangeApprovalDecision) -> Self {
|
|
Self::PatchApproval { id, decision }
|
|
}
|
|
|
|
pub(crate) fn resolve_elicitation(
|
|
server_name: String,
|
|
request_id: AppServerRequestId,
|
|
decision: McpServerElicitationAction,
|
|
content: Option<Value>,
|
|
meta: Option<Value>,
|
|
) -> Self {
|
|
Self::ResolveElicitation {
|
|
server_name,
|
|
request_id,
|
|
decision,
|
|
content,
|
|
meta,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn user_input_answer(id: String, response: ToolRequestUserInputResponse) -> Self {
|
|
Self::UserInputAnswer { id, response }
|
|
}
|
|
|
|
pub(crate) fn request_permissions_response(
|
|
id: String,
|
|
response: RequestPermissionsResponse,
|
|
) -> Self {
|
|
Self::RequestPermissionsResponse { id, response }
|
|
}
|
|
|
|
pub(crate) fn reload_user_config() -> Self {
|
|
Self::ReloadUserConfig
|
|
}
|
|
|
|
pub(crate) fn list_skills(cwds: Vec<PathBuf>, force_reload: bool) -> Self {
|
|
Self::ListSkills { cwds, force_reload }
|
|
}
|
|
|
|
pub(crate) fn compact() -> Self {
|
|
Self::Compact
|
|
}
|
|
|
|
pub(crate) fn set_thread_name(name: String) -> Self {
|
|
Self::SetThreadName { name }
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub(crate) fn shutdown() -> Self {
|
|
Self::Shutdown
|
|
}
|
|
|
|
pub(crate) fn thread_rollback(num_turns: u32) -> Self {
|
|
Self::ThreadRollback { num_turns }
|
|
}
|
|
|
|
pub(crate) fn review(target: ReviewTarget) -> Self {
|
|
Self::Review { target }
|
|
}
|
|
|
|
pub(crate) fn approve_guardian_denied_action(event: GuardianAssessmentEvent) -> Self {
|
|
Self::ApproveGuardianDeniedAction { event }
|
|
}
|
|
|
|
pub(crate) fn is_review(&self) -> bool {
|
|
matches!(self, Self::Review { .. })
|
|
}
|
|
}
|
|
|
|
impl From<&AppCommand> for AppCommand {
|
|
fn from(value: &AppCommand) -> Self {
|
|
value.clone()
|
|
}
|
|
}
|