From 93154c3378b6c4cd168ddb1e779e8ea25f85b6e9 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Mon, 6 Apr 2026 15:29:25 -0300 Subject: [PATCH] fix(tui): forward cwd changes through app-server Forward standalone turn-context overrides from the TUI to loaded app-server threads via `thread/turnContext/update`, so `/cwd` updates the core thread state instead of only local UI state. Add protocol schemas and regression coverage showing thread-scoped shell commands use the updated cwd after the context override. --- .../schema/json/ClientRequest.json | 140 ++++++ .../codex_app_server_protocol.schemas.json | 147 ++++++ .../codex_app_server_protocol.v2.schemas.json | 147 ++++++ .../v2/ThreadTurnContextUpdateParams.json | 454 ++++++++++++++++++ .../v2/ThreadTurnContextUpdateResponse.json | 5 + .../schema/typescript/ClientRequest.ts | 3 +- .../v2/ThreadTurnContextUpdateParams.ts | 48 ++ .../v2/ThreadTurnContextUpdateResponse.ts | 5 + .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 5 + .../app-server-protocol/src/protocol/v2.rs | 63 +++ codex-rs/app-server/README.md | 1 + .../app-server/src/codex_message_processor.rs | 75 +++ .../app-server/tests/common/mcp_process.rs | 10 + .../tests/suite/v2/thread_shell_command.rs | 96 ++++ codex-rs/tui/src/app.rs | 31 +- codex-rs/tui/src/app_server_session.rs | 41 ++ 17 files changed, 1271 insertions(+), 2 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateResponse.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 7c94419844..1ccf0d55d9 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -3106,6 +3106,122 @@ }, "type": "object" }, + "ThreadTurnContextUpdateParams": { + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the approval policy for subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on subsequent turns." + }, + "cwd": { + "description": "Override the working directory for subsequent turns and thread-scoped operations.", + "type": [ + "string", + "null" + ] + }, + "effort": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ], + "description": "Override the reasoning effort for subsequent turns." + }, + "model": { + "description": "Override the model for subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ], + "description": "Override the personality for subsequent turns." + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ], + "description": "Override the sandbox policy for subsequent turns." + }, + "serviceTier": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ServiceTier" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ], + "description": "Override the service tier for subsequent turns." + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ], + "description": "Override the reasoning summary for subsequent turns." + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "ThreadUnarchiveParams": { "properties": { "threadId": { @@ -3605,6 +3721,30 @@ "title": "Thread/name/setRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/turnContext/update" + ], + "title": "Thread/turnContext/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadTurnContextUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/turnContext/updateRequest", + "type": "object" + }, { "properties": { "id": { 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 a589903032..1fdb214066 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 @@ -386,6 +386,30 @@ "title": "Thread/name/setRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "thread/turnContext/update" + ], + "title": "Thread/turnContext/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadTurnContextUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/turnContext/updateRequest", + "type": "object" + }, { "properties": { "id": { @@ -14165,6 +14189,129 @@ "title": "ThreadTokenUsageUpdatedNotification", "type": "object" }, + "ThreadTurnContextUpdateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the approval policy for subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on subsequent turns." + }, + "cwd": { + "description": "Override the working directory for subsequent turns and thread-scoped operations.", + "type": [ + "string", + "null" + ] + }, + "effort": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ], + "description": "Override the reasoning effort for subsequent turns." + }, + "model": { + "description": "Override the model for subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/v2/Personality" + }, + { + "type": "null" + } + ], + "description": "Override the personality for subsequent turns." + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SandboxPolicy" + }, + { + "type": "null" + } + ], + "description": "Override the sandbox policy for subsequent turns." + }, + "serviceTier": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/v2/ServiceTier" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ], + "description": "Override the service tier for subsequent turns." + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningSummary" + }, + { + "type": "null" + } + ], + "description": "Override the reasoning summary for subsequent turns." + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadTurnContextUpdateParams", + "type": "object" + }, + "ThreadTurnContextUpdateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadTurnContextUpdateResponse", + "type": "object" + }, "ThreadUnarchiveParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { 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 f041f8aae8..f1c10868f4 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 @@ -961,6 +961,30 @@ "title": "Thread/name/setRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/turnContext/update" + ], + "title": "Thread/turnContext/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadTurnContextUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/turnContext/updateRequest", + "type": "object" + }, { "properties": { "id": { @@ -12020,6 +12044,129 @@ "title": "ThreadTokenUsageUpdatedNotification", "type": "object" }, + "ThreadTurnContextUpdateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the approval policy for subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on subsequent turns." + }, + "cwd": { + "description": "Override the working directory for subsequent turns and thread-scoped operations.", + "type": [ + "string", + "null" + ] + }, + "effort": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ], + "description": "Override the reasoning effort for subsequent turns." + }, + "model": { + "description": "Override the model for subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ], + "description": "Override the personality for subsequent turns." + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ], + "description": "Override the sandbox policy for subsequent turns." + }, + "serviceTier": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ServiceTier" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ], + "description": "Override the service tier for subsequent turns." + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ], + "description": "Override the reasoning summary for subsequent turns." + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadTurnContextUpdateParams", + "type": "object" + }, + "ThreadTurnContextUpdateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadTurnContextUpdateResponse", + "type": "object" + }, "ThreadUnarchiveParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateParams.json new file mode 100644 index 0000000000..8a3347c6ac --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateParams.json @@ -0,0 +1,454 @@ +{ + "$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`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", + "enum": [ + "user", + "guardian_subagent" + ], + "type": "string" + }, + "AskForApproval": { + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "granular": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "granular" + ], + "title": "GranularAskForApproval", + "type": "object" + } + ] + }, + "CollaborationMode": { + "description": "Collaboration mode for a Codex session.", + "properties": { + "mode": { + "$ref": "#/definitions/ModeKind" + }, + "settings": { + "$ref": "#/definitions/Settings" + } + }, + "required": [ + "mode", + "settings" + ], + "type": "object" + }, + "ModeKind": { + "description": "Initial collaboration mode to use when the TUI starts.", + "enum": [ + "plan", + "default" + ], + "type": "string" + }, + "NetworkAccess": { + "enum": [ + "restricted", + "enabled" + ], + "type": "string" + }, + "Personality": { + "enum": [ + "none", + "friendly", + "pragmatic" + ], + "type": "string" + }, + "ReadOnlyAccess": { + "oneOf": [ + { + "properties": { + "includePlatformDefaults": { + "default": true, + "type": "boolean" + }, + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "fullAccess" + ], + "title": "FullAccessReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullAccessReadOnlyAccess", + "type": "object" + } + ] + }, + "ReasoningEffort": { + "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "type": "string" + }, + "ReasoningSummary": { + "description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries", + "oneOf": [ + { + "enum": [ + "auto", + "concise", + "detailed" + ], + "type": "string" + }, + { + "description": "Option to disable reasoning summaries.", + "enum": [ + "none" + ], + "type": "string" + } + ] + }, + "SandboxPolicy": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "dangerFullAccess" + ], + "title": "DangerFullAccessSandboxPolicyType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DangerFullAccessSandboxPolicy", + "type": "object" + }, + { + "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "default": { + "type": "fullAccess" + } + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": [ + "readOnly" + ], + "title": "ReadOnlySandboxPolicyType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ReadOnlySandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "allOf": [ + { + "$ref": "#/definitions/NetworkAccess" + } + ], + "default": "restricted" + }, + "type": { + "enum": [ + "externalSandbox" + ], + "title": "ExternalSandboxSandboxPolicyType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ExternalSandboxSandboxPolicy", + "type": "object" + }, + { + "properties": { + "excludeSlashTmp": { + "default": false, + "type": "boolean" + }, + "excludeTmpdirEnvVar": { + "default": false, + "type": "boolean" + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, + "readOnlyAccess": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "default": { + "type": "fullAccess" + } + }, + "type": { + "enum": [ + "workspaceWrite" + ], + "title": "WorkspaceWriteSandboxPolicyType", + "type": "string" + }, + "writableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "type" + ], + "title": "WorkspaceWriteSandboxPolicy", + "type": "object" + } + ] + }, + "ServiceTier": { + "enum": [ + "fast", + "flex" + ], + "type": "string" + }, + "Settings": { + "description": "Settings for a collaboration mode.", + "properties": { + "developer_instructions": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "reasoning_effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "model" + ], + "type": "object" + } + }, + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the approval policy for subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on subsequent turns." + }, + "cwd": { + "description": "Override the working directory for subsequent turns and thread-scoped operations.", + "type": [ + "string", + "null" + ] + }, + "effort": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ], + "description": "Override the reasoning effort for subsequent turns." + }, + "model": { + "description": "Override the model for subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ], + "description": "Override the personality for subsequent turns." + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ], + "description": "Override the sandbox policy for subsequent turns." + }, + "serviceTier": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ServiceTier" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ], + "description": "Override the service tier for subsequent turns." + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ], + "description": "Override the reasoning summary for subsequent turns." + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadTurnContextUpdateParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateResponse.json new file mode 100644 index 0000000000..2c713c94f1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadTurnContextUpdateResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index e33a986359..574a573f87 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -54,6 +54,7 @@ import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams"; import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams"; import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams"; import type { ThreadStartParams } from "./v2/ThreadStartParams"; +import type { ThreadTurnContextUpdateParams } from "./v2/ThreadTurnContextUpdateParams"; import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams"; import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams"; import type { TurnInterruptParams } from "./v2/TurnInterruptParams"; @@ -64,4 +65,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/turnContext/update", id: RequestId, params: ThreadTurnContextUpdateParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateParams.ts new file mode 100644 index 0000000000..24f7cc308d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateParams.ts @@ -0,0 +1,48 @@ +// 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 { CollaborationMode } from "../CollaborationMode"; +import type { Personality } from "../Personality"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ReasoningSummary } from "../ReasoningSummary"; +import type { ServiceTier } from "../ServiceTier"; +import type { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; + +export type ThreadTurnContextUpdateParams = {threadId: string, /** + * Override the working directory for subsequent turns and thread-scoped + * operations. + */ +cwd?: string | null, /** + * Override the approval policy for subsequent turns. + */ +approvalPolicy?: AskForApproval | null, /** + * Override where approval requests are routed for review on subsequent + * turns. + */ +approvalsReviewer?: ApprovalsReviewer | null, /** + * Override the sandbox policy for subsequent turns. + */ +sandboxPolicy?: SandboxPolicy | null, /** + * Override the model for subsequent turns. + */ +model?: string | null, /** + * Override the reasoning effort for subsequent turns. + */ +effort?: ReasoningEffort | null | null, /** + * Override the reasoning summary for subsequent turns. + */ +summary?: ReasoningSummary | null, /** + * Override the service tier for subsequent turns. + */ +serviceTier?: ServiceTier | null | null, /** + * EXPERIMENTAL - Set a pre-set collaboration mode for subsequent turns. + * + * Takes precedence over model, reasoning effort, and developer + * instructions if set. + */ +collaborationMode?: CollaborationMode | null, /** + * Override the personality for subsequent turns. + */ +personality?: Personality | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateResponse.ts new file mode 100644 index 0000000000..f68ca13bfe --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateResponse.ts @@ -0,0 +1,5 @@ +// 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. + +export type ThreadTurnContextUpdateResponse = Record; 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 495572fd78..95a6c0cba6 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -310,6 +310,8 @@ export type { ThreadStatus } from "./ThreadStatus"; export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification"; export type { ThreadTokenUsage } from "./ThreadTokenUsage"; export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification"; +export type { ThreadTurnContextUpdateParams } from "./ThreadTurnContextUpdateParams"; +export type { ThreadTurnContextUpdateResponse } from "./ThreadTurnContextUpdateResponse"; export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams"; export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse"; export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index ebd1ba11c5..4d8bb938e9 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -280,6 +280,11 @@ client_request_definitions! { params: v2::ThreadSetNameParams, response: v2::ThreadSetNameResponse, }, + ThreadTurnContextUpdate => "thread/turnContext/update" { + params: v2::ThreadTurnContextUpdateParams, + inspect_params: true, + response: v2::ThreadTurnContextUpdateResponse, + }, ThreadMetadataUpdate => "thread/metadata/update" { params: v2::ThreadMetadataUpdateParams, response: v2::ThreadMetadataUpdateResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 82b568515c..50835ad41d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2891,6 +2891,69 @@ pub struct ThreadUnarchiveParams { #[ts(export_to = "v2/")] pub struct ThreadSetNameResponse {} +#[derive( + Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadTurnContextUpdateParams { + pub thread_id: String, + /// Override the working directory for subsequent turns and thread-scoped + /// operations. + #[ts(optional = nullable)] + pub cwd: Option, + /// Override the approval policy for subsequent turns. + #[experimental(nested)] + #[ts(optional = nullable)] + pub approval_policy: Option, + /// Override where approval requests are routed for review on subsequent + /// turns. + #[ts(optional = nullable)] + pub approvals_reviewer: Option, + /// Override the sandbox policy for subsequent turns. + #[ts(optional = nullable)] + pub sandbox_policy: Option, + /// Override the model for subsequent turns. + #[ts(optional = nullable)] + pub model: Option, + /// Override the reasoning effort for subsequent turns. + #[serde( + default, + deserialize_with = "super::serde_helpers::deserialize_double_option", + serialize_with = "super::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub effort: Option>, + /// Override the reasoning summary for subsequent turns. + #[ts(optional = nullable)] + pub summary: Option, + /// Override the service tier for subsequent turns. + #[serde( + default, + deserialize_with = "super::serde_helpers::deserialize_double_option", + serialize_with = "super::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub service_tier: Option>, + /// EXPERIMENTAL - Set a pre-set collaboration mode for subsequent turns. + /// + /// Takes precedence over model, reasoning effort, and developer + /// instructions if set. + #[experimental("thread/turnContext/update.collaborationMode")] + #[ts(optional = nullable)] + pub collaboration_mode: Option, + /// Override the personality for subsequent turns. + #[ts(optional = nullable)] + pub personality: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadTurnContextUpdateResponse {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c24c3fd7e8..33a8f0ae83 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -143,6 +143,7 @@ Example with notification opt-out: - `thread/archive` — move a thread’s rollout file into the archived directory; returns `{}` on success and emits `thread/archived`. - `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server shuts down and unloads the thread, then emits `thread/closed`. - `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success and emits `thread/name/updated` to initialized, opted-in clients. Thread names are not required to be unique; name lookups resolve to the most recently updated thread. +- `thread/turnContext/update` — update a loaded thread’s defaults for subsequent turns and thread-scoped operations, such as `cwd`, approval policy, approvals reviewer, sandbox policy, model, reasoning effort, service tier, collaboration mode, or personality; returns `{}` on success. - `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`. - `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. - `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. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ff1dddf2fe..26c7e935a2 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -158,6 +158,8 @@ use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStartedNotification; use codex_app_server_protocol::ThreadStatus; +use codex_app_server_protocol::ThreadTurnContextUpdateParams; +use codex_app_server_protocol::ThreadTurnContextUpdateResponse; use codex_app_server_protocol::ThreadUnarchiveParams; use codex_app_server_protocol::ThreadUnarchiveResponse; use codex_app_server_protocol::ThreadUnarchivedNotification; @@ -739,6 +741,10 @@ impl CodexMessageProcessor { self.thread_set_name(to_connection_request_id(request_id), params) .await; } + ClientRequest::ThreadTurnContextUpdate { request_id, params } => { + self.thread_turn_context_update(to_connection_request_id(request_id), params) + .await; + } ClientRequest::ThreadMetadataUpdate { request_id, params } => { self.thread_metadata_update(to_connection_request_id(request_id), params) .await; @@ -2670,6 +2676,75 @@ impl CodexMessageProcessor { .await; } + async fn thread_turn_context_update( + &self, + request_id: ConnectionRequestId, + params: ThreadTurnContextUpdateParams, + ) { + let ThreadTurnContextUpdateParams { + thread_id, + cwd, + approval_policy, + approvals_reviewer, + sandbox_policy, + model, + effort, + summary, + service_tier, + collaboration_mode, + personality, + } = params; + let (_, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + let collaboration_modes_config = CollaborationModesConfig { + default_mode_request_user_input: thread.enabled(Feature::DefaultModeRequestUserInput), + }; + let collaboration_mode = collaboration_mode.map(|mode| { + self.normalize_turn_start_collaboration_mode(mode, collaboration_modes_config) + }); + + match self + .submit_core_op( + &request_id, + thread.as_ref(), + Op::OverrideTurnContext { + cwd, + approval_policy: approval_policy.map(AskForApproval::to_core), + approvals_reviewer: approvals_reviewer + .map(codex_app_server_protocol::ApprovalsReviewer::to_core), + sandbox_policy: sandbox_policy.map(|p| p.to_core()), + windows_sandbox_level: None, + model, + effort, + summary, + service_tier, + collaboration_mode, + personality, + }, + ) + .await + { + Ok(_) => { + self.outgoing + .send_response(request_id, ThreadTurnContextUpdateResponse {}) + .await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to update thread turn context: {err}"), + ) + .await; + } + } + } + async fn thread_metadata_update( &self, request_id: ConnectionRequestId, diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 28e48dfdaf..42314f6d3e 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -73,6 +73,7 @@ use codex_app_server_protocol::ThreadRollbackParams; use codex_app_server_protocol::ThreadSetNameParams; use codex_app_server_protocol::ThreadShellCommandParams; use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadTurnContextUpdateParams; use codex_app_server_protocol::ThreadUnarchiveParams; use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::TurnCompletedNotification; @@ -367,6 +368,15 @@ impl McpProcess { self.send_request("thread/name/set", params).await } + /// Send a `thread/turnContext/update` JSON-RPC request. + pub async fn send_thread_turn_context_update_request( + &mut self, + params: ThreadTurnContextUpdateParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("thread/turnContext/update", params).await + } + /// Send a `thread/metadata/update` JSON-RPC request. pub async fn send_thread_metadata_update_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs b/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs index d5e6903272..24588baf9a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs @@ -22,6 +22,8 @@ use codex_app_server_protocol::ThreadShellCommandParams; use codex_app_server_protocol::ThreadShellCommandResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::ThreadTurnContextUpdateParams; +use codex_app_server_protocol::ThreadTurnContextUpdateResponse; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; @@ -321,6 +323,89 @@ async fn thread_shell_command_uses_existing_active_turn() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_shell_command_uses_updated_turn_context_cwd() -> Result<()> { + let tmp = TempDir::new()?; + let codex_home = tmp.path().join("codex_home"); + std::fs::create_dir(&codex_home)?; + let original_workspace = tmp.path().join("original"); + std::fs::create_dir(&original_workspace)?; + let updated_workspace = tmp.path().join("updated"); + std::fs::create_dir(&updated_workspace)?; + + let server = create_mock_responses_server_sequence(vec![]).await; + create_config_toml( + codex_home.as_path(), + &server.uri(), + "never", + &BTreeMap::default(), + )?; + + let mut mcp = McpProcess::new(codex_home.as_path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let start_id = mcp + .send_thread_start_request(ThreadStartParams { + cwd: Some(original_workspace.display().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 update_id = mcp + .send_thread_turn_context_update_request(ThreadTurnContextUpdateParams { + thread_id: thread.id.clone(), + cwd: Some(updated_workspace.clone()), + ..Default::default() + }) + .await?; + let update_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(update_id)), + ) + .await??; + let _: ThreadTurnContextUpdateResponse = + to_response::(update_resp)?; + + let resolved_workspace = std::fs::canonicalize(updated_workspace.as_path())?; + let (shell_command, expected_output) = current_shell_pwd_command(resolved_workspace.as_path()); + let shell_id = mcp + .send_thread_shell_command_request(ThreadShellCommandParams { + thread_id: thread.id, + command: shell_command, + }) + .await?; + let shell_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(shell_id)), + ) + .await??; + let _: ThreadShellCommandResponse = to_response::(shell_resp)?; + + let started = wait_for_command_execution_started(&mut mcp, /*expected_id*/ None).await?; + let ThreadItem::CommandExecution { id, source, .. } = &started.item else { + unreachable!("helper returns command execution item"); + }; + let command_id = id.clone(); + assert_eq!(source, &CommandExecutionSource::UserShell); + + let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?; + let ThreadItem::CommandExecution { + aggregated_output, .. + } = &completed.item + else { + unreachable!("helper returns command execution item"); + }; + assert_eq!(aggregated_output.as_deref(), Some(expected_output.as_str())); + + Ok(()) +} + fn current_shell_output_command(text: &str) -> Result<(String, String)> { let command_and_output = match default_user_shell().name() { "powershell" => { @@ -339,6 +424,17 @@ fn current_shell_output_command(text: &str) -> Result<(String, String)> { Ok(command_and_output) } +fn current_shell_pwd_command(cwd: &Path) -> (String, String) { + match default_user_shell().name() { + "powershell" => ( + "(Get-Location).Path".to_string(), + format!("{}\r\n", cwd.display()), + ), + "cmd" => ("cd".to_string(), format!("{}\r\n", cwd.display())), + _ => ("pwd".to_string(), format!("{}\n", cwd.display())), + } +} + async fn wait_for_command_execution_started( mcp: &mut McpProcess, expected_id: Option<&str>, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index cdf68d9515..0973835485 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -2403,7 +2403,36 @@ impl App { app_server.reload_user_config().await?; Ok(true) } - AppCommandView::OverrideTurnContext { .. } => Ok(true), + AppCommandView::OverrideTurnContext { + cwd, + approval_policy, + approvals_reviewer, + sandbox_policy, + windows_sandbox_level: _, + model, + effort, + summary, + service_tier, + collaboration_mode, + personality, + } => { + app_server + .thread_turn_context_update( + thread_id, + (*cwd).clone(), + *approval_policy, + *approvals_reviewer, + (*sandbox_policy).clone(), + (*model).clone(), + *effort, + *summary, + *service_tier, + (*collaboration_mode).clone(), + *personality, + ) + .await?; + Ok(true) + } _ => Ok(false), } } diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 08e8d237b5..52fecea6d8 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -54,6 +54,8 @@ use codex_app_server_protocol::ThreadShellCommandParams; use codex_app_server_protocol::ThreadShellCommandResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::ThreadTurnContextUpdateParams; +use codex_app_server_protocol::ThreadTurnContextUpdateResponse; use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::ThreadUnsubscribeResponse; use codex_app_server_protocol::Turn; @@ -644,6 +646,45 @@ impl AppServerSession { Ok(()) } + #[allow(clippy::too_many_arguments)] + pub(crate) async fn thread_turn_context_update( + &mut self, + thread_id: ThreadId, + cwd: Option, + approval_policy: Option, + approvals_reviewer: Option, + sandbox_policy: Option, + model: Option, + effort: Option>, + summary: Option, + service_tier: Option>, + collaboration_mode: Option, + personality: Option, + ) -> Result<()> { + let request_id = self.next_request_id(); + let _: ThreadTurnContextUpdateResponse = self + .client + .request_typed(ClientRequest::ThreadTurnContextUpdate { + request_id, + params: ThreadTurnContextUpdateParams { + thread_id: thread_id.to_string(), + cwd, + approval_policy: approval_policy.map(Into::into), + approvals_reviewer: approvals_reviewer.map(Into::into), + sandbox_policy: sandbox_policy.map(Into::into), + model, + effort, + summary, + service_tier, + collaboration_mode, + personality, + }, + }) + .await + .wrap_err("thread/turnContext/update failed in TUI")?; + Ok(()) + } + pub(crate) async fn thread_realtime_start( &mut self, thread_id: ThreadId,