From 4554fcc19ee5acab1cbf334a28a74d531973114e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 13 May 2026 21:54:07 -0700 Subject: [PATCH] app-server: use permission ids and runtime workspace roots --- .../analytics/src/analytics_client_tests.rs | 2 + codex-rs/analytics/src/client_tests.rs | 3 + .../schema/json/ClientRequest.json | 59 ------------ .../codex_app_server_protocol.schemas.json | 92 ------------------ .../codex_app_server_protocol.v2.schemas.json | 92 ------------------ .../schema/json/v2/ThreadForkParams.json | 63 ------------ .../schema/json/v2/ThreadForkResponse.json | 33 ------- .../schema/json/v2/ThreadResumeParams.json | 63 ------------ .../schema/json/v2/ThreadResumeResponse.json | 33 ------- .../schema/json/v2/ThreadStartParams.json | 59 ------------ .../schema/json/v2/ThreadStartResponse.json | 33 ------- .../schema/json/v2/TurnStartParams.json | 59 ------------ .../typescript/v2/ActivePermissionProfile.ts | 8 +- .../v2/ActivePermissionProfileModification.ts | 6 -- .../v2/PermissionProfileModificationParams.ts | 6 -- .../v2/PermissionProfileSelectionParams.ts | 6 -- .../schema/typescript/v2/index.ts | 3 - .../src/protocol/common.rs | 2 + .../src/protocol/v2/permissions.rs | 73 -------------- .../src/protocol/v2/tests.rs | 1 + .../src/protocol/v2/thread.rs | 51 +++++++--- .../src/protocol/v2/turn.rs | 15 +-- codex-rs/app-server/README.md | 14 ++- codex-rs/app-server/src/lib.rs | 6 +- .../src/message_processor_tracing_tests.rs | 1 + codex-rs/app-server/src/request_processors.rs | 2 - .../command_exec_processor.rs | 9 +- .../request_processors/thread_lifecycle.rs | 2 + .../request_processors/thread_processor.rs | 32 ++++++- .../thread_processor_tests.rs | 2 + .../src/request_processors/thread_summary.rs | 13 +-- .../src/request_processors/turn_processor.rs | 48 +++++++++- .../app-server/tests/suite/v2/skills_list.rs | 1 + .../tests/suite/v2/thread_resume.rs | 73 ++++++++++++++ .../app-server/tests/suite/v2/thread_start.rs | 42 ++++++++ .../app-server/tests/suite/v2/turn_start.rs | 8 +- codex-rs/core/src/codex_thread.rs | 4 + codex-rs/core/src/config/config_tests.rs | 9 ++ codex-rs/core/src/config/mod.rs | 43 +++++++++ codex-rs/core/src/guardian/review_session.rs | 13 ++- codex-rs/core/src/guardian/tests.rs | 15 ++- codex-rs/core/src/session/handlers.rs | 3 + codex-rs/core/src/session/mod.rs | 3 +- codex-rs/core/src/session/session.rs | 19 +++- codex-rs/core/src/session/tests.rs | 35 ++++--- codex-rs/core/src/session/turn_context.rs | 29 +++++- .../src/tools/handlers/multi_agents_tests.rs | 2 +- codex-rs/core/tests/suite/unified_exec.rs | 19 ++-- codex-rs/core/tests/suite/user_shell_cmd.rs | 5 +- codex-rs/exec/src/lib.rs | 44 ++++----- codex-rs/exec/src/lib_tests.rs | 1 + codex-rs/protocol/src/protocol.rs | 5 + 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_session_state.rs | 2 + codex-rs/tui/src/app_server_session.rs | 96 +++++++++++++++---- codex-rs/tui/src/chatwidget.rs | 19 +++- .../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 | 1 + codex-rs/tui/src/history_cell.rs | 1 + codex-rs/tui/src/session_state.rs | 1 + 67 files changed, 596 insertions(+), 823 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 diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 5b4a72229b..7b9ab4f9d6 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(), + runtime_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(), + runtime_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..885875346d 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -153,6 +153,7 @@ fn sample_thread_start_response() -> ClientResponsePayload { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, @@ -170,6 +171,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, @@ -187,6 +189,7 @@ fn sample_thread_fork_response() -> ClientResponsePayload { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: AppServerAskForApproval::OnFailure, approvals_reviewer: AppServerApprovalsReviewer::User, diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a6fe99b35e..f40d16feb6 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", 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 6da572813f..089e90ad56 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", 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 e338f4151f..d9023869f7 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", 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..102cfa0299 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -1,10 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "AbsolutePathBuf": { - "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - "type": "string" - }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", "enum": [ @@ -64,65 +60,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", 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..4eb85f4ed3 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" }, 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..27674afc7b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -1,10 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "AbsolutePathBuf": { - "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - "type": "string" - }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", "enum": [ @@ -298,65 +294,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", 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..312d289e41 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" }, 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..c363f2e78d 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" }, 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..086d3c6ed1 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", 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/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..4c572b37c9 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2294,6 +2294,7 @@ mod tests { model_provider: "openai".to_string(), service_tier: None, cwd, + runtime_workspace_roots: Vec::new(), instruction_sources: vec![absolute_path("/tmp/AGENTS.md")], approval_policy: v2::AskForApproval::OnFailure, approvals_reviewer: v2::ApprovalsReviewer::User, @@ -2338,6 +2339,7 @@ mod tests { "modelProvider": "openai", "serviceTier": null, "cwd": absolute_path_string("tmp"), + "runtimeWorkspaceRoots": [], "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..513ce41a12 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/")] 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..9c2f998ae8 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3469,6 +3469,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() { responsesapi_client_metadata: None, environments: None, cwd: None, + runtime_workspace_roots: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 458722b3a2..6ce3d533a6 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, + /// Replace the thread's runtime workspace roots. Relative paths are + /// resolved against the effective cwd for the thread. + #[experimental("thread/start.runtimeWorkspaceRoots")] + #[ts(optional = nullable)] + pub runtime_workspace_roots: Option>, #[experimental(nested)] #[ts(optional = nullable)] pub approval_policy: Option, @@ -116,12 +120,10 @@ 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 profile id for this thread. 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 +197,11 @@ pub struct ThreadStartResponse { pub model_provider: String, pub service_tier: Option, pub cwd: AbsolutePathBuf, + /// Thread-scoped runtime workspace roots used to materialize + /// `:workspace_roots`. + #[experimental("thread/start.runtimeWorkspaceRoots")] + #[serde(default)] + pub runtime_workspace_roots: Vec, /// Instruction source files currently loaded for this thread. #[serde(default)] pub instruction_sources: Vec, @@ -264,6 +271,11 @@ pub struct ThreadResumeParams { pub service_tier: Option>, #[ts(optional = nullable)] pub cwd: Option, + /// Replace the thread's runtime workspace roots. Relative paths are + /// resolved against the effective cwd for the thread. + #[experimental("thread/resume.runtimeWorkspaceRoots")] + #[ts(optional = nullable)] + pub runtime_workspace_roots: Option>, #[experimental(nested)] #[ts(optional = nullable)] pub approval_policy: Option, @@ -273,12 +285,11 @@ pub struct ThreadResumeParams { pub approvals_reviewer: Option, #[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 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 +321,11 @@ pub struct ThreadResumeResponse { pub model_provider: String, pub service_tier: Option, pub cwd: AbsolutePathBuf, + /// Thread-scoped runtime workspace roots used to materialize + /// `:workspace_roots`. + #[experimental("thread/resume.runtimeWorkspaceRoots")] + #[serde(default)] + pub runtime_workspace_roots: Vec, /// Instruction source files currently loaded for this thread. #[serde(default)] pub instruction_sources: Vec, @@ -370,6 +386,11 @@ pub struct ThreadForkParams { pub service_tier: Option>, #[ts(optional = nullable)] pub cwd: Option, + /// Replace the thread's runtime workspace roots. Relative paths are + /// resolved against the effective cwd for the thread. + #[experimental("thread/fork.runtimeWorkspaceRoots")] + #[ts(optional = nullable)] + pub runtime_workspace_roots: Option>, #[experimental(nested)] #[ts(optional = nullable)] pub approval_policy: Option, @@ -379,12 +400,11 @@ pub struct ThreadForkParams { pub approvals_reviewer: Option, #[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 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 +439,11 @@ pub struct ThreadForkResponse { pub model_provider: String, pub service_tier: Option, pub cwd: AbsolutePathBuf, + /// Thread-scoped runtime workspace roots used to materialize + /// `:workspace_roots`. + #[experimental("thread/fork.runtimeWorkspaceRoots")] + #[serde(default)] + pub runtime_workspace_roots: Vec, /// Instruction source files currently loaded for this thread. #[serde(default)] pub instruction_sources: Vec, 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..267cf90307 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,12 @@ pub struct TurnStartParams { /// Override the working directory for this turn and subsequent turns. #[ts(optional = nullable)] pub cwd: Option, + /// Replace the thread's runtime workspace roots for this turn and + /// subsequent turns. Relative paths are resolved against the effective + /// cwd for the turn. + #[experimental("turn/start.runtimeWorkspaceRoots")] + #[ts(optional = nullable)] + pub runtime_workspace_roots: Option>, /// Override the approval policy for this turn and subsequent turns. #[experimental(nested)] #[ts(optional = nullable)] @@ -75,13 +80,11 @@ pub struct TurnStartParams { /// Override the sandbox policy for this turn and subsequent turns. #[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/README.md b/codex-rs/app-server/README.md index 788fc9e7c3..e9350c2572 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -130,10 +130,10 @@ Example with notification opt-out: ## API Overview -- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. For permissions, prefer experimental `permissions` profile selection; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. +- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective thread cwd. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. - `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. - `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`. -- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read response `permissionProfile` for the exact active runtime permissions and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. +- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots, `permissionProfile` for the exact active runtime permissions, and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. - `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. @@ -156,7 +156,7 @@ Example with notification opt-out: - `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. - `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted. - `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. -- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Prefer experimental `permissions` profile selection for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". +- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective turn cwd. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". - `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success. - `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. @@ -234,7 +234,9 @@ Start a fresh thread when you need a new Codex conversation. "approvalPolicy": "never", "sandbox": "workspaceWrite", // Prefer experimental profile selection: - // "permissions": { "type": "profile", "id": ":workspace" } + // "permissions": ":workspace" + // Experimental runtime roots for :workspace_roots materialization: + // "runtimeWorkspaceRoots": ["/Users/me/project", "/Users/me/openai"], // Do not send both "sandbox" and "permissions". "personality": "friendly", "serviceName": "my_app_server_client", // optional metrics tag (`service_name`) @@ -647,7 +649,9 @@ You can optionally specify config overrides on the new turn. If specified, these "networkAccess": true }, // Prefer experimental profile selection: - // "permissions": { "type": "profile", "id": ":workspace" } + // "permissions": ":workspace" + // Experimental runtime roots for :workspace_roots materialization: + // "runtimeWorkspaceRoots": ["/Users/me/project", "/Users/me/openai"], // Do not send both "sandboxPolicy" and "permissions". "model": "gpt-5.1-codex", "effort": "medium", diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index b4e5f9a04d..083d5c16d7 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -580,9 +580,9 @@ pub async fn run_main_with_transport_options( range: None, }); } - if let Some(warning) = - codex_core::config::system_bwrap_warning(config.permissions.permission_profile.get()) - { + if let Some(warning) = codex_core::config::system_bwrap_warning( + config.permissions.permission_profile_constraint().get(), + ) { config_warnings.push(ConfigWarningNotification { summary: warning, details: None, 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..a9625d3086 100644 --- a/codex-rs/app-server/src/message_processor_tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor_tracing_tests.rs @@ -659,6 +659,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { }], responsesapi_client_metadata: None, cwd: None, + runtime_workspace_roots: None, approval_policy: None, sandbox_policy: None, permissions: None, 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..537213a2cd 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,10 @@ 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_constraint() + .get(), /*policy_decider*/ None, /*blocked_request_observer*/ None, managed_network_requirements_enabled, @@ -243,7 +246,7 @@ 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 @@ -264,7 +267,7 @@ impl CommandExecRequestProcessor { ); self.config .permissions - .permission_profile + .permission_profile_constraint() .can_set(&permission_profile) .map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?; permission_profile 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..c64066ee93 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -604,6 +604,7 @@ pub(super) async fn handle_pending_thread_resume_request( permission_profile, active_permission_profile, cwd, + workspace_roots, reasoning_effort, .. } = pending.config_snapshot; @@ -620,6 +621,7 @@ pub(super) async fn handle_pending_thread_resume_request( model_provider: model_provider_id, service_tier, cwd, + runtime_workspace_roots: workspace_roots, instruction_sources, approval_policy: approval_policy.into(), approvals_reviewer: approvals_reviewer.into(), 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..73cf7dafa9 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -57,6 +57,25 @@ fn collect_resume_override_mismatches( )); } } + if let Some(requested_runtime_workspace_roots) = request.runtime_workspace_roots.as_ref() { + let base_cwd = request + .cwd + .as_deref() + .map(|cwd| { + AbsolutePathBuf::resolve_path_against_base(cwd, config_snapshot.cwd.as_path()) + }) + .unwrap_or_else(|| config_snapshot.cwd.clone()); + let requested_runtime_workspace_roots = requested_runtime_workspace_roots + .iter() + .map(|path| AbsolutePathBuf::resolve_path_against_base(path, base_cwd.as_path())) + .collect::>(); + if requested_runtime_workspace_roots != config_snapshot.workspace_roots { + mismatch_details.push(format!( + "runtime_workspace_roots requested={requested_runtime_workspace_roots:?} active={:?}", + config_snapshot.workspace_roots + )); + } + } if let Some(requested_approval) = request.approval_policy.as_ref() { let active_approval: AskForApproval = config_snapshot.approval_policy.into(); if requested_approval != &active_approval { @@ -802,6 +821,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + runtime_workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -835,6 +855,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + runtime_workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -1171,6 +1192,7 @@ impl ThreadRequestProcessor { model_provider: config_snapshot.model_provider_id, service_tier: config_snapshot.service_tier, cwd: config_snapshot.cwd, + runtime_workspace_roots: config_snapshot.workspace_roots, instruction_sources, approval_policy: config_snapshot.approval_policy.into(), approvals_reviewer: config_snapshot.approvals_reviewer.into(), @@ -1212,10 +1234,11 @@ impl ThreadRequestProcessor { model_provider: Option, service_tier: Option>, cwd: Option, + runtime_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 +1248,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd: cwd.map(PathBuf::from), + workspace_roots: runtime_workspace_roots, approval_policy: approval_policy .map(codex_app_server_protocol::AskForApproval::to_core), approvals_reviewer: approvals_reviewer @@ -2349,6 +2373,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + runtime_workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -2384,6 +2409,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + runtime_workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -2521,6 +2547,7 @@ impl ThreadRequestProcessor { model_provider: session_configured.model_provider_id, service_tier: session_configured.service_tier, cwd: session_configured.cwd, + runtime_workspace_roots: config_snapshot.workspace_roots, instruction_sources, approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), @@ -2985,6 +3012,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + runtime_workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -3050,6 +3078,7 @@ impl ThreadRequestProcessor { model_provider, service_tier, cwd, + runtime_workspace_roots, approval_policy, approvals_reviewer, sandbox, @@ -3179,6 +3208,7 @@ impl ThreadRequestProcessor { model_provider: session_configured.model_provider_id, service_tier: session_configured.service_tier, cwd: session_configured.cwd, + runtime_workspace_roots: config_snapshot.workspace_roots, instruction_sources, approval_policy: session_configured.approval_policy.into(), approvals_reviewer: session_configured.approvals_reviewer.into(), 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..bc5a6f181c 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, + runtime_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..f78b24614b 100644 --- a/codex-rs/app-server/src/request_processors/thread_summary.rs +++ b/codex-rs/app-server/src/request_processors/thread_summary.rs @@ -177,21 +177,12 @@ 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( 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..f78a3980c6 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -355,8 +355,16 @@ impl TurnRequestProcessor { .map(V2UserInput::into_core) .collect(); let turn_has_input = !mapped_items.is_empty(); + let runtime_workspace_roots_request = params.runtime_workspace_roots.clone(); + let snapshot = if params.permissions.is_some() || runtime_workspace_roots_request.is_some() + { + Some(thread.config_snapshot().await) + } else { + None + }; let has_any_overrides = params.cwd.is_some() + || runtime_workspace_roots_request.is_some() || params.approval_policy.is_some() || params.approvals_reviewer.is_some() || params.sandbox_policy.is_some() @@ -375,6 +383,29 @@ impl TurnRequestProcessor { } let cwd = params.cwd; + let runtime_workspace_roots = if let Some(workspace_roots) = + runtime_workspace_roots_request.clone() + { + let Some(snapshot) = snapshot.as_ref() else { + return Err(internal_error( + "turn/start runtime workspace roots missing thread snapshot", + )); + }; + let base_cwd = cwd + .as_ref() + .map(|cwd| AbsolutePathBuf::resolve_path_against_base(cwd, snapshot.cwd.as_path())) + .unwrap_or_else(|| snapshot.cwd.clone()); + Some( + workspace_roots + .into_iter() + .map(|path| { + AbsolutePathBuf::resolve_path_against_base(path, base_cwd.as_path()) + }) + .collect::>(), + ) + } else { + None + }; let approval_policy = params.approval_policy.map(AskForApproval::to_core); let approvals_reviewer = params .approvals_reviewer @@ -382,9 +413,22 @@ impl TurnRequestProcessor { let sandbox_policy = params.sandbox_policy.map(|p| p.to_core()); let (permission_profile, active_permission_profile) = if let Some(permissions) = params.permissions { - let snapshot = thread.config_snapshot().await; + let Some(snapshot) = snapshot.as_ref() else { + return Err(internal_error( + "turn/start permission selection missing thread snapshot", + )); + }; let mut overrides = ConfigOverrides { cwd: cwd.clone(), + workspace_roots: Some(runtime_workspace_roots_request.clone().unwrap_or_else( + || { + snapshot + .workspace_roots + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect() + }, + )), codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), ..Default::default() @@ -432,6 +476,7 @@ impl TurnRequestProcessor { thread .validate_turn_context_overrides(CodexThreadTurnContextOverrides { cwd: cwd.clone(), + workspace_roots: runtime_workspace_roots.clone(), approval_policy, approvals_reviewer, sandbox_policy: sandbox_policy.clone(), @@ -457,6 +502,7 @@ impl TurnRequestProcessor { final_output_json_schema: params.output_schema, responsesapi_client_metadata: params.responsesapi_client_metadata, cwd, + workspace_roots: runtime_workspace_roots, approval_policy, approvals_reviewer, sandbox_policy, 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..db982bc5d0 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, + runtime_workspace_roots: None, approval_policy: None, approvals_reviewer: None, sandbox: None, 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..07140beb99 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -184,6 +184,79 @@ async fn thread_resume_rejects_unmaterialized_thread() -> Result<()> { Ok(()) } +#[tokio::test] +async fn turn_start_updates_runtime_workspace_roots_for_loaded_thread() -> 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 extra_root_tmp = TempDir::new()?; + let extra_root = extra_root_tmp.path().join("extra-root"); + std::fs::create_dir_all(&extra_root)?; + + 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 { + model: Some("gpt-5.4".to_string()), + ..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: "Hello".to_string(), + text_elements: Vec::new(), + }], + runtime_workspace_roots: Some(vec![extra_root.clone()]), + ..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 resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: thread.id, + exclude_turns: true, + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), + ) + .await??; + let ThreadResumeResponse { + runtime_workspace_roots, + .. + } = to_response::(resume_resp)?; + + assert_eq!( + runtime_workspace_roots, + vec![AbsolutePathBuf::from_absolute_path(extra_root)?] + ); + + Ok(()) +} + #[tokio::test] async fn thread_goal_get_rejects_unmaterialized_thread() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 78155d8c9a..c69ddf9cb4 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -236,6 +236,48 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_start_resolves_runtime_workspace_roots_against_cwd() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; + + let cwd_tmp = TempDir::new()?; + let cwd = cwd_tmp.path().to_path_buf(); + let relative_root = PathBuf::from("extra-root"); + std::fs::create_dir_all(cwd.join(&relative_root))?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let req_id = mcp + .send_thread_start_request(ThreadStartParams { + cwd: Some(cwd.to_string_lossy().to_string()), + runtime_workspace_roots: Some(vec![relative_root.clone()]), + ..Default::default() + }) + .await?; + + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(req_id)), + ) + .await??; + let ThreadStartResponse { + cwd: response_cwd, + runtime_workspace_roots, + .. + } = to_response::(resp)?; + + assert_eq!(response_cwd, cwd.abs()); + assert_eq!( + runtime_workspace_roots, + vec![cwd_tmp.path().join(relative_root).abs()] + ); + + Ok(()) +} + #[tokio::test] async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; 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..8467bb8bbd 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; @@ -779,10 +778,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,6 +1886,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { }], responsesapi_client_metadata: None, cwd: Some(first_cwd.clone()), + runtime_workspace_roots: None, approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { @@ -1931,6 +1928,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { }], responsesapi_client_metadata: None, cwd: Some(second_cwd.clone()), + runtime_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/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index be5b0ccb88..91bd7580d7 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, @@ -82,6 +83,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, @@ -258,6 +260,7 @@ impl CodexThread { ) -> ConstraintResult<()> { let CodexThreadTurnContextOverrides { cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox_policy, @@ -283,6 +286,7 @@ impl CodexThread { let updates = SessionSettingsUpdate { cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox_policy, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index c86a703d45..da55652b41 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1875,6 +1875,11 @@ async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots( ) .await?; + assert_eq!( + config.effective_workspace_roots(), + vec![cwd.abs(), runtime_root.abs(), profile_root.abs()] + ); + let policy = config.permissions.file_system_sandbox_policy(); for root in [cwd.abs(), runtime_root.abs(), profile_root.abs()] { assert!( @@ -7379,6 +7384,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { permission_profile: Constrained::allow_any(PermissionProfile::read_only()), active_permission_profile: Some(ActivePermissionProfile::new(":read-only")), workspace_roots: vec![fixture.cwd()], + profile_workspace_roots: Vec::new(), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -7829,6 +7835,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { permission_profile: Constrained::allow_any(PermissionProfile::read_only()), active_permission_profile: Some(ActivePermissionProfile::new(":read-only")), workspace_roots: vec![fixture.cwd()], + profile_workspace_roots: Vec::new(), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -7993,6 +8000,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { permission_profile: Constrained::allow_any(PermissionProfile::read_only()), active_permission_profile: Some(ActivePermissionProfile::new(":read-only")), workspace_roots: vec![fixture.cwd()], + profile_workspace_roots: Vec::new(), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -8142,6 +8150,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { permission_profile: Constrained::allow_any(PermissionProfile::read_only()), active_permission_profile: Some(ActivePermissionProfile::new(":read-only")), workspace_roots: vec![fixture.cwd()], + profile_workspace_roots: Vec::new(), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 734cfee36a..4871273997 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -250,6 +250,11 @@ pub struct Permissions { /// Thread-scoped runtime workspace roots. Symbolic `:project_roots` /// entries in `permission_profile` are materialized against these roots. workspace_roots: Vec, + /// Immutable workspace roots configured directly on the selected profile. + /// These remain separate from thread-scoped runtime workspace roots so the + /// runtime can update only the latter without mutating the selected + /// profile. + profile_workspace_roots: Vec, /// Effective network configuration applied to all spawned processes. pub network: Option, /// Whether the model may request a login shell for shell-based tools. @@ -282,6 +287,7 @@ impl Permissions { permission_profile, active_permission_profile: None, workspace_roots: Vec::new(), + profile_workspace_roots: Vec::new(), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -304,6 +310,7 @@ impl Permissions { ) { self.permission_profile = permission_profile; self.active_permission_profile = None; + self.profile_workspace_roots.clear(); } /// Replace the full constrained profile value and preserve the active @@ -315,6 +322,20 @@ impl Permissions { ) { self.permission_profile = permission_profile; self.active_permission_profile = active_permission_profile; + self.profile_workspace_roots.clear(); + } + + /// Replace the full constrained profile value together with the named + /// profile sidecar and its immutable profile-defined workspace roots. + pub fn replace_permission_profile_constraint_with_active_profile_and_workspace_roots( + &mut self, + permission_profile: Constrained, + active_permission_profile: Option, + profile_workspace_roots: Vec, + ) { + self.permission_profile = permission_profile; + self.active_permission_profile = active_permission_profile; + self.profile_workspace_roots = profile_workspace_roots; } /// Record the active profile id only if it still describes the current @@ -343,6 +364,14 @@ impl Permissions { &self.workspace_roots } + pub fn set_profile_workspace_roots(&mut self, workspace_roots: Vec) { + self.profile_workspace_roots = workspace_roots; + } + + pub fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] { + &self.profile_workspace_roots + } + fn materialized_permission_profile(&self) -> PermissionProfile { self.permission_profile .get() @@ -441,6 +470,7 @@ impl Permissions { self.permission_profile.set(permission_profile)?; self.active_permission_profile = None; + self.profile_workspace_roots.clear(); Ok(()) } @@ -466,6 +496,7 @@ impl Permissions { self.permission_profile.set(permission_profile)?; self.active_permission_profile = active_permission_profile; + self.profile_workspace_roots.clear(); Ok(()) } } @@ -1197,6 +1228,13 @@ impl Config { Ok(()) } + pub fn effective_workspace_roots(&self) -> Vec { + let mut workspace_roots = self.workspace_roots.clone(); + workspace_roots.extend(self.permissions.profile_workspace_roots().iter().cloned()); + dedupe_absolute_paths(&mut workspace_roots); + workspace_roots + } + pub fn to_models_manager_config(&self) -> ModelsManagerConfig { ModelsManagerConfig { model_context_window: self.model_context_window, @@ -2485,6 +2523,7 @@ impl Config { permission_profile, file_system_sandbox_policy, mut active_permission_profile, + profile_workspace_roots, ) = if let Some(mut permission_profile) = permission_profile { let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); @@ -2537,6 +2576,7 @@ impl Config { permission_profile, file_system_sandbox_policy, None, + Vec::new(), ) } else if profiles_are_active { let default_permissions = default_permissions.unwrap_or_else(|| { @@ -2625,6 +2665,7 @@ impl Config { permission_profile, file_system_sandbox_policy, active_permission_profile, + configured_workspace_roots, ) } else { let configured_network_proxy_config = NetworkProxyConfig::default(); @@ -2683,6 +2724,7 @@ impl Config { permission_profile, file_system_sandbox_policy, None, + Vec::new(), ) }; if enable_network_proxy && permission_profile.network_sandbox_policy().is_enabled() { @@ -3176,6 +3218,7 @@ impl Config { permission_profile: constrained_permission_profile.value, active_permission_profile, workspace_roots, + profile_workspace_roots, network, allow_login_shell, shell_environment_policy, diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index d3fae0f2e5..763ce240c9 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -896,9 +896,11 @@ pub(crate) fn build_guardian_review_session_config( guardian_config.developer_instructions = None; guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); let sandbox_policy = SandboxPolicy::new_read_only_policy(); - guardian_config.permissions.permission_profile = Constrained::allow_only( - PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy), - ); + guardian_config + .permissions + .replace_permission_profile_constraint(Constrained::allow_only( + PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy), + )); guardian_config .permissions .set_legacy_sandbox_policy(sandbox_policy, guardian_config.cwd.as_path()) @@ -924,7 +926,10 @@ pub(crate) fn build_guardian_review_session_config( guardian_config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints( live_network_config, network_constraints, - guardian_config.permissions.permission_profile.get(), + guardian_config + .permissions + .permission_profile_constraint() + .get(), )?); } for feature in [ diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 7601ef44c3..c7d4dc61f4 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -2163,7 +2163,10 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() { }), ..Default::default() }), - parent_config.permissions.permission_profile.get(), + parent_config + .permissions + .permission_profile_constraint() + .get(), ) .expect("network proxy spec"); parent_config.permissions.network = Some(network.clone()); @@ -2190,7 +2193,10 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() { Constrained::allow_only(AskForApproval::Never) ); assert_eq!( - guardian_config.permissions.permission_profile, + guardian_config + .permissions + .permission_profile_constraint() + .clone(), Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_read_only_policy(), )) @@ -2230,7 +2236,10 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() { NetworkProxySpec::from_config_and_constraints( parent_network, /*requirements*/ None, - parent_config.permissions.permission_profile.get(), + parent_config + .permissions + .permission_profile_constraint() + .get(), ) .expect("parent network proxy spec"), ); diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 5cb37f9828..c2783b5782 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -146,6 +146,7 @@ pub(super) async fn user_input_or_turn_inner( approval_policy: Some(approval_policy), approvals_reviewer, sandbox_policy: Some(sandbox_policy), + workspace_roots: None, permission_profile, active_permission_profile: None, windows_sandbox_level: None, @@ -163,6 +164,7 @@ pub(super) async fn user_input_or_turn_inner( } Op::UserInputWithTurnContext { cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox_policy, @@ -195,6 +197,7 @@ pub(super) async fn user_input_or_turn_inner( items, SessionSettingsUpdate { cwd, + workspace_roots, approval_policy, approvals_reviewer, sandbox_policy, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index fd3f31771d..ac3127e3d8 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -615,10 +615,11 @@ impl Codex { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: environment_selections.to_selections(), diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 5b1b8c83e4..d5be0c34b8 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -75,6 +75,9 @@ pub(crate) struct SessionConfiguration { /// execution sandbox are resolved against this directory **instead** of /// the process-wide current working directory. pub(super) cwd: AbsolutePathBuf, + /// Thread-scoped runtime workspace roots for materializing symbolic + /// workspace permissions at session runtime. + pub(super) workspace_roots: Vec, /// Directory containing all Codex state for this session. pub(super) codex_home: AbsolutePathBuf, /// Optional user-facing name for the thread, updated during the session. @@ -104,7 +107,10 @@ impl SessionConfiguration { } pub(super) fn permission_profile(&self) -> PermissionProfile { - self.permission_profile.get().clone() + self.permission_profile + .get() + .clone() + .materialize_project_roots_with_workspace_roots(&self.workspace_roots) } pub(super) fn active_permission_profile(&self) -> Option { @@ -126,7 +132,7 @@ impl SessionConfiguration { } pub(super) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { - self.permission_profile.get().file_system_sandbox_policy() + self.permission_profile().file_system_sandbox_policy() } pub(super) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy { @@ -143,6 +149,7 @@ impl SessionConfiguration { permission_profile: self.permission_profile(), active_permission_profile: self.active_permission_profile(), cwd: self.cwd.clone(), + workspace_roots: self.workspace_roots.clone(), ephemeral: self.original_config_do_not_use.ephemeral, reasoning_effort: self.collaboration_mode.reasoning_effort(), personality: self.personality, @@ -222,6 +229,9 @@ impl SessionConfiguration { let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path(); next_configuration.cwd = absolute_cwd; + if let Some(workspace_roots) = updates.workspace_roots.clone() { + next_configuration.workspace_roots = workspace_roots; + } if let Some(permission_profile) = updates.permission_profile.clone() { let active_permission_profile = @@ -309,6 +319,7 @@ impl SessionConfiguration { #[derive(Default, Clone)] pub(crate) struct SessionSettingsUpdate { pub(crate) cwd: Option, + pub(crate) workspace_roots: Option>, pub(crate) approval_policy: Option, pub(crate) approvals_reviewer: Option, pub(crate) sandbox_policy: Option, @@ -769,7 +780,7 @@ impl Session { let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy( spec, current_exec_policy.as_ref(), - config.permissions.permission_profile.get(), + config.permissions.permission_profile_constraint().get(), network_policy_decider.as_ref().map(Arc::clone), blocked_request_observer.as_ref().map(Arc::clone), managed_network_requirements_configured, @@ -833,7 +844,7 @@ impl Session { // setup is straightforward enough and performs well. mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( &config.permissions.approval_policy, - &config.permissions.permission_profile, + config.permissions.permission_profile_constraint(), ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 36e4e866d9..330840f5db 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2063,8 +2063,10 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() -> }; let expected_sandbox_policy = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { - config.permissions.permission_profile = codex_config::Constrained::allow_any( - PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy), + config.permissions.replace_permission_profile_constraint( + codex_config::Constrained::allow_any(PermissionProfile::from_legacy_sandbox_policy( + &sandbox_policy, + )), ); config .set_legacy_sandbox_policy(sandbox_policy) @@ -2819,10 +2821,11 @@ async fn set_rate_limits_retains_previous_credits() { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: Vec::new(), @@ -2923,10 +2926,11 @@ async fn set_rate_limits_updates_plan_type_when_present() { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: Vec::new(), @@ -3396,10 +3400,11 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: Vec::new(), @@ -3929,10 +3934,11 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: Vec::new(), @@ -4038,10 +4044,11 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: default_environments, @@ -4085,7 +4092,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( &config.permissions.approval_policy, - &config.permissions.permission_profile, + config.permissions.permission_profile_constraint(), ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( @@ -4270,10 +4277,11 @@ async fn make_session_with_config_and_rx( compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: default_environments, @@ -4373,10 +4381,11 @@ async fn make_session_with_history_source_and_agent_control_and_rx( compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: default_environments, @@ -5026,6 +5035,7 @@ fn op_kind_distinguishes_turn_ops() { final_output_json_schema: None, responsesapi_client_metadata: None, cwd: None, + workspace_roots: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, @@ -5890,10 +5900,11 @@ where compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, - permission_profile: config.permissions.permission_profile.clone(), + permission_profile: config.permissions.permission_profile_constraint().clone(), active_permission_profile: config.permissions.active_permission_profile(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), + workspace_roots: config.workspace_roots.clone(), codex_home: config.codex_home.clone(), thread_name: None, environments: default_environments, @@ -5937,7 +5948,7 @@ where let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( &config.permissions.approval_policy, - &config.permissions.permission_profile, + config.permissions.permission_profile_constraint(), ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 8caec7aaea..a22d03b3a8 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -420,14 +420,27 @@ impl Session { let config = session_configuration.original_config_do_not_use.clone(); let mut per_turn_config = (*config).clone(); per_turn_config.cwd = cwd; + per_turn_config.workspace_roots = session_configuration.workspace_roots.clone(); + per_turn_config + .permissions + .set_workspace_roots(session_configuration.workspace_roots.clone()); per_turn_config.model_reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; per_turn_config.service_tier = session_configuration.service_tier.clone(); per_turn_config.personality = session_configuration.personality; per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer; - per_turn_config.permissions.permission_profile = - session_configuration.permission_profile.clone(); + let profile_workspace_roots = per_turn_config + .permissions + .profile_workspace_roots() + .to_vec(); + per_turn_config + .permissions + .replace_permission_profile_constraint_with_active_profile_and_workspace_roots( + session_configuration.permission_profile.clone(), + session_configuration.active_permission_profile.clone(), + profile_workspace_roots, + ); let permission_profile = session_configuration.permission_profile(); let resolved_web_search_mode = resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &permission_profile); @@ -454,8 +467,16 @@ impl Session { Self::build_per_turn_config(session_configuration, session_configuration.cwd.clone()); config.model = Some(session_configuration.collaboration_mode.model().to_string()); config.permissions.approval_policy = session_configuration.approval_policy.clone(); - config.permissions.active_permission_profile = - session_configuration.active_permission_profile.clone(); + config.workspace_roots = session_configuration.workspace_roots.clone(); + config + .permissions + .set_workspace_roots(session_configuration.workspace_roots.clone()); + config + .permissions + .set_active_permission_profile_for_current_profile( + session_configuration.active_permission_profile.clone(), + Some(session_configuration.permission_profile.get()), + ); config } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 1a58561737..ec0024a2a7 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -3776,7 +3776,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() { #[allow(deprecated)] let turn_cwd = turn.cwd.clone(); let sandbox_policy = pick_allowed_sandbox_policy( - &turn.config.permissions.permission_profile, + turn.config.permissions.permission_profile_constraint(), turn.config.legacy_sandbox_policy(), turn_cwd.as_path(), ); diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 76ed9cdcbf..b91f2f6857 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -906,9 +906,11 @@ allow_local_binding = true .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never); - config.permissions.permission_profile = Constrained::allow_any( - PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy_for_config), - ); + config + .permissions + .replace_permission_profile_constraint(Constrained::allow_any( + PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy_for_config), + )); }); let test = builder.build_remote_aware(server).await?; assert!( @@ -2752,10 +2754,13 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> { }, access: FileSystemAccessMode::None, }); - config.permissions.permission_profile = - Constrained::allow_any(PermissionProfile::from_runtime_permissions( - &file_system_sandbox_policy, - NetworkSandboxPolicy::Restricted, + config + .permissions + .replace_permission_profile_constraint(Constrained::allow_any( + PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + ), )); }); let TestCodex { diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 1285b9f925..aaa21412b3 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -344,11 +344,12 @@ async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Re let server = responses::start_mock_server().await; let mut builder = core_test_support::test_codex::test_codex().with_config(|config| { let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy(); - config.permissions.permission_profile = + config.permissions.replace_permission_profile_constraint( codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, NetworkSandboxPolicy::Restricted, - )); + )), + ); }); let test = builder.build(&server).await?; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 7825d163c9..75c66a7934 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -24,8 +24,6 @@ use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::McpServerElicitationAction; use codex_app_server_protocol::McpServerElicitationRequestResponse; -use codex_app_server_protocol::PermissionProfileModificationParams; -use codex_app_server_protocol::PermissionProfileSelectionParams; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ReviewStartParams; use codex_app_server_protocol::ReviewStartResponse; @@ -82,7 +80,6 @@ use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::config_types::SandboxMode; use codex_protocol::models::ActivePermissionProfile; -use codex_protocol::models::ActivePermissionProfileModification; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewRequest; @@ -413,6 +410,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result permission_profile: None, default_permissions: None, cwd: resolved_cwd, + workspace_roots: None, model_provider: model_provider.clone(), service_tier: None, codex_self_exe: arg0_paths.codex_self_exe.clone(), @@ -754,8 +752,9 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { // is using. event_processor.print_config_summary(&config, &prompt_summary, &session_configured); if !json_mode - && let Some(message) = - codex_core::config::system_bwrap_warning(config.permissions.permission_profile.get()) + && let Some(message) = codex_core::config::system_bwrap_warning( + config.permissions.permission_profile_constraint().get(), + ) { event_processor.process_warning(message); } @@ -785,6 +784,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { responsesapi_client_metadata: None, environments: None, cwd: Some(default_cwd), + runtime_workspace_roots: None, approval_policy: Some(default_approval_policy.into()), approvals_reviewer: None, sandbox_policy: None, @@ -956,6 +956,13 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { model: config.model.clone(), model_provider: Some(config.model_provider_id.clone()), cwd: Some(config.cwd.to_string_lossy().to_string()), + runtime_workspace_roots: Some( + config + .workspace_roots + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect(), + ), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox: sandbox.flatten(), @@ -979,6 +986,13 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa model: config.model.clone(), model_provider: Some(config.model_provider_id.clone()), cwd: Some(config.cwd.to_string_lossy().to_string()), + runtime_workspace_roots: Some( + config + .workspace_roots + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect(), + ), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox: sandbox.flatten(), @@ -988,29 +1002,15 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa } } -fn permissions_selection_from_config(config: &Config) -> Option { +fn permissions_selection_from_config(config: &Config) -> Option { config .permissions .active_permission_profile() .map(permissions_selection_from_active_profile) } -fn permissions_selection_from_active_profile( - active: ActivePermissionProfile, -) -> PermissionProfileSelectionParams { - let modifications = active - .modifications - .into_iter() - .map(|modification| match modification { - ActivePermissionProfileModification::AdditionalWritableRoot { path } => { - PermissionProfileModificationParams::AdditionalWritableRoot { path } - } - }) - .collect::>(); - PermissionProfileSelectionParams::Profile { - id: active.id, - modifications: (!modifications.is_empty()).then_some(modifications), - } +fn permissions_selection_from_active_profile(active: ActivePermissionProfile) -> String { + active.id } fn sandbox_mode_from_permission_profile( diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index c12b483e89..df1b7c9ee5 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -558,6 +558,7 @@ fn sample_thread_start_response() -> ThreadStartResponse { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp").abs(), + runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: codex_app_server_protocol::AskForApproval::OnRequest, approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::AutoReview, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 91fd02d858..976f59ee43 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -462,6 +462,11 @@ pub enum Op { #[serde(skip_serializing_if = "Option::is_none")] cwd: Option, + /// Updated runtime workspace roots used to materialize symbolic + /// `:project_roots` / `:workspace_roots` filesystem permissions. + #[serde(skip_serializing_if = "Option::is_none")] + workspace_roots: Option>, + /// Updated command approval policy. #[serde(skip_serializing_if = "Option::is_none")] approval_policy: Option, diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index d20ffb8218..7f58ca3733 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -658,6 +658,7 @@ mod tests { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: next_cwd.clone().abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, diff --git a/codex-rs/tui/src/app/startup_prompts.rs b/codex-rs/tui/src/app/startup_prompts.rs index 41972e6751..3449e04382 100644 --- a/codex-rs/tui/src/app/startup_prompts.rs +++ b/codex-rs/tui/src/app/startup_prompts.rs @@ -67,7 +67,7 @@ pub(super) fn emit_project_config_warnings(app_event_tx: &AppEventSender, config pub(super) fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &Config) { let Some(message) = crate::legacy_core::config::system_bwrap_warning( - config.permissions.permission_profile.get(), + config.permissions.permission_profile_constraint().get(), ) else { return; }; diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index ec20fffea3..87ac1e3f82 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3857,6 +3857,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/tmp/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::High), message_history: None, @@ -4103,6 +4104,7 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: cwd.abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, @@ -4678,6 +4680,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, @@ -4741,6 +4744,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, @@ -4833,6 +4837,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, @@ -5068,6 +5073,7 @@ async fn refreshed_snapshot_session_persists_resumed_turns() { )]; let resumed_session = ThreadSessionState { cwd: test_path_buf("/tmp/refreshed").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), ..initial_session.clone() }; @@ -5232,6 +5238,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, @@ -5353,6 +5360,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/tmp/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index 431bf5f804..30f68dc640 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -352,6 +352,7 @@ mod tests { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: cwd.abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 4f5f48cfc8..e89bdda7ac 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -71,6 +71,7 @@ impl App { permission_profile: permission_profile.clone(), active_permission_profile: active_permission_profile.clone(), cwd: thread.cwd.clone(), + runtime_workspace_roots: self.config.workspace_roots.clone(), instruction_source_paths: Vec::new(), reasoning_effort: self.chat_widget.current_reasoning_effort(), message_history: None, @@ -146,6 +147,7 @@ mod tests { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: cwd.abs(), + runtime_workspace_roots: vec![cwd.abs()], instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index ba228c4d57..da707f6832 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -34,8 +34,6 @@ use codex_app_server_protocol::MemoryResetResponse; use codex_app_server_protocol::Model as ApiModel; 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::RateLimitSnapshot; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ReviewDelivery; @@ -108,7 +106,6 @@ use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; use codex_protocol::approvals::GuardianAssessmentEvent; use codex_protocol::models::ActivePermissionProfile; -use codex_protocol::models::ActivePermissionProfileModification; use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelAvailabilityNux; @@ -576,6 +573,7 @@ impl AppServerSession { responsesapi_client_metadata: None, environments: None, cwd: Some(cwd), + runtime_workspace_roots: None, approval_policy: Some(approval_policy), approvals_reviewer: Some(approvals_reviewer.into()), sandbox_policy, @@ -1172,22 +1170,8 @@ fn sandbox_mode_from_permission_profile( } } -fn permissions_selection_from_active_profile( - active: ActivePermissionProfile, -) -> PermissionProfileSelectionParams { - let modifications = active - .modifications - .into_iter() - .map(|modification| match modification { - ActivePermissionProfileModification::AdditionalWritableRoot { path } => { - PermissionProfileModificationParams::AdditionalWritableRoot { path } - } - }) - .collect::>(); - PermissionProfileSelectionParams::Profile { - id: active.id, - modifications: (!modifications.is_empty()).then_some(modifications), - } +fn permissions_selection_from_active_profile(active: ActivePermissionProfile) -> String { + active.id } fn turn_permissions_overrides( @@ -1197,7 +1181,7 @@ fn turn_permissions_overrides( thread_params_mode: ThreadParamsMode, ) -> ( Option, - Option, + Option, ) { let permissions = if matches!(thread_params_mode, ThreadParamsMode::Embedded) { active_permission_profile.map(permissions_selection_from_active_profile) @@ -1221,7 +1205,7 @@ fn turn_permissions_overrides( fn permissions_selection_from_config( config: &Config, thread_params_mode: ThreadParamsMode, -) -> Option { +) -> Option { if matches!(thread_params_mode, ThreadParamsMode::Remote) { return None; } @@ -1253,6 +1237,13 @@ fn thread_start_params_from_config( model_provider: thread_params_mode.model_provider_from_config(config), service_tier: service_tier_override_from_config(config), cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override), + runtime_workspace_roots: Some( + config + .workspace_roots + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect(), + ), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox, @@ -1288,6 +1279,13 @@ fn thread_resume_params_from_config( model_provider: thread_params_mode.model_provider_from_config(&config), service_tier: service_tier_override_from_config(&config), cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override), + runtime_workspace_roots: Some( + config + .workspace_roots + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect(), + ), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(&config), sandbox, @@ -1320,6 +1318,13 @@ fn thread_fork_params_from_config( model_provider: thread_params_mode.model_provider_from_config(&config), service_tier: service_tier_override_from_config(&config), cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override), + runtime_workspace_roots: Some( + config + .workspace_roots + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect(), + ), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(&config), sandbox, @@ -1417,6 +1422,7 @@ async fn thread_session_state_from_thread_start_response( permission_profile, response.active_permission_profile.clone().map(Into::into), response.cwd.clone(), + response.runtime_workspace_roots.clone(), response.instruction_sources.clone(), response.reasoning_effort, config, @@ -1449,6 +1455,7 @@ async fn thread_session_state_from_thread_resume_response( permission_profile, response.active_permission_profile.clone().map(Into::into), response.cwd.clone(), + response.runtime_workspace_roots.clone(), response.instruction_sources.clone(), response.reasoning_effort, config, @@ -1481,6 +1488,7 @@ async fn thread_session_state_from_thread_fork_response( permission_profile, response.active_permission_profile.clone().map(Into::into), response.cwd.clone(), + response.runtime_workspace_roots.clone(), response.instruction_sources.clone(), response.reasoning_effort, config, @@ -1523,6 +1531,7 @@ async fn thread_session_state_from_thread_response( permission_profile: PermissionProfile, active_permission_profile: Option, cwd: AbsolutePathBuf, + runtime_workspace_roots: Vec, instruction_source_paths: Vec, reasoning_effort: Option, config: &Config, @@ -1537,6 +1546,11 @@ async fn thread_session_state_from_thread_response( let history_config = codex_message_history::HistoryConfig::new(config.codex_home.clone(), &config.history); let (log_id, entry_count) = codex_message_history::history_metadata(&history_config).await; + let runtime_workspace_roots = if runtime_workspace_roots.is_empty() { + vec![cwd.clone()] + } else { + runtime_workspace_roots + }; Ok(ThreadSessionState { thread_id, forked_from_id, @@ -1550,6 +1564,7 @@ async fn thread_session_state_from_thread_response( permission_profile, active_permission_profile, cwd, + runtime_workspace_roots, instruction_source_paths, reasoning_effort, message_history: Some(MessageHistoryMetadata { @@ -1627,6 +1642,16 @@ mod tests { ); assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string())); + assert_eq!( + params.runtime_workspace_roots, + Some( + config + .workspace_roots + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect() + ) + ); assert_eq!(params.sandbox, None); assert_eq!( params.permissions, @@ -1721,6 +1746,13 @@ mod tests { &config.permissions.permission_profile(), config.cwd.as_path(), ); + let expected_runtime_workspace_roots = Some( + config + .workspace_roots + .iter() + .map(AbsolutePathBuf::to_path_buf) + .collect::>(), + ); let start = thread_start_params_from_config( &config, @@ -1744,6 +1776,18 @@ mod tests { assert_eq!(start.cwd, None); assert_eq!(resume.cwd, None); assert_eq!(fork.cwd, None); + assert_eq!( + start.runtime_workspace_roots, + expected_runtime_workspace_roots + ); + assert_eq!( + resume.runtime_workspace_roots, + expected_runtime_workspace_roots + ); + assert_eq!( + fork.runtime_workspace_roots, + expected_runtime_workspace_roots + ); assert_eq!(start.model_provider, None); assert_eq!(resume.model_provider, None); assert_eq!(fork.model_provider, None); @@ -2014,6 +2058,10 @@ mod tests { model_provider: "openai".to_string(), service_tier: None, cwd: test_path_buf("/tmp/project").abs(), + runtime_workspace_roots: vec![ + test_path_buf("/tmp/project").abs(), + test_path_buf("/tmp/project/extra").abs(), + ], instruction_sources: vec![test_path_buf("/tmp/project/AGENTS.md").abs()], approval_policy: codex_app_server_protocol::AskForApproval::Never, approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User, @@ -2034,6 +2082,10 @@ mod tests { .await .expect("resume response should map"); assert_eq!(started.session.forked_from_id, Some(forked_from_id)); + assert_eq!( + started.session.runtime_workspace_roots, + response.runtime_workspace_roots + ); assert_eq!( started.session.instruction_source_paths, response.instruction_sources @@ -2137,6 +2189,7 @@ mod tests { /*active_permission_profile*/ None, test_path_buf("/tmp/project").abs(), Vec::new(), + Vec::new(), /*reasoning_effort*/ None, &config, ) @@ -2171,6 +2224,7 @@ mod tests { /*active_permission_profile*/ None, test_path_buf("/tmp/project").abs(), Vec::new(), + Vec::new(), /*reasoning_effort*/ None, &config, ) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index f8bb6cd778..7e9ce784ba 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -946,6 +946,15 @@ impl ChatWidget { self.current_rollout_path = session.rollout_path.clone(); self.current_cwd = Some(session.cwd.to_path_buf()); self.config.cwd = session.cwd.clone(); + let runtime_workspace_roots = if session.runtime_workspace_roots.is_empty() { + vec![session.cwd.clone()] + } else { + session.runtime_workspace_roots.clone() + }; + self.config.workspace_roots = runtime_workspace_roots.clone(); + self.config + .permissions + .set_workspace_roots(runtime_workspace_roots); self.effective_service_tier = session.service_tier.clone(); if let Err(err) = self .config @@ -966,10 +975,12 @@ impl ChatWidget { ); if let Err(err) = permission_sync { tracing::warn!(%err, "failed to sync permissions from SessionConfigured"); - self.config.permissions.permission_profile = - Constrained::allow_only(session.permission_profile.clone()); - self.config.permissions.active_permission_profile = - session.active_permission_profile.clone(); + self.config + .permissions + .replace_permission_profile_constraint_with_active_profile( + Constrained::allow_only(session.permission_profile.clone()), + session.active_permission_profile.clone(), + ); } self.config.approvals_reviewer = session.approvals_reviewer; self.status_line_project_root_name_cache = None; diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index 95e8317e9c..96477c8d38 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -28,6 +28,7 @@ async fn submission_preserves_text_elements_and_local_images() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -131,6 +132,7 @@ async fn submission_includes_configured_permission_profile() { permission_profile: expected_permission_profile.clone(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -180,6 +182,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() { permission_profile: expected_permission_profile.clone(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -221,6 +224,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -314,6 +318,7 @@ async fn enter_with_only_remote_images_submits_user_turn() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -377,6 +382,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -415,6 +421,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -453,6 +460,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -494,6 +502,7 @@ async fn submission_prefers_selected_duplicate_skill_path() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 2655c8cf95..d54783615c 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -957,6 +957,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index d77823c3cd..7620ba2e66 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -29,6 +29,7 @@ async fn resumed_initial_messages_render_history() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -99,6 +100,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -167,6 +169,7 @@ async fn replayed_user_message_preserves_remote_image_urls() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -266,6 +269,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { permission_profile: expected_permission_profile, active_permission_profile: None, cwd: expected_cwd.clone(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -322,6 +326,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { permission_profile: expected_permission_profile, active_permission_profile: None, cwd: test_path_buf("/home/user/external").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -360,6 +365,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -414,6 +420,7 @@ async fn replayed_user_message_with_only_local_images_renders_history_cell() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -684,6 +691,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_project_path().abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, @@ -729,6 +737,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_project_path().abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index df3615c0fd..4c66041271 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -584,6 +584,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure permission_profile: PermissionProfile::workspace_write(), active_permission_profile: None, cwd: test_project_path().abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, @@ -631,6 +632,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w permission_profile, active_permission_profile: None, cwd, + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index 9b8540e5aa..62968faa47 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -1217,6 +1217,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, @@ -1460,6 +1461,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 53b60e0d8d..acd6b7111b 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -2230,6 +2230,7 @@ async fn session_configured_clears_goal_status_footer() { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: Some(ReasoningEffortConfig::default()), message_history: None, diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 4521fe9a7b..4b826d9108 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -3959,6 +3959,7 @@ mod tests { permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/tmp/project").abs(), + runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, message_history: None, diff --git a/codex-rs/tui/src/session_state.rs b/codex-rs/tui/src/session_state.rs index e4d2dbab96..a672b47d28 100644 --- a/codex-rs/tui/src/session_state.rs +++ b/codex-rs/tui/src/session_state.rs @@ -42,6 +42,7 @@ pub(crate) struct ThreadSessionState { /// when the server knows it. pub(crate) active_permission_profile: Option, pub(crate) cwd: AbsolutePathBuf, + pub(crate) runtime_workspace_roots: Vec, pub(crate) instruction_source_paths: Vec, pub(crate) reasoning_effort: Option, pub(crate) message_history: Option,