From 4c80435ebad5de8c89813b1c6ccfd78228dff6d2 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 15 May 2026 10:58:50 -0700 Subject: [PATCH 1/3] telemetry: tag sandboxes from permission profiles (#22791) ## Why Sandbox telemetry tags should be derived from the active permission profile, not from a legacy `SandboxPolicy`, so the tagging code stays aligned with the permissions migration and does not preserve a policy-shaped production helper only for tests. ## What Changed - Removed the production `sandbox_tag(&SandboxPolicy, ...)` helper. - Updated sandbox tag tests to construct the relevant `PermissionProfile` values directly. - Kept the platform-specific sandbox tag behavior under the existing `permission_profile_sandbox_tag` path. ## How To Review The production change is in `codex-rs/core/src/sandbox_tags.rs`. Most of the diff is test cleanup that replaces legacy policy setup with permission profiles, so review the expected tag assertions rather than the old helper mechanics. ## Verification - `cargo test -p codex-core sandbox_tag` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22791). * #22795 * #22792 * __->__ #22791 --- codex-rs/core/src/sandbox_tags.rs | 14 -------------- codex-rs/core/src/sandbox_tags_tests.rs | 20 ++++++++++---------- codex-rs/core/src/turn_metadata_tests.rs | 10 ++++++---- 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/codex-rs/core/src/sandbox_tags.rs b/codex-rs/core/src/sandbox_tags.rs index f6db4da918..2973a5bf94 100644 --- a/codex-rs/core/src/sandbox_tags.rs +++ b/codex-rs/core/src/sandbox_tags.rs @@ -1,24 +1,10 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; -#[cfg(test)] -use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; use codex_sandboxing::policy_transforms::should_require_platform_sandbox; use std::path::Path; -#[cfg(test)] -pub(crate) fn sandbox_tag( - policy: &SandboxPolicy, - windows_sandbox_level: WindowsSandboxLevel, -) -> &'static str { - permission_profile_sandbox_tag( - &PermissionProfile::from_legacy_sandbox_policy(policy), - windows_sandbox_level, - /*enforce_managed_network*/ false, - ) -} - pub(crate) fn permission_profile_sandbox_tag( profile: &PermissionProfile, windows_sandbox_level: WindowsSandboxLevel, diff --git a/codex-rs/core/src/sandbox_tags_tests.rs b/codex-rs/core/src/sandbox_tags_tests.rs index 8b00de9ccd..64dc50574f 100644 --- a/codex-rs/core/src/sandbox_tags_tests.rs +++ b/codex-rs/core/src/sandbox_tags_tests.rs @@ -1,6 +1,5 @@ use super::permission_profile_policy_tag; use super::permission_profile_sandbox_tag; -use super::sandbox_tag; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; @@ -10,8 +9,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::NetworkAccess; -use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; use codex_utils_absolute_path::AbsolutePathBuf; @@ -20,29 +17,32 @@ use std::path::Path; #[test] fn danger_full_access_is_untagged_even_when_linux_sandbox_defaults_apply() { - let actual = sandbox_tag( - &SandboxPolicy::DangerFullAccess, + let actual = permission_profile_sandbox_tag( + &PermissionProfile::Disabled, WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, ); assert_eq!(actual, "none"); } #[test] fn external_sandbox_keeps_external_tag_when_linux_sandbox_defaults_apply() { - let actual = sandbox_tag( - &SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Enabled, + let actual = permission_profile_sandbox_tag( + &PermissionProfile::External { + network: NetworkSandboxPolicy::Enabled, }, WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, ); assert_eq!(actual, "external"); } #[test] fn default_linux_sandbox_uses_platform_sandbox_tag() { - let actual = sandbox_tag( - &SandboxPolicy::new_read_only_policy(), + let actual = permission_profile_sandbox_tag( + &PermissionProfile::read_only(), WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, ); let expected = get_platform_sandbox(/*windows_sandbox_enabled*/ false) .map(SandboxType::as_metric_tag) diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index a9c7a7b87d..2f79927103 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -1,9 +1,8 @@ use super::*; -use crate::sandbox_tags::sandbox_tag; +use crate::sandbox_tags::permission_profile_sandbox_tag; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::ThreadSource; use core_test_support::PathBufExt; use core_test_support::PathExt; @@ -89,7 +88,6 @@ async fn build_turn_metadata_header_includes_has_changes_for_clean_repo() { fn turn_metadata_state_uses_platform_sandbox_tag() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); - let sandbox_policy = SandboxPolicy::new_read_only_policy(); let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( @@ -110,7 +108,11 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { let thread_id = json.get("thread_id").and_then(Value::as_str); let thread_source = json.get("thread_source").and_then(Value::as_str); - let expected_sandbox = sandbox_tag(&sandbox_policy, WindowsSandboxLevel::Disabled); + let expected_sandbox = permission_profile_sandbox_tag( + &permission_profile, + WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, + ); assert_eq!(sandbox_name, Some(expected_sandbox)); assert_eq!(session_id, Some("session-a")); assert_eq!(thread_id, Some("thread-a")); From bcf35da3edecb9cb55982ea8962d66685b9426d6 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 15 May 2026 10:58:53 -0700 Subject: [PATCH 2/3] app-server: stop returning thread permission profiles --- .../analytics/src/analytics_client_tests.rs | 2 - codex-rs/analytics/src/client_tests.rs | 9 - .../codex_app_server_protocol.schemas.json | 6 +- .../codex_app_server_protocol.v2.schemas.json | 6 +- .../schema/json/v2/ThreadForkResponse.json | 327 +----------------- .../schema/json/v2/ThreadResumeResponse.json | 327 +----------------- .../schema/json/v2/ThreadStartResponse.json | 327 +----------------- .../typescript/v2/ThreadForkResponse.ts | 3 +- .../typescript/v2/ThreadResumeResponse.ts | 3 +- .../typescript/v2/ThreadStartResponse.ts | 3 +- .../src/protocol/common.rs | 3 +- .../src/protocol/v2/tests.rs | 3 - .../src/protocol/v2/thread.rs | 25 +- codex-rs/app-server/README.md | 2 +- .../request_processors/thread_lifecycle.rs | 1 - .../request_processors/thread_processor.rs | 3 - codex-rs/exec/src/lib.rs | 12 +- codex-rs/exec/src/lib_tests.rs | 11 +- codex-rs/tui/src/app_server_session.rs | 115 +++--- 19 files changed, 73 insertions(+), 1115 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 7b9ab4f9d6..ed626699d3 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -206,7 +206,6 @@ fn sample_thread_start_response( approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, - permission_profile: None, active_permission_profile: None, reasoning_effort: None, }) @@ -263,7 +262,6 @@ fn sample_thread_resume_response_with_source( approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, - permission_profile: None, active_permission_profile: None, reasoning_effort: None, }) diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index 885875346d..bfb224d899 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -12,7 +12,6 @@ use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer; use codex_app_server_protocol::AskForApproval as AppServerAskForApproval; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientResponsePayload; -use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy; use codex_app_server_protocol::SessionSource as AppServerSessionSource; @@ -29,7 +28,6 @@ use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus as AppServerTurnStatus; use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; -use codex_protocol::models::PermissionProfile as CorePermissionProfile; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; use std::collections::HashSet; @@ -142,10 +140,6 @@ fn sample_thread(thread_id: &str) -> Thread { } } -fn sample_permission_profile() -> AppServerPermissionProfile { - CorePermissionProfile::Disabled.into() -} - fn sample_thread_start_response() -> ClientResponsePayload { ClientResponsePayload::ThreadStart(ThreadStartResponse { thread: sample_thread("thread-1"), @@ -158,7 +152,6 @@ fn sample_thread_start_response() -> ClientResponsePayload { approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, - permission_profile: Some(sample_permission_profile()), active_permission_profile: None, reasoning_effort: None, }) @@ -176,7 +169,6 @@ fn sample_thread_resume_response() -> ClientResponsePayload { approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, - permission_profile: Some(sample_permission_profile()), active_permission_profile: None, reasoning_effort: None, }) @@ -194,7 +186,6 @@ fn sample_thread_fork_response() -> ClientResponsePayload { approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, - permission_profile: Some(sample_permission_profile()), active_permission_profile: None, reasoning_effort: None, }) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 8b292f667d..935ec98e88 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -15527,7 +15527,7 @@ "$ref": "#/definitions/v2/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ @@ -17019,7 +17019,7 @@ "$ref": "#/definitions/v2/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ @@ -17327,7 +17327,7 @@ "$ref": "#/definitions/v2/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 16e548e8c2..ccf68b28d8 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -13351,7 +13351,7 @@ "$ref": "#/definitions/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ @@ -14843,7 +14843,7 @@ "$ref": "#/definitions/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ @@ -15151,7 +15151,7 @@ "$ref": "#/definitions/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 4eb85f4ed3..1608b2f48c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -470,202 +470,6 @@ ], "type": "string" }, - "FileSystemAccessMode": { - "enum": [ - "read", - "write", - "none" - ], - "type": "string" - }, - "FileSystemPath": { - "oneOf": [ - { - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "path" - ], - "title": "PathFileSystemPathType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "PathFileSystemPath", - "type": "object" - }, - { - "properties": { - "pattern": { - "type": "string" - }, - "type": { - "enum": [ - "glob_pattern" - ], - "title": "GlobPatternFileSystemPathType", - "type": "string" - } - }, - "required": [ - "pattern", - "type" - ], - "title": "GlobPatternFileSystemPath", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "special" - ], - "title": "SpecialFileSystemPathType", - "type": "string" - }, - "value": { - "$ref": "#/definitions/FileSystemSpecialPath" - } - }, - "required": [ - "type", - "value" - ], - "title": "SpecialFileSystemPath", - "type": "object" - } - ] - }, - "FileSystemSandboxEntry": { - "properties": { - "access": { - "$ref": "#/definitions/FileSystemAccessMode" - }, - "path": { - "$ref": "#/definitions/FileSystemPath" - } - }, - "required": [ - "access", - "path" - ], - "type": "object" - }, - "FileSystemSpecialPath": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "root" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "RootFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "minimal" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "MinimalFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "project_roots" - ], - "type": "string" - }, - "subpath": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "tmpdir" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "TmpdirFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "slash_tmp" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "SlashTmpFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "unknown" - ], - "type": "string" - }, - "path": { - "type": "string" - }, - "subpath": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind", - "path" - ], - "type": "object" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -904,135 +708,6 @@ } ] }, - "PermissionProfile": { - "oneOf": [ - { - "description": "Codex owns sandbox construction for this profile.", - "properties": { - "fileSystem": { - "$ref": "#/definitions/PermissionProfileFileSystemPermissions" - }, - "network": { - "$ref": "#/definitions/PermissionProfileNetworkPermissions" - }, - "type": { - "enum": [ - "managed" - ], - "title": "ManagedPermissionProfileType", - "type": "string" - } - }, - "required": [ - "fileSystem", - "network", - "type" - ], - "title": "ManagedPermissionProfile", - "type": "object" - }, - { - "description": "Do not apply an outer sandbox.", - "properties": { - "type": { - "enum": [ - "disabled" - ], - "title": "DisabledPermissionProfileType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "DisabledPermissionProfile", - "type": "object" - }, - { - "description": "Filesystem isolation is enforced by an external caller.", - "properties": { - "network": { - "$ref": "#/definitions/PermissionProfileNetworkPermissions" - }, - "type": { - "enum": [ - "external" - ], - "title": "ExternalPermissionProfileType", - "type": "string" - } - }, - "required": [ - "network", - "type" - ], - "title": "ExternalPermissionProfile", - "type": "object" - } - ] - }, - "PermissionProfileFileSystemPermissions": { - "oneOf": [ - { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" - }, - "type": "array" - }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedPermissionProfileFileSystemPermissionsType", - "type": "string" - } - }, - "required": [ - "entries", - "type" - ], - "title": "RestrictedPermissionProfileFileSystemPermissions", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "unrestricted" - ], - "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "UnrestrictedPermissionProfileFileSystemPermissions", - "type": "object" - } - ] - }, - "PermissionProfileNetworkPermissions": { - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -2572,7 +2247,7 @@ "$ref": "#/definitions/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 312d289e41..302c2e1069 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -470,202 +470,6 @@ ], "type": "string" }, - "FileSystemAccessMode": { - "enum": [ - "read", - "write", - "none" - ], - "type": "string" - }, - "FileSystemPath": { - "oneOf": [ - { - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "path" - ], - "title": "PathFileSystemPathType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "PathFileSystemPath", - "type": "object" - }, - { - "properties": { - "pattern": { - "type": "string" - }, - "type": { - "enum": [ - "glob_pattern" - ], - "title": "GlobPatternFileSystemPathType", - "type": "string" - } - }, - "required": [ - "pattern", - "type" - ], - "title": "GlobPatternFileSystemPath", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "special" - ], - "title": "SpecialFileSystemPathType", - "type": "string" - }, - "value": { - "$ref": "#/definitions/FileSystemSpecialPath" - } - }, - "required": [ - "type", - "value" - ], - "title": "SpecialFileSystemPath", - "type": "object" - } - ] - }, - "FileSystemSandboxEntry": { - "properties": { - "access": { - "$ref": "#/definitions/FileSystemAccessMode" - }, - "path": { - "$ref": "#/definitions/FileSystemPath" - } - }, - "required": [ - "access", - "path" - ], - "type": "object" - }, - "FileSystemSpecialPath": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "root" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "RootFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "minimal" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "MinimalFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "project_roots" - ], - "type": "string" - }, - "subpath": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "tmpdir" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "TmpdirFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "slash_tmp" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "SlashTmpFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "unknown" - ], - "type": "string" - }, - "path": { - "type": "string" - }, - "subpath": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind", - "path" - ], - "type": "object" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -904,135 +708,6 @@ } ] }, - "PermissionProfile": { - "oneOf": [ - { - "description": "Codex owns sandbox construction for this profile.", - "properties": { - "fileSystem": { - "$ref": "#/definitions/PermissionProfileFileSystemPermissions" - }, - "network": { - "$ref": "#/definitions/PermissionProfileNetworkPermissions" - }, - "type": { - "enum": [ - "managed" - ], - "title": "ManagedPermissionProfileType", - "type": "string" - } - }, - "required": [ - "fileSystem", - "network", - "type" - ], - "title": "ManagedPermissionProfile", - "type": "object" - }, - { - "description": "Do not apply an outer sandbox.", - "properties": { - "type": { - "enum": [ - "disabled" - ], - "title": "DisabledPermissionProfileType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "DisabledPermissionProfile", - "type": "object" - }, - { - "description": "Filesystem isolation is enforced by an external caller.", - "properties": { - "network": { - "$ref": "#/definitions/PermissionProfileNetworkPermissions" - }, - "type": { - "enum": [ - "external" - ], - "title": "ExternalPermissionProfileType", - "type": "string" - } - }, - "required": [ - "network", - "type" - ], - "title": "ExternalPermissionProfile", - "type": "object" - } - ] - }, - "PermissionProfileFileSystemPermissions": { - "oneOf": [ - { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" - }, - "type": "array" - }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedPermissionProfileFileSystemPermissionsType", - "type": "string" - } - }, - "required": [ - "entries", - "type" - ], - "title": "RestrictedPermissionProfileFileSystemPermissions", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "unrestricted" - ], - "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "UnrestrictedPermissionProfileFileSystemPermissions", - "type": "object" - } - ] - }, - "PermissionProfileNetworkPermissions": { - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -2572,7 +2247,7 @@ "$ref": "#/definitions/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index c363f2e78d..9dc08614cf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -470,202 +470,6 @@ ], "type": "string" }, - "FileSystemAccessMode": { - "enum": [ - "read", - "write", - "none" - ], - "type": "string" - }, - "FileSystemPath": { - "oneOf": [ - { - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "path" - ], - "title": "PathFileSystemPathType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "PathFileSystemPath", - "type": "object" - }, - { - "properties": { - "pattern": { - "type": "string" - }, - "type": { - "enum": [ - "glob_pattern" - ], - "title": "GlobPatternFileSystemPathType", - "type": "string" - } - }, - "required": [ - "pattern", - "type" - ], - "title": "GlobPatternFileSystemPath", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "special" - ], - "title": "SpecialFileSystemPathType", - "type": "string" - }, - "value": { - "$ref": "#/definitions/FileSystemSpecialPath" - } - }, - "required": [ - "type", - "value" - ], - "title": "SpecialFileSystemPath", - "type": "object" - } - ] - }, - "FileSystemSandboxEntry": { - "properties": { - "access": { - "$ref": "#/definitions/FileSystemAccessMode" - }, - "path": { - "$ref": "#/definitions/FileSystemPath" - } - }, - "required": [ - "access", - "path" - ], - "type": "object" - }, - "FileSystemSpecialPath": { - "oneOf": [ - { - "properties": { - "kind": { - "enum": [ - "root" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "RootFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "minimal" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "MinimalFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "project_roots" - ], - "type": "string" - }, - "subpath": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind" - ], - "title": "KindFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "tmpdir" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "TmpdirFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "slash_tmp" - ], - "type": "string" - } - }, - "required": [ - "kind" - ], - "title": "SlashTmpFileSystemSpecialPath", - "type": "object" - }, - { - "properties": { - "kind": { - "enum": [ - "unknown" - ], - "type": "string" - }, - "path": { - "type": "string" - }, - "subpath": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "kind", - "path" - ], - "type": "object" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -904,135 +708,6 @@ } ] }, - "PermissionProfile": { - "oneOf": [ - { - "description": "Codex owns sandbox construction for this profile.", - "properties": { - "fileSystem": { - "$ref": "#/definitions/PermissionProfileFileSystemPermissions" - }, - "network": { - "$ref": "#/definitions/PermissionProfileNetworkPermissions" - }, - "type": { - "enum": [ - "managed" - ], - "title": "ManagedPermissionProfileType", - "type": "string" - } - }, - "required": [ - "fileSystem", - "network", - "type" - ], - "title": "ManagedPermissionProfile", - "type": "object" - }, - { - "description": "Do not apply an outer sandbox.", - "properties": { - "type": { - "enum": [ - "disabled" - ], - "title": "DisabledPermissionProfileType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "DisabledPermissionProfile", - "type": "object" - }, - { - "description": "Filesystem isolation is enforced by an external caller.", - "properties": { - "network": { - "$ref": "#/definitions/PermissionProfileNetworkPermissions" - }, - "type": { - "enum": [ - "external" - ], - "title": "ExternalPermissionProfileType", - "type": "string" - } - }, - "required": [ - "network", - "type" - ], - "title": "ExternalPermissionProfile", - "type": "object" - } - ] - }, - "PermissionProfileFileSystemPermissions": { - "oneOf": [ - { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" - }, - "type": "array" - }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedPermissionProfileFileSystemPermissionsType", - "type": "string" - } - }, - "required": [ - "entries", - "type" - ], - "title": "RestrictedPermissionProfileFileSystemPermissions", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "unrestricted" - ], - "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "UnrestrictedPermissionProfileFileSystemPermissions", - "type": "object" - } - ] - }, - "PermissionProfileNetworkPermissions": { - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -2572,7 +2247,7 @@ "$ref": "#/definitions/SandboxPolicy" } ], - "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." }, "serviceTier": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts index c44533ec1a..c5b1201c26 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -16,7 +16,6 @@ instructionSources: Array, approvalPolicy: AskForApproval, /** */ approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `permissionProfile` when they need exact runtime - * permissions. + * should prefer `activePermissionProfile` for profile provenance. */ sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index f91756c7c6..7a4f90377c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -16,7 +16,6 @@ instructionSources: Array, approvalPolicy: AskForApproval, /** */ approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `permissionProfile` when they need exact runtime - * permissions. + * should prefer `activePermissionProfile` for profile provenance. */ sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index 9573bd7dee..38859a3805 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -16,7 +16,6 @@ instructionSources: Array, approvalPolicy: AskForApproval, /** */ approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `permissionProfile` when they need exact runtime - * permissions. + * should prefer `activePermissionProfile` for profile provenance. */ sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index e0cd330fff..f7e04b3dc5 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -223,6 +223,7 @@ macro_rules! client_request_definitions { /// Typed response from the server to the client. #[derive(Serialize, Deserialize, Debug, Clone)] + #[allow(clippy::large_enum_variant)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ClientResponse { $( @@ -2301,7 +2302,6 @@ mod tests { approval_policy: v2::AskForApproval::OnFailure, approvals_reviewer: v2::ApprovalsReviewer::User, sandbox: v2::SandboxPolicy::DangerFullAccess, - permission_profile: None, active_permission_profile: None, reasoning_effort: None, }, @@ -2348,7 +2348,6 @@ mod tests { "sandbox": { "type": "dangerFullAccess" }, - "permissionProfile": null, "activePermissionProfile": null, "reasoningEffort": null } diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 50058cb686..d17a3dd988 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3498,9 +3498,6 @@ fn thread_lifecycle_responses_default_missing_optional_fields() { assert_eq!(start.instruction_sources, Vec::::new()); assert_eq!(resume.instruction_sources, Vec::::new()); assert_eq!(fork.instruction_sources, Vec::::new()); - assert_eq!(start.permission_profile, None); - assert_eq!(resume.permission_profile, None); - assert_eq!(fork.permission_profile, None); assert_eq!(start.active_permission_profile, None); assert_eq!(resume.active_permission_profile, None); assert_eq!(fork.active_permission_profile, None); diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index a3321436f6..ffe2c353d1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -1,7 +1,6 @@ use super::ActivePermissionProfile; use super::ApprovalsReviewer; use super::AskForApproval; -use super::PermissionProfile; use super::PermissionProfileSelectionParams; use super::SandboxMode; use super::SandboxPolicy; @@ -213,14 +212,8 @@ pub struct ThreadStartResponse { /// Reviewer currently used for approval requests on this thread. pub approvals_reviewer: ApprovalsReviewer, /// Legacy sandbox policy retained for compatibility. Experimental clients - /// should prefer `permissionProfile` when they need exact runtime - /// permissions. + /// should prefer `activePermissionProfile` for profile provenance. pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/start.permissionProfile")] - #[serde(default)] - pub permission_profile: Option, /// Named or implicit built-in profile that produced the active /// permissions, when known. #[experimental("thread/start.activePermissionProfile")] @@ -339,14 +332,8 @@ pub struct ThreadResumeResponse { /// Reviewer currently used for approval requests on this thread. pub approvals_reviewer: ApprovalsReviewer, /// Legacy sandbox policy retained for compatibility. Experimental clients - /// should prefer `permissionProfile` when they need exact runtime - /// permissions. + /// should prefer `activePermissionProfile` for profile provenance. pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/resume.permissionProfile")] - #[serde(default)] - pub permission_profile: Option, /// Named or implicit built-in profile that produced the active /// permissions, when known. #[experimental("thread/resume.activePermissionProfile")] @@ -459,14 +446,8 @@ pub struct ThreadForkResponse { /// Reviewer currently used for approval requests on this thread. pub approvals_reviewer: ApprovalsReviewer, /// Legacy sandbox policy retained for compatibility. Experimental clients - /// should prefer `permissionProfile` when they need exact runtime - /// permissions. + /// should prefer `activePermissionProfile` for profile provenance. pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/fork.permissionProfile")] - #[serde(default)] - pub permission_profile: Option, /// Named or implicit built-in profile that produced the active /// permissions, when known. #[experimental("thread/fork.activePermissionProfile")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 09c450614f..4d21eb2846 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -133,7 +133,7 @@ Example with notification opt-out: - `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective thread cwd. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. - `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. - `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`. -- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots, `permissionProfile` for the exact active runtime permissions, and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. +- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. - `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. diff --git a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs index c64066ee93..4781df8350 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -626,7 +626,6 @@ pub(super) async fn handle_pending_thread_resume_request( approval_policy: approval_policy.into(), approvals_reviewer: approvals_reviewer.into(), sandbox, - permission_profile: Some(permission_profile.into()), active_permission_profile, reasoning_effort, }; diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 3038feeef1..21160950ab 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -1199,7 +1199,6 @@ impl ThreadRequestProcessor { approval_policy: config_snapshot.approval_policy.into(), approvals_reviewer: config_snapshot.approvals_reviewer.into(), sandbox, - permission_profile: Some(config_snapshot.permission_profile.into()), active_permission_profile, reasoning_effort: config_snapshot.reasoning_effort, }; @@ -2554,7 +2553,6 @@ impl ThreadRequestProcessor { approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), sandbox, - permission_profile: Some(config_snapshot.permission_profile.into()), active_permission_profile, reasoning_effort: session_configured.reasoning_effort, }; @@ -3215,7 +3213,6 @@ impl ThreadRequestProcessor { approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), sandbox, - permission_profile: Some(config_snapshot.permission_profile.into()), active_permission_profile, reasoning_effort: session_configured.reasoning_effort, }; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 9f7174ec68..1004124ba5 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -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, diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 367bba5d05..92dc0678e5 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -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, } diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 0470d24db6..42197e5b9c 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -565,7 +565,6 @@ impl AppServerSession { active_permission_profile, cwd.as_path(), workspace_roots, - self.thread_params_mode(), ); self.client .request_typed(ClientRequest::TurnStart { @@ -1189,19 +1188,12 @@ fn turn_permissions_overrides( active_permission_profile: Option, cwd: &std::path::Path, _workspace_roots: &[AbsolutePathBuf], - thread_params_mode: ThreadParamsMode, ) -> ( Option, Option, ) { - let permissions = if matches!(thread_params_mode, ThreadParamsMode::Embedded) { - active_permission_profile.map(permissions_selection_from_active_profile) - } else { - None - }; - let sandbox_policy = (matches!(thread_params_mode, ThreadParamsMode::Remote) - || permissions.is_none()) - .then(|| { + let permissions = active_permission_profile.map(permissions_selection_from_active_profile); + let sandbox_policy = permissions.is_none().then(|| { let legacy_profile = legacy_compatible_permission_profile(permission_profile, cwd); let policy = legacy_profile .to_legacy_sandbox_policy(cwd) @@ -1415,7 +1407,6 @@ async fn thread_session_state_from_thread_start_response( ) -> Result { let permission_profile = permission_profile_from_thread_response( &response.sandbox, - response.permission_profile.as_ref(), response.cwd.as_path(), config, thread_params_mode, @@ -1448,7 +1439,6 @@ async fn thread_session_state_from_thread_resume_response( ) -> Result { let permission_profile = permission_profile_from_thread_response( &response.sandbox, - response.permission_profile.as_ref(), response.cwd.as_path(), config, thread_params_mode, @@ -1481,7 +1471,6 @@ async fn thread_session_state_from_thread_fork_response( ) -> Result { let permission_profile = permission_profile_from_thread_response( &response.sandbox, - response.permission_profile.as_ref(), response.cwd.as_path(), config, thread_params_mode, @@ -1509,14 +1498,10 @@ async fn thread_session_state_from_thread_fork_response( fn permission_profile_from_thread_response( sandbox: &codex_app_server_protocol::SandboxPolicy, - permission_profile: Option<&codex_app_server_protocol::PermissionProfile>, cwd: &std::path::Path, config: &Config, thread_params_mode: ThreadParamsMode, ) -> PermissionProfile { - if let Some(permission_profile) = permission_profile { - return permission_profile.clone().into(); - } match thread_params_mode { ThreadParamsMode::Embedded => config.permissions.effective_permission_profile(), ThreadParamsMode::Remote => { @@ -1701,7 +1686,6 @@ mod tests { Some(active_permission_profile), cwd.as_path(), &workspace_roots, - ThreadParamsMode::Embedded, ); assert_eq!(sandbox_policy, None); @@ -1721,7 +1705,6 @@ mod tests { Some(active_permission_profile), cwd.as_path(), &workspace_roots, - ThreadParamsMode::Embedded, ); assert_eq!(sandbox_policy, None); @@ -1734,7 +1717,7 @@ mod tests { } #[test] - fn embedded_turn_permissions_fall_back_to_sandbox_without_active_profile() { + fn turn_permissions_fall_back_to_sandbox_without_active_profile() { let cwd = test_path_buf("/workspace/project").abs(); let (sandbox_policy, permissions) = turn_permissions_overrides( @@ -1742,7 +1725,6 @@ mod tests { /*active_permission_profile*/ None, cwd.as_path(), std::slice::from_ref(&cwd), - ThreadParamsMode::Embedded, ); assert_eq!( @@ -1755,26 +1737,44 @@ mod tests { } #[test] - fn remote_turn_permissions_use_sandbox_even_with_active_profile() { + fn remote_turn_permissions_preserve_active_profile_selection() { let cwd = test_path_buf("/workspace/project").abs(); + let permission_profile: PermissionProfile = AppServerPermissionProfile::Managed { + file_system: PermissionProfileFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::ProjectRoots { + subpath: Some(".env".into()), + }, + }, + access: FileSystemAccessMode::None, + }, + ], + glob_scan_max_depth: None, + }, + network: PermissionProfileNetworkPermissions { enabled: false }, + } + .into(); + let active_permission_profile = ActivePermissionProfile::new("strict"); + let expected_permissions = + permissions_selection_from_active_profile(active_permission_profile.clone()); let (sandbox_policy, permissions) = turn_permissions_overrides( - &PermissionProfile::read_only(), - Some(ActivePermissionProfile::new( - BUILT_IN_PERMISSION_PROFILE_READ_ONLY, - )), + &permission_profile, + Some(active_permission_profile), cwd.as_path(), std::slice::from_ref(&cwd), - ThreadParamsMode::Remote, ); - assert_eq!( - sandbox_policy, - Some(codex_app_server_protocol::SandboxPolicy::ReadOnly { - network_access: false - }) - ); - assert_eq!(permissions, None); + assert_eq!(sandbox_policy, None); + assert_eq!(permissions, Some(expected_permissions)); } #[tokio::test] @@ -2109,7 +2109,6 @@ mod tests { .to_legacy_sandbox_policy(test_path_buf("/tmp/project").as_path()) .expect("read-only profile must be legacy-compatible") .into(), - permission_profile: Some(read_only_profile.clone().into()), active_permission_profile: None, reasoning_effort: None, }; @@ -2147,61 +2146,43 @@ mod tests { } #[tokio::test] - async fn remote_thread_response_prefers_permission_profile_over_legacy_sandbox() { + async fn remote_thread_response_uses_legacy_sandbox_fallback() { let temp_dir = tempfile::tempdir().expect("tempdir"); let config = build_config(&temp_dir).await; let cwd = test_path_buf("/tmp/project").abs(); - let fallback_sandbox = PermissionProfile::read_only() + let sandbox = PermissionProfile::read_only() .to_legacy_sandbox_policy(cwd.as_path()) .expect("read-only profile must be legacy-compatible") .into(); - let response_profile = AppServerPermissionProfile::Managed { - file_system: PermissionProfileFileSystemPermissions::Restricted { - entries: vec![ - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::ProjectRoots { - subpath: Some(".env".into()), - }, - }, - access: FileSystemAccessMode::None, - }, - ], - glob_scan_max_depth: None, - }, - network: PermissionProfileNetworkPermissions { enabled: false }, - }; - let split_profile: PermissionProfile = response_profile.clone().into(); assert_eq!( permission_profile_from_thread_response( - &fallback_sandbox, - Some(&response_profile), + &sandbox, cwd.as_path(), &config, ThreadParamsMode::Remote, ), - split_profile + PermissionProfile::read_only() ); } #[tokio::test] - async fn embedded_thread_response_prefers_permission_profile_when_present() { + async fn embedded_thread_response_uses_local_config_profile() { let temp_dir = tempfile::tempdir().expect("tempdir"); - let config = build_config(&temp_dir).await; + let config = ConfigBuilder::default() + .codex_home(temp_dir.path().to_path_buf()) + .harness_overrides(ConfigOverrides { + default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()), + ..ConfigOverrides::default() + }) + .build() + .await + .expect("config should build"); let cwd = test_path_buf("/tmp/project").abs(); - let response_profile = PermissionProfile::read_only().into(); assert_eq!( permission_profile_from_thread_response( &codex_app_server_protocol::SandboxPolicy::DangerFullAccess, - Some(&response_profile), cwd.as_path(), &config, ThreadParamsMode::Embedded, From 6186e5e672bc857ca0fd04ff848264f3ced33e20 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 15 May 2026 10:58:53 -0700 Subject: [PATCH 3/3] core: construct test permission profiles directly --- .../src/config/network_proxy_spec_tests.rs | 29 ++++------ codex-rs/core/src/session/tests.rs | 55 +++++++++---------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/codex-rs/core/src/config/network_proxy_spec_tests.rs b/codex-rs/core/src/config/network_proxy_spec_tests.rs index 14b7c1c330..6dfb1e3a25 100644 --- a/codex-rs/core/src/config/network_proxy_spec_tests.rs +++ b/codex-rs/core/src/config/network_proxy_spec_tests.rs @@ -5,13 +5,8 @@ use codex_network_proxy::NetworkDomainPermission; use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::SandboxPolicy; use pretty_assertions::assert_eq; -fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { - PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) -} - fn domain_permissions( entries: impl IntoIterator, ) -> NetworkDomainPermissionsToml { @@ -62,7 +57,7 @@ fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_read_only_policy()), + &PermissionProfile::read_only(), ) .expect("config should stay within the managed allowlist"); @@ -97,7 +92,7 @@ fn requirements_allowed_domains_do_not_override_user_denies_for_same_pattern() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &PermissionProfile::workspace_write(), ) .expect("managed allowlist should not erase a user deny"); @@ -129,7 +124,7 @@ fn requirements_allowlist_expansion_keeps_user_entries_mutable() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &PermissionProfile::workspace_write(), ) .expect("managed baseline should still allow user edits"); @@ -207,7 +202,7 @@ fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), + &PermissionProfile::Disabled, ) .expect("yolo mode should pin the effective policy to the managed baseline"); @@ -241,7 +236,7 @@ fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &PermissionProfile::workspace_write(), ) .expect("managed baseline should still load"); @@ -270,7 +265,7 @@ fn managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses() let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &PermissionProfile::workspace_write(), ) .expect("managed-only allowlist should still load"); @@ -300,7 +295,7 @@ fn managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domain let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &PermissionProfile::workspace_write(), ) .expect("managed-only mode should treat missing managed allowlist as empty"); @@ -324,7 +319,7 @@ fn managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_m let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), + &PermissionProfile::Disabled, ) .expect("managed-only mode should treat missing managed allowlist as empty"); @@ -351,7 +346,7 @@ fn deny_only_requirements_do_not_create_allow_constraints_in_full_access() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), + &PermissionProfile::Disabled, ) .expect("deny-only requirements should not constrain the allowlist"); @@ -384,7 +379,7 @@ fn allow_only_requirements_do_not_create_deny_constraints_in_full_access() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), + &PermissionProfile::Disabled, ) .expect("allow-only requirements should not constrain the denylist"); @@ -417,7 +412,7 @@ fn requirements_denied_domains_are_a_baseline_for_default_mode() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &PermissionProfile::workspace_write(), ) .expect("default mode should merge managed and user deny entries"); @@ -452,7 +447,7 @@ fn requirements_denylist_expansion_keeps_user_entries_mutable() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &PermissionProfile::workspace_write(), ) .expect("managed baseline should still allow user edits"); diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 9265072dde..aa89efe1d4 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -172,10 +172,6 @@ use std::time::Duration as StdDuration; mod guardian_tests; -fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { - PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) -} - struct InstructionsTestCase { slug: &'static str, expects_apply_patch_description: bool, @@ -670,10 +666,11 @@ fn validated_network_policy_amendment_host_rejects_mismatch() { #[tokio::test] async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyhow::Result<()> { + let permission_profile = PermissionProfile::workspace_write(); let spec = crate::config::NetworkProxySpec::from_config_and_constraints( NetworkProxyConfig::default(), /*requirements*/ None, - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &permission_profile, )?; let mut exec_policy = Policy::empty(); exec_policy.add_network_rule( @@ -686,7 +683,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho let (started_proxy, _) = Session::start_managed_network_proxy( &spec, &exec_policy, - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &permission_profile, /*network_policy_decider*/ None, /*blocked_request_observer*/ None, /*managed_network_requirements_enabled*/ false, @@ -705,6 +702,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho #[tokio::test] async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules() -> anyhow::Result<()> { + let permission_profile = PermissionProfile::workspace_write(); let spec = crate::config::NetworkProxySpec::from_config_and_constraints( NetworkProxyConfig::default(), Some(NetworkConstraints { @@ -717,7 +715,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules() managed_allowed_domains_only: Some(true), ..Default::default() }), - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &permission_profile, )?; let mut exec_policy = Policy::empty(); exec_policy.add_network_rule( @@ -730,7 +728,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules() let (started_proxy, _) = Session::start_managed_network_proxy( &spec, &exec_policy, - &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + &permission_profile, /*network_policy_decider*/ None, /*blocked_request_observer*/ None, /*managed_network_requirements_enabled*/ false, @@ -748,13 +746,14 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules() #[tokio::test] async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::Result<()> { + let full_access_permission_profile = PermissionProfile::Disabled; let spec = crate::config::NetworkProxySpec::from_config_and_constraints( NetworkProxyConfig::default(), Some(NetworkConstraints { enabled: Some(true), ..Default::default() }), - &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), + &full_access_permission_profile, )?; let exec_policy = Policy::empty(); let decider_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0)); @@ -769,7 +768,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R let (started_proxy, _) = Session::start_managed_network_proxy( &spec, &exec_policy, - &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), + &full_access_permission_profile, Some(network_policy_decider), /*blocked_request_observer*/ None, /*managed_network_requirements_enabled*/ true, @@ -777,9 +776,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R ) .await?; - let spec = spec.recompute_for_permission_profile(&permission_profile_for_sandbox_policy( - &SandboxPolicy::new_workspace_write_policy(), - ))?; + let spec = spec.recompute_for_permission_profile(&PermissionProfile::workspace_write())?; spec.apply_to_started_proxy(&started_proxy).await?; let current_cfg = started_proxy.proxy().current_cfg().await?; assert_eq!(current_cfg.network.allowed_domains(), None); @@ -818,6 +815,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R #[tokio::test] async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow::Result<()> { let (mut session, _turn_context) = make_session_and_context().await; + let initial_permission_profile = PermissionProfile::workspace_write(); let initial_policy = SandboxPolicy::new_workspace_write_policy(); let mut network_config = NetworkProxyConfig::default(); @@ -836,12 +834,12 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow let spec = crate::config::NetworkProxySpec::from_config_and_constraints( network_config, Some(requirements), - &permission_profile_for_sandbox_policy(&initial_policy), + &initial_permission_profile, )?; let (started_proxy, _) = Session::start_managed_network_proxy( &spec, &Policy::empty(), - &permission_profile_for_sandbox_policy(&initial_policy), + &initial_permission_profile, /*network_policy_decider*/ None, /*blocked_request_observer*/ None, /*managed_network_requirements_enabled*/ false, @@ -870,9 +868,7 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow state.session_configuration.original_config_do_not_use = Arc::new(config); state .session_configuration - .set_permission_profile_for_tests(PermissionProfile::from_legacy_sandbox_policy( - &initial_policy, - )) + .set_permission_profile_for_tests(initial_permission_profile) .expect("test setup should allow permission profile"); } session.services.network_proxy = Some(started_proxy); @@ -913,7 +909,7 @@ async fn danger_full_access_turns_do_not_expose_managed_network_proxy() -> anyho enabled: Some(true), ..Default::default() }), - &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), + &PermissionProfile::Disabled, )?; let session = make_session_with_config(move |config| { @@ -979,7 +975,7 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an enabled: Some(true), ..Default::default() }), - &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), + &PermissionProfile::Disabled, )?; let session = make_session_with_config(move |config| { @@ -1047,6 +1043,7 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an #[tokio::test] async fn workspace_write_turns_continue_to_expose_managed_network_proxy() -> anyhow::Result<()> { + let permission_profile = PermissionProfile::workspace_write(); let sandbox_policy = SandboxPolicy::new_workspace_write_policy(); let network_spec = crate::config::NetworkProxySpec::from_config_and_constraints( NetworkProxyConfig::default(), @@ -1054,7 +1051,7 @@ async fn workspace_write_turns_continue_to_expose_managed_network_proxy() -> any enabled: Some(true), ..Default::default() }), - &permission_profile_for_sandbox_policy(&sandbox_policy), + &permission_profile, )?; let session = make_session_with_config(move |config| { @@ -1074,6 +1071,7 @@ async fn workspace_write_turns_continue_to_expose_managed_network_proxy() -> any #[tokio::test] async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::Result<()> { + let permission_profile = PermissionProfile::workspace_write(); let sandbox_policy = SandboxPolicy::new_workspace_write_policy(); let network_spec = crate::config::NetworkProxySpec::from_config_and_constraints( NetworkProxyConfig::default(), @@ -1081,7 +1079,7 @@ async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::R enabled: Some(true), ..Default::default() }), - &permission_profile_for_sandbox_policy(&sandbox_policy), + &permission_profile, )?; let (session, rx) = make_session_with_config_and_rx(move |config| { @@ -2127,13 +2125,14 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() -> let sandbox_policy = SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Restricted, }; - let expected_sandbox_policy = sandbox_policy.clone(); + let permission_profile = PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }; + let expected_permission_profile = permission_profile.clone(); let mut builder = test_codex().with_config(move |config| { config .permissions - .set_permission_profile(PermissionProfile::from_legacy_sandbox_policy( - &sandbox_policy, - )) + .set_permission_profile(permission_profile.clone()) .expect("set permission profile"); config .set_legacy_sandbox_policy(sandbox_policy) @@ -2142,10 +2141,6 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() -> let test = builder.build(&server).await?; - let expected_permission_profile = - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &expected_sandbox_policy, - ); assert_eq!( test.session_configured.permission_profile, expected_permission_profile, "ExternalSandbox is represented explicitly instead of as a lossy root-write profile"