mirror of
https://github.com/openai/codex.git
synced 2026-05-22 03:54:18 +00:00
app-server: expose thread permission profiles (#18278)
## Why The `PermissionProfile` migration needs app-server clients to see the same constrained permission model that core is using at runtime. Before this PR, thread lifecycle responses only exposed the legacy `SandboxPolicy` shape, so clients still had to infer active permissions from sandbox fields. That makes downstream resume, fork, and override flows harder to make `PermissionProfile`-first. External sandbox policies are intentionally excluded from this canonical view. External enforcement cannot be round-tripped as a `PermissionProfile`, and exposing a lossy root-write profile would let clients accidentally change sandbox semantics if they echo the profile back later. ## What changed - Adds the app-server v2 `PermissionProfile` wire shape, including filesystem permissions and glob scan depth metadata. - Adds `PermissionProfileNetworkPermissions` so the profile response does not expose active network state through the older additional-permissions naming. - Returns `permissionProfile` from thread start, resume, and fork responses when the active sandbox can be represented as a `PermissionProfile`. - Keeps legacy `sandbox` in those responses for compatibility and documents `permissionProfile` as canonical when present. - Makes lifecycle `permissionProfile` nullable and returns `null` for `ExternalSandbox` to avoid exposing a lossy profile. - Regenerates the app-server JSON schema and TypeScript fixtures. ## Verification - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-app-server thread_response_permission_profile_omits_external_sandbox -- --nocapture` - `cargo check --tests -p codex-analytics -p codex-exec -p codex-tui` - `just fix -p codex-app-server-protocol -p codex-app-server -p codex-analytics -p codex-exec -p codex-tui` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18278). * #18279 * __->__ #18278
This commit is contained in:
@@ -2691,6 +2691,11 @@ impl CodexMessageProcessor {
|
||||
/*has_in_progress_turn*/ false,
|
||||
);
|
||||
|
||||
let permission_profile = thread_response_permission_profile(
|
||||
&config_snapshot.sandbox_policy,
|
||||
config_snapshot.permission_profile,
|
||||
);
|
||||
|
||||
let response = ThreadStartResponse {
|
||||
thread: thread.clone(),
|
||||
model: config_snapshot.model,
|
||||
@@ -2701,6 +2706,7 @@ impl CodexMessageProcessor {
|
||||
approval_policy: config_snapshot.approval_policy.into(),
|
||||
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
|
||||
sandbox: config_snapshot.sandbox_policy.into(),
|
||||
permission_profile,
|
||||
reasoning_effort: config_snapshot.reasoning_effort,
|
||||
};
|
||||
if listener_task_context.general_analytics_enabled {
|
||||
@@ -4423,6 +4429,10 @@ impl CodexMessageProcessor {
|
||||
thread_status,
|
||||
/*has_live_in_progress_turn*/ false,
|
||||
);
|
||||
let permission_profile = thread_response_permission_profile(
|
||||
&session_configured.sandbox_policy,
|
||||
codex_thread.config_snapshot().await.permission_profile,
|
||||
);
|
||||
|
||||
let response = ThreadResumeResponse {
|
||||
thread,
|
||||
@@ -4434,6 +4444,7 @@ impl CodexMessageProcessor {
|
||||
approval_policy: session_configured.approval_policy.into(),
|
||||
approvals_reviewer: session_configured.approvals_reviewer.into(),
|
||||
sandbox: session_configured.sandbox_policy.into(),
|
||||
permission_profile,
|
||||
reasoning_effort: session_configured.reasoning_effort,
|
||||
};
|
||||
if self.config.features.enabled(Feature::GeneralAnalytics) {
|
||||
@@ -5068,6 +5079,10 @@ impl CodexMessageProcessor {
|
||||
.await,
|
||||
/*has_in_progress_turn*/ false,
|
||||
);
|
||||
let permission_profile = thread_response_permission_profile(
|
||||
&session_configured.sandbox_policy,
|
||||
forked_thread.config_snapshot().await.permission_profile,
|
||||
);
|
||||
|
||||
let response = ThreadForkResponse {
|
||||
thread: thread.clone(),
|
||||
@@ -5079,6 +5094,7 @@ impl CodexMessageProcessor {
|
||||
approval_policy: session_configured.approval_policy.into(),
|
||||
approvals_reviewer: session_configured.approvals_reviewer.into(),
|
||||
sandbox: session_configured.sandbox_policy.into(),
|
||||
permission_profile,
|
||||
reasoning_effort: session_configured.reasoning_effort,
|
||||
};
|
||||
if self.config.features.enabled(Feature::GeneralAnalytics) {
|
||||
@@ -8456,11 +8472,15 @@ async fn handle_pending_thread_resume_request(
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
cwd,
|
||||
reasoning_effort,
|
||||
..
|
||||
} = pending.config_snapshot;
|
||||
let instruction_sources = pending.instruction_sources;
|
||||
let permission_profile =
|
||||
thread_response_permission_profile(&sandbox_policy, permission_profile);
|
||||
|
||||
let response = ThreadResumeResponse {
|
||||
thread,
|
||||
model,
|
||||
@@ -8471,6 +8491,7 @@ async fn handle_pending_thread_resume_request(
|
||||
approval_policy: approval_policy.into(),
|
||||
approvals_reviewer: approvals_reviewer.into(),
|
||||
sandbox: sandbox_policy.into(),
|
||||
permission_profile,
|
||||
reasoning_effort,
|
||||
};
|
||||
let token_usage_thread = response.thread.clone();
|
||||
@@ -9573,6 +9594,20 @@ fn with_thread_spawn_agent_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_response_permission_profile(
|
||||
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
|
||||
permission_profile: codex_protocol::models::PermissionProfile,
|
||||
) -> Option<codex_app_server_protocol::PermissionProfile> {
|
||||
match sandbox_policy {
|
||||
codex_protocol::protocol::SandboxPolicy::DangerFullAccess
|
||||
| codex_protocol::protocol::SandboxPolicy::ReadOnly { .. }
|
||||
| codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
Some(permission_profile.into())
|
||||
}
|
||||
codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
timestamp.and_then(|ts| {
|
||||
chrono::DateTime::parse_from_rfc3339(ts)
|
||||
@@ -10061,6 +10096,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_response_permission_profile_omits_external_sandbox() {
|
||||
let cwd = test_path_buf("/tmp").abs();
|
||||
let profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
cwd.as_path(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
thread_response_permission_profile(
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
|
||||
},
|
||||
profile.clone(),
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
thread_response_permission_profile(&SandboxPolicy::DangerFullAccess, profile.clone()),
|
||||
Some(profile.into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_load_error_marks_cloud_requirements_failures_for_relogin() {
|
||||
let err = std::io::Error::other(CloudRequirementsLoadError::new(
|
||||
|
||||
Reference in New Issue
Block a user