From 292542616a5b7f8f6956a9324358767c7ea55cf3 Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Wed, 18 Feb 2026 13:03:16 -0800 Subject: [PATCH] app-server support for Windows sandbox setup. (#12025) app-server support for initiating Windows sandbox setup. server responds quickly to setup request and makes a future RPC call back to client when the setup finishes. The TUI implementation is unaffected but in a future PR I'll update the TUI to use the shared setup helper (`windows_sandbox.run_windows_sandbox_setup`) --- .../schema/json/ClientRequest.json | 42 ++++ .../schema/json/ServerNotification.json | 48 +++++ .../codex_app_server_protocol.schemas.json | 100 ++++++++++ ...dowsSandboxSetupCompletedNotification.json | 32 +++ .../v2/WindowsSandboxSetupStartParams.json | 22 +++ .../v2/WindowsSandboxSetupStartResponse.json | 13 ++ .../schema/typescript/ClientRequest.ts | 3 +- .../schema/typescript/ServerNotification.ts | 3 +- ...indowsSandboxSetupCompletedNotification.ts | 6 + .../typescript/v2/WindowsSandboxSetupMode.ts | 5 + .../v2/WindowsSandboxSetupStartParams.ts | 6 + .../v2/WindowsSandboxSetupStartResponse.ts | 5 + .../schema/typescript/v2/index.ts | 4 + .../src/protocol/common.rs | 6 + .../app-server-protocol/src/protocol/v2.rs | 31 +++ codex-rs/app-server/README.md | 5 + .../app-server/src/codex_message_processor.rs | 60 ++++++ .../app-server/tests/common/mcp_process.rs | 9 + codex-rs/app-server/tests/suite/v2/mod.rs | 1 + .../tests/suite/v2/windows_sandbox_setup.rs | 63 ++++++ codex-rs/core/src/windows_sandbox.rs | 183 ++++++++++++++++++ 21 files changed, 645 insertions(+), 2 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupCompletedNotification.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupCompletedNotification.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupMode.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartResponse.ts create mode 100644 codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 1e8ffa9dd..37ce9c89f 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -3311,6 +3311,24 @@ "type": "object" } ] + }, + "WindowsSandboxSetupMode": { + "enum": [ + "elevated", + "unelevated" + ], + "type": "string" + }, + "WindowsSandboxSetupStartParams": { + "properties": { + "mode": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + } + }, + "required": [ + "mode" + ], + "type": "object" } }, "description": "Request from the client to the server.", @@ -3939,6 +3957,30 @@ "title": "McpServerStatus/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "windowsSandbox/setupStart" + ], + "title": "WindowsSandbox/setupStartRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WindowsSandboxSetupStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "WindowsSandbox/setupStartRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 71256baa0..24e3f072c 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -8055,6 +8055,34 @@ } ] }, + "WindowsSandboxSetupCompletedNotification": { + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "mode", + "success" + ], + "type": "object" + }, + "WindowsSandboxSetupMode": { + "enum": [ + "elevated", + "unelevated" + ], + "type": "string" + }, "WindowsWorldWritableWarningNotification": { "properties": { "extraCount": { @@ -8747,6 +8775,26 @@ "title": "Windows/worldWritableWarningNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "windowsSandbox/setupCompleted" + ], + "title": "WindowsSandbox/setupCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WindowsSandboxSetupCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WindowsSandbox/setupCompletedNotification", + "type": "object" + }, { "properties": { "method": { 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 e540fc68b..ffcbdc842 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 @@ -1059,6 +1059,30 @@ "title": "McpServerStatus/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "windowsSandbox/setupStart" + ], + "title": "WindowsSandbox/setupStartRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/WindowsSandboxSetupStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "WindowsSandbox/setupStartRequest", + "type": "object" + }, { "properties": { "id": { @@ -8708,6 +8732,26 @@ "title": "Windows/worldWritableWarningNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "windowsSandbox/setupCompleted" + ], + "title": "WindowsSandbox/setupCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/WindowsSandboxSetupCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WindowsSandbox/setupCompletedNotification", + "type": "object" + }, { "properties": { "method": { @@ -17081,6 +17125,62 @@ ], "type": "string" }, + "WindowsSandboxSetupCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "$ref": "#/definitions/v2/WindowsSandboxSetupMode" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "mode", + "success" + ], + "title": "WindowsSandboxSetupCompletedNotification", + "type": "object" + }, + "WindowsSandboxSetupMode": { + "enum": [ + "elevated", + "unelevated" + ], + "type": "string" + }, + "WindowsSandboxSetupStartParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "mode": { + "$ref": "#/definitions/v2/WindowsSandboxSetupMode" + } + }, + "required": [ + "mode" + ], + "title": "WindowsSandboxSetupStartParams", + "type": "object" + }, + "WindowsSandboxSetupStartResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "started": { + "type": "boolean" + } + }, + "required": [ + "started" + ], + "title": "WindowsSandboxSetupStartResponse", + "type": "object" + }, "WindowsWorldWritableWarningNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupCompletedNotification.json new file mode 100644 index 000000000..9ed9632fd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupCompletedNotification.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "WindowsSandboxSetupMode": { + "enum": [ + "elevated", + "unelevated" + ], + "type": "string" + } + }, + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "mode", + "success" + ], + "title": "WindowsSandboxSetupCompletedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json new file mode 100644 index 000000000..6f0663d20 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartParams.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "WindowsSandboxSetupMode": { + "enum": [ + "elevated", + "unelevated" + ], + "type": "string" + } + }, + "properties": { + "mode": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + } + }, + "required": [ + "mode" + ], + "title": "WindowsSandboxSetupStartParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartResponse.json new file mode 100644 index 000000000..ce35665bc --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/WindowsSandboxSetupStartResponse.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "started": { + "type": "boolean" + } + }, + "required": [ + "started" + ], + "title": "WindowsSandboxSetupStartResponse", + "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 40757d9d9..834459287 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -53,8 +53,9 @@ import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams"; import type { TurnInterruptParams } from "./v2/TurnInterruptParams"; import type { TurnStartParams } from "./v2/TurnStartParams"; import type { TurnSteerParams } from "./v2/TurnSteerParams"; +import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupStartParams"; /** * 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/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "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": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "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": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "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": "config/read", id: RequestId, params: ConfigReadParams, } | { "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": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; +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/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "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": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "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": "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": "config/read", id: RequestId, params: ConfigReadParams, } | { "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": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index 8ac8322e9..82572bd02 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -37,9 +37,10 @@ import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification"; import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification"; import type { TurnStartedNotification } from "./v2/TurnStartedNotification"; +import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification"; import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification"; /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupCompletedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupCompletedNotification.ts new file mode 100644 index 000000000..d4c0b6cfa --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupCompletedNotification.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; + +export type WindowsSandboxSetupCompletedNotification = { mode: WindowsSandboxSetupMode, success: boolean, error: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupMode.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupMode.ts new file mode 100644 index 000000000..a74bea424 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupMode.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 WindowsSandboxSetupMode = "elevated" | "unelevated"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts new file mode 100644 index 000000000..21daf6462 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; + +export type WindowsSandboxSetupStartParams = { mode: WindowsSandboxSetupMode, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartResponse.ts new file mode 100644 index 000000000..a19004948 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WindowsSandboxSetupStartResponse.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 WindowsSandboxSetupStartResponse = { started: boolean, }; 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 d10b7f7d5..7fb9681cc 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -201,5 +201,9 @@ export type { TurnSteerParams } from "./TurnSteerParams"; export type { TurnSteerResponse } from "./TurnSteerResponse"; export type { UserInput } from "./UserInput"; export type { WebSearchAction } from "./WebSearchAction"; +export type { WindowsSandboxSetupCompletedNotification } from "./WindowsSandboxSetupCompletedNotification"; +export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; +export type { WindowsSandboxSetupStartParams } from "./WindowsSandboxSetupStartParams"; +export type { WindowsSandboxSetupStartResponse } from "./WindowsSandboxSetupStartResponse"; export type { WindowsWorldWritableWarningNotification } from "./WindowsWorldWritableWarningNotification"; export type { WriteStatus } from "./WriteStatus"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index d3f906bca..9f580de00 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -309,6 +309,11 @@ client_request_definitions! { response: v2::ListMcpServerStatusResponse, }, + WindowsSandboxSetupStart => "windowsSandbox/setupStart" { + params: v2::WindowsSandboxSetupStartParams, + response: v2::WindowsSandboxSetupStartResponse, + }, + LoginAccount => "account/login/start" { params: v2::LoginAccountParams, inspect_params: true, @@ -805,6 +810,7 @@ server_notification_definitions! { /// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox. WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification), + WindowsSandboxSetupCompleted => "windowsSandbox/setupCompleted" (v2::WindowsSandboxSetupCompletedNotification), #[serde(rename = "account/login/completed")] #[ts(rename = "account/login/completed")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 030d82ce7..4c3d95378 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3193,6 +3193,37 @@ pub struct WindowsWorldWritableWarningNotification { pub failed_scan: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum WindowsSandboxSetupMode { + Elevated, + Unelevated, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsSandboxSetupStartParams { + pub mode: WindowsSandboxSetupMode, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsSandboxSetupStartResponse { + pub started: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsSandboxSetupCompletedNotification { + pub mode: WindowsSandboxSetupMode, + pub success: bool, + pub error: Option, +} + /// Deprecated: Use `ContextCompaction` item type instead. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index bb378264e..0b3b98c61 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -143,6 +143,7 @@ Example with notification opt-out: - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. - `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination. +- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); returns `{ started: true }` immediately and later emits `windowsSandbox/setupCompleted`. - `feedback/upload` — submit a feedback report (classification + optional reason/logs and conversation_id); returns the tracking thread id. - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). - `config/read` — fetch the effective config on disk after resolving config layering. @@ -544,6 +545,10 @@ The fuzzy file search session API emits per-query notifications: - `fuzzyFileSearch/sessionUpdated` — `{ sessionId, query, files }` with the current matching files for the active query. - `fuzzyFileSearch/sessionCompleted` — `{ sessionId, query }` once indexing/matching for that query has completed. +### Windows sandbox setup events + +- `windowsSandbox/setupCompleted` — `{ mode, success, error }` after a `windowsSandbox/setupStart` request finishes. + ### Turn events The app-server streams JSON-RPC notifications while a turn is running. Each turn starts with `turn/started` (initial `turn`) and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 5a6a246a3..b4663ea16 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -160,6 +160,10 @@ use codex_app_server_protocol::TurnSteerResponse; use codex_app_server_protocol::UserInfoResponse; use codex_app_server_protocol::UserInput as V2UserInput; use codex_app_server_protocol::UserSavedConfig; +use codex_app_server_protocol::WindowsSandboxSetupCompletedNotification; +use codex_app_server_protocol::WindowsSandboxSetupMode; +use codex_app_server_protocol::WindowsSandboxSetupStartParams; +use codex_app_server_protocol::WindowsSandboxSetupStartResponse; use codex_app_server_protocol::build_turns_from_rollout_items; use codex_backend_client::Client as BackendClient; use codex_chatgpt::connectors; @@ -216,6 +220,8 @@ use codex_core::skills::remote::list_remote_skills; use codex_core::state_db::StateDbHandle; use codex_core::state_db::get_state_db; use codex_core::windows_sandbox::WindowsSandboxLevelExt; +use codex_core::windows_sandbox::WindowsSandboxSetupMode as CoreWindowsSandboxSetupMode; +use codex_core::windows_sandbox::WindowsSandboxSetupRequest; use codex_feedback::CodexFeedback; use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; @@ -659,6 +665,10 @@ impl CodexMessageProcessor { self.list_mcp_server_status(to_connection_request_id(request_id), params) .await; } + ClientRequest::WindowsSandboxSetupStart { request_id, params } => { + self.windows_sandbox_setup_start(to_connection_request_id(request_id), params) + .await; + } ClientRequest::LoginAccount { request_id, params } => { self.login_v2(to_connection_request_id(request_id), params) .await; @@ -6056,6 +6066,56 @@ impl CodexMessageProcessor { } } + async fn windows_sandbox_setup_start( + &mut self, + request_id: ConnectionRequestId, + params: WindowsSandboxSetupStartParams, + ) { + self.outgoing + .send_response( + request_id.clone(), + WindowsSandboxSetupStartResponse { started: true }, + ) + .await; + + let mode = match params.mode { + WindowsSandboxSetupMode::Elevated => CoreWindowsSandboxSetupMode::Elevated, + WindowsSandboxSetupMode::Unelevated => CoreWindowsSandboxSetupMode::Unelevated, + }; + let config = Arc::clone(&self.config); + let outgoing = ThreadScopedOutgoingMessageSender::new( + Arc::clone(&self.outgoing), + vec![request_id.connection_id], + ); + + tokio::spawn(async move { + let setup_request = WindowsSandboxSetupRequest { + mode, + policy: config.permissions.sandbox_policy.get().clone(), + policy_cwd: config.cwd.clone(), + command_cwd: config.cwd.clone(), + env_map: std::env::vars().collect(), + codex_home: config.codex_home.clone(), + active_profile: config.active_profile.clone(), + }; + let setup_result = + codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await; + let notification = WindowsSandboxSetupCompletedNotification { + mode: match mode { + CoreWindowsSandboxSetupMode::Elevated => WindowsSandboxSetupMode::Elevated, + CoreWindowsSandboxSetupMode::Unelevated => WindowsSandboxSetupMode::Unelevated, + }, + success: setup_result.is_ok(), + error: setup_result.err().map(|err| err.to_string()), + }; + outgoing + .send_server_notification(ServerNotification::WindowsSandboxSetupCompleted( + notification, + )) + .await; + }); + } + async fn resolve_rollout_path(&self, conversation_id: ThreadId) -> Option { match self.thread_manager.get_thread(conversation_id).await { Ok(conv) => conv.rollout_path(), diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 77ce94e6f..eac38e145 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -64,6 +64,7 @@ use codex_app_server_protocol::ThreadUnarchiveParams; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnSteerParams; +use codex_app_server_protocol::WindowsSandboxSetupStartParams; use codex_core::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR; use tokio::process::Command; @@ -589,6 +590,14 @@ impl McpProcess { self.send_request("review/start", params).await } + pub async fn send_windows_sandbox_setup_start_request( + &mut self, + params: WindowsSandboxSetupStartParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("windowsSandbox/setupStart", params).await + } + /// Send a `cancelLoginChatGpt` JSON-RPC request. pub async fn send_cancel_login_chat_gpt_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index e7705c295..5cfe7de46 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -30,3 +30,4 @@ mod turn_interrupt; mod turn_start; mod turn_start_zsh_fork; mod turn_steer; +mod windows_sandbox_setup; diff --git a/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs b/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs new file mode 100644 index 000000000..1f069b3ca --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs @@ -0,0 +1,63 @@ +use anyhow::Context; +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::create_mock_responses_server_sequence_unchecked; +use app_test_support::to_response; +use app_test_support::write_mock_responses_config_toml; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::WindowsSandboxSetupCompletedNotification; +use codex_app_server_protocol::WindowsSandboxSetupMode; +use codex_app_server_protocol::WindowsSandboxSetupStartParams; +use codex_app_server_protocol::WindowsSandboxSetupStartResponse; +use pretty_assertions::assert_eq; +use std::collections::BTreeMap; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); + +#[tokio::test] +async fn windows_sandbox_setup_start_emits_completion_notification() -> Result<()> { + let responses = Vec::new(); + let server = create_mock_responses_server_sequence_unchecked(responses).await; + let codex_home = TempDir::new()?; + write_mock_responses_config_toml( + codex_home.path(), + &server.uri(), + &BTreeMap::new(), + 500_000, + Some(false), + "mock_provider", + "compact prompt", + )?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_windows_sandbox_setup_start_request(WindowsSandboxSetupStartParams { + mode: WindowsSandboxSetupMode::Unelevated, + }) + .await?; + let response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let start_payload: WindowsSandboxSetupStartResponse = to_response(response)?; + assert!(start_payload.started); + + let notification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("windowsSandbox/setupCompleted"), + ) + .await??; + let payload: WindowsSandboxSetupCompletedNotification = serde_json::from_value( + notification + .params + .context("missing windowsSandbox/setupCompleted params")?, + )?; + + assert_eq!(payload.mode, WindowsSandboxSetupMode::Unelevated); + Ok(()) +} diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 363bec3c7..6e0067aa0 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -1,16 +1,20 @@ use crate::config::Config; use crate::config::ConfigToml; +use crate::config::edit::ConfigEditsBuilder; use crate::config::profile::ConfigProfile; use crate::config::types::WindowsSandboxModeToml; +use crate::default_client::originator; use crate::features::Feature; use crate::features::Features; use crate::features::FeaturesToml; use crate::protocol::SandboxPolicy; +use codex_otel::sanitize_metric_tag_value; use codex_protocol::config_types::WindowsSandboxLevel; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; +use std::time::Instant; /// Kill switch for the elevated sandbox NUX on Windows. /// @@ -243,6 +247,185 @@ pub fn run_setup_refresh_with_extra_read_roots( anyhow::bail!("Windows sandbox read-root refresh is only supported on Windows") } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WindowsSandboxSetupMode { + Elevated, + Unelevated, +} + +#[derive(Debug, Clone)] +pub struct WindowsSandboxSetupRequest { + pub mode: WindowsSandboxSetupMode, + pub policy: SandboxPolicy, + pub policy_cwd: PathBuf, + pub command_cwd: PathBuf, + pub env_map: HashMap, + pub codex_home: PathBuf, + pub active_profile: Option, +} + +pub async fn run_windows_sandbox_setup(request: WindowsSandboxSetupRequest) -> anyhow::Result<()> { + let start = Instant::now(); + let mode = request.mode; + let originator_tag = sanitize_metric_tag_value(originator().value.as_str()); + let result = run_windows_sandbox_setup_and_persist(request).await; + + match result { + Ok(()) => { + emit_windows_sandbox_setup_success_metrics( + mode, + originator_tag.as_str(), + start.elapsed(), + ); + Ok(()) + } + Err(err) => { + emit_windows_sandbox_setup_failure_metrics( + mode, + originator_tag.as_str(), + start.elapsed(), + &err, + ); + Err(err) + } + } +} + +async fn run_windows_sandbox_setup_and_persist( + request: WindowsSandboxSetupRequest, +) -> anyhow::Result<()> { + let mode = request.mode; + let policy = request.policy; + let policy_cwd = request.policy_cwd; + let command_cwd = request.command_cwd; + let env_map = request.env_map; + let codex_home = request.codex_home; + let active_profile = request.active_profile; + let setup_codex_home = codex_home.clone(); + + let setup_result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { + match mode { + WindowsSandboxSetupMode::Elevated => { + if !sandbox_setup_is_complete(setup_codex_home.as_path()) { + run_elevated_setup( + &policy, + policy_cwd.as_path(), + command_cwd.as_path(), + &env_map, + setup_codex_home.as_path(), + )?; + } + } + WindowsSandboxSetupMode::Unelevated => { + run_legacy_setup_preflight( + &policy, + policy_cwd.as_path(), + command_cwd.as_path(), + &env_map, + setup_codex_home.as_path(), + )?; + } + } + Ok(()) + }) + .await + .map_err(|join_err| anyhow::anyhow!("windows sandbox setup task failed: {join_err}"))?; + + setup_result?; + + ConfigEditsBuilder::new(codex_home.as_path()) + .with_profile(active_profile.as_deref()) + .set_windows_sandbox_mode(windows_sandbox_setup_mode_tag(mode)) + .clear_legacy_windows_sandbox_keys() + .apply() + .await + .map_err(|err| anyhow::anyhow!("failed to persist windows sandbox mode: {err}")) +} + +fn emit_windows_sandbox_setup_success_metrics( + mode: WindowsSandboxSetupMode, + originator_tag: &str, + duration: std::time::Duration, +) { + let Some(metrics) = codex_otel::metrics::global() else { + return; + }; + let mode_tag = windows_sandbox_setup_mode_tag(mode); + let _ = metrics.record_duration( + "codex.windows_sandbox.setup_duration_ms", + duration, + &[ + ("result", "success"), + ("originator", originator_tag), + ("mode", mode_tag), + ], + ); + let _ = metrics.counter( + "codex.windows_sandbox.setup_success", + 1, + &[("originator", originator_tag), ("mode", mode_tag)], + ); +} + +fn emit_windows_sandbox_setup_failure_metrics( + mode: WindowsSandboxSetupMode, + originator_tag: &str, + duration: std::time::Duration, + _err: &anyhow::Error, +) { + let Some(metrics) = codex_otel::metrics::global() else { + return; + }; + let mode_tag = windows_sandbox_setup_mode_tag(mode); + let _ = metrics.record_duration( + "codex.windows_sandbox.setup_duration_ms", + duration, + &[ + ("result", "failure"), + ("originator", originator_tag), + ("mode", mode_tag), + ], + ); + let _ = metrics.counter( + "codex.windows_sandbox.setup_failure", + 1, + &[("originator", originator_tag), ("mode", mode_tag)], + ); + + if matches!(mode, WindowsSandboxSetupMode::Elevated) { + #[cfg(target_os = "windows")] + { + let mut failure_tags: Vec<(&str, &str)> = vec![("originator", originator_tag)]; + let mut code_tag: Option = None; + let mut message_tag: Option = None; + if let Some((code, message)) = elevated_setup_failure_details(_err) { + code_tag = Some(code); + message_tag = Some(message); + } + if let Some(code) = code_tag.as_deref() { + failure_tags.push(("code", code)); + } + if let Some(message) = message_tag.as_deref() { + failure_tags.push(("message", message)); + } + let _ = metrics.counter(elevated_setup_failure_metric_name(_err), 1, &failure_tags); + } + } else { + let _ = metrics.counter( + "codex.windows_sandbox.legacy_setup_preflight_failed", + 1, + &[("originator", originator_tag)], + ); + } +} + +fn windows_sandbox_setup_mode_tag(mode: WindowsSandboxSetupMode) -> &'static str { + match mode { + WindowsSandboxSetupMode::Elevated => "elevated", + WindowsSandboxSetupMode::Unelevated => "unelevated", + } +} + #[cfg(test)] mod tests { use super::*;