app-server: stop returning thread permission profiles (#22792)

## Why

The app-server thread lifecycle API should no longer expose the full
`PermissionProfile` value. After the permissions-profile migration,
clients should round-trip only the active profile identity through
`activePermissionProfile` and `permissions` when that identity is known.

The full profile is server-side config. Treating a response-derived
legacy sandbox projection as a new local profile can lose named-profile
restrictions and accidentally widen permissions on the next turn. The
legacy `sandbox` response field remains only as the
compatibility/display fallback.

## What Changed

- Removed `permissionProfile` from `ThreadStartResponse`,
`ThreadResumeResponse`, and `ThreadForkResponse`.
- Stopped populating that field in app-server thread start/resume/fork
responses.
- Updated embedded exec/TUI response mapping to derive display
permission state from local config or the legacy sandbox fallback
instead of a response profile value.
- Added a TUI turn override shape that distinguishes preserving server
permissions, selecting an active profile id, and sending a legacy
sandbox for an explicit local override.
- Preserved remote app-server permissions across turns by sending
`permissions` only when an `activePermissionProfile` id is known, and
otherwise sending no sandbox override unless the user selected a local
override.
- Kept embedded `thread/resume` hydration server-authored when
`activePermissionProfile` is absent, which matches the live-thread
attach path where the server ignores requested overrides.
- Updated the app-server README to remove the obsolete lifecycle
response `permissionProfile` reference. The remaining
`permissionProfile` README references are request-side permission
overrides.
- Regenerated app-server JSON schema and TypeScript fixtures.
- Kept the generated typed response enum exempt from
`large_enum_variant`, matching the existing payload enum exemption after
the lifecycle response variants shrank.

## How To Review

Start with `codex-rs/app-server-protocol/src/protocol/v2/thread.rs` to
confirm the response shape, then check the response construction in
`codex-rs/app-server/src/request_processors`. The generated schema and
TypeScript fixture changes are mechanical follow-through from the
protocol removal.

The TUI behavior is the delicate part: review
`codex-rs/tui/src/app_server_session.rs` for response hydration and
turn-start override projection, then
`codex-rs/tui/src/app/thread_routing.rs` for the decision about whether
the next turn should preserve the server snapshot, send an active
profile id, or send a legacy sandbox for an explicit local override.

## Verification

- `just write-app-server-schema`
- `cargo test -p codex-app-server-protocol
thread_lifecycle_responses_default_missing_optional_fields`
- `cargo test -p codex-exec
session_configured_from_thread_response_uses_permission_profile_from_config`
- `cargo test -p codex-tui --lib thread_response`
- `cargo test -p codex-tui turn_permissions_`
- `cargo test -p codex-tui
resume_response_restores_turns_from_thread_items`
- `cargo test -p codex-analytics
track_response_only_enqueues_analytics_relevant_responses`
- `just fix -p codex-analytics`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-tui`
- `just argument-comment-lint`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22792).
* #22795
* __->__ #22792
This commit is contained in:
Michael Bolin
2026-05-15 12:45:48 -07:00
committed by GitHub
Unverified
parent 6afe00efda
commit 83bbb4f326
22 changed files with 249 additions and 1170 deletions
+2 -10
View File
@@ -1089,11 +1089,7 @@ fn session_configured_from_thread_start_response(
response.service_tier.clone(),
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| config.permissions.effective_permission_profile()),
config.permissions.effective_permission_profile(),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
@@ -1114,11 +1110,7 @@ fn session_configured_from_thread_resume_response(
response.service_tier.clone(),
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| config.permissions.effective_permission_profile()),
config.permissions.effective_permission_profile(),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
+6 -5
View File
@@ -528,7 +528,7 @@ async fn session_configured_from_thread_response_uses_review_policy_from_respons
}
#[tokio::test]
async fn session_configured_from_thread_response_uses_permission_profile_from_response() {
async fn session_configured_from_thread_response_uses_permission_profile_from_config() {
let codex_home = tempdir().expect("create temp codex home");
let cwd = tempdir().expect("create temp cwd");
let config = ConfigBuilder::default()
@@ -537,13 +537,15 @@ async fn session_configured_from_thread_response_uses_permission_profile_from_re
.build()
.await
.expect("build config");
let mut response = sample_thread_start_response();
response.permission_profile = Some(PermissionProfile::Disabled.into());
let response = sample_thread_start_response();
let event = session_configured_from_thread_start_response(&response, &config)
.expect("build bootstrap session configured event");
assert_eq!(event.permission_profile, PermissionProfile::Disabled);
assert_eq!(
event.permission_profile,
config.permissions.effective_permission_profile()
);
}
fn sample_thread_start_response() -> ThreadStartResponse {
@@ -583,7 +585,6 @@ fn sample_thread_start_response() -> ThreadStartResponse {
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
}