From eb53109bdd35e967e4be4a5dd1135d9d36496f0c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 13 May 2026 11:42:07 -0700 Subject: [PATCH 1/2] permissions: move workspace roots onto thread state --- codex-rs/Cargo.lock | 3 + .../analytics/src/analytics_client_tests.rs | 2 + codex-rs/analytics/src/client_tests.rs | 15 +- .../schema/json/ClientRequest.json | 74 +-- .../codex_app_server_protocol.schemas.json | 149 ++---- .../codex_app_server_protocol.v2.schemas.json | 149 ++---- .../schema/json/v2/CommandExecParams.json | 7 - .../schema/json/v2/ThreadForkParams.json | 62 +-- .../schema/json/v2/ThreadForkResponse.json | 54 +-- .../schema/json/v2/ThreadResumeParams.json | 62 +-- .../schema/json/v2/ThreadResumeResponse.json | 54 +-- .../schema/json/v2/ThreadStartParams.json | 59 --- .../schema/json/v2/ThreadStartResponse.json | 54 +-- .../schema/json/v2/TurnStartParams.json | 68 +-- .../typescript/v2/ActivePermissionProfile.ts | 8 +- .../v2/ActivePermissionProfileModification.ts | 6 - .../v2/PermissionProfileModificationParams.ts | 6 - .../v2/PermissionProfileSelectionParams.ts | 6 - .../schema/typescript/v2/SandboxPolicy.ts | 3 +- .../schema/typescript/v2/ThreadForkParams.ts | 7 +- .../typescript/v2/ThreadForkResponse.ts | 11 +- .../typescript/v2/ThreadResumeParams.ts | 7 +- .../typescript/v2/ThreadResumeResponse.ts | 11 +- .../typescript/v2/ThreadStartResponse.ts | 11 +- .../schema/typescript/v2/TurnStartParams.ts | 4 +- .../schema/typescript/v2/index.ts | 3 - .../src/protocol/common.rs | 3 + .../src/protocol/v2/permissions.rs | 130 ++--- .../src/protocol/v2/tests.rs | 50 +- .../src/protocol/v2/thread.rs | 85 ++-- .../src/protocol/v2/turn.rs | 18 +- codex-rs/app-server/src/in_process.rs | 185 ++++--- codex-rs/app-server/src/lib.rs | 2 +- codex-rs/app-server/src/message_processor.rs | 138 +++--- .../src/message_processor_tracing_tests.rs | 137 +++--- codex-rs/app-server/src/request_processors.rs | 2 - .../command_exec_processor.rs | 31 +- .../request_processors/thread_lifecycle.rs | 9 +- .../request_processors/thread_processor.rs | 35 +- .../thread_processor_tests.rs | 2 + .../src/request_processors/thread_summary.rs | 29 +- .../src/request_processors/turn_processor.rs | 6 + codex-rs/app-server/tests/common/rollout.rs | 2 + .../tests/suite/conversation_summary.rs | 1 + .../app-server/tests/suite/v2/process_exec.rs | 7 +- .../app-server/tests/suite/v2/skills_list.rs | 1 + .../app-server/tests/suite/v2/thread_fork.rs | 83 ++++ .../app-server/tests/suite/v2/thread_list.rs | 1 + .../app-server/tests/suite/v2/thread_read.rs | 1 + .../tests/suite/v2/thread_resume.rs | 82 +++- .../tests/suite/v2/thread_unarchive.rs | 207 ++++---- .../app-server/tests/suite/v2/turn_start.rs | 34 +- .../tests/suite/v2/turn_start_zsh_fork.rs | 3 +- codex-rs/cli/src/debug_sandbox.rs | 20 +- codex-rs/config/src/config_requirements.rs | 4 - codex-rs/config/src/config_toml.rs | 4 +- codex-rs/core/src/codex_thread.rs | 36 +- .../core/src/config/config_loader_tests.rs | 6 +- codex-rs/core/src/config/config_tests.rs | 109 +++-- codex-rs/core/src/config/mod.rs | 210 +++++--- codex-rs/core/src/config/permissions.rs | 4 +- .../src/context/permissions_instructions.rs | 33 +- .../context/permissions_instructions_tests.rs | 42 +- .../sandbox_mode/workspace_write.md | 2 +- .../core/src/context_manager/history_tests.rs | 2 + codex-rs/core/src/context_manager/updates.rs | 11 +- codex-rs/core/src/exec.rs | 62 +-- codex-rs/core/src/exec_tests.rs | 62 ++- codex-rs/core/src/guardian/review_session.rs | 10 +- codex-rs/core/src/guardian/tests.rs | 8 +- .../core/src/personality_migration_tests.rs | 1 + codex-rs/core/src/rollout.rs | 4 + codex-rs/core/src/safety_tests.rs | 21 +- codex-rs/core/src/session/handlers.rs | 4 + codex-rs/core/src/session/mod.rs | 10 +- codex-rs/core/src/session/review.rs | 1 + .../session/rollout_reconstruction_tests.rs | 16 + codex-rs/core/src/session/session.rs | 134 ++++-- codex-rs/core/src/session/tests.rs | 162 +++++-- codex-rs/core/src/session/turn_context.rs | 22 +- .../src/tools/handlers/apply_patch_tests.rs | 2 - .../src/tools/handlers/multi_agents_common.rs | 1 + .../src/tools/handlers/multi_agents_tests.rs | 2 +- codex-rs/core/src/tools/orchestrator.rs | 3 + .../core/src/tools/runtimes/apply_patch.rs | 1 + .../src/tools/runtimes/apply_patch_tests.rs | 2 + codex-rs/core/src/tools/runtimes/mod_tests.rs | 1 + .../tools/runtimes/shell/unix_escalation.rs | 6 +- codex-rs/core/src/tools/sandboxing.rs | 2 + codex-rs/core/tests/common/test_codex.rs | 2 + codex-rs/core/tests/suite/approvals.rs | 4 - codex-rs/core/tests/suite/exec.rs | 1 + .../core/tests/suite/permissions_messages.rs | 10 +- .../core/tests/suite/personality_migration.rs | 2 + codex-rs/core/tests/suite/prompt_caching.rs | 9 +- .../core/tests/suite/request_permissions.rs | 1 - codex-rs/core/tests/suite/resume_warning.rs | 2 + codex-rs/core/tests/suite/sqlite_state.rs | 1 + codex-rs/core/tests/suite/unified_exec.rs | 19 +- codex-rs/core/tests/suite/user_shell_cmd.rs | 5 +- codex-rs/core/tests/suite/windows_sandbox.rs | 2 + codex-rs/exec-server/src/fs_sandbox.rs | 1 + codex-rs/exec/Cargo.toml | 1 + .../src/event_processor_with_human_output.rs | 90 +--- ...event_processor_with_human_output_tests.rs | 81 ---- codex-rs/exec/src/lib.rs | 155 ++++-- codex-rs/exec/src/lib_tests.rs | 218 ++++++++- .../tests/event_processor_with_json_output.rs | 1 + codex-rs/exec/tests/suite/sandbox.rs | 20 +- codex-rs/file-system/src/lib.rs | 22 +- .../linux-sandbox/tests/suite/landlock.rs | 2 + codex-rs/mcp-server/src/outgoing_message.rs | 5 + codex-rs/memories/write/src/phase2.rs | 33 +- codex-rs/protocol/src/models.rs | 38 +- codex-rs/protocol/src/permissions.rs | 140 +++--- codex-rs/protocol/src/protocol.rs | 418 +++++++++++++++- codex-rs/rollout/Cargo.toml | 1 + codex-rs/rollout/src/config.rs | 17 + codex-rs/rollout/src/metadata_tests.rs | 3 + codex-rs/rollout/src/recorder.rs | 1 + codex-rs/rollout/src/recorder_tests.rs | 4 + codex-rs/rollout/src/session_index_tests.rs | 1 + codex-rs/rollout/src/tests.rs | 1 + codex-rs/sandboxing/src/manager.rs | 49 +- codex-rs/sandboxing/src/manager_tests.rs | 42 ++ codex-rs/sandboxing/src/seatbelt_tests.rs | 118 ++--- codex-rs/state/src/extract.rs | 8 + codex-rs/state/src/runtime/threads.rs | 2 + codex-rs/thread-manager-sample/src/main.rs | 17 +- codex-rs/thread-store/Cargo.toml | 1 + .../thread-store/src/local/create_thread.rs | 1 + .../thread-store/src/local/list_threads.rs | 1 + .../thread-store/src/local/live_writer.rs | 1 + codex-rs/thread-store/src/local/mod.rs | 4 + .../src/local/update_thread_metadata.rs | 1 + .../thread-store/src/thread_metadata_sync.rs | 1 + codex-rs/thread-store/src/types.rs | 4 + codex-rs/tui/src/app/config_persistence.rs | 1 + codex-rs/tui/src/app/startup_prompts.rs | 2 +- codex-rs/tui/src/app/tests.rs | 8 + codex-rs/tui/src/app/thread_events.rs | 1 + codex-rs/tui/src/app/thread_routing.rs | 1 + codex-rs/tui/src/app/thread_session_state.rs | 6 + codex-rs/tui/src/app_server_session.rs | 453 ++++++++++++------ codex-rs/tui/src/chatwidget.rs | 11 +- .../tui/src/chatwidget/status_surfaces.rs | 4 +- .../chatwidget/tests/composer_submission.rs | 9 + .../tui/src/chatwidget/tests/exec_flow.rs | 1 + .../src/chatwidget/tests/history_replay.rs | 9 + .../tui/src/chatwidget/tests/permissions.rs | 2 + .../tui/src/chatwidget/tests/plan_mode.rs | 2 + .../src/chatwidget/tests/status_and_layout.rs | 31 ++ codex-rs/tui/src/history_cell.rs | 3 +- codex-rs/tui/src/lib.rs | 3 +- codex-rs/tui/src/permission_compat.rs | 95 ---- codex-rs/tui/src/session_state.rs | 3 + codex-rs/tui/src/status/card.rs | 88 ++-- codex-rs/tui/src/status/tests.rs | 131 +++-- codex-rs/utils/sandbox-summary/Cargo.toml | 2 +- .../sandbox-summary/src/config_summary.rs | 26 +- codex-rs/utils/sandbox-summary/src/lib.rs | 2 +- .../sandbox-summary/src/sandbox_summary.rs | 308 +++++++++--- codex-rs/windows-sandbox-rs/Cargo.toml | 2 +- codex-rs/windows-sandbox-rs/src/allow.rs | 78 +-- .../windows-sandbox-rs/src/elevated_impl.rs | 1 - codex-rs/windows-sandbox-rs/src/lib.rs | 35 +- codex-rs/windows-sandbox-rs/src/setup.rs | 59 ++- codex-rs/windows-sandbox-rs/src/spawn_prep.rs | 44 +- .../src/unified_exec/backends/legacy.rs | 1 + 169 files changed, 3771 insertions(+), 2411 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfileModification.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileModificationParams.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSelectionParams.ts delete mode 100644 codex-rs/tui/src/permission_compat.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 25827331b2..d1b3655835 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2709,6 +2709,7 @@ dependencies = [ "codex-utils-cargo-bin", "codex-utils-cli", "codex-utils-oss", + "codex-utils-sandbox-summary", "core_test_support", "libc", "opentelemetry", @@ -3526,6 +3527,7 @@ dependencies = [ "codex-otel", "codex-protocol", "codex-state", + "codex-utils-absolute-path", "codex-utils-path", "codex-utils-string", "pretty_assertions", @@ -3718,6 +3720,7 @@ dependencies = [ "codex-protocol", "codex-rollout", "codex-state", + "codex-utils-absolute-path", "codex-utils-path", "pretty_assertions", "serde", diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 5b4a72229b..be7332c8a5 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -201,6 +201,7 @@ fn sample_thread_start_response( model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, @@ -257,6 +258,7 @@ fn sample_thread_resume_response_with_source( model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index 71c46d808a..db182c6674 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"), @@ -153,11 +147,12 @@ fn sample_thread_start_response() -> ClientResponsePayload { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, - permission_profile: Some(sample_permission_profile()), + permission_profile: None, active_permission_profile: None, reasoning_effort: None, }) @@ -170,11 +165,12 @@ fn sample_thread_resume_response() -> ClientResponsePayload { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, - permission_profile: Some(sample_permission_profile()), + permission_profile: None, active_permission_profile: None, reasoning_effort: None, }) @@ -187,11 +183,12 @@ fn sample_thread_fork_response() -> ClientResponsePayload { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, sandbox: AppServerSandboxPolicy::DangerFullAccess, - permission_profile: Some(sample_permission_profile()), + permission_profile: None, active_permission_profile: None, reasoning_effort: None, }) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a6fe99b35e..31768b4627 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1850,31 +1850,6 @@ } ] }, - "PermissionProfileModificationParams": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParamsType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParams", - "type": "object" - } - ] - }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { @@ -1886,40 +1861,6 @@ ], "type": "object" }, - "PermissionProfileSelectionParams": { - "oneOf": [ - { - "description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.", - "properties": { - "id": { - "type": "string" - }, - "modifications": { - "items": { - "$ref": "#/definitions/PermissionProfileModificationParams" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "enum": [ - "profile" - ], - "title": "ProfilePermissionProfileSelectionParamsType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ProfilePermissionProfileSelectionParams", - "type": "object" - } - ] - }, "Personality": { "enum": [ "none", @@ -3144,13 +3085,6 @@ ], "title": "WorkspaceWriteSandboxPolicyType", "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" } }, "required": [ @@ -3417,7 +3351,8 @@ { "type": "null" } - ] + ], + "description": "Deprecated for fork. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "type": [ @@ -3828,7 +3763,8 @@ { "type": "null" } - ] + ], + "description": "Deprecated for resume. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "type": [ @@ -4208,7 +4144,7 @@ "type": "null" } ], - "description": "Override the sandbox policy for this turn and subsequent turns." + "description": "Deprecated for turns. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "description": "Override the service tier for this turn and subsequent turns.", 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 667607d43e..435e335826 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 @@ -5607,14 +5607,6 @@ "id": { "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", "type": "string" - }, - "modifications": { - "default": [], - "description": "Bounded user-requested modifications applied on top of the named profile, if any.", - "items": { - "$ref": "#/definitions/v2/ActivePermissionProfileModification" - }, - "type": "array" } }, "required": [ @@ -5622,31 +5614,6 @@ ], "type": "object" }, - "ActivePermissionProfileModification": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/v2/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootActivePermissionProfileModificationType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootActivePermissionProfileModification", - "type": "object" - } - ] - }, "AddCreditsNudgeCreditType": { "enum": [ "credits", @@ -11740,31 +11707,6 @@ } ] }, - "PermissionProfileModificationParams": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/v2/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParamsType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParams", - "type": "object" - } - ] - }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { @@ -11776,40 +11718,6 @@ ], "type": "object" }, - "PermissionProfileSelectionParams": { - "oneOf": [ - { - "description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.", - "properties": { - "id": { - "type": "string" - }, - "modifications": { - "items": { - "$ref": "#/definitions/v2/PermissionProfileModificationParams" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "enum": [ - "profile" - ], - "title": "ProfilePermissionProfileSelectionParamsType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ProfilePermissionProfileSelectionParams", - "type": "object" - } - ] - }, "Personality": { "enum": [ "none", @@ -14501,13 +14409,6 @@ ], "title": "WorkspaceWriteSandboxPolicyType", "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/v2/AbsolutePathBuf" - }, - "type": "array" } }, "required": [ @@ -15495,7 +15396,8 @@ { "type": "null" } - ] + ], + "description": "Deprecated for fork. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "type": [ @@ -15555,6 +15457,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -15571,7 +15485,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 `permissionProfile`." }, "serviceTier": { "type": [ @@ -16998,7 +16912,8 @@ { "type": "null" } - ] + ], + "description": "Deprecated for resume. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "type": [ @@ -17047,6 +16962,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -17063,7 +16990,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 `permissionProfile`." }, "serviceTier": { "type": [ @@ -17355,6 +17282,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/v2/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -17371,7 +17310,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 `permissionProfile`." }, "serviceTier": { "type": [ @@ -18040,7 +17979,7 @@ "type": "null" } ], - "description": "Override the sandbox policy for this turn and subsequent turns." + "description": "Deprecated for turns. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "description": "Override the service tier for this turn and subsequent turns.", 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 c1a99eddda..5775b179db 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 @@ -143,14 +143,6 @@ "id": { "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", "type": "string" - }, - "modifications": { - "default": [], - "description": "Bounded user-requested modifications applied on top of the named profile, if any.", - "items": { - "$ref": "#/definitions/ActivePermissionProfileModification" - }, - "type": "array" } }, "required": [ @@ -158,31 +150,6 @@ ], "type": "object" }, - "ActivePermissionProfileModification": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootActivePermissionProfileModificationType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootActivePermissionProfileModification", - "type": "object" - } - ] - }, "AddCreditsNudgeCreditType": { "enum": [ "credits", @@ -8289,31 +8256,6 @@ } ] }, - "PermissionProfileModificationParams": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParamsType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParams", - "type": "object" - } - ] - }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { @@ -8325,40 +8267,6 @@ ], "type": "object" }, - "PermissionProfileSelectionParams": { - "oneOf": [ - { - "description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.", - "properties": { - "id": { - "type": "string" - }, - "modifications": { - "items": { - "$ref": "#/definitions/PermissionProfileModificationParams" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "enum": [ - "profile" - ], - "title": "ProfilePermissionProfileSelectionParamsType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ProfilePermissionProfileSelectionParams", - "type": "object" - } - ] - }, "Personality": { "enum": [ "none", @@ -11050,13 +10958,6 @@ ], "title": "WorkspaceWriteSandboxPolicyType", "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" } }, "required": [ @@ -13319,7 +13220,8 @@ { "type": "null" } - ] + ], + "description": "Deprecated for fork. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "type": [ @@ -13379,6 +13281,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -13395,7 +13309,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 `permissionProfile`." }, "serviceTier": { "type": [ @@ -14822,7 +14736,8 @@ { "type": "null" } - ] + ], + "description": "Deprecated for resume. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "type": [ @@ -14871,6 +14786,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -14887,7 +14814,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 `permissionProfile`." }, "serviceTier": { "type": [ @@ -15179,6 +15106,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -15195,7 +15134,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 `permissionProfile`." }, "serviceTier": { "type": [ @@ -15864,7 +15803,7 @@ "type": "null" } ], - "description": "Override the sandbox policy for this turn and subsequent turns." + "description": "Deprecated for turns. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "description": "Override the service tier for this turn and subsequent turns.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json index f29483862c..6a1098fda4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -441,13 +441,6 @@ ], "title": "WorkspaceWriteSandboxPolicyType", "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index 29d67403cd..3b3a81bb92 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -64,65 +64,6 @@ } ] }, - "PermissionProfileModificationParams": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParamsType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParams", - "type": "object" - } - ] - }, - "PermissionProfileSelectionParams": { - "oneOf": [ - { - "description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.", - "properties": { - "id": { - "type": "string" - }, - "modifications": { - "items": { - "$ref": "#/definitions/PermissionProfileModificationParams" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "enum": [ - "profile" - ], - "title": "ProfilePermissionProfileSelectionParamsType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ProfilePermissionProfileSelectionParams", - "type": "object" - } - ] - }, "SandboxMode": { "enum": [ "read-only", @@ -212,7 +153,8 @@ { "type": "null" } - ] + ], + "description": "Deprecated for fork. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "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 6e74ab4ac8..76451490d3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -18,14 +18,6 @@ "id": { "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", "type": "string" - }, - "modifications": { - "default": [], - "description": "Bounded user-requested modifications applied on top of the named profile, if any.", - "items": { - "$ref": "#/definitions/ActivePermissionProfileModification" - }, - "type": "array" } }, "required": [ @@ -33,31 +25,6 @@ ], "type": "object" }, - "ActivePermissionProfileModification": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootActivePermissionProfileModificationType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootActivePermissionProfileModification", - "type": "object" - } - ] - }, "AgentPath": { "type": "string" }, @@ -1160,13 +1127,6 @@ ], "title": "WorkspaceWriteSandboxPolicyType", "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" } }, "required": [ @@ -2589,6 +2549,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -2605,7 +2577,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 `permissionProfile`." }, "serviceTier": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 5f07fe0149..69b12a365c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -298,65 +298,6 @@ } ] }, - "PermissionProfileModificationParams": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParamsType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParams", - "type": "object" - } - ] - }, - "PermissionProfileSelectionParams": { - "oneOf": [ - { - "description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.", - "properties": { - "id": { - "type": "string" - }, - "modifications": { - "items": { - "$ref": "#/definitions/PermissionProfileModificationParams" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "enum": [ - "profile" - ], - "title": "ProfilePermissionProfileSelectionParamsType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ProfilePermissionProfileSelectionParams", - "type": "object" - } - ] - }, "Personality": { "enum": [ "none", @@ -1091,7 +1032,8 @@ { "type": "null" } - ] + ], + "description": "Deprecated for resume. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "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 727b7a3fb2..1cfc7931a6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -18,14 +18,6 @@ "id": { "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", "type": "string" - }, - "modifications": { - "default": [], - "description": "Bounded user-requested modifications applied on top of the named profile, if any.", - "items": { - "$ref": "#/definitions/ActivePermissionProfileModification" - }, - "type": "array" } }, "required": [ @@ -33,31 +25,6 @@ ], "type": "object" }, - "ActivePermissionProfileModification": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootActivePermissionProfileModificationType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootActivePermissionProfileModification", - "type": "object" - } - ] - }, "AgentPath": { "type": "string" }, @@ -1160,13 +1127,6 @@ ], "title": "WorkspaceWriteSandboxPolicyType", "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" } }, "required": [ @@ -2589,6 +2549,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -2605,7 +2577,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 `permissionProfile`." }, "serviceTier": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 9a60049a61..99b25490ab 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -90,65 +90,6 @@ ], "type": "object" }, - "PermissionProfileModificationParams": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParamsType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParams", - "type": "object" - } - ] - }, - "PermissionProfileSelectionParams": { - "oneOf": [ - { - "description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.", - "properties": { - "id": { - "type": "string" - }, - "modifications": { - "items": { - "$ref": "#/definitions/PermissionProfileModificationParams" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "enum": [ - "profile" - ], - "title": "ProfilePermissionProfileSelectionParamsType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ProfilePermissionProfileSelectionParams", - "type": "object" - } - ] - }, "Personality": { "enum": [ "none", 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 bf03f0fb55..46139bac81 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -18,14 +18,6 @@ "id": { "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", "type": "string" - }, - "modifications": { - "default": [], - "description": "Bounded user-requested modifications applied on top of the named profile, if any.", - "items": { - "$ref": "#/definitions/ActivePermissionProfileModification" - }, - "type": "array" } }, "required": [ @@ -33,31 +25,6 @@ ], "type": "object" }, - "ActivePermissionProfileModification": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootActivePermissionProfileModificationType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootActivePermissionProfileModification", - "type": "object" - } - ] - }, "AgentPath": { "type": "string" }, @@ -1160,13 +1127,6 @@ ], "title": "WorkspaceWriteSandboxPolicyType", "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" } }, "required": [ @@ -2589,6 +2549,18 @@ "modelProvider": { "type": "string" }, + "permissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Exact effective permissions used by this thread. Read-only: clients may select permissions by id and update workspace roots, but cannot replace this value through lifecycle or turn APIs." + }, "reasoningEffort": { "anyOf": [ { @@ -2605,7 +2577,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 `permissionProfile`." }, "serviceTier": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index 1ef33d4301..7d8ba1e0b4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -114,65 +114,6 @@ ], "type": "string" }, - "PermissionProfileModificationParams": { - "oneOf": [ - { - "description": "Additional concrete directory that should be writable.", - "properties": { - "path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": { - "enum": [ - "additionalWritableRoot" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParamsType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "AdditionalWritableRootPermissionProfileModificationParams", - "type": "object" - } - ] - }, - "PermissionProfileSelectionParams": { - "oneOf": [ - { - "description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.", - "properties": { - "id": { - "type": "string" - }, - "modifications": { - "items": { - "$ref": "#/definitions/PermissionProfileModificationParams" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "enum": [ - "profile" - ], - "title": "ProfilePermissionProfileSelectionParamsType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ProfilePermissionProfileSelectionParams", - "type": "object" - } - ] - }, "Personality": { "enum": [ "none", @@ -295,13 +236,6 @@ ], "title": "WorkspaceWriteSandboxPolicyType", "type": "string" - }, - "writableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" } }, "required": [ @@ -576,7 +510,7 @@ "type": "null" } ], - "description": "Override the sandbox policy for this turn and subsequent turns." + "description": "Deprecated for turns. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile." }, "serviceTier": { "description": "Override the service tier for this turn and subsequent turns.", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfile.ts index cbc8c6ef0a..73f9efcab5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfile.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfile.ts @@ -1,7 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ActivePermissionProfileModification } from "./ActivePermissionProfileModification"; export type ActivePermissionProfile = { /** @@ -13,9 +12,4 @@ id: string, * Parent profile identifier once permissions profiles support * inheritance. This is currently always `null`. */ -extends: string | null, -/** - * Bounded user-requested modifications applied on top of the named - * profile, if any. - */ -modifications: Array, }; +extends: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfileModification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfileModification.ts deleted file mode 100644 index 1cbee6868a..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ActivePermissionProfileModification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type ActivePermissionProfileModification = { "type": "additionalWritableRoot", path: AbsolutePathBuf, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileModificationParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileModificationParams.ts deleted file mode 100644 index c619edcea8..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileModificationParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type PermissionProfileModificationParams = { "type": "additionalWritableRoot", path: AbsolutePathBuf, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSelectionParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSelectionParams.ts deleted file mode 100644 index a415bd0028..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileSelectionParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PermissionProfileModificationParams } from "./PermissionProfileModificationParams"; - -export type PermissionProfileSelectionParams = { "type": "profile", id: string, modifications?: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts index 5575701ff2..1715d7710e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts @@ -1,7 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { NetworkAccess } from "./NetworkAccess"; -export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; +export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index 6076a4bb14..167a6f1465 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -23,7 +23,12 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** +approvalsReviewer?: ApprovalsReviewer | null, /** + * Deprecated for fork. When present, the server treats this as a + * compatibility spelling for selecting a matching named permissions + * profile. + */ +sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** * Optional client-supplied analytics source classification for this forked thread. */ threadSource?: ThreadSource | null}; 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..25afa1b7d2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -5,6 +5,7 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; @@ -16,7 +17,11 @@ 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 `permissionProfile`. */ -sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; +sandbox: SandboxPolicy, /** + * Exact effective permissions used by this thread. Read-only: clients may + * select permissions by id and update workspace roots, but cannot replace + * this value through lifecycle or turn APIs. + */ +permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index 6d1dbdca4f..feae3906ba 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -25,4 +25,9 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null}; +approvalsReviewer?: ApprovalsReviewer | null, /** + * Deprecated for resume. When present, the server treats this as a + * compatibility spelling for selecting a matching named permissions + * profile. + */ +sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | 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..edccd243d8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -5,6 +5,7 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; @@ -16,7 +17,11 @@ 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 `permissionProfile`. */ -sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; +sandbox: SandboxPolicy, /** + * Exact effective permissions used by this thread. Read-only: clients may + * select permissions by id and update workspace roots, but cannot replace + * this value through lifecycle or turn APIs. + */ +permissionProfile: PermissionProfile | null, 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..62bb8b776a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -5,6 +5,7 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; @@ -16,7 +17,11 @@ 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 `permissionProfile`. */ -sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; +sandbox: SandboxPolicy, /** + * Exact effective permissions used by this thread. Read-only: clients may + * select permissions by id and update workspace roots, but cannot replace + * this value through lifecycle or turn APIs. + */ +permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index b04919d86b..e6daf0b557 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -21,7 +21,9 @@ approvalPolicy?: AskForApproval | null, /** * subsequent turns. */ approvalsReviewer?: ApprovalsReviewer | null, /** - * Override the sandbox policy for this turn and subsequent turns. + * Deprecated for turns. When present, the server treats this as a + * compatibility spelling for selecting a matching named permissions + * profile. */ sandboxPolicy?: SandboxPolicy | null, /** * Override the model for this turn and subsequent turns. diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index ab6eaefb5a..4e78e1c31c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -5,7 +5,6 @@ export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedN export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification"; export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; export type { ActivePermissionProfile } from "./ActivePermissionProfile"; -export type { ActivePermissionProfileModification } from "./ActivePermissionProfileModification"; export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType"; export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus"; export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; @@ -257,9 +256,7 @@ export type { PatchChangeKind } from "./PatchChangeKind"; export type { PermissionGrantScope } from "./PermissionGrantScope"; export type { PermissionProfile } from "./PermissionProfile"; export type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions"; -export type { PermissionProfileModificationParams } from "./PermissionProfileModificationParams"; export type { PermissionProfileNetworkPermissions } from "./PermissionProfileNetworkPermissions"; -export type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams"; export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams"; export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse"; export type { PlanDeltaNotification } from "./PlanDeltaNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index b70af1a22b..a86d444906 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 { $( @@ -2294,6 +2295,7 @@ mod tests { model_provider: "openai".to_string(), service_tier: None, cwd, + workspace_roots: vec![], instruction_sources: vec![absolute_path("/tmp/AGENTS.md")], approval_policy: v2::AskForApproval::OnFailure, approvals_reviewer: v2::ApprovalsReviewer::User, @@ -2338,6 +2340,7 @@ mod tests { "modelProvider": "openai", "serviceTier": null, "cwd": absolute_path_string("tmp"), + "workspaceRoots": [], "instructionSources": [absolute_path_string("tmp/AGENTS.md")], "approvalPolicy": "on-failure", "approvalsReviewer": "user", diff --git a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs index 86614a6aeb..919b2542cd 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs @@ -5,7 +5,6 @@ use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalPro use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction; use codex_protocol::models::ActivePermissionProfile as CoreActivePermissionProfile; -use codex_protocol::models::ActivePermissionProfileModification as CoreActivePermissionProfileModification; use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions; @@ -437,41 +436,6 @@ pub struct ActivePermissionProfile { /// inheritance. This is currently always `null`. #[serde(default)] pub extends: Option, - /// Bounded user-requested modifications applied on top of the named - /// profile, if any. - #[serde(default)] - pub modifications: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum ActivePermissionProfileModification { - /// Additional concrete directory that should be writable. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - AdditionalWritableRoot { path: AbsolutePathBuf }, -} - -impl From for ActivePermissionProfileModification { - fn from(value: CoreActivePermissionProfileModification) -> Self { - match value { - CoreActivePermissionProfileModification::AdditionalWritableRoot { path } => { - Self::AdditionalWritableRoot { path } - } - } - } -} - -impl From for CoreActivePermissionProfileModification { - fn from(value: ActivePermissionProfileModification) -> Self { - match value { - ActivePermissionProfileModification::AdditionalWritableRoot { path } => { - Self::AdditionalWritableRoot { path } - } - } - } } impl From for ActivePermissionProfile { @@ -479,11 +443,6 @@ impl From for ActivePermissionProfile { Self { id: value.id, extends: value.extends, - modifications: value - .modifications - .into_iter() - .map(ActivePermissionProfileModification::from) - .collect(), } } } @@ -493,42 +452,10 @@ impl From for CoreActivePermissionProfile { Self { id: value.id, extends: value.extends, - modifications: value - .modifications - .into_iter() - .map(CoreActivePermissionProfileModification::from) - .collect(), } } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum PermissionProfileSelectionParams { - /// Select a named built-in or user-defined profile and optionally apply - /// bounded modifications that Codex knows how to validate. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Profile { - id: String, - #[ts(optional = nullable)] - modifications: Option>, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum PermissionProfileModificationParams { - /// Additional concrete directory that should be writable. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - AdditionalWritableRoot { path: AbsolutePathBuf }, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -607,14 +534,16 @@ pub enum SandboxPolicy { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] WorkspaceWrite { - #[serde(default)] - writable_roots: Vec, #[serde(default)] network_access: bool, #[serde(default)] exclude_tmpdir_env_var: bool, #[serde(default)] exclude_slash_tmp: bool, + #[serde(default, skip_serializing)] + #[schemars(skip)] + #[ts(skip)] + legacy_writable_roots: Vec, }, } @@ -690,10 +619,10 @@ impl<'de> Deserialize<'de> for SandboxPolicy { )); } Ok(SandboxPolicy::WorkspaceWrite { - writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, + legacy_writable_roots: writable_roots, }) } } @@ -720,18 +649,60 @@ impl SandboxPolicy { } } SandboxPolicy::WorkspaceWrite { - writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, + legacy_writable_roots: _, } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: writable_roots.clone(), network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, }, } } + + pub fn legacy_writable_roots(&self) -> &[AbsolutePathBuf] { + match self { + SandboxPolicy::WorkspaceWrite { + legacy_writable_roots, + .. + } => legacy_writable_roots, + SandboxPolicy::DangerFullAccess + | SandboxPolicy::ReadOnly { .. } + | SandboxPolicy::ExternalSandbox { .. } => &[], + } + } + + pub fn to_permission_profile_for_cwd(&self, cwd: &std::path::Path) -> CorePermissionProfile { + match self { + SandboxPolicy::WorkspaceWrite { + legacy_writable_roots, + .. + } if legacy_writable_roots.is_empty() => { + CorePermissionProfile::from_legacy_sandbox_policy_for_cwd(&self.to_core(), cwd) + } + SandboxPolicy::WorkspaceWrite { + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + legacy_writable_roots, + } => CorePermissionProfile::workspace_write_with( + legacy_writable_roots, + if *network_access { + CoreNetworkSandboxPolicy::Enabled + } else { + CoreNetworkSandboxPolicy::Restricted + }, + *exclude_tmpdir_env_var, + *exclude_slash_tmp, + ), + SandboxPolicy::DangerFullAccess + | SandboxPolicy::ReadOnly { .. } + | SandboxPolicy::ExternalSandbox { .. } => { + CorePermissionProfile::from_legacy_sandbox_policy_for_cwd(&self.to_core(), cwd) + } + } + } } impl From for SandboxPolicy { @@ -752,15 +723,14 @@ impl From for SandboxPolicy { } } codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { - writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, } => SandboxPolicy::WorkspaceWrite { - writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, + legacy_writable_roots: Vec::new(), }, } } 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 f7041cc721..ae5d0cf05b 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -2020,17 +2020,16 @@ fn mcp_server_elicitation_response_serializes_nullable_content() { #[test] fn sandbox_policy_round_trips_workspace_write_access() { let v2_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + legacy_writable_roots: Vec::new(), }; let core_policy = v2_policy.to_core(); assert_eq!( core_policy, codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -2061,10 +2060,9 @@ fn sandbox_policy_deserializes_legacy_read_only_full_access_field() { #[test] fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() { - let writable_root = absolute_path("/workspace"); let policy = serde_json::from_value::(json!({ "type": "workspaceWrite", - "writableRoots": [writable_root], + "writableRoots": [], "readOnlyAccess": { "type": "fullAccess" }, @@ -2076,14 +2074,38 @@ fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() { assert_eq!( policy, SandboxPolicy::WorkspaceWrite { - writable_roots: vec![absolute_path("/workspace")], network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + legacy_writable_roots: Vec::new(), } ); } +#[test] +fn sandbox_policy_deserializes_legacy_workspace_write_writable_roots_field() { + let writable_root = absolute_path("/workspace"); + let policy = serde_json::from_value::(json!({ + "type": "workspaceWrite", + "writableRoots": [writable_root], + "networkAccess": false, + "excludeTmpdirEnvVar": false, + "excludeSlashTmp": false + })) + .expect("workspace-write policy should accept legacy writableRoots field"); + assert_eq!(policy.legacy_writable_roots(), &[writable_root]); + + assert_eq!( + serde_json::to_value(policy).expect("policy should serialize"), + json!({ + "type": "workspaceWrite", + "networkAccess": false, + "excludeTmpdirEnvVar": false, + "excludeSlashTmp": false + }) + ); +} + #[test] fn sandbox_policy_rejects_legacy_read_only_restricted_access_field() { let err = serde_json::from_value::(json!({ @@ -3439,9 +3461,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); @@ -3469,6 +3488,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() { responsesapi_client_metadata: None, environments: None, cwd: None, + workspace_roots: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, @@ -3486,6 +3506,20 @@ fn turn_start_params_preserve_explicit_null_service_tier() { assert_eq!(serialized_without_override.get("serviceTier"), None); } +#[test] +fn turn_start_permissions_uses_profile_id_string_shape() { + let params: TurnStartParams = serde_json::from_value(json!({ + "threadId": "thread-1", + "input": [], + "permissions": ":workspace" + })) + .expect("turn start params should deserialize"); + assert_eq!(params.permissions, Some(":workspace".to_string())); + + let serialized = serde_json::to_value(¶ms).expect("params should serialize"); + assert_eq!(serialized["permissions"], json!(":workspace")); +} + #[test] fn turn_start_params_round_trip_environments() { let cwd = test_absolute_path(); 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 458722b3a2..5a52c4b626 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -2,7 +2,6 @@ use super::ActivePermissionProfile; use super::ApprovalsReviewer; use super::AskForApproval; use super::PermissionProfile; -use super::PermissionProfileSelectionParams; use super::SandboxMode; use super::SandboxPolicy; use super::Thread; @@ -107,6 +106,11 @@ pub struct ThreadStartParams { pub service_tier: Option>, #[ts(optional = nullable)] pub cwd: Option, + /// Optional workspace roots for this thread. Omitted uses the server's + /// configured roots, usually seeded from `cwd`. + #[experimental("thread/start.workspaceRoots")] + #[ts(optional = nullable)] + pub workspace_roots: Option>, #[experimental(nested)] #[ts(optional = nullable)] pub approval_policy: Option, @@ -116,12 +120,11 @@ pub struct ThreadStartParams { pub approvals_reviewer: Option, #[ts(optional = nullable)] pub sandbox: Option, - /// Named profile selection for this thread. Cannot be combined with - /// `sandbox`. Use bounded `modifications` for supported turn/thread - /// adjustments instead of replacing the full permissions profile. + /// Named permissions profile id for this new thread's initial permissions. + /// Cannot be combined with `sandbox`. #[experimental("thread/start.permissions")] #[ts(optional = nullable)] - pub permissions: Option, + pub permissions: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -195,6 +198,11 @@ pub struct ThreadStartResponse { pub model_provider: String, pub service_tier: Option, pub cwd: AbsolutePathBuf, + /// Workspace roots used to realize symbolic `:project_roots` permission + /// entries for this thread. + #[experimental("thread/start.workspaceRoots")] + #[serde(default)] + pub workspace_roots: Vec, /// Instruction source files currently loaded for this thread. #[serde(default)] pub instruction_sources: Vec, @@ -203,12 +211,11 @@ 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 `permissionProfile`. pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/start.permissionProfile")] + /// Exact effective permissions used by this thread. Read-only: clients may + /// select permissions by id and update workspace roots, but cannot replace + /// this value through lifecycle or turn APIs. #[serde(default)] pub permission_profile: Option, /// Named or implicit built-in profile that produced the active @@ -264,6 +271,11 @@ pub struct ThreadResumeParams { pub service_tier: Option>, #[ts(optional = nullable)] pub cwd: Option, + /// Optional replacement workspace roots for the resumed thread. Omitted + /// preserves the persisted or configured roots. + #[experimental("thread/resume.workspaceRoots")] + #[ts(optional = nullable)] + pub workspace_roots: Option>, #[experimental(nested)] #[ts(optional = nullable)] pub approval_policy: Option, @@ -271,14 +283,16 @@ pub struct ThreadResumeParams { /// and subsequent turns. #[ts(optional = nullable)] pub approvals_reviewer: Option, + /// Deprecated for resume. When present, the server treats this as a + /// compatibility spelling for selecting a matching named permissions + /// profile. #[ts(optional = nullable)] pub sandbox: Option, - /// Named profile selection for the resumed thread. Cannot be combined - /// with `sandbox`. Use bounded `modifications` for supported thread - /// adjustments instead of replacing the full permissions profile. + /// Named permissions profile id for the resumed thread. Cannot be combined + /// with `sandbox`. #[experimental("thread/resume.permissions")] #[ts(optional = nullable)] - pub permissions: Option, + pub permissions: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -310,6 +324,11 @@ pub struct ThreadResumeResponse { pub model_provider: String, pub service_tier: Option, pub cwd: AbsolutePathBuf, + /// Workspace roots used to realize symbolic `:project_roots` permission + /// entries for this thread. + #[experimental("thread/resume.workspaceRoots")] + #[serde(default)] + pub workspace_roots: Vec, /// Instruction source files currently loaded for this thread. #[serde(default)] pub instruction_sources: Vec, @@ -318,12 +337,11 @@ 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 `permissionProfile`. pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/resume.permissionProfile")] + /// Exact effective permissions used by this thread. Read-only: clients may + /// select permissions by id and update workspace roots, but cannot replace + /// this value through lifecycle or turn APIs. #[serde(default)] pub permission_profile: Option, /// Named or implicit built-in profile that produced the active @@ -370,6 +388,11 @@ pub struct ThreadForkParams { pub service_tier: Option>, #[ts(optional = nullable)] pub cwd: Option, + /// Optional replacement workspace roots for the forked thread. Omitted + /// preserves the source thread roots when available. + #[experimental("thread/fork.workspaceRoots")] + #[ts(optional = nullable)] + pub workspace_roots: Option>, #[experimental(nested)] #[ts(optional = nullable)] pub approval_policy: Option, @@ -377,14 +400,16 @@ pub struct ThreadForkParams { /// and subsequent turns. #[ts(optional = nullable)] pub approvals_reviewer: Option, + /// Deprecated for fork. When present, the server treats this as a + /// compatibility spelling for selecting a matching named permissions + /// profile. #[ts(optional = nullable)] pub sandbox: Option, - /// Named profile selection for the forked thread. Cannot be combined with - /// `sandbox`. Use bounded `modifications` for supported thread - /// adjustments instead of replacing the full permissions profile. + /// Named permissions profile id for the forked thread. Cannot be combined + /// with `sandbox`. #[experimental("thread/fork.permissions")] #[ts(optional = nullable)] - pub permissions: Option, + pub permissions: Option, #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] @@ -419,6 +444,11 @@ pub struct ThreadForkResponse { pub model_provider: String, pub service_tier: Option, pub cwd: AbsolutePathBuf, + /// Workspace roots used to realize symbolic `:project_roots` permission + /// entries for this thread. + #[experimental("thread/fork.workspaceRoots")] + #[serde(default)] + pub workspace_roots: Vec, /// Instruction source files currently loaded for this thread. #[serde(default)] pub instruction_sources: Vec, @@ -427,12 +457,11 @@ 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 `permissionProfile`. pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/fork.permissionProfile")] + /// Exact effective permissions used by this thread. Read-only: clients may + /// select permissions by id and update workspace roots, but cannot replace + /// this value through lifecycle or turn APIs. #[serde(default)] pub permission_profile: Option, /// Named or implicit built-in profile that produced the active diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index 61a09bfbf5..7dff5363c9 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -1,6 +1,5 @@ use super::ApprovalsReviewer; use super::AskForApproval; -use super::PermissionProfileSelectionParams; use super::SandboxPolicy; use super::Turn; use codex_experimental_api_macros::ExperimentalApi; @@ -64,6 +63,11 @@ pub struct TurnStartParams { /// Override the working directory for this turn and subsequent turns. #[ts(optional = nullable)] pub cwd: Option, + /// Replace the workspace roots for this turn and subsequent turns. Omitted + /// preserves the current roots. + #[experimental("turn/start.workspaceRoots")] + #[ts(optional = nullable)] + pub workspace_roots: Option>, /// Override the approval policy for this turn and subsequent turns. #[experimental(nested)] #[ts(optional = nullable)] @@ -72,16 +76,16 @@ pub struct TurnStartParams { /// subsequent turns. #[ts(optional = nullable)] pub approvals_reviewer: Option, - /// Override the sandbox policy for this turn and subsequent turns. + /// Deprecated for turns. When present, the server treats this as a + /// compatibility spelling for selecting a matching named permissions + /// profile. #[ts(optional = nullable)] pub sandbox_policy: Option, - /// Select a named permissions profile for this turn and subsequent turns. - /// Cannot be combined with `sandboxPolicy`. Use bounded `modifications` - /// for supported turn adjustments instead of replacing the full - /// permissions profile. + /// Select a named permissions profile id for this turn and subsequent + /// turns. Cannot be combined with `sandboxPolicy`. #[experimental("turn/start.permissions")] #[ts(optional = nullable)] - pub permissions: Option, + pub permissions: Option, /// Override the model for this turn and subsequent turns. #[ts(optional = nullable)] pub model: Option, diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index c75c2d5ad1..07ec10d017 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -448,12 +448,12 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult(name: &str, future: F) + where + F: Future + Send + 'static, + { + const TEST_STACK_SIZE_BYTES: usize = 4 * 1024 * 1024; + + let handle = std::thread::Builder::new() + .name(name.to_string()) + .stack_size(TEST_STACK_SIZE_BYTES) + .spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("current-thread runtime should build"); + runtime.block_on(Box::pin(future)); }) - .await - .expect("request transport should work") - .expect("request should succeed"); - assert!(response.is_object()); + .expect("stack-sized test thread should spawn"); - let _parsed: ConfigRequirementsReadResponse = - serde_json::from_value(response).expect("response should match v2 schema"); - client - .shutdown() - .await - .expect("in-process runtime should shutdown cleanly"); + assert!(handle.join().is_ok(), "{name} thread panicked"); } - #[tokio::test] - async fn in_process_start_uses_requested_session_source_for_thread_start() { - for (requested_source, expected_source) in [ - (SessionSource::Cli, ApiSessionSource::Cli), - (SessionSource::Exec, ApiSessionSource::Exec), - ] { - let client = start_test_client(requested_source).await; - let response = client - .request(ClientRequest::ThreadStart { - request_id: RequestId::Integer(2), - params: ThreadStartParams { - ephemeral: Some(true), - ..ThreadStartParams::default() - }, - }) - .await - .expect("request transport should work") - .expect("thread/start should succeed"); - let parsed: ThreadStartResponse = - serde_json::from_value(response).expect("thread/start response should parse"); - assert_eq!(parsed.thread.source, expected_source); - client - .shutdown() - .await - .expect("in-process runtime should shutdown cleanly"); - } + #[test] + fn in_process_start_initializes_and_handles_typed_v2_request() { + run_current_thread_test_with_stack( + "in_process_start_initializes_and_handles_typed_v2_request", + async { + let client = start_test_client(SessionSource::Cli).await; + let response = client + .request(ClientRequest::ConfigRequirementsRead { + request_id: RequestId::Integer(1), + params: None, + }) + .await + .expect("request transport should work") + .expect("request should succeed"); + assert!(response.is_object()); + + let _parsed: ConfigRequirementsReadResponse = + serde_json::from_value(response).expect("response should match v2 schema"); + client + .shutdown() + .await + .expect("in-process runtime should shutdown cleanly"); + }, + ); } - #[tokio::test] - async fn in_process_start_clamps_zero_channel_capacity() { - let client = - start_test_client_with_capacity(SessionSource::Cli, /*channel_capacity*/ 0).await; - let response = loop { - match client - .request(ClientRequest::ConfigRequirementsRead { - request_id: RequestId::Integer(4), - params: None, - }) - .await - { - Ok(response) => break response.expect("request should succeed"), - Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { - tokio::task::yield_now().await; + #[test] + fn in_process_start_uses_requested_session_source_for_thread_start() { + run_current_thread_test_with_stack( + "in_process_start_uses_requested_session_source_for_thread_start", + async { + for (requested_source, expected_source) in [ + (SessionSource::Cli, ApiSessionSource::Cli), + (SessionSource::Exec, ApiSessionSource::Exec), + ] { + let client = start_test_client(requested_source).await; + let response = client + .request(ClientRequest::ThreadStart { + request_id: RequestId::Integer(2), + params: ThreadStartParams { + ephemeral: Some(true), + ..ThreadStartParams::default() + }, + }) + .await + .expect("request transport should work") + .expect("thread/start should succeed"); + let parsed: ThreadStartResponse = serde_json::from_value(response) + .expect("thread/start response should parse"); + assert_eq!(parsed.thread.source, expected_source); + client + .shutdown() + .await + .expect("in-process runtime should shutdown cleanly"); } - Err(err) => panic!("request transport should work: {err}"), - } - }; - let _parsed: ConfigRequirementsReadResponse = - serde_json::from_value(response).expect("response should match v2 schema"); - client - .shutdown() - .await - .expect("in-process runtime should shutdown cleanly"); + }, + ); + } + + #[test] + fn in_process_start_clamps_zero_channel_capacity() { + run_current_thread_test_with_stack( + "in_process_start_clamps_zero_channel_capacity", + async { + let client = start_test_client_with_capacity( + SessionSource::Cli, + /*channel_capacity*/ 0, + ) + .await; + let response = loop { + match client + .request(ClientRequest::ConfigRequirementsRead { + request_id: RequestId::Integer(4), + params: None, + }) + .await + { + Ok(response) => break response.expect("request should succeed"), + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { + tokio::task::yield_now().await; + } + Err(err) => panic!("request transport should work: {err}"), + } + }; + let _parsed: ConfigRequirementsReadResponse = + serde_json::from_value(response).expect("response should match v2 schema"); + client + .shutdown() + .await + .expect("in-process runtime should shutdown cleanly"); + }, + ); } #[test] diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index b4e5f9a04d..0e507cdced 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -581,7 +581,7 @@ pub async fn run_main_with_transport_options( }); } if let Some(warning) = - codex_core::config::system_bwrap_warning(config.permissions.permission_profile.get()) + codex_core::config::system_bwrap_warning(config.permissions.permission_profile_ref()) { config_warnings.push(ConfigWarningNotification { summary: warning, diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index a4eb9b6c9d..44a29041ec 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; use std::future::Future; +use std::pin::Pin; use std::sync::Arc; use std::sync::OnceLock; use std::sync::atomic::AtomicBool; @@ -501,13 +502,13 @@ impl MessageProcessor { self.account_processor.clear_external_auth(); } - pub(crate) async fn process_request( - self: &Arc, + pub(crate) fn process_request<'a>( + self: &'a Arc, connection_id: ConnectionId, request: JSONRPCRequest, - transport: &AppServerTransport, + transport: &'a AppServerTransport, session: Arc, - ) { + ) -> Pin + Send + 'a>> { let request_method = request.method.as_str(); tracing::trace!( ?connection_id, @@ -525,52 +526,55 @@ impl MessageProcessor { tracestate: trace.tracestate.clone(), }); let request_context = RequestContext::new(request_id.clone(), request_span, request_trace); - Self::run_request_with_context( - Arc::clone(&self.outgoing), - request_context.clone(), - async { - let codex_request = serde_json::to_value(&request) - .map_err(|err| invalid_request(format!("Invalid request: {err}"))) - .and_then(|request_json| { - serde_json::from_value::(request_json) - .map_err(|err| invalid_request(format!("Invalid request: {err}"))) - }); - let result = match codex_request { - Ok(codex_request) => { - // Websocket callers finalize outbound readiness in lib.rs after mirroring - // session state into outbound state and sending initialize notifications to - // this specific connection. Passing `None` avoids marking the connection - // ready too early from inside the shared request handler. - self.handle_client_request( - request_id.clone(), - codex_request, - Arc::clone(&session), - /*outbound_initialized*/ None, - request_context.clone(), - ) - .await + Box::pin(async move { + Self::run_request_with_context( + Arc::clone(&self.outgoing), + request_context.clone(), + Box::pin(async move { + let codex_request = serde_json::to_value(&request) + .map_err(|err| invalid_request(format!("Invalid request: {err}"))) + .and_then(|request_json| { + serde_json::from_value::(request_json) + .map_err(|err| invalid_request(format!("Invalid request: {err}"))) + }); + let result = match codex_request { + Ok(codex_request) => { + // Websocket callers finalize outbound readiness in lib.rs after + // mirroring session state into outbound state and sending initialize + // notifications to this specific connection. Passing `None` avoids + // marking the connection ready too early from inside the shared + // request handler. + self.handle_client_request( + request_id.clone(), + codex_request, + Arc::clone(&session), + /*outbound_initialized*/ None, + request_context.clone(), + ) + .await + } + Err(error) => Err(error), + }; + if let Err(error) = result { + self.outgoing.send_error(request_id.clone(), error).await; } - Err(error) => Err(error), - }; - if let Err(error) = result { - self.outgoing.send_error(request_id.clone(), error).await; - } - }, - ) - .await; + }), + ) + .await; + }) } /// Handles a typed request path used by in-process embedders. /// /// This bypasses JSON request deserialization but keeps identical request /// semantics by delegating to `handle_client_request`. - pub(crate) async fn process_client_request( - self: &Arc, + pub(crate) fn process_client_request<'a>( + self: &'a Arc, connection_id: ConnectionId, request: ClientRequest, session: Arc, - outbound_initialized: &AtomicBool, - ) { + outbound_initialized: &'a AtomicBool, + ) -> Pin + Send + 'a>> { let request_id = ConnectionRequestId { connection_id, request_id: request.id().clone(), @@ -584,28 +588,30 @@ impl MessageProcessor { request_id = ?request_id.request_id, "app-server typed request" ); - Self::run_request_with_context( - Arc::clone(&self.outgoing), - request_context.clone(), - async { - // In-process clients do not have the websocket transport loop that performs - // post-initialize bookkeeping, so they still finalize outbound readiness in - // the shared request handler. - let result = self - .handle_client_request( - request_id.clone(), - request, - Arc::clone(&session), - Some(outbound_initialized), - request_context.clone(), - ) - .await; - if let Err(error) = result { - self.outgoing.send_error(request_id.clone(), error).await; - } - }, - ) - .await; + Box::pin(async move { + Self::run_request_with_context( + Arc::clone(&self.outgoing), + request_context.clone(), + Box::pin(async move { + // In-process clients do not have the websocket transport loop that performs + // post-initialize bookkeeping, so they still finalize outbound readiness in + // the shared request handler. + let result = self + .handle_client_request( + request_id.clone(), + request, + Arc::clone(&session), + Some(outbound_initialized), + request_context.clone(), + ) + .await; + if let Err(error) = result { + self.outgoing.send_error(request_id.clone(), error).await; + } + }), + ) + .await; + }) } pub(crate) async fn process_notification(&self, notification: JSONRPCNotification) { @@ -621,13 +627,11 @@ impl MessageProcessor { tracing::info!("<- typed notification: {:?}", notification); } - async fn run_request_with_context( + async fn run_request_with_context<'a>( outgoing: Arc, request_context: RequestContext, - request_fut: F, - ) where - F: Future, - { + request_fut: Pin + Send + 'a>>, + ) { outgoing .register_request_context(request_context.clone()) .await; diff --git a/codex-rs/app-server/src/message_processor_tracing_tests.rs b/codex-rs/app-server/src/message_processor_tracing_tests.rs index c955d06ba2..a36a2b974d 100644 --- a/codex-rs/app-server/src/message_processor_tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor_tracing_tests.rs @@ -632,77 +632,80 @@ fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() -> Resul ) } -#[tokio::test(flavor = "current_thread")] #[serial(app_server_tracing)] -async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { - let mut harness = TracingHarness::new().await?; - let thread_start_response = harness.start_thread(/*request_id*/ 2, /*trace*/ None).await; - let thread_id = thread_start_response.thread.id.clone(); +#[test] +fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { + run_current_thread_test_with_stack("turn_start_jsonrpc_span_parents_core_turn_spans", async { + let mut harness = TracingHarness::new().await?; + let thread_start_response = harness.start_thread(/*request_id*/ 2, /*trace*/ None).await; + let thread_id = thread_start_response.thread.id.clone(); - harness.reset_tracing(); + harness.reset_tracing(); - let RemoteTrace { - trace_id: remote_trace_id, - parent_span_id: remote_parent_span_id, - context: remote_trace, - } = RemoteTrace::new("00000000000000000000000000000077", "0000000000000088"); - let turn_start_response: TurnStartResponse = harness - .request( - ClientRequest::TurnStart { - request_id: RequestId::Integer(3), - params: TurnStartParams { - environments: None, - thread_id, - input: vec![UserInput::Text { - text: "hello".to_string(), - text_elements: Vec::new(), - }], - responsesapi_client_metadata: None, - cwd: None, - approval_policy: None, - sandbox_policy: None, - permissions: None, - approvals_reviewer: None, - model: None, - service_tier: None, - effort: None, - summary: None, - personality: None, - output_schema: None, - collaboration_mode: None, + let RemoteTrace { + trace_id: remote_trace_id, + parent_span_id: remote_parent_span_id, + context: remote_trace, + } = RemoteTrace::new("00000000000000000000000000000077", "0000000000000088"); + let turn_start_response: TurnStartResponse = harness + .request( + ClientRequest::TurnStart { + request_id: RequestId::Integer(3), + params: TurnStartParams { + environments: None, + thread_id, + input: vec![UserInput::Text { + text: "hello".to_string(), + text_elements: Vec::new(), + }], + responsesapi_client_metadata: None, + cwd: None, + workspace_roots: None, + approval_policy: None, + sandbox_policy: None, + permissions: None, + approvals_reviewer: None, + model: None, + service_tier: None, + effort: None, + summary: None, + personality: None, + output_schema: None, + collaboration_mode: None, + }, }, - }, - Some(remote_trace), - ) - .await; - let spans = wait_for_exported_spans(harness.tracing, |spans| { - spans.iter().any(|span| { - span.span_kind == SpanKind::Server - && span_attr(span, "rpc.method") == Some("turn/start") - && span.span_context.trace_id() == remote_trace_id - }) && spans.iter().any(|span| { - span_attr(span, "codex.op") == Some("user_input") - && span.span_context.trace_id() == remote_trace_id + Some(remote_trace), + ) + .await; + let spans = wait_for_exported_spans(harness.tracing, |spans| { + spans.iter().any(|span| { + span.span_kind == SpanKind::Server + && span_attr(span, "rpc.method") == Some("turn/start") + && span.span_context.trace_id() == remote_trace_id + }) && spans.iter().any(|span| { + span_attr(span, "codex.op") == Some("user_input") + && span.span_context.trace_id() == remote_trace_id + }) }) + .await; + + let server_request_span = + find_rpc_span_with_trace(&spans, SpanKind::Server, "turn/start", remote_trace_id); + let core_turn_span = + find_span_with_trace(&spans, remote_trace_id, "codex.op=user_input", |span| { + span_attr(span, "codex.op") == Some("user_input") + }); + + assert_eq!(server_request_span.parent_span_id, remote_parent_span_id); + assert!(server_request_span.parent_span_is_remote); + assert_eq!(server_request_span.span_context.trace_id(), remote_trace_id); + assert_eq!( + span_attr(server_request_span, "turn.id"), + Some(turn_start_response.turn.id.as_str()) + ); + assert_span_descends_from(&spans, core_turn_span, server_request_span); + harness.shutdown().await; + + Ok(()) }) - .await; - - let server_request_span = - find_rpc_span_with_trace(&spans, SpanKind::Server, "turn/start", remote_trace_id); - let core_turn_span = - find_span_with_trace(&spans, remote_trace_id, "codex.op=user_input", |span| { - span_attr(span, "codex.op") == Some("user_input") - }); - - assert_eq!(server_request_span.parent_span_id, remote_parent_span_id); - assert!(server_request_span.parent_span_is_remote); - assert_eq!(server_request_span.span_context.trace_id(), remote_trace_id); - assert_eq!( - span_attr(server_request_span, "turn.id"), - Some(turn_start_response.turn.id.as_str()) - ); - assert_span_descends_from(&spans, core_turn_span, server_request_span); - harness.shutdown().await; - - Ok(()) } diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index cf68f638ba..3462242710 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -103,8 +103,6 @@ use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::MockExperimentalMethodResponse; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; -use codex_app_server_protocol::PermissionProfileModificationParams; -use codex_app_server_protocol::PermissionProfileSelectionParams; use codex_app_server_protocol::PluginDetail; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallResponse; diff --git a/codex-rs/app-server/src/request_processors/command_exec_processor.rs b/codex-rs/app-server/src/request_processors/command_exec_processor.rs index 3236a67627..f3729b2aeb 100644 --- a/codex-rs/app-server/src/request_processors/command_exec_processor.rs +++ b/codex-rs/app-server/src/request_processors/command_exec_processor.rs @@ -164,7 +164,7 @@ impl CommandExecRequestProcessor { let started_network_proxy = match self.config.permissions.network.as_ref() { Some(spec) => match spec .start_proxy( - self.config.permissions.permission_profile.get(), + self.config.permissions.permission_profile_ref(), /*policy_decider*/ None, /*blocked_request_observer*/ None, managed_network_requirements_enabled, @@ -200,7 +200,8 @@ impl CommandExecRequestProcessor { } else { ExecCapturePolicy::ShellTool }; - let sandbox_cwd = if permission_profile.is_some() { + let has_request_permission_profile = permission_profile.is_some(); + let sandbox_cwd = if has_request_permission_profile { cwd.clone() } else { self.config.cwd.clone() @@ -243,28 +244,20 @@ impl CommandExecRequestProcessor { ); self.config .permissions - .permission_profile + .permission_profile_constraint() .can_set(&effective_permission_profile) .map_err(|err| invalid_request(format!("invalid permission profile: {err}")))?; effective_permission_profile - } else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) { + } else if let Some(policy) = sandbox_policy { + let legacy_policy = policy.to_core(); self.config .permissions - .can_set_legacy_sandbox_policy(&policy, &sandbox_cwd) + .can_set_legacy_sandbox_policy(&legacy_policy, &sandbox_cwd) .map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?; - let file_system_sandbox_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd); - let network_sandbox_policy = - codex_protocol::permissions::NetworkSandboxPolicy::from(&policy); - let permission_profile = - codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( - codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(&policy), - &file_system_sandbox_policy, - network_sandbox_policy, - ); + let permission_profile = policy.to_permission_profile_for_cwd(&sandbox_cwd); self.config .permissions - .permission_profile + .permission_profile_constraint() .can_set(&permission_profile) .map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?; permission_profile @@ -283,10 +276,16 @@ impl CommandExecRequestProcessor { None => None, }; + let workspace_roots = if has_request_permission_profile { + vec![sandbox_cwd.clone()] + } else { + self.config.workspace_roots.clone() + }; let exec_request = codex_core::exec::build_exec_request( exec_params, &effective_permission_profile, &sandbox_cwd, + &workspace_roots, &codex_linux_sandbox_exe, use_legacy_landlock, ) 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 7dab206d85..b814649a2e 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -604,11 +604,15 @@ pub(super) async fn handle_pending_thread_resume_request( permission_profile, active_permission_profile, cwd, + workspace_roots, reasoning_effort, .. } = pending.config_snapshot; let instruction_sources = pending.instruction_sources; - let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path()); + let sandbox = + thread_response_sandbox_policy(&permission_profile, &workspace_roots, cwd.as_path()); + let response_permission_profile = + thread_response_permission_profile(&permission_profile, &workspace_roots); let active_permission_profile = thread_response_active_permission_profile(active_permission_profile); let session_id = conversation.session_configured().session_id.to_string(); @@ -624,7 +628,8 @@ 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()), + permission_profile: Some(response_permission_profile.into()), + workspace_roots, 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 46fb8ed80d..c4899d7593 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -802,6 +802,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -835,6 +836,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -1160,8 +1162,13 @@ impl ThreadRequestProcessor { let sandbox = thread_response_sandbox_policy( &config_snapshot.permission_profile, + &config_snapshot.workspace_roots, config_snapshot.cwd.as_path(), ); + let permission_profile = thread_response_permission_profile( + &config_snapshot.permission_profile, + &config_snapshot.workspace_roots, + ); let active_permission_profile = thread_response_active_permission_profile(config_snapshot.active_permission_profile); @@ -1175,7 +1182,8 @@ 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()), + permission_profile: Some(permission_profile.into()), + workspace_roots: config_snapshot.workspace_roots, active_permission_profile, reasoning_effort: config_snapshot.reasoning_effort, }; @@ -1212,10 +1220,11 @@ impl ThreadRequestProcessor { model_provider: Option, service_tier: Option>, cwd: Option, + workspace_roots: Option>, approval_policy: Option, approvals_reviewer: Option, sandbox: Option, - permissions: Option, + permissions: Option, base_instructions: Option, developer_instructions: Option, personality: Option, @@ -1225,6 +1234,8 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd: cwd.map(PathBuf::from), + workspace_roots: workspace_roots + .map(|roots| roots.into_iter().map(|root| root.to_path_buf()).collect()), approval_policy: approval_policy .map(codex_app_server_protocol::AskForApproval::to_core), approvals_reviewer: approvals_reviewer @@ -2349,6 +2360,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -2384,6 +2396,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -2505,8 +2518,13 @@ impl ThreadRequestProcessor { let config_snapshot = codex_thread.config_snapshot().await; let sandbox = thread_response_sandbox_policy( &config_snapshot.permission_profile, + &config_snapshot.workspace_roots, config_snapshot.cwd.as_path(), ); + let permission_profile = thread_response_permission_profile( + &config_snapshot.permission_profile, + &config_snapshot.workspace_roots, + ); let active_permission_profile = thread_response_active_permission_profile( config_snapshot.active_permission_profile, ); @@ -2525,7 +2543,8 @@ 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()), + permission_profile: Some(permission_profile.into()), + workspace_roots: config_snapshot.workspace_roots, active_permission_profile, reasoning_effort: session_configured.reasoning_effort, }; @@ -2985,6 +3004,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -3050,6 +3070,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -3168,8 +3189,13 @@ impl ThreadRequestProcessor { let config_snapshot = forked_thread.config_snapshot().await; let sandbox = thread_response_sandbox_policy( &config_snapshot.permission_profile, + &config_snapshot.workspace_roots, config_snapshot.cwd.as_path(), ); + let permission_profile = thread_response_permission_profile( + &config_snapshot.permission_profile, + &config_snapshot.workspace_roots, + ); let active_permission_profile = thread_response_active_permission_profile(config_snapshot.active_permission_profile); @@ -3183,7 +3209,8 @@ 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()), + permission_profile: Some(permission_profile.into()), + workspace_roots: config_snapshot.workspace_roots, active_permission_profile, reasoning_effort: session_configured.reasoning_effort, }; diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 8bda80d6cf..c65a822885 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -631,6 +631,7 @@ mod thread_processor_behavior_tests { model_provider: None, service_tier: Some(Some("priority".to_string())), cwd: None, + workspace_roots: None, approval_policy: None, approvals_reviewer: None, sandbox: None, @@ -651,6 +652,7 @@ mod thread_processor_behavior_tests { permission_profile: codex_protocol::models::PermissionProfile::Disabled, active_permission_profile: None, cwd, + workspace_roots: Vec::new(), ephemeral: false, reasoning_effort: None, personality: None, diff --git a/codex-rs/app-server/src/request_processors/thread_summary.rs b/codex-rs/app-server/src/request_processors/thread_summary.rs index 875bd3deaf..5ad172e96e 100644 --- a/codex-rs/app-server/src/request_processors/thread_summary.rs +++ b/codex-rs/app-server/src/request_processors/thread_summary.rs @@ -177,37 +177,38 @@ pub(super) fn thread_response_active_permission_profile( pub(super) fn apply_permission_profile_selection_to_config_overrides( overrides: &mut ConfigOverrides, - permissions: Option, + permissions: Option, ) { - let Some(PermissionProfileSelectionParams::Profile { id, modifications }) = permissions else { + let Some(id) = permissions else { return; }; overrides.default_permissions = Some(id); - overrides - .additional_writable_roots - .extend(modifications.unwrap_or_default().into_iter().map( - |modification| match modification { - PermissionProfileModificationParams::AdditionalWritableRoot { path } => { - path.to_path_buf() - } - }, - )); } pub(super) fn thread_response_sandbox_policy( permission_profile: &codex_protocol::models::PermissionProfile, + workspace_roots: &[AbsolutePathBuf], cwd: &Path, ) -> codex_app_server_protocol::SandboxPolicy { - let file_system_policy = permission_profile.file_system_sandbox_policy(); + let materialized_permission_profile = + thread_response_permission_profile(permission_profile, workspace_roots); + let file_system_policy = materialized_permission_profile.file_system_sandbox_policy(); let sandbox_policy = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile( - permission_profile, + &materialized_permission_profile, &file_system_policy, - permission_profile.network_sandbox_policy(), + materialized_permission_profile.network_sandbox_policy(), cwd, ); sandbox_policy.into() } +pub(super) fn thread_response_permission_profile( + permission_profile: &codex_protocol::models::PermissionProfile, + workspace_roots: &[AbsolutePathBuf], +) -> codex_protocol::models::PermissionProfile { + permission_profile.materialize_project_roots_with_workspace_roots(workspace_roots) +} + #[cfg(test)] fn parse_datetime(timestamp: Option<&str>) -> Option> { timestamp.and_then(|ts| { diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index d1dae4ef46..07e8298770 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -357,6 +357,7 @@ impl TurnRequestProcessor { let turn_has_input = !mapped_items.is_empty(); let has_any_overrides = params.cwd.is_some() + || params.workspace_roots.is_some() || params.approval_policy.is_some() || params.approvals_reviewer.is_some() || params.sandbox_policy.is_some() @@ -375,6 +376,7 @@ impl TurnRequestProcessor { } let cwd = params.cwd; + let workspace_roots = params.workspace_roots; let approval_policy = params.approval_policy.map(AskForApproval::to_core); let approvals_reviewer = params .approvals_reviewer @@ -432,6 +434,9 @@ impl TurnRequestProcessor { thread .validate_turn_context_overrides(CodexThreadTurnContextOverrides { cwd: cwd.clone(), + workspace_roots: workspace_roots + .clone() + .map(|roots| roots.into_iter().map(|root| root.to_path_buf()).collect()), approval_policy, approvals_reviewer, sandbox_policy: sandbox_policy.clone(), @@ -457,6 +462,7 @@ impl TurnRequestProcessor { final_output_json_schema: params.output_schema, responsesapi_client_metadata: params.responsesapi_client_metadata, cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox_policy, diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index 6b2a9a0abe..6c113297d7 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -135,6 +135,7 @@ pub fn create_fake_rollout_with_source( forked_from_id: None, timestamp: meta_rfc3339.to_string(), cwd: PathBuf::from("/"), + workspace_roots: Vec::new(), originator: "codex".to_string(), cli_version: "0.0.0".to_string(), source, @@ -219,6 +220,7 @@ pub fn create_fake_rollout_with_text_elements( forked_from_id: None, timestamp: meta_rfc3339.to_string(), cwd: PathBuf::from("/"), + workspace_roots: Vec::new(), originator: "codex".to_string(), cli_version: "0.0.0".to_string(), source: SessionSource::Cli, diff --git a/codex-rs/app-server/tests/suite/conversation_summary.rs b/codex-rs/app-server/tests/suite/conversation_summary.rs index 5253850107..370cd32029 100644 --- a/codex-rs/app-server/tests/suite/conversation_summary.rs +++ b/codex-rs/app-server/tests/suite/conversation_summary.rs @@ -130,6 +130,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> dynamic_tools: Vec::new(), metadata: ThreadPersistenceMetadata { cwd: None, + workspace_roots: Vec::new(), model_provider: "test-provider".to_string(), memory_mode: ThreadMemoryMode::Disabled, }, diff --git a/codex-rs/app-server/tests/suite/v2/process_exec.rs b/codex-rs/app-server/tests/suite/v2/process_exec.rs index 5dd3e84b4c..3794ac43f4 100644 --- a/codex-rs/app-server/tests/suite/v2/process_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/process_exec.rs @@ -83,8 +83,7 @@ async fn process_spawn_returns_before_exit_and_emits_exit_notification() -> Resu .await?; assert_eq!(response.result, serde_json::json!({})); - wait_for_file(&probe_file).await?; - assert_eq!(std::fs::read_to_string(&probe_file)?, "process"); + wait_for_file_contents(&probe_file, "process").await?; std::fs::write(&release_file, "release")?; let exited = read_process_exited(&mut mcp).await?; @@ -239,9 +238,9 @@ async fn read_process_exited(mcp: &mut McpProcess) -> Result Result<()> { +async fn wait_for_file_contents(path: &Path, expected: &str) -> Result<()> { timeout(DEFAULT_READ_TIMEOUT, async { - while !path.exists() { + while std::fs::read_to_string(path).ok().as_deref() != Some(expected) { sleep(Duration::from_millis(20)).await; } }) diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 416b2515ad..cebd019627 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -614,6 +614,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<( model_provider: None, service_tier: None, cwd: None, + workspace_roots: None, approval_policy: None, approvals_reviewer: None, sandbox: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index 3eb262bd2b..bdfa6af39b 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -29,6 +29,7 @@ use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; use codex_config::types::AuthCredentialsStoreMode; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; @@ -216,6 +217,88 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_fork_preserves_persisted_workspace_roots_when_request_omits_them() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let repo = TempDir::new()?; + let shared = TempDir::new()?; + let workspace_roots = vec![ + AbsolutePathBuf::try_from(repo.path())?, + AbsolutePathBuf::try_from(shared.path())?, + ]; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let start_id = mcp + .send_thread_start_request(ThreadStartParams { + cwd: Some(repo.path().display().to_string()), + permissions: Some(":workspace".to_string()), + workspace_roots: Some(workspace_roots.clone()), + ..Default::default() + }) + .await?; + let start_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(start_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(start_resp)?; + + let turn_id = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![UserInput::Text { + text: "seed history".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let fork_id = mcp + .send_thread_fork_request(ThreadForkParams { + thread_id: thread.id, + ..Default::default() + }) + .await?; + let fork_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(fork_id)), + ) + .await??; + let ThreadForkResponse { + workspace_roots: forked_roots, + permission_profile, + .. + } = to_response::(fork_resp)?; + + assert_eq!(forked_roots, workspace_roots); + let permission_profile: codex_protocol::models::PermissionProfile = permission_profile + .expect("fork response should include exact permissions") + .into(); + assert!( + permission_profile + .file_system_sandbox_policy() + .can_write_path_with_cwd(shared.path(), repo.path()) + ); + + Ok(()) +} + #[tokio::test] async fn thread_fork_can_load_source_by_path() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/app-server/tests/suite/v2/thread_list.rs b/codex-rs/app-server/tests/suite/v2/thread_list.rs index 80254d8f47..c8872c47bf 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_list.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_list.rs @@ -610,6 +610,7 @@ sqlite = true codex_home: codex_home.path().to_path_buf(), sqlite_home: codex_home.path().to_path_buf(), cwd: codex_home.path().to_path_buf(), + workspace_roots: Vec::new(), model_provider_id: "mock_provider".to_string(), generate_memories: false, }; diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index fa143254a5..61c90aa5a5 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -1291,6 +1291,7 @@ async fn seed_pathless_store_thread( dynamic_tools: Vec::new(), metadata: ThreadPersistenceMetadata { cwd: None, + workspace_roots: Vec::new(), model_provider: "test-provider".to_string(), memory_mode: ThreadMemoryMode::Disabled, }, diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 64dfe0beb7..b9ae71cbec 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -1474,6 +1474,7 @@ stream_max_retries = 0 forked_from_id: None, timestamp: "2025-01-05T12:00:00Z".to_string(), cwd: repo_path.clone(), + workspace_roots: Vec::new(), originator: "codex".to_string(), cli_version: "0.0.0".to_string(), source: RolloutSessionSource::Cli, @@ -2970,6 +2971,64 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_resume_preserves_persisted_workspace_roots_when_request_omits_them() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let repo = TempDir::new()?; + let shared = TempDir::new()?; + let workspace_roots = vec![ + AbsolutePathBuf::try_from(repo.path())?, + AbsolutePathBuf::try_from(shared.path())?, + ]; + + let RestartedThreadFixture { + mut mcp, thread_id, .. + } = start_materialized_thread_with_params_and_restart( + codex_home.path(), + "seed history", + ThreadStartParams { + model: Some("gpt-5.4".to_string()), + cwd: Some(repo.path().display().to_string()), + permissions: Some(":workspace".to_string()), + workspace_roots: Some(workspace_roots.clone()), + ..Default::default() + }, + ) + .await?; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id, + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), + ) + .await??; + let ThreadResumeResponse { + workspace_roots: resumed_roots, + permission_profile, + .. + } = to_response::(resume_resp)?; + + assert_eq!(resumed_roots, workspace_roots); + let permission_profile: codex_protocol::models::PermissionProfile = permission_profile + .expect("resume response should include exact permissions") + .into(); + assert!( + permission_profile + .file_system_sandbox_policy() + .can_write_path_with_cwd(shared.path(), repo.path()) + ); + + Ok(()) +} + struct RestartedThreadFixture { mcp: McpProcess, thread_id: String, @@ -2980,16 +3039,27 @@ struct RestartedThreadFixture { async fn start_materialized_thread_and_restart( codex_home: &Path, seed_text: &str, +) -> Result { + start_materialized_thread_with_params_and_restart( + codex_home, + seed_text, + ThreadStartParams { + model: Some("gpt-5.4".to_string()), + ..Default::default() + }, + ) + .await +} + +async fn start_materialized_thread_with_params_and_restart( + codex_home: &Path, + seed_text: &str, + start_params: ThreadStartParams, ) -> Result { let mut first_mcp = McpProcess::new(codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, first_mcp.initialize()).await??; - let start_id = first_mcp - .send_thread_start_request(ThreadStartParams { - model: Some("gpt-5.4".to_string()), - ..Default::default() - }) - .await?; + let start_id = first_mcp.send_thread_start_request(start_params).await?; let start_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, first_mcp.read_stream_until_response_message(RequestId::Integer(start_id)), diff --git a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs index d2f8f268a2..a761b801a6 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs @@ -44,6 +44,7 @@ use pretty_assertions::assert_eq; use serde_json::Value; use std::fs::FileTimes; use std::fs::OpenOptions; +use std::future::Future; use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -54,6 +55,28 @@ use uuid::Uuid; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); +fn run_current_thread_test_with_stack(name: &str, future: F) -> Result<()> +where + F: Future> + Send + 'static, +{ + const TEST_STACK_SIZE_BYTES: usize = 4 * 1024 * 1024; + + let handle = std::thread::Builder::new() + .name(name.to_string()) + .stack_size(TEST_STACK_SIZE_BYTES) + .spawn(move || -> Result<()> { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + runtime.block_on(Box::pin(future)) + })?; + + match handle.join() { + Ok(result) => result, + Err(_) => Err(anyhow::anyhow!("{name} thread panicked")), + } +} + #[tokio::test] async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; @@ -197,97 +220,103 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result Ok(()) } -#[tokio::test] -async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> { - let codex_home = TempDir::new()?; - let store_id = Uuid::new_v4().to_string(); - create_config_toml_with_in_memory_thread_store(codex_home.path(), &store_id)?; - let store = InMemoryThreadStore::for_id(store_id.clone()); - let _in_memory_store = InMemoryThreadStoreId { store_id }; - let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000126")?; - let parent_thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000127")?; - store - .create_thread(CreateThreadParams { - thread_id, - forked_from_id: Some(parent_thread_id), - source: SessionSource::Cli, - thread_source: None, - base_instructions: BaseInstructions::default(), - dynamic_tools: Vec::new(), - metadata: ThreadPersistenceMetadata { - cwd: None, - model_provider: "test-provider".to_string(), - memory_mode: ThreadMemoryMode::Disabled, - }, - event_persistence_mode: ThreadEventPersistenceMode::default(), - }) - .await?; - store - .update_thread_metadata(UpdateThreadMetadataParams { - thread_id, - patch: ThreadMetadataPatch { - name: Some(Some("named pathless thread".to_string())), - ..Default::default() - }, - include_archived: true, - }) - .await?; +#[test] +fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> { + run_current_thread_test_with_stack( + "thread_unarchive_preserves_pathless_store_metadata", + async { + let codex_home = TempDir::new()?; + let store_id = Uuid::new_v4().to_string(); + create_config_toml_with_in_memory_thread_store(codex_home.path(), &store_id)?; + let store = InMemoryThreadStore::for_id(store_id.clone()); + let _in_memory_store = InMemoryThreadStoreId { store_id }; + let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000126")?; + let parent_thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000127")?; + store + .create_thread(CreateThreadParams { + thread_id, + forked_from_id: Some(parent_thread_id), + source: SessionSource::Cli, + thread_source: None, + base_instructions: BaseInstructions::default(), + dynamic_tools: Vec::new(), + metadata: ThreadPersistenceMetadata { + cwd: None, + workspace_roots: Vec::new(), + model_provider: "test-provider".to_string(), + memory_mode: ThreadMemoryMode::Disabled, + }, + event_persistence_mode: ThreadEventPersistenceMode::default(), + }) + .await?; + store + .update_thread_metadata(UpdateThreadMetadataParams { + thread_id, + patch: ThreadMetadataPatch { + name: Some(Some("named pathless thread".to_string())), + ..Default::default() + }, + include_archived: true, + }) + .await?; - let loader_overrides = LoaderOverrides::without_managed_config_for_tests(); - let config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .loader_overrides(loader_overrides.clone()) - .build() - .await?; - let client = in_process::start(InProcessStartArgs { - arg0_paths: Arg0DispatchPaths::default(), - config: Arc::new(config), - cli_overrides: Vec::new(), - loader_overrides, - strict_config: false, - cloud_requirements: CloudRequirementsLoader::default(), - thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader), - feedback: CodexFeedback::new(), - log_db: None, - state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), - config_warnings: Vec::new(), - session_source: SessionSource::Cli, - enable_codex_api_key_env: false, - initialize: InitializeParams { - client_info: ClientInfo { - name: "codex-app-server-tests".to_string(), - title: None, - version: "0.1.0".to_string(), - }, - capabilities: Some(InitializeCapabilities { - experimental_api: true, - ..Default::default() - }), + let loader_overrides = LoaderOverrides::without_managed_config_for_tests(); + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .loader_overrides(loader_overrides.clone()) + .build() + .await?; + let client = in_process::start(InProcessStartArgs { + arg0_paths: Arg0DispatchPaths::default(), + config: Arc::new(config), + cli_overrides: Vec::new(), + loader_overrides, + strict_config: false, + cloud_requirements: CloudRequirementsLoader::default(), + thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader), + feedback: CodexFeedback::new(), + log_db: None, + state_db: None, + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + config_warnings: Vec::new(), + session_source: SessionSource::Cli, + enable_codex_api_key_env: false, + initialize: InitializeParams { + client_info: ClientInfo { + name: "codex-app-server-tests".to_string(), + title: None, + version: "0.1.0".to_string(), + }, + capabilities: Some(InitializeCapabilities { + experimental_api: true, + ..Default::default() + }), + }, + channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, + }) + .await?; + + let result = client + .request(ClientRequest::ThreadUnarchive { + request_id: RequestId::Integer(1), + params: ThreadUnarchiveParams { + thread_id: thread_id.to_string(), + }, + }) + .await? + .expect("thread/unarchive should succeed"); + let ThreadUnarchiveResponse { thread } = serde_json::from_value(result)?; + + assert_eq!(thread.id, thread_id.to_string()); + assert_eq!(thread.path, None); + assert_eq!(thread.forked_from_id, Some(parent_thread_id.to_string())); + assert_eq!(thread.name, Some("named pathless thread".to_string())); + + client.shutdown().await?; + Ok(()) }, - channel_capacity: in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, - }) - .await?; - - let result = client - .request(ClientRequest::ThreadUnarchive { - request_id: RequestId::Integer(1), - params: ThreadUnarchiveParams { - thread_id: thread_id.to_string(), - }, - }) - .await? - .expect("thread/unarchive should succeed"); - let ThreadUnarchiveResponse { thread } = serde_json::from_value(result)?; - - assert_eq!(thread.id, thread_id.to_string()); - assert_eq!(thread.path, None); - assert_eq!(thread.forked_from_id, Some(parent_thread_id.to_string())); - assert_eq!(thread.name, Some("named pathless thread".to_string())); - - client.shutdown().await?; - Ok(()) + ) } fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 524b795b81..d21fae4e10 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -34,7 +34,6 @@ use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::PatchChangeKind; -use codex_app_server_protocol::PermissionProfileSelectionParams; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerRequestResolvedNotification; @@ -329,9 +328,27 @@ async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills( let warning: WarningNotification = serde_json::from_value(params).expect("deserialize warning notification"); assert_eq!(warning.thread_id.as_deref(), Some(thread.id.as_str())); - assert_eq!( - warning.message, - "Exceeded skills context budget of 2%. All skill descriptions were removed and 7 additional skills were not included in the model-visible skills list." + const WARNING_PREFIX: &str = + "Exceeded skills context budget of 2%. All skill descriptions were removed and "; + const WARNING_SUFFIX: &str = + " additional skills were not included in the model-visible skills list."; + assert!( + warning.message.starts_with(WARNING_PREFIX), + "unexpected warning prefix: {}", + warning.message + ); + assert!( + warning.message.ends_with(WARNING_SUFFIX), + "unexpected warning suffix: {}", + warning.message + ); + let trimmed_count = warning.message + [WARNING_PREFIX.len()..warning.message.len() - WARNING_SUFFIX.len()] + .parse::() + .expect("warning should include a numeric trimmed skills count"); + assert!( + trimmed_count > 0, + "warning should report at least one trimmed skill" ); timeout( @@ -779,10 +796,7 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() text: "Hello".to_string(), text_elements: Vec::new(), }], - permissions: Some(PermissionProfileSelectionParams::Profile { - id: ":danger-no-sandbox".to_string(), - modifications: None, - }), + permissions: Some(":danger-no-sandbox".to_string()), ..Default::default() }) .await?; @@ -1890,13 +1904,14 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { }], responsesapi_client_metadata: None, cwd: Some(first_cwd.clone()), + workspace_roots: Some(vec![first_cwd.try_into()?]), approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: vec![first_cwd.try_into()?], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + legacy_writable_roots: Vec::new(), }), permissions: None, model: Some("mock-model".to_string()), @@ -1931,6 +1946,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { }], responsesapi_client_metadata: None, cwd: Some(second_cwd.clone()), + workspace_roots: None, approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess), diff --git a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs index 31247418e5..c14263318e 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs @@ -533,12 +533,13 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2() text_elements: Vec::new(), }], cwd: Some(workspace.clone()), + workspace_roots: Some(vec![workspace.clone().try_into()?]), approval_policy: Some(codex_app_server_protocol::AskForApproval::UnlessTrusted), sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: vec![workspace.clone().try_into()?], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + legacy_writable_roots: Vec::new(), }), model: Some("mock-model".to_string()), effort: Some(codex_protocol::openai_models::ReasoningEffort::Medium), diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 30c60d7b95..6125461cd0 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -230,7 +230,7 @@ async fn run_command_under_sandbox( let network_proxy = match config.permissions.network.as_ref() { Some(spec) => Some( spec.start_proxy( - config.permissions.permission_profile.get(), + config.permissions.permission_profile_ref(), /*policy_decider*/ None, /*blocked_request_observer*/ None, managed_network_requirements_enabled, @@ -285,7 +285,7 @@ async fn run_command_under_sandbox( let args = create_linux_sandbox_command_args_for_permission_profile( command, cwd.as_path(), - &config.permissions.permission_profile(), + config.permissions.permission_profile_ref(), sandbox_policy_cwd.as_path(), use_legacy_landlock, allow_network_for_proxy(managed_network_requirements_enabled), @@ -777,6 +777,16 @@ mod tests { Ok(()) } + fn workspace_write_policy_for_codex_home( + codex_home: &TempDir, + ) -> codex_protocol::permissions::FileSystemSandboxPolicy { + let memories_root = AbsolutePathBuf::try_from(codex_home.path().join("memories")) + .expect("codex home tempdir should be absolute"); + codex_protocol::models::PermissionProfile::workspace_write() + .file_system_sandbox_policy() + .with_additional_legacy_workspace_writable_roots(std::slice::from_ref(&memories_root)) + } + #[tokio::test] async fn debug_sandbox_honors_active_permission_profiles() -> anyhow::Result<()> { let codex_home = TempDir::new()?; @@ -964,8 +974,7 @@ mod tests { assert_eq!( config.permissions.file_system_sandbox_policy(), - codex_protocol::models::PermissionProfile::workspace_write() - .file_system_sandbox_policy() + workspace_write_policy_for_codex_home(&codex_home) ); Ok(()) @@ -998,8 +1007,7 @@ mod tests { assert_eq!( config.permissions.file_system_sandbox_policy(), - codex_protocol::models::PermissionProfile::workspace_write() - .file_system_sandbox_policy() + workspace_write_policy_for_codex_home(&codex_home) ); Ok(()) diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 9faa846137..374ece14ed 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -2191,7 +2191,6 @@ allowed_approvals_reviewers = ["user"] let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; - let root = if cfg!(windows) { "C:\\repo" } else { "/repo" }; assert!( requirements .permission_profile @@ -2201,7 +2200,6 @@ allowed_approvals_reviewers = ["user"] .is_ok() ); let workspace_write_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -2312,9 +2310,7 @@ allowed_approvals_reviewers = ["user"] ); let requirements = ConfigRequirements::try_from(requirements_with_sources)?; - let root = if cfg!(windows) { "C:\\repo" } else { "/repo" }; let workspace_write_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 06993e046a..53a7c0f6d4 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -760,7 +760,7 @@ impl ConfigToml { SandboxMode::ReadOnly => PermissionProfile::read_only(), SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { Some(SandboxWorkspaceWrite { - writable_roots, + writable_roots: _, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, @@ -771,7 +771,7 @@ impl ConfigToml { NetworkSandboxPolicy::Restricted }; PermissionProfile::workspace_write_with( - writable_roots, + &[], network_policy, *exclude_tmpdir_env_var, *exclude_slash_tmp, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index be5b0ccb88..d5d087c068 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -59,6 +59,7 @@ pub struct ThreadConfigSnapshot { pub permission_profile: PermissionProfile, pub active_permission_profile: Option, pub cwd: AbsolutePathBuf, + pub workspace_roots: Vec, pub ephemeral: bool, pub reasoning_effort: Option, pub personality: Option, @@ -68,11 +69,15 @@ pub struct ThreadConfigSnapshot { impl ThreadConfigSnapshot { pub fn sandbox_policy(&self) -> SandboxPolicy { - let file_system_sandbox_policy = self.permission_profile.file_system_sandbox_policy(); + let permission_profile = self + .permission_profile + .clone() + .materialize_project_roots_with_workspace_roots(&self.workspace_roots); + let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy(); codex_sandboxing::compatibility_sandbox_policy_for_permission_profile( - &self.permission_profile, + &permission_profile, &file_system_sandbox_policy, - self.permission_profile.network_sandbox_policy(), + permission_profile.network_sandbox_policy(), self.cwd.as_path(), ) } @@ -82,6 +87,7 @@ impl ThreadConfigSnapshot { #[derive(Clone, Default)] pub struct CodexThreadTurnContextOverrides { pub cwd: Option, + pub workspace_roots: Option>, pub approval_policy: Option, pub approvals_reviewer: Option, pub sandbox_policy: Option, @@ -256,8 +262,26 @@ impl CodexThread { &self, overrides: CodexThreadTurnContextOverrides, ) -> ConstraintResult<()> { + let updates = self.turn_context_updates_from_overrides(overrides).await; + self.codex.session.validate_settings(&updates).await + } + + /// Apply persistent thread context overrides immediately. + pub async fn update_turn_context_overrides( + &self, + overrides: CodexThreadTurnContextOverrides, + ) -> ConstraintResult<()> { + let updates = self.turn_context_updates_from_overrides(overrides).await; + self.codex.session.update_settings(updates).await + } + + async fn turn_context_updates_from_overrides( + &self, + overrides: CodexThreadTurnContextOverrides, + ) -> SessionSettingsUpdate { let CodexThreadTurnContextOverrides { cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox_policy, @@ -281,8 +305,9 @@ impl CodexThread { .with_updates(model, effort, /*developer_instructions*/ None) }; - let updates = SessionSettingsUpdate { + SessionSettingsUpdate { cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox_policy, @@ -294,8 +319,7 @@ impl CodexThread { service_tier, personality, ..Default::default() - }; - self.codex.session.validate_settings(&updates).await + } } /// Use sparingly: this is intended to be removed soon. diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 136a422849..85ce9a8d56 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -775,9 +775,10 @@ writable_roots = ["~/code"] let expected_root = AbsolutePathBuf::from_absolute_path(home.join("code"))?; match &config.legacy_sandbox_policy() { - SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { + SandboxPolicy::WorkspaceWrite { .. } => { assert_eq!( - writable_roots + config + .workspace_roots .iter() .filter(|root| **root == expected_root) .count(), @@ -841,7 +842,6 @@ allowed_sandbox_modes = ["read-only"] .permission_profile .can_set(&PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 9fc8c2643f..58dc5ba1df 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -68,7 +68,6 @@ use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; use codex_protocol::config_types::ServiceTier; use codex_protocol::models::ActivePermissionProfile; -use codex_protocol::models::ActivePermissionProfileModification; use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxEnforcement; @@ -100,6 +99,14 @@ use std::path::Path; use std::time::Duration; use tempfile::TempDir; +fn materialized_file_system_sandbox_policy(config: &Config) -> FileSystemSandboxPolicy { + config + .permissions + .permission_profile() + .materialize_project_roots_with_workspace_roots(&config.workspace_roots) + .file_system_sandbox_policy() +} + fn stdio_mcp(command: &str) -> McpServerConfig { McpServerConfig { transport: McpServerTransportConfig::Stdio { @@ -1364,7 +1371,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: }, FileSystemSandboxEntry { path: FileSystemPath::Path { - path: memories_root.clone(), + path: memories_root, }, access: FileSystemAccessMode::Write, }, @@ -1373,7 +1380,6 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: assert_eq!( &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { - writable_roots: vec![memories_root], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1550,7 +1556,6 @@ async fn permission_profile_override_applies_runtime_roots_to_legacy_projection( assert_eq!( &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { - writable_roots: vec![memories_root], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1751,7 +1756,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl ) .await?; - let policy = config.permissions.file_system_sandbox_policy(); + let policy = materialized_file_system_sandbox_policy(&config); assert_eq!( config .permissions @@ -1772,7 +1777,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl } #[tokio::test] -async fn default_permissions_read_only_applies_additional_writable_roots_as_modifications() +async fn default_permissions_read_only_records_additional_writable_roots_as_workspace_roots() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; @@ -1793,18 +1798,18 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi ) .await?; - let policy = config.permissions.file_system_sandbox_policy(); - assert!( - policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()), - "expected additional writable root to modify :read-only, policy: {policy:?}" + let policy = materialized_file_system_sandbox_policy(&config); + assert_eq!( + policy, + PermissionProfile::read_only().file_system_sandbox_policy() + ); + assert_eq!( + config.workspace_roots, + vec![cwd.path().abs(), extra_root.clone()] ); assert_eq!( config.permissions.active_permission_profile(), - Some( - ActivePermissionProfile::new(":read-only").with_modifications(vec![ - ActivePermissionProfileModification::AdditionalWritableRoot { path: extra_root }, - ]) - ) + Some(ActivePermissionProfile::new(":read-only")) ); Ok(()) } @@ -1835,7 +1840,7 @@ async fn explicit_builtin_workspace_profile_ignores_legacy_workspace_write_setti ) .await?; - let policy = config.permissions.file_system_sandbox_policy(); + let policy = materialized_file_system_sandbox_policy(&config); assert_eq!( config.permissions.network_sandbox_policy(), NetworkSandboxPolicy::Restricted @@ -1875,7 +1880,7 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std:: ) .await?; - let policy = config.permissions.file_system_sandbox_policy(); + let policy = materialized_file_system_sandbox_policy(&config); assert_eq!( config .permissions @@ -1943,7 +1948,7 @@ async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_se ) .await?; - let policy = config.permissions.file_system_sandbox_policy(); + let policy = materialized_file_system_sandbox_policy(&config); assert!( policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()), "expected implicit :workspace to preserve sandbox_workspace_write.writable_roots, policy: {policy:?}" @@ -1960,12 +1965,11 @@ async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_se ); match config.legacy_sandbox_policy() { SandboxPolicy::WorkspaceWrite { - writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, } => { - assert!(writable_roots.contains(&extra_root)); + assert!(config.workspace_roots.contains(&extra_root)); assert!(network_access); assert!(exclude_tmpdir_env_var); assert!(!exclude_slash_tmp); @@ -2009,7 +2013,7 @@ async fn implicit_builtin_workspace_profile_preserves_add_dir_metadata_carveouts ) .await?; - let policy = config.permissions.file_system_sandbox_policy(); + let policy = materialized_file_system_sandbox_policy(&config); let extra_root = extra_root.path().abs(); assert!( policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()), @@ -2184,9 +2188,6 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() ) .await?; - let memories_root = AbsolutePathBuf::from_absolute_path(std::fs::canonicalize( - codex_home.path().join("memories"), - )?)?; assert!( config .permissions @@ -2196,7 +2197,6 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() assert_eq!( &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { - writable_roots: vec![external_write_path, memories_root], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -2871,7 +2871,6 @@ trust_level = "trusted" assert_eq!( resolution, SandboxPolicy::WorkspaceWrite { - writable_roots: vec![writable_root.clone()], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -2911,7 +2910,6 @@ exclude_slash_tmp = true assert_eq!( resolution, SandboxPolicy::WorkspaceWrite { - writable_roots: vec![writable_root], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -2975,13 +2973,21 @@ exclude_slash_tmp = true NetworkSandboxPolicy::from(&sandbox_policy), "case `{name}` should preserve network semantics from legacy config" ); - assert_eq!( - file_system_policy - .to_legacy_sandbox_policy(network_policy, cwd.path()) - .unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")), - sandbox_policy, - "case `{name}` should preserve its legacy compatibility projection" - ); + let direct_legacy_projection = + file_system_policy.to_legacy_sandbox_policy(network_policy, cwd.path()); + if name == "workspace-write" && !cfg!(target_os = "windows") { + assert!( + direct_legacy_projection.is_err(), + "case `{name}` should require the compatibility projection for split workspace roots" + ); + } else { + assert_eq!( + direct_legacy_projection + .unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")), + sandbox_policy, + "case `{name}` should preserve its legacy compatibility projection" + ); + } match name.as_str() { "danger-full-access" | "read-only" => { @@ -3023,14 +3029,19 @@ exclude_slash_tmp = true }) ); assert!( - file_system_policy + config.workspace_roots.contains(&extra_root), + "case `{name}` should store legacy writable roots as thread workspace roots" + ); + assert!( + materialized_file_system_sandbox_policy(&config) .entries .contains(&FileSystemSandboxEntry { path: FileSystemPath::Path { path: extra_root.clone(), }, access: FileSystemAccessMode::Write, - }) + }), + "case `{name}` should materialize workspace roots into runtime write access" ); for subpath in [".git", ".agents", ".codex"] { assert!( @@ -3773,14 +3784,15 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result< } } else { match &config.legacy_sandbox_policy() { - SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { + SandboxPolicy::WorkspaceWrite { .. } => { assert_eq!( - writable_roots + config + .workspace_roots .iter() .filter(|root| **root == expected_backend) .count(), 1, - "expected single writable root entry for {}", + "expected single workspace root entry for {}", expected_backend.display() ); } @@ -3840,13 +3852,16 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result "expected memories root directory to exist at {}", memories_root.display() ); - let expected_memories_root = memories_root.abs(); + let expected_memories_root = + AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(&memories_root)?)?; match &config.legacy_sandbox_policy() { - SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { + SandboxPolicy::WorkspaceWrite { .. } => { + let writable_roots = materialized_file_system_sandbox_policy(&config) + .get_writable_roots_with_cwd(config.cwd.as_path()); assert_eq!( writable_roots .iter() - .filter(|root| **root == expected_memories_root) + .filter(|root| root.root == expected_memories_root) .count(), 1, "expected single writable root entry for {}", @@ -7293,6 +7308,8 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { user_instructions: None, notify: None, cwd: fixture.cwd(), + workspace_roots: vec![fixture.cwd()], + workspace_roots_explicit: false, cli_auth_credentials_store_mode: Default::default(), mcp_servers: Constrained::allow_any(HashMap::new()), mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode( @@ -7741,6 +7758,8 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { user_instructions: None, notify: None, cwd: fixture.cwd(), + workspace_roots: vec![fixture.cwd()], + workspace_roots_explicit: false, cli_auth_credentials_store_mode: Default::default(), mcp_servers: Constrained::allow_any(HashMap::new()), mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode( @@ -7903,6 +7922,8 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { user_instructions: None, notify: None, cwd: fixture.cwd(), + workspace_roots: vec![fixture.cwd()], + workspace_roots_explicit: false, cli_auth_credentials_store_mode: Default::default(), mcp_servers: Constrained::allow_any(HashMap::new()), mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode( @@ -8050,6 +8071,8 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { user_instructions: None, notify: None, cwd: fixture.cwd(), + workspace_roots: vec![fixture.cwd()], + workspace_roots_explicit: false, cli_auth_credentials_store_mode: Default::default(), mcp_servers: Constrained::allow_any(HashMap::new()), mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode( @@ -9084,7 +9107,7 @@ async fn requirements_web_search_mode_overrides_danger_full_access_default() -> assert_eq!( resolve_web_search_mode_for_turn( &config.web_search_mode, - &config.permissions.permission_profile(), + config.permissions.permission_profile_ref(), ), WebSearchMode::Cached, ); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 3b4e674446..73e79f3f26 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -88,7 +88,6 @@ use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::ActivePermissionProfile; -use codex_protocol::models::ActivePermissionProfileModification; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxEnforcement; use codex_protocol::openai_models::ModelsResponse; @@ -243,10 +242,10 @@ pub struct Permissions { pub approval_policy: Constrained, /// Canonical effective runtime permissions after config requirements and /// runtime readable-root additions have been applied. - pub permission_profile: Constrained, + permission_profile: Constrained, /// Named or implicit built-in profile selected by config, rather than an /// ad-hoc override. - pub active_permission_profile: Option, + active_permission_profile: Option, /// Effective network configuration applied to all spawned processes. pub network: Option, /// Whether the model may request a login shell for shell-based tools. @@ -268,6 +267,35 @@ pub struct Permissions { } impl Permissions { + /// Build permissions from the two constrained values that are required for + /// a minimal in-process configuration. + pub fn from_approval_and_profile( + approval_policy: Constrained, + permission_profile: Constrained, + ) -> Self { + Self { + approval_policy, + permission_profile, + active_permission_profile: None, + network: None, + allow_login_shell: true, + shell_environment_policy: ShellEnvironmentPolicy::default(), + windows_sandbox_mode: None, + windows_sandbox_private_desktop: true, + } + } + + /// Borrow the constrained canonical profile. This is for code that must + /// preserve or inspect the active requirements wrapper without mutating it. + pub fn permission_profile_constraint(&self) -> &Constrained { + &self.permission_profile + } + + /// Borrow the canonical effective runtime permissions without cloning. + pub fn permission_profile_ref(&self) -> &PermissionProfile { + self.permission_profile.get() + } + /// Effective runtime permissions after config requirements and runtime /// readable-root additions have been applied. pub fn permission_profile(&self) -> PermissionProfile { @@ -279,6 +307,45 @@ impl Permissions { self.active_permission_profile.clone() } + /// Replace the full constrained profile value and clear any profile-name + /// sidecar because the new constraint may no longer match that name. + pub fn replace_permission_profile_constraint( + &mut self, + permission_profile: Constrained, + ) { + self.permission_profile = permission_profile; + self.active_permission_profile = None; + } + + /// Replace the full constrained profile value and preserve the active + /// profile sidecar when the caller has already validated both together. + pub fn replace_permission_profile_constraint_with_active_profile( + &mut self, + permission_profile: Constrained, + active_permission_profile: Option, + ) { + self.permission_profile = permission_profile; + self.active_permission_profile = active_permission_profile; + } + + /// Record the active profile id only if it still describes the current + /// canonical profile. + pub fn set_active_permission_profile_for_current_profile( + &mut self, + active_permission_profile: Option, + expected_permission_profile: Option<&PermissionProfile>, + ) { + self.active_permission_profile = + match (active_permission_profile, expected_permission_profile) { + (Some(active_permission_profile), Some(expected_permission_profile)) + if self.permission_profile.get() == expected_permission_profile => + { + Some(active_permission_profile) + } + _ => None, + }; + } + /// Effective filesystem sandbox policy derived from the canonical profile. pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { self.permission_profile.get().file_system_sandbox_policy() @@ -580,6 +647,13 @@ pub struct Config { /// layer are resolved against this path. pub cwd: AbsolutePathBuf, + /// Absolute roots that define the writable project/workspace set for + /// symbolic `:project_roots` permission entries. + pub workspace_roots: Vec, + /// Whether workspace roots were supplied explicitly by the caller or by + /// persisted thread state, rather than defaulted from cwd. + pub workspace_roots_explicit: bool, + /// Preferred store for CLI auth credentials. /// file (default): Use a file in the Codex home directory. /// keyring: Use an OS-specific keyring service. @@ -1860,6 +1934,11 @@ fn apply_managed_filesystem_constraints( } } +fn dedupe_absolute_paths(paths: &mut Vec) { + let mut seen = HashSet::new(); + paths.retain(|path| seen.insert(path.clone())); +} + /// Optional overrides for user configuration (e.g., from CLI flags). #[derive(Default, Debug, Clone)] pub struct ConfigOverrides { @@ -1889,6 +1968,9 @@ pub struct ConfigOverrides { pub bypass_hook_trust: Option, /// Additional directories that should be treated as writable roots for this session. pub additional_writable_roots: Vec, + /// Explicit workspace roots for this session. When set, this is the full + /// root list rather than an additive override. + pub workspace_roots: Option>, } /// Resolves the OSS provider from CLI override, profile config, or global config. @@ -2177,6 +2259,7 @@ impl Config { ephemeral, bypass_hook_trust, additional_writable_roots, + workspace_roots: workspace_roots_override, } = overrides; let bypass_hook_trust = bypass_hook_trust.unwrap_or_default(); @@ -2273,11 +2356,10 @@ impl Config { } } }))?; - let mut additional_writable_roots: Vec = additional_writable_roots + let requested_additional_writable_roots: Vec = additional_writable_roots .into_iter() .map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path())) .collect(); - let requested_additional_writable_roots = additional_writable_roots.clone(); let repo_root = resolve_root_git_project_for_trust(fs, &resolved_cwd).await; let active_project = cfg .get_active_project( @@ -2319,12 +2401,7 @@ impl Config { }; let memories_root = memory_root(&codex_home); std::fs::create_dir_all(&memories_root)?; - if !additional_writable_roots - .iter() - .any(|existing| existing == &memories_root) - { - additional_writable_roots.push(memories_root); - } + let internal_writable_roots = vec![memories_root]; let profiles_are_active = default_permissions_override.is_some() || matches!( @@ -2334,6 +2411,41 @@ impl Config { || permission_config_syntax.is_none(); let using_implicit_builtin_profile = permission_config_syntax.is_none() && default_permissions.is_none(); + let should_seed_legacy_workspace_roots = + default_permissions.is_none() + && matches!( + permission_config_syntax, + None | Some(PermissionConfigSyntax::Legacy) + ); + let legacy_workspace_roots_explicit = should_seed_legacy_workspace_roots + && cfg + .sandbox_workspace_write + .as_ref() + .is_some_and(|sandbox_workspace_write| { + !sandbox_workspace_write.writable_roots.is_empty() + }); + let workspace_roots_explicit = workspace_roots_override.is_some() + || !requested_additional_writable_roots.is_empty() + || legacy_workspace_roots_explicit; + let mut workspace_roots = match workspace_roots_override { + Some(workspace_roots) => workspace_roots + .into_iter() + .map(|path| { + AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()) + }) + .collect(), + None => { + let mut workspace_roots = vec![resolved_cwd.clone()]; + workspace_roots.extend(requested_additional_writable_roots.clone()); + if should_seed_legacy_workspace_roots + && let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref() + { + workspace_roots.extend(sandbox_workspace_write.writable_roots.clone()); + } + workspace_roots + } + }; + dedupe_absolute_paths(&mut workspace_roots); let ( mut configured_network_proxy_config, permission_profile, @@ -2370,10 +2482,7 @@ impl Config { ); if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { file_system_sandbox_policy = file_system_sandbox_policy - .with_additional_writable_roots( - resolved_cwd.as_path(), - &additional_writable_roots, - ); + .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( permission_profile.enforcement(), &file_system_sandbox_policy, @@ -2424,28 +2533,8 @@ impl Config { resolved_cwd.as_path(), ); if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { - file_system_sandbox_policy = if using_implicit_builtin_profile { - file_system_sandbox_policy - .with_additional_legacy_workspace_writable_roots( - &additional_writable_roots, - ) - } else { - file_system_sandbox_policy.with_additional_writable_roots( - resolved_cwd.as_path(), - &additional_writable_roots, - ) - }; - permission_profile = PermissionProfile::from_runtime_permissions( - &file_system_sandbox_policy, - network_sandbox_policy, - ); - } else if matches!(permission_profile, PermissionProfile::Managed { .. }) - && !requested_additional_writable_roots.is_empty() - { - file_system_sandbox_policy = file_system_sandbox_policy.with_additional_writable_roots( - resolved_cwd.as_path(), - &requested_additional_writable_roots, - ); + file_system_sandbox_policy = file_system_sandbox_policy + .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); permission_profile = PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, network_sandbox_policy, @@ -2462,22 +2551,7 @@ impl Config { // when doing so would lose roots, network, or tmp settings. None } else { - let active_permission_profile = if !requested_additional_writable_roots.is_empty() - && matches!(permission_profile, PermissionProfile::Managed { .. }) - { - ActivePermissionProfile::new(default_permissions).with_modifications( - requested_additional_writable_roots - .iter() - .cloned() - .map(|path| { - ActivePermissionProfileModification::AdditionalWritableRoot { path } - }) - .collect(), - ) - } else { - ActivePermissionProfile::new(default_permissions) - }; - Some(active_permission_profile) + Some(ActivePermissionProfile::new(default_permissions)) }; ( configured_network_proxy_config, @@ -2516,25 +2590,25 @@ impl Config { } let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); - // `additional_writable_roots` is a legacy workspace-write knob. It - // only applies when the derived managed profile has workspace-style - // write access to the project roots; read-only, disabled, external, - // and future non-workspace profiles must not silently grow extra - // write access. + // Internal writable roots only apply when the derived managed + // profile has workspace-style write access to the project roots; + // read-only, disabled, external, and future non-workspace profiles + // must not silently grow extra write access. + let materialized_file_system_sandbox_policy = permission_profile + .materialize_project_roots_with_workspace_roots(&workspace_roots) + .file_system_sandbox_policy(); if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed) - && file_system_sandbox_policy.can_write_path_with_cwd( + && materialized_file_system_sandbox_policy.can_write_path_with_cwd( resolved_cwd.as_path(), resolved_cwd.as_path(), ) - && !file_system_sandbox_policy.has_full_disk_write_access() + && !materialized_file_system_sandbox_policy.has_full_disk_write_access() { - // Keep legacy behavior for extra writable roots while storing - // the result as the canonical permission profile. Explicit - // extra roots are concrete paths, so their metadata carveouts - // are also concrete rather than symbolic `:project_roots` - // entries. + // Keep Codex runtime write access while storing the result as + // the canonical permission profile. Workspace roots themselves + // are held separately on the thread. file_system_sandbox_policy = file_system_sandbox_policy - .with_additional_legacy_workspace_writable_roots(&additional_writable_roots); + .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( permission_profile.enforcement(), &file_system_sandbox_policy, @@ -3032,6 +3106,8 @@ impl Config { model_provider_id, model_provider, cwd: resolved_cwd, + workspace_roots, + workspace_roots_explicit, startup_warnings, permissions: Permissions { approval_policy: constrained_approval_policy.value, @@ -3323,7 +3399,7 @@ impl Config { pub fn managed_network_requirements_enabled(&self) -> bool { !matches!( - self.permissions.permission_profile.get(), + self.permissions.permission_profile_ref(), PermissionProfile::Disabled ) && self .config_layer_stack diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 6b6021ad34..eb4da0d447 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -68,12 +68,12 @@ pub(crate) fn builtin_permission_profile( BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()), BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write { Some(SandboxWorkspaceWrite { - writable_roots, + writable_roots: _, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, }) => PermissionProfile::workspace_write_with( - writable_roots, + &[], if *network_access { NetworkSandboxPolicy::Enabled } else { diff --git a/codex-rs/core/src/context/permissions_instructions.rs b/codex-rs/core/src/context/permissions_instructions.rs index 0ccd6c33a7..4f30c644c0 100644 --- a/codex-rs/core/src/context/permissions_instructions.rs +++ b/codex-rs/core/src/context/permissions_instructions.rs @@ -30,6 +30,8 @@ const SANDBOX_MODE_DANGER_FULL_ACCESS: &str = const SANDBOX_MODE_WORKSPACE_WRITE: &str = include_str!("prompts/permissions/sandbox_mode/workspace_write.md"); const SANDBOX_MODE_READ_ONLY: &str = include_str!("prompts/permissions/sandbox_mode/read_only.md"); +const MAX_WRITABLE_ROOTS_IN_PROMPT: usize = 8; +const MAX_WRITABLE_ROOT_LABEL_CHARS: usize = 160; static SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE: LazyLock