tui: carry permission profiles on user turns

This commit is contained in:
Michael Bolin
2026-04-20 15:04:17 -07:00
parent acef277d13
commit 2cd879430e
46 changed files with 246 additions and 8 deletions
+2
View File
@@ -2607,6 +2607,7 @@ impl App {
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
model,
effort,
summary,
@@ -2690,6 +2691,7 @@ impl App {
approvals_reviewer
.unwrap_or(self.chat_widget.config_ref().approvals_reviewer),
sandbox_policy.clone(),
permission_profile.clone(),
model.to_string(),
effort,
*summary,
+6
View File
@@ -8,6 +8,7 @@ use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::mcp::RequestId as McpRequestId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ConversationAudioParams;
@@ -44,6 +45,7 @@ pub(crate) enum AppCommandView<'a> {
approval_policy: AskForApproval,
approvals_reviewer: &'a Option<ApprovalsReviewer>,
sandbox_policy: &'a SandboxPolicy,
permission_profile: &'a Option<PermissionProfile>,
model: &'a str,
effort: Option<ReasoningEffortConfig>,
summary: &'a Option<ReasoningSummaryConfig>,
@@ -140,6 +142,7 @@ impl AppCommand {
cwd: PathBuf,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
permission_profile: Option<PermissionProfile>,
model: String,
effort: Option<ReasoningEffortConfig>,
summary: Option<ReasoningSummaryConfig>,
@@ -154,6 +157,7 @@ impl AppCommand {
approval_policy,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model,
effort,
summary,
@@ -290,6 +294,7 @@ impl AppCommand {
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
model,
effort,
summary,
@@ -303,6 +308,7 @@ impl AppCommand {
approval_policy: *approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
model,
effort: *effort,
summary,
+13 -7
View File
@@ -482,6 +482,7 @@ impl AppServerSession {
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
sandbox_policy: SandboxPolicy,
permission_profile: Option<PermissionProfile>,
model: String,
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
summary: Option<codex_protocol::config_types::ReasoningSummary>,
@@ -491,14 +492,19 @@ impl AppServerSession {
output_schema: Option<serde_json::Value>,
) -> Result<TurnStartResponse> {
let request_id = self.next_request_id();
let (sandbox_policy, permission_profile) = match sandbox_policy {
policy @ SandboxPolicy::ExternalSandbox { .. } => (Some(policy.into()), None),
policy => {
let permission_profile =
PermissionProfile::from_legacy_sandbox_policy(&policy, &cwd);
let (sandbox_policy, permission_profile) =
if let Some(permission_profile) = permission_profile {
(None, Some(permission_profile.into()))
}
};
} else {
match sandbox_policy {
policy @ SandboxPolicy::ExternalSandbox { .. } => (Some(policy.into()), None),
policy => {
let permission_profile =
PermissionProfile::from_legacy_sandbox_policy(&policy, &cwd);
(None, Some(permission_profile.into()))
}
}
};
self.client
.request_typed(ClientRequest::TurnStart {
request_id,
+9
View File
@@ -5820,11 +5820,20 @@ impl ChatWidget {
.filter(|_| self.config.features.enabled(Feature::Personality))
.filter(|_| self.current_model_supports_personality());
let service_tier = Some(self.config.service_tier);
let permission_profile = if matches!(
self.config.permissions.sandbox_policy.get(),
SandboxPolicy::ExternalSandbox { .. }
) {
None
} else {
Some(self.config.permissions.permission_profile())
};
let op = AppCommand::user_turn(
items,
self.config.cwd.to_path_buf(),
self.config.permissions.approval_policy.value(),
self.config.permissions.sandbox_policy.get().clone(),
permission_profile,
effective_mode.model().to_string(),
effective_mode.reasoning_effort(),
/*summary*/ None,
@@ -86,6 +86,75 @@ async fn submission_preserves_text_elements_and_local_images() {
assert!(stored_remote_image_urls.is_empty());
}
#[tokio::test]
async fn submission_includes_configured_permission_profile() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let conversation_id = ThreadId::new();
let rollout_file = NamedTempFile::new().unwrap();
let expected_permission_profile = PermissionProfile {
network: Some(NetworkPermissions {
enabled: Some(false),
}),
file_system: Some(FileSystemPermissions {
entries: vec![
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Root,
},
access: codex_protocol::permissions::FileSystemAccessMode::Read,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::GlobPattern {
pattern: "/home/user/project/secrets/**".to_string(),
},
access: codex_protocol::permissions::FileSystemAccessMode::None,
},
],
glob_scan_max_depth: None,
}),
};
let configured = codex_protocol::protocol::SessionConfiguredEvent {
session_id: conversation_id,
forked_from_id: None,
thread_name: None,
model: "test-model".to_string(),
model_provider_id: "test-provider".to_string(),
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: Some(expected_permission_profile.clone()),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
history_entry_count: 0,
initial_messages: None,
network_proxy: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
};
chat.handle_codex_event(Event {
id: "initial".into(),
msg: EventMsg::SessionConfigured(configured),
});
drain_insert_history(&mut rx);
chat.bottom_pane.set_composer_text(
"submit with configured permissions".to_string(),
Vec::new(),
Vec::new(),
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let permission_profile = match next_submit_op(&mut op_rx) {
Op::UserTurn {
permission_profile, ..
} => permission_profile,
other => panic!("expected Op::UserTurn, got {other:?}"),
};
assert_eq!(permission_profile, Some(expected_permission_profile));
}
#[tokio::test]
async fn submission_with_remote_and_local_images_keeps_local_placeholder_numbering() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;