diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index dd5e1a9306..725804542d 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -3772,6 +3772,44 @@ ], "type": "object" }, + "TurnOverrideActiveContextParams": { + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the active turn approval policy and the default for subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on the active turn and subsequent turns." + }, + "expectedTurnId": { + "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "expectedTurnId", + "threadId" + ], + "type": "object" + }, "TurnStartParams": { "properties": { "approvalPolicy": { @@ -5051,6 +5089,30 @@ "title": "Turn/steerRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "turn/overrideActiveContext" + ], + "title": "Turn/overrideActiveContextRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnOverrideActiveContextParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Turn/overrideActiveContextRequest", + "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 cf54aca282..2a295d63a6 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 @@ -1169,6 +1169,30 @@ "title": "Turn/steerRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "turn/overrideActiveContext" + ], + "title": "Turn/overrideActiveContextRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/TurnOverrideActiveContextParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Turn/overrideActiveContextRequest", + "type": "object" + }, { "properties": { "id": { @@ -16520,6 +16544,59 @@ "title": "TurnInterruptResponse", "type": "object" }, + "TurnOverrideActiveContextParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the active turn approval policy and the default for subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on the active turn and subsequent turns." + }, + "expectedTurnId": { + "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "expectedTurnId", + "threadId" + ], + "title": "TurnOverrideActiveContextParams", + "type": "object" + }, + "TurnOverrideActiveContextResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "turnId": { + "type": "string" + } + }, + "required": [ + "turnId" + ], + "title": "TurnOverrideActiveContextResponse", + "type": "object" + }, "TurnPlanStep": { "properties": { "status": { 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 506d92e604..6c7058b41d 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 @@ -1857,6 +1857,30 @@ "title": "Turn/steerRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "turn/overrideActiveContext" + ], + "title": "Turn/overrideActiveContextRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnOverrideActiveContextParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Turn/overrideActiveContextRequest", + "type": "object" + }, { "properties": { "id": { @@ -14414,6 +14438,59 @@ "title": "TurnInterruptResponse", "type": "object" }, + "TurnOverrideActiveContextParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the active turn approval policy and the default for subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on the active turn and subsequent turns." + }, + "expectedTurnId": { + "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "expectedTurnId", + "threadId" + ], + "title": "TurnOverrideActiveContextParams", + "type": "object" + }, + "TurnOverrideActiveContextResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "turnId": { + "type": "string" + } + }, + "required": [ + "turnId" + ], + "title": "TurnOverrideActiveContextResponse", + "type": "object" + }, "TurnPlanStep": { "properties": { "status": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnOverrideActiveContextParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnOverrideActiveContextParams.json new file mode 100644 index 0000000000..9a51c1f9b5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnOverrideActiveContextParams.json @@ -0,0 +1,100 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "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" + } + ] + } + }, + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the active turn approval policy and the default for subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on the active turn and subsequent turns." + }, + "expectedTurnId": { + "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "expectedTurnId", + "threadId" + ], + "title": "TurnOverrideActiveContextParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnOverrideActiveContextResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnOverrideActiveContextResponse.json new file mode 100644 index 0000000000..4d4e9e83d9 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnOverrideActiveContextResponse.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "turnId": { + "type": "string" + } + }, + "required": [ + "turnId" + ], + "title": "TurnOverrideActiveContextResponse", + "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 3a65075b64..20c1297dd3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -68,6 +68,7 @@ import type { ThreadTurnsListParams } from "./v2/ThreadTurnsListParams"; import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams"; import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams"; import type { TurnInterruptParams } from "./v2/TurnInterruptParams"; +import type { TurnOverrideActiveContextParams } from "./v2/TurnOverrideActiveContextParams"; import type { TurnStartParams } from "./v2/TurnStartParams"; import type { TurnSteerParams } from "./v2/TurnSteerParams"; import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupStartParams"; @@ -75,4 +76,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/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "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": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "device/key/create", id: RequestId, params: DeviceKeyCreateParams, } | { "method": "device/key/public", id: RequestId, params: DeviceKeyPublicParams, } | { "method": "device/key/sign", id: RequestId, params: DeviceKeySignParams, } | { "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": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "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": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "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/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/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "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": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "device/key/create", id: RequestId, params: DeviceKeyCreateParams, } | { "method": "device/key/public", id: RequestId, params: DeviceKeyPublicParams, } | { "method": "device/key/sign", id: RequestId, params: DeviceKeySignParams, } | { "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/overrideActiveContext", id: RequestId, params: TurnOverrideActiveContextParams, } | { "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": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "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": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "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/TurnOverrideActiveContextParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnOverrideActiveContextParams.ts new file mode 100644 index 0000000000..52c7e4edc2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnOverrideActiveContextParams.ts @@ -0,0 +1,22 @@ +// 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 { ApprovalsReviewer } from "./ApprovalsReviewer"; +import type { AskForApproval } from "./AskForApproval"; + +export type TurnOverrideActiveContextParams = { threadId: string, +/** + * Required active turn id precondition. The request fails when it does not + * match the currently active turn. + */ +expectedTurnId: string, +/** + * Override the active turn approval policy and the default for subsequent + * turns. + */ +approvalPolicy?: AskForApproval | null, +/** + * Override where approval requests are routed for review on the active + * turn and subsequent turns. + */ +approvalsReviewer?: ApprovalsReviewer | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnOverrideActiveContextResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnOverrideActiveContextResponse.ts new file mode 100644 index 0000000000..c8ed29cc3b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnOverrideActiveContextResponse.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 TurnOverrideActiveContextResponse = { turnId: string, }; 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 837b265fc8..2a4aa700cb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -381,6 +381,8 @@ export type { TurnEnvironmentParams } from "./TurnEnvironmentParams"; export type { TurnError } from "./TurnError"; export type { TurnInterruptParams } from "./TurnInterruptParams"; export type { TurnInterruptResponse } from "./TurnInterruptResponse"; +export type { TurnOverrideActiveContextParams } from "./TurnOverrideActiveContextParams"; +export type { TurnOverrideActiveContextResponse } from "./TurnOverrideActiveContextResponse"; export type { TurnPlanStep } from "./TurnPlanStep"; export type { TurnPlanStepStatus } from "./TurnPlanStepStatus"; export type { TurnPlanUpdatedNotification } from "./TurnPlanUpdatedNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 503cb6bc17..e67674714c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -439,6 +439,11 @@ client_request_definitions! { inspect_params: true, response: v2::TurnSteerResponse, }, + TurnOverrideActiveContext => "turn/overrideActiveContext" { + params: v2::TurnOverrideActiveContextParams, + inspect_params: true, + response: v2::TurnOverrideActiveContextResponse, + }, TurnInterrupt => "turn/interrupt" { params: v2::TurnInterruptParams, response: v2::TurnInterruptResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index f8bc91c01e..3c7efc92e5 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -4950,6 +4950,34 @@ pub struct TurnSteerResponse { pub turn_id: String, } +#[derive( + Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnOverrideActiveContextParams { + pub thread_id: String, + /// Required active turn id precondition. The request fails when it does not + /// match the currently active turn. + pub expected_turn_id: String, + /// Override the active turn approval policy and the default for subsequent + /// turns. + #[experimental(nested)] + #[ts(optional = nullable)] + pub approval_policy: Option, + /// Override where approval requests are routed for review on the active + /// turn and subsequent turns. + #[ts(optional = nullable)] + pub approvals_reviewer: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnOverrideActiveContextResponse { + pub turn_id: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -8826,6 +8854,30 @@ mod tests { assert_eq!(reason, Some("askForApproval.granular")); } + #[test] + fn client_request_turn_override_active_context_granular_approval_policy_is_marked_experimental() + { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason( + &crate::ClientRequest::TurnOverrideActiveContext { + request_id: crate::RequestId::Integer(5), + params: TurnOverrideActiveContextParams { + thread_id: "thr_123".to_string(), + expected_turn_id: "turn_123".to_string(), + approval_policy: Some(AskForApproval::Granular { + sandbox_approval: false, + rules: true, + skill_approval: false, + request_permissions: false, + mcp_elicitations: true, + }), + approvals_reviewer: None, + }, + }, + ); + + assert_eq!(reason, Some("askForApproval.granular")); + } + #[test] fn mcp_server_elicitation_response_round_trips_rmcp_result() { let rmcp_result = rmcp::model::CreateElicitationResult { diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 2452cba36b..db5fb3d1d4 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -158,6 +158,7 @@ Example with notification opt-out: - `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. 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/overrideActiveContext` — update approval policy or approvals reviewer for the currently active regular turn and subsequent turns; returns the active `turnId`. - `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"`. - `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. - `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`. @@ -735,6 +736,23 @@ not emit `turn/started` and does not accept turn context overrides. active turn, or the active turn kind does not accept same-turn steering (for example review or manual compaction), the request fails with an `invalid request` error. +Use `turn/overrideActiveContext` to change approval behavior for an already in-flight regular turn +without sending additional input. The same values become the defaults for subsequent turns. + +```json +{ "method": "turn/overrideActiveContext", "id": 33, "params": { + "threadId": "thr_123", + "expectedTurnId": "turn_456", + "approvalPolicy": "never", + "approvalsReviewer": "user" +} } +{ "id": 33, "result": { "turnId": "turn_456" } } +``` + +`expectedTurnId` is required. At least one of `approvalPolicy` or `approvalsReviewer` must be set. +If there is no active turn, `expectedTurnId` does not match the active turn, or the active turn kind +does not support active context overrides, the request fails with an `invalid request` error. + ### Example: Request a code review Use `review/start` to run Codex’s reviewer on the currently checked-out project. The request takes the thread id plus a `target` describing what should be reviewed: diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 0eb465e54a..b431643ae6 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -201,6 +201,8 @@ use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnInterruptResponse; +use codex_app_server_protocol::TurnOverrideActiveContextParams; +use codex_app_server_protocol::TurnOverrideActiveContextResponse; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; @@ -1016,6 +1018,10 @@ impl CodexMessageProcessor { self.turn_steer(to_connection_request_id(request_id), params) .await; } + ClientRequest::TurnOverrideActiveContext { request_id, params } => { + self.turn_override_active_context(to_connection_request_id(request_id), params) + .await; + } ClientRequest::TurnInterrupt { request_id, params } => { self.turn_interrupt(to_connection_request_id(request_id), params) .await; @@ -7078,6 +7084,77 @@ impl CodexMessageProcessor { } } + async fn turn_override_active_context( + &self, + request_id: ConnectionRequestId, + params: TurnOverrideActiveContextParams, + ) { + let (_, thread) = match self.load_thread(¶ms.thread_id).await { + Ok(v) => v, + Err(error) => { + self.track_error_response(&request_id, &error, /*error_type*/ None); + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + if params.expected_turn_id.is_empty() { + self.send_invalid_request_error( + request_id, + "expectedTurnId must not be empty".to_string(), + ) + .await; + return; + } + if params.approval_policy.is_none() && params.approvals_reviewer.is_none() { + self.send_invalid_request_error( + request_id, + "at least one of approvalPolicy or approvalsReviewer must be set".to_string(), + ) + .await; + return; + } + + self.outgoing + .record_request_turn_id(&request_id, ¶ms.expected_turn_id) + .await; + + match thread + .override_active_turn_context( + ¶ms.expected_turn_id, + params.approval_policy.map(AskForApproval::to_core), + params + .approvals_reviewer + .map(codex_app_server_protocol::ApprovalsReviewer::to_core), + ) + .await + { + Ok(turn_id) => { + let response = TurnOverrideActiveContextResponse { turn_id }; + if self.config.features.enabled(Feature::GeneralAnalytics) { + self.analytics_events_client.track_response( + request_id.connection_id.0, + ClientResponse::TurnOverrideActiveContext { + request_id: request_id.request_id.clone(), + response: response.clone(), + }, + ); + } + self.outgoing.send_response(request_id, response).await; + } + Err(CodexErr::InvalidRequest(message)) => { + self.send_invalid_request_error(request_id, message).await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to override active turn context: {err}"), + ) + .await; + } + } + } + async fn prepare_realtime_conversation_thread( &self, request_id: ConnectionRequestId, diff --git a/codex-rs/core/src/apply_patch.rs b/codex-rs/core/src/apply_patch.rs index c05a459049..aed0fa2005 100644 --- a/codex-rs/core/src/apply_patch.rs +++ b/codex-rs/core/src/apply_patch.rs @@ -37,7 +37,7 @@ pub(crate) async fn apply_patch( ) -> InternalApplyPatchInvocation { match assess_patch_safety( &action, - turn_context.approval_policy.value(), + turn_context.approval_policy(), turn_context.sandbox_policy.get(), file_system_sandbox_policy, &turn_context.cwd, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 7c8e9fa68a..8bbad70666 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -116,6 +116,18 @@ impl CodexThread { .await } + pub async fn override_active_turn_context( + &self, + expected_turn_id: &str, + approval_policy: Option, + approvals_reviewer: Option, + ) -> CodexResult { + self.codex + .session + .override_active_turn_context(expected_turn_id, approval_policy, approvals_reviewer) + .await + } + pub async fn set_app_server_client_info( &self, app_server_client_name: Option, diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 0b8b7a5887..a479653d91 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -50,7 +50,7 @@ fn build_permissions_update_item( let prev = previous?; if prev.sandbox_policy == *next.sandbox_policy.get() - && prev.approval_policy == next.approval_policy.value() + && prev.approval_policy == next.approval_policy() { return None; } @@ -58,8 +58,8 @@ fn build_permissions_update_item( Some( PermissionsInstructions::from_policy( next.sandbox_policy.get(), - next.approval_policy.value(), - next.config.approvals_reviewer, + next.approval_policy(), + next.approvals_reviewer(), exec_policy, &next.cwd, next.features.enabled(Feature::ExecPermissionApprovals), diff --git a/codex-rs/core/src/guardian/review.rs b/codex-rs/core/src/guardian/review.rs index ce01ac970e..38864d1ea3 100644 --- a/codex-rs/core/src/guardian/review.rs +++ b/codex-rs/core/src/guardian/review.rs @@ -142,9 +142,9 @@ fn guardian_risk_level_str(level: GuardianRiskLevel) -> &'static str { /// earlier in the flow. pub(crate) fn routes_approval_to_guardian(turn: &TurnContext) -> bool { matches!( - turn.approval_policy.value(), + turn.approval_policy(), AskForApproval::OnRequest | AskForApproval::Granular(_) - ) && turn.config.approvals_reviewer == ApprovalsReviewer::GuardianSubagent + ) && turn.approvals_reviewer() == ApprovalsReviewer::GuardianSubagent } pub(crate) fn is_guardian_reviewer_source( diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index c647d146d4..4ddec421cf 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -479,7 +479,7 @@ fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str); } fn hook_permission_mode(turn_context: &TurnContext) -> String { - match turn_context.approval_policy.value() { + match turn_context.approval_policy() { AskForApproval::Never => "bypassPermissions", AskForApproval::UnlessTrusted | AskForApproval::OnFailure diff --git a/codex-rs/core/src/mcp_skill_dependencies.rs b/codex-rs/core/src/mcp_skill_dependencies.rs index c711d1a158..97b023585c 100644 --- a/codex-rs/core/src/mcp_skill_dependencies.rs +++ b/codex-rs/core/src/mcp_skill_dependencies.rs @@ -220,7 +220,7 @@ async fn should_install_mcp_dependencies( cancellation_token: &CancellationToken, ) -> bool { if mcp_permission_prompt_is_auto_approved( - turn_context.approval_policy.value(), + turn_context.approval_policy(), turn_context.sandbox_policy.get(), ) { return true; diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 82bbc7ca4a..cdacc674ae 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -818,7 +818,7 @@ async fn maybe_request_mcp_tool_approval( approval_mode: AppToolApproval, ) -> Option { if mcp_permission_prompt_is_auto_approved( - turn_context.approval_policy.value(), + turn_context.approval_policy(), turn_context.sandbox_policy.get(), ) { return None; diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index bbc66141ec..d6cd13b089 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -4,6 +4,8 @@ use crate::realtime_conversation::handle_start as handle_realtime_conversation_s use crate::realtime_conversation::handle_text as handle_realtime_conversation_text; use async_channel::Receiver; use codex_otel::set_parent_from_w3c_trace_context; +use codex_protocol::config_types::ApprovalsReviewer; +use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Submission; use tracing::Instrument; use tracing::debug_span; @@ -113,6 +115,28 @@ pub async fn override_turn_context(sess: &Session, sub_id: String, updates: Sess } } +pub async fn override_active_turn_context( + sess: &Session, + sub_id: String, + turn_id: String, + approval_policy: Option, + approvals_reviewer: Option, +) { + if let Err(err) = sess + .override_active_turn_context(&turn_id, approval_policy, approvals_reviewer) + .await + { + sess.send_event_raw(Event { + id: sub_id, + msg: EventMsg::Error(ErrorEvent { + message: err.to_string(), + codex_error_info: Some(CodexErrorInfo::BadRequest), + }), + }) + .await; + } +} + pub async fn user_input_or_turn(sess: &Arc, sub_id: String, op: Op) { user_input_or_turn_inner( sess, @@ -1099,6 +1123,21 @@ pub(super) async fn submission_loop( .await; false } + Op::OverrideActiveTurnContext { + turn_id, + approval_policy, + approvals_reviewer, + } => { + override_active_turn_context( + &sess, + sub.id.clone(), + turn_id, + approval_policy, + approvals_reviewer, + ) + .await; + false + } Op::UserInput { .. } | Op::UserTurn { .. } => { user_input_or_turn(&sess, sub.id.clone(), sub.op).await; false diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 2e4a3301ea..7f9b1b4795 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -225,11 +225,12 @@ impl Session { guard.cancel(); *guard = CancellationToken::new(); } + let approval_policy = turn_context.approval_policy_constrained(); let (refreshed_manager, cancel_token) = McpConnectionManager::new( &mcp_servers, store_mode, auth_statuses, - &turn_context.config.permissions.approval_policy, + &approval_policy, turn_context.sub_id.clone(), self.get_tx_event(), turn_context.sandbox_policy.get().clone(), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 6a6ed86a8d..74db9b5d66 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -1310,6 +1310,85 @@ impl Session { Ok(()) } + pub(crate) async fn override_active_turn_context( + &self, + expected_turn_id: &str, + approval_policy: Option, + approvals_reviewer: Option, + ) -> CodexResult { + if expected_turn_id.is_empty() { + return Err(CodexErr::InvalidRequest( + "expected turn id must not be empty".to_string(), + )); + } + if approval_policy.is_none() && approvals_reviewer.is_none() { + return Err(CodexErr::InvalidRequest( + "at least one of approval_policy or approvals_reviewer must be set".to_string(), + )); + } + + let turn_contexts = { + let active = self.active_turn.lock().await; + let active_turn = active.as_ref().ok_or_else(|| { + CodexErr::InvalidRequest("no active turn to override".to_string()) + })?; + + let Some((active_turn_id, active_task)) = active_turn.tasks.first() else { + return Err(CodexErr::InvalidRequest( + "no active turn to override".to_string(), + )); + }; + + if expected_turn_id != active_turn_id { + return Err(CodexErr::InvalidRequest(format!( + "expected active turn id `{expected_turn_id}` but found `{active_turn_id}`" + ))); + } + + if active_task.kind != crate::state::TaskKind::Regular { + return Err(CodexErr::InvalidRequest( + "only regular turns support active context overrides".to_string(), + )); + } + + active_turn + .tasks + .values() + .map(|task| Arc::clone(&task.turn_context)) + .collect::>() + }; + + let updated_approval_policy = { + let updates = SessionSettingsUpdate { + approval_policy, + approvals_reviewer, + ..Default::default() + }; + let mut state = self.state.lock().await; + let updated = state + .session_configuration + .apply(&updates) + .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; + let updated_approval_policy = approval_policy.map(|_| updated.approval_policy.clone()); + state.session_configuration = updated; + updated_approval_policy + }; + + for turn_context in turn_contexts { + turn_context.override_active_approval_context(approval_policy, approvals_reviewer); + } + + if let Some(approval_policy) = updated_approval_policy { + self.services + .mcp_connection_manager + .read() + .await + .set_approval_policy(&approval_policy); + } + + Ok(expected_turn_id.to_string()) + } + pub(crate) async fn set_session_startup_prewarm( &self, startup_prewarm: SessionStartupPrewarmHandle, @@ -1882,7 +1961,7 @@ impl Session { cwd: AbsolutePathBuf, cancellation_token: CancellationToken, ) -> Option { - match turn_context.as_ref().approval_policy.value() { + match turn_context.as_ref().approval_policy() { AskForApproval::Never => { return Some(RequestPermissionsResponse { permissions: RequestPermissionProfile::default(), @@ -2412,8 +2491,8 @@ impl Session { developer_sections.push( PermissionsInstructions::from_policy( turn_context.sandbox_policy.get(), - turn_context.approval_policy.value(), - turn_context.config.approvals_reviewer, + turn_context.approval_policy(), + turn_context.approvals_reviewer(), self.services.exec_policy.current().as_ref(), &turn_context.cwd, turn_context diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 62f4c9a871..15a90d7ff2 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -63,6 +63,7 @@ pub(super) async fn spawn_review_thread( let mut per_turn_config = (*config).clone(); per_turn_config.model = Some(model.clone()); per_turn_config.features = review_features.clone(); + per_turn_config.approvals_reviewer = parent_turn_context.approvals_reviewer(); if let Err(err) = per_turn_config.web_search_mode.set(review_web_search_mode) { let fallback_value = per_turn_config.web_search_mode.value(); tracing::warn!( @@ -122,7 +123,8 @@ pub(super) async fn spawn_review_thread( compact_prompt: parent_turn_context.compact_prompt.clone(), collaboration_mode: parent_turn_context.collaboration_mode.clone(), personality: parent_turn_context.personality, - approval_policy: parent_turn_context.approval_policy.clone(), + approval_policy: parent_turn_context.approval_policy_constrained(), + active_overrides: Default::default(), sandbox_policy: parent_turn_context.sandbox_policy.clone(), file_system_sandbox_policy: parent_turn_context.file_system_sandbox_policy.clone(), network_sandbox_policy: parent_turn_context.network_sandbox_policy, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index bf0623bcb5..2b13dcb2c0 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3723,6 +3723,15 @@ fn op_kind_distinguishes_turn_ops() { .kind(), "override_turn_context" ); + assert_eq!( + Op::OverrideActiveTurnContext { + turn_id: "turn-1".to_string(), + approval_policy: None, + approvals_reviewer: None, + } + .kind(), + "override_active_turn_context" + ); assert_eq!( Op::UserInput { environments: None, @@ -5681,6 +5690,84 @@ async fn steer_input_requires_active_turn() { assert!(matches!(err, SteerInputError::NoActiveTurn(_))); } +#[tokio::test] +async fn override_active_turn_context_updates_current_turn_and_defaults() { + let (sess, tc, _rx) = make_session_and_context_with_rx().await; + let input = vec![UserInput::Text { + text: "hello".to_string(), + text_elements: Vec::new(), + }]; + sess.spawn_task( + Arc::clone(&tc), + input, + NeverEndingTask { + kind: TaskKind::Regular, + listen_to_cancellation_token: false, + }, + ) + .await; + + let turn_id = sess + .override_active_turn_context( + &tc.sub_id, + Some(AskForApproval::Never), + Some(ApprovalsReviewer::GuardianSubagent), + ) + .await + .expect("active context override should succeed"); + + assert_eq!(turn_id, tc.sub_id); + assert_eq!(tc.approval_policy(), AskForApproval::Never); + assert_eq!(tc.approvals_reviewer(), ApprovalsReviewer::GuardianSubagent); + + let state = sess.state.lock().await; + assert_eq!( + state.session_configuration.approval_policy.value(), + AskForApproval::Never + ); + assert_eq!( + state.session_configuration.approvals_reviewer, + ApprovalsReviewer::GuardianSubagent + ); +} + +#[tokio::test] +async fn override_active_turn_context_rejects_missing_or_wrong_active_turn() { + let (sess, tc, _rx) = make_session_and_context_with_rx().await; + + let err = sess + .override_active_turn_context(&tc.sub_id, Some(AskForApproval::Never), None) + .await + .expect_err("override without active turn should fail"); + assert!(matches!(err, CodexErr::InvalidRequest(_))); + + let input = vec![UserInput::Text { + text: "hello".to_string(), + text_elements: Vec::new(), + }]; + sess.spawn_task( + Arc::clone(&tc), + input, + NeverEndingTask { + kind: TaskKind::Regular, + listen_to_cancellation_token: false, + }, + ) + .await; + + let err = sess + .override_active_turn_context("other-turn", Some(AskForApproval::Never), None) + .await + .expect_err("mismatched turn id should fail"); + assert!(matches!(err, CodexErr::InvalidRequest(_))); + + let err = sess + .override_active_turn_context(&tc.sub_id, None, None) + .await + .expect_err("empty override should fail"); + assert!(matches!(err, CodexErr::InvalidRequest(_))); +} + #[tokio::test] async fn steer_input_enforces_expected_turn_id() { let (sess, tc, _rx) = make_session_and_context_with_rx().await; diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 780cee5f87..ecbd1fb2df 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -507,7 +507,7 @@ pub(crate) async fn run_turn( if !needs_follow_up { last_agent_message = sampling_request_last_agent_message; - let stop_hook_permission_mode = match turn_context.approval_policy.value() { + let stop_hook_permission_mode = match turn_context.approval_policy() { AskForApproval::Never => "bypassPermissions", AskForApproval::UnlessTrusted | AskForApproval::OnFailure @@ -697,8 +697,8 @@ async fn track_turn_resolved_config_analytics( reasoning_effort: turn_context.reasoning_effort, reasoning_summary: Some(turn_context.reasoning_summary), service_tier: turn_context.config.service_tier, - approval_policy: turn_context.approval_policy.value(), - approvals_reviewer: turn_context.config.approvals_reviewer, + approval_policy: turn_context.approval_policy(), + approvals_reviewer: turn_context.approvals_reviewer(), sandbox_network_access: turn_context.network_sandbox_policy.is_enabled(), collaboration_mode: turn_context.collaboration_mode.mode, personality: turn_context.personality, @@ -1871,7 +1871,7 @@ async fn try_run_sampling_request( ) -> CodexResult { feedback_tags!( model = turn_context.model_info.slug.clone(), - approval_policy = turn_context.approval_policy.value(), + approval_policy = turn_context.approval_policy(), sandbox_policy = turn_context.sandbox_policy.get(), effort = turn_context.reasoning_effort, auth_mode = sess.services.auth_manager.auth_mode(), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 80c00e75cd..04d9f03a25 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -3,6 +3,7 @@ use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_sandboxing::policy_transforms::merge_permission_profiles; +use std::sync::RwLock; pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool { matches!( @@ -34,6 +35,12 @@ pub(crate) struct TurnEnvironment { pub(crate) cwd: AbsolutePathBuf, } +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct ActiveTurnContextOverrides { + approval_policy: Option, + approvals_reviewer: Option, +} + /// The context needed for a single turn of the thread. #[derive(Debug)] pub(crate) struct TurnContext { @@ -63,6 +70,7 @@ pub(crate) struct TurnContext { pub(crate) collaboration_mode: CollaborationMode, pub(crate) personality: Option, pub(crate) approval_policy: Constrained, + pub(crate) active_overrides: RwLock, pub(crate) sandbox_policy: Constrained, pub(crate) file_system_sandbox_policy: FileSystemSandboxPolicy, pub(crate) network_sandbox_policy: NetworkSandboxPolicy, @@ -84,6 +92,47 @@ pub(crate) struct TurnContext { pub(crate) turn_timing_state: Arc, } impl TurnContext { + pub(crate) fn approval_policy(&self) -> AskForApproval { + self.active_overrides + .read() + .ok() + .and_then(|overrides| overrides.approval_policy) + .unwrap_or_else(|| self.approval_policy.value()) + } + + pub(crate) fn approval_policy_constrained(&self) -> Constrained { + let mut approval_policy = self.approval_policy.clone(); + approval_policy + .set(self.approval_policy()) + .unwrap_or_else(|err| { + panic!("active approval policy override was validated before being applied: {err}") + }); + approval_policy + } + + pub(crate) fn approvals_reviewer(&self) -> ApprovalsReviewer { + self.active_overrides + .read() + .ok() + .and_then(|overrides| overrides.approvals_reviewer) + .unwrap_or(self.config.approvals_reviewer) + } + + pub(crate) fn override_active_approval_context( + &self, + approval_policy: Option, + approvals_reviewer: Option, + ) { + if let Ok(mut overrides) = self.active_overrides.write() { + if let Some(approval_policy) = approval_policy { + overrides.approval_policy = Some(approval_policy); + } + if let Some(approvals_reviewer) = approvals_reviewer { + overrides.approvals_reviewer = Some(approvals_reviewer); + } + } + } + pub(crate) fn permission_profile(&self) -> PermissionProfile { PermissionProfile::from_runtime_permissions( &self.file_system_sandbox_policy, @@ -144,6 +193,11 @@ impl TurnContext { Some(reasoning_effort), /*developer_instructions*/ None, ); + let active_overrides = self + .active_overrides + .read() + .map(|overrides| *overrides) + .unwrap_or_default(); let features = self.features.clone(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -197,6 +251,7 @@ impl TurnContext { collaboration_mode, personality: self.personality, approval_policy: self.approval_policy.clone(), + active_overrides: RwLock::new(active_overrides), sandbox_policy: self.sandbox_policy.clone(), file_system_sandbox_policy: self.file_system_sandbox_policy.clone(), network_sandbox_policy: self.network_sandbox_policy, @@ -270,7 +325,7 @@ impl TurnContext { cwd: self.cwd.to_path_buf(), current_date: self.current_date.clone(), timezone: self.timezone.clone(), - approval_policy: self.approval_policy.value(), + approval_policy: self.approval_policy(), sandbox_policy: self.sandbox_policy.get().clone(), network: self.turn_context_network_item(), file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(), @@ -449,6 +504,7 @@ impl Session { collaboration_mode: session_configuration.collaboration_mode.clone(), personality: session_configuration.personality, approval_policy: session_configuration.approval_policy.clone(), + active_overrides: RwLock::new(ActiveTurnContextOverrides::default()), sandbox_policy: session_configuration.sandbox_policy.clone(), file_system_sandbox_policy: session_configuration.file_system_sandbox_policy.clone(), network_sandbox_policy: session_configuration.network_sandbox_policy, diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 8eca2caf3f..e96b28a99f 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -426,7 +426,7 @@ impl ToolHandler for ApplyPatchHandler { &req, &tool_ctx, turn.as_ref(), - turn.approval_policy.value(), + turn.approval_policy(), ) .await .map(|result| result.output); @@ -534,7 +534,7 @@ pub(crate) async fn intercept_apply_patch( &req, &tool_ctx, turn.as_ref(), - turn.approval_policy.value(), + turn.approval_policy(), ) .await .map(|result| result.output); diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 5efe4e22c2..65f4313b15 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -260,7 +260,7 @@ pub(crate) fn apply_spawn_agent_runtime_overrides( config .permissions .approval_policy - .set(turn.approval_policy.value()) + .set(turn.approval_policy()) .map_err(|err| { FunctionCallError::RespondToModel(format!("approval_policy is invalid: {err}")) })?; diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 00343d3cea..af435b6a93 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -449,7 +449,7 @@ impl ShellHandler { || { normalize_and_validate_additional_permissions( additional_permissions_allowed, - turn.approval_policy.value(), + turn.approval_policy(), effective_additional_permissions.sandbox_permissions, effective_additional_permissions.additional_permissions, effective_additional_permissions.permissions_preapproved, @@ -468,11 +468,11 @@ impl ShellHandler { .requests_sandbox_override() && !effective_additional_permissions.permissions_preapproved && !matches!( - turn.approval_policy.value(), + turn.approval_policy(), codex_protocol::protocol::AskForApproval::OnRequest ) { - let approval_policy = turn.approval_policy.value(); + let approval_policy = turn.approval_policy(); return Err(FunctionCallError::RespondToModel(format!( "approval policy is {approval_policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {approval_policy:?}" ))); @@ -514,7 +514,7 @@ impl ShellHandler { .exec_policy .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &exec_params.command, - approval_policy: turn.approval_policy.value(), + approval_policy: turn.approval_policy(), sandbox_policy: turn.sandbox_policy.get(), file_system_sandbox_policy: &turn.file_system_sandbox_policy, sandbox_permissions: if effective_additional_permissions.permissions_preapproved { @@ -559,13 +559,7 @@ impl ShellHandler { tool_name, }; let out = orchestrator - .run( - &mut runtime, - &req, - &tool_ctx, - &turn, - turn.approval_policy.value(), - ) + .run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy()) .await .map(|result| result.output); let event_ctx = ToolEventCtx::new( diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index b25f5a69b7..365a40e759 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -251,11 +251,11 @@ impl ToolHandler for UnifiedExecHandler { .requests_sandbox_override() && !effective_additional_permissions.permissions_preapproved && !matches!( - context.turn.approval_policy.value(), + context.turn.approval_policy(), codex_protocol::protocol::AskForApproval::OnRequest ) { - let approval_policy = context.turn.approval_policy.value(); + let approval_policy = context.turn.approval_policy(); manager.release_process_id(process_id).await; return Err(FunctionCallError::RespondToModel(format!( "approval policy is {approval_policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {approval_policy:?}" @@ -275,7 +275,7 @@ impl ToolHandler for UnifiedExecHandler { || { normalize_and_validate_additional_permissions( additional_permissions_allowed, - context.turn.approval_policy.value(), + context.turn.approval_policy(), effective_additional_permissions.sandbox_permissions, effective_additional_permissions.additional_permissions, effective_additional_permissions.permissions_preapproved, diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 4be18bad84..6c6096566d 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -359,7 +359,7 @@ impl NetworkApprovalService { .await; return NetworkDecision::deny(REASON_NOT_ALLOWED); } - if !allows_network_approval_flow(turn_context.approval_policy.value()) { + if !allows_network_approval_flow(turn_context.approval_policy()) { pending.set_decision(PendingApprovalDecision::Deny).await; self.pending_host_approvals.lock().await.remove(&key); self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 361de9a96b..cdc0d42f79 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -199,7 +199,7 @@ pub(super) async fn try_run_zsh_fork( turn: Arc::clone(&ctx.turn), call_id: ctx.call_id.clone(), tool_name: GuardianCommandSource::Shell, - approval_policy: ctx.turn.approval_policy.value(), + approval_policy: ctx.turn.approval_policy(), sandbox_policy: command_executor.sandbox_policy.clone(), file_system_sandbox_policy: command_executor.file_system_sandbox_policy.clone(), network_sandbox_policy: command_executor.network_sandbox_policy, @@ -271,7 +271,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( turn: Arc::clone(&ctx.turn), call_id: ctx.call_id.clone(), tool_name: GuardianCommandSource::UnifiedExec, - approval_policy: ctx.turn.approval_policy.value(), + approval_policy: ctx.turn.approval_policy(), sandbox_policy: exec_request.sandbox_policy.clone(), file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(), network_sandbox_policy: exec_request.network_sandbox_policy, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index ef1f3bd81c..6e2dc39fd7 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -792,7 +792,7 @@ impl UnifiedExecProcessManager { .exec_policy .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &request.command, - approval_policy: context.turn.approval_policy.value(), + approval_policy: context.turn.approval_policy(), sandbox_policy: context.turn.sandbox_policy.get(), file_system_sandbox_policy: &context.turn.file_system_sandbox_policy, sandbox_permissions: if request.additional_permissions_preapproved { @@ -832,7 +832,7 @@ impl UnifiedExecProcessManager { &req, &tool_ctx, &context.turn, - context.turn.approval_policy.value(), + context.turn.approval_policy(), ) .await .map(|result| (result.output, result.deferred_network_approval)) diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 1b8021bc70..0075e73bc2 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -569,6 +569,23 @@ pub enum Op { personality: Option, }, + /// Override approval settings for the currently active turn and future turns. + /// + /// This is intentionally narrower than [`Op::OverrideTurnContext`]: it only + /// affects approval behavior that can safely change while a turn is running. + OverrideActiveTurnContext { + /// Active turn id that must match before the override is applied. + turn_id: String, + + /// Updated command approval policy. + #[serde(skip_serializing_if = "Option::is_none")] + approval_policy: Option, + + /// Updated approval reviewer for approval prompts. + #[serde(skip_serializing_if = "Option::is_none")] + approvals_reviewer: Option, + }, + /// Approve a command execution ExecApproval { /// The id of the submission we are approving @@ -803,6 +820,7 @@ impl Op { Self::UserTurn { .. } => "user_turn", Self::InterAgentCommunication { .. } => "inter_agent_communication", Self::OverrideTurnContext { .. } => "override_turn_context", + Self::OverrideActiveTurnContext { .. } => "override_active_turn_context", Self::ExecApproval { .. } => "exec_approval", Self::PatchApproval { .. } => "patch_approval", Self::ResolveElicitation { .. } => "resolve_elicitation",