core: set permission profiles from snapshots (#22920)

## Why

#22891 moved the TUI turn-command path to pass `ActivePermissionProfile`
instead of the full `PermissionProfile`, but the remaining
config/session bridge still accepted the concrete `PermissionProfile`
and active profile id as separate arguments. That shape made it too easy
for future callers to update the concrete profile and active profile id
out of sync.

This PR makes the trusted session snapshot path pass one coherent value
into `Permissions`, while keeping `requirements.toml` enforcement owned
by the existing constrained permission state.

## What Changed

- Added `PermissionProfileSnapshot` as the public snapshot value for
trusted session/config synchronization.
- Changed `Permissions::set_permission_profile_from_session_snapshot()`
and `replace_permission_profile_from_session_snapshot()` to take a
`PermissionProfileSnapshot`.
- Updated the replacement path to derive its constrained
`PermissionProfile` from the snapshot, so callers cannot pass a separate
profile that disagrees with the snapshot.
- Removed the internal tuple-style
`PermissionProfileState::set_active_permission_profile()` mutation path.
- Updated core session projection and TUI call sites to construct
explicit legacy or active snapshots.
- Documented the snapshot constructors so legacy use and id/profile
mismatch hazards are called out at the API boundary.
- Added a focused config test that verifies snapshot updates still
respect existing permission constraints.

## How To Review

1. Start with `codex-rs/core/src/config/resolved_permission_profile.rs`;
`PermissionProfileSnapshot` is the public wrapper, while
`ResolvedPermissionProfile` stays internal.
2. Check `codex-rs/core/src/config/mod.rs` to confirm both
session-snapshot setters validate through `PermissionProfileState` and
no longer accept loose profile/id pairs.
3. Skim `codex-rs/core/src/session/session.rs` for the session
projection path; it now builds the snapshot before installing it.
4. Skim the TUI changes as call-site migration from loose argument pairs
to explicit snapshot construction.

## Verification

- `cargo test -p codex-core
permission_snapshot_setter_preserves_permission_constraints`
- `cargo test -p codex-tui status_permissions_`
- `cargo test -p codex-tui
session_configured_preserves_profile_workspace_roots`
- `just fix -p codex-core -p codex-tui`
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-05-16 07:26:18 -07:00
1 parent de9c5c0226
commit 108234b5eb
15 files changed
+225 -132

No files matched your search

+7 -5
View File
@@ -128,10 +128,10 @@ impl App {
if let Err(err) = config
.permissions
.set_permission_profile_from_session_snapshot(
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
permission_profile.clone(),
Some(active_permission_profile),
)
active_permission_profile,
))
{
tracing::warn!(error = %err, "{log_message}");
self.chat_widget
@@ -328,8 +328,10 @@ impl App {
&& let Err(err) = self
.chat_widget
.set_permission_profile_from_session_snapshot(
permission_profile.clone(),
active_permission_profile_override.clone(),
PermissionProfileSnapshot::from_session_snapshot(
permission_profile.clone(),
active_permission_profile_override.clone(),
),
)
{
tracing::error!(
+4 -2
View File
@@ -1422,8 +1422,10 @@ impl App {
if let Err(err) = self
.chat_widget
.set_permission_profile_from_session_snapshot(
permission_profile_for_chat,
Some(active_permission_profile),
PermissionProfileSnapshot::active(
permission_profile_for_chat,
active_permission_profile,
),
)
{
tracing::warn!(%err, "failed to set permission profile on chat config");
+5 -6
View File
@@ -28,6 +28,7 @@ use crate::app_command::AppCommand as Op;
use crate::diff_model::FileChange;
use crate::legacy_core::config::ConfigBuilder;
use crate::legacy_core::config::ConfigOverrides;
use crate::legacy_core::config::PermissionProfileSnapshot;
use crate::legacy_core::config::TerminalResizeReflowMaxRows;
use codex_app_server_protocol::AdditionalFileSystemPermissions;
use codex_app_server_protocol::AdditionalNetworkPermissions;
@@ -1730,10 +1731,9 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor
app.chat_widget
.set_approval_policy(AskForApproval::OnRequest);
app.chat_widget
.set_permission_profile_from_session_snapshot(
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::legacy(
PermissionProfile::workspace_write(),
/*active_profile*/ None,
)?;
))?;
app.update_feature_flags(vec![(Feature::GuardianApproval, false)])
.await;
@@ -3153,10 +3153,9 @@ async fn side_fork_config_inherits_parent_thread_runtime_settings() {
app.chat_widget
.set_approval_policy(AskForApproval::OnRequest);
app.chat_widget
.set_permission_profile_from_session_snapshot(
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::legacy(
parent_permission_profile.clone(),
/*active_profile*/ None,
)
))
.expect("test permission profile should be accepted");
app.chat_widget
.set_approvals_reviewer(ApprovalsReviewer::AutoReview);
+4 -3
View File
@@ -120,6 +120,7 @@ mod tests {
use crate::app::side::SideThreadState;
use crate::app::test_support::make_test_app;
use crate::app::thread_events::ThreadEventChannel;
use crate::legacy_core::config::PermissionProfileSnapshot;
use crate::test_support::PathBufExt;
use crate::test_support::test_path_buf;
use codex_app_server_protocol::AskForApproval;
@@ -201,10 +202,10 @@ mod tests {
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
app.chat_widget.handle_thread_session(main_session.clone());
app.chat_widget
.set_permission_profile_from_session_snapshot(
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
expected_permission_profile.clone(),
Some(expected_active_permission_profile.clone()),
)
expected_active_permission_profile.clone(),
))
.expect("set widget permission profile");
app.config
.permissions