tui: carry permission profiles on user turns (#18285)

## Why

Per-turn permission overrides should use the same canonical profile
abstraction as session configuration. That lets TUI submissions preserve
exact configured permissions without round-tripping through legacy
sandbox fields.

## What changed

This adds `permission_profile` to user-turn operations, threads it
through TUI/app-server submission paths, fills the new field in existing
test fixtures, and adds coverage that composer submission includes the
configured profile.

## Verification

- `cargo test -p codex-tui permissions -- --nocapture`
- `cargo test -p codex-core --test all permissions_messages --
--nocapture`























































---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18285).
* #18288
* #18287
* #18286
* __->__ #18285
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-04-23 11:54:17 -07:00
1 parent f11583b8f6
commit f90cc0ee64
45 files changed
+239 -10

No files matched your search

@@ -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;