From 9aa19c5e5f3a42df5f01277a54dfcaca181f0f61 Mon Sep 17 00:00:00 2001 From: acrognale-oai Date: Tue, 26 May 2026 12:02:07 -0400 Subject: [PATCH] feat(runtime-install): run installs through executor processes --- codex-rs/Cargo.lock | 23 +- codex-rs/Cargo.toml | 2 + .../schema/json/ClientRequest.json | 27 +- .../schema/json/ServerNotification.json | 65 ++++ .../codex_app_server_protocol.schemas.json | 114 +++++- .../codex_app_server_protocol.v2.schemas.json | 114 +++++- .../json/v2/RuntimeInstallCancelResponse.json | 22 ++ .../schema/json/v2/RuntimeInstallParams.json | 4 +- .../RuntimeInstallProgressNotification.json | 49 +++ .../schema/typescript/ClientRequest.ts | 2 +- .../schema/typescript/ServerNotification.ts | 3 +- .../v2/RuntimeInstallCancelResponse.ts | 6 + .../v2/RuntimeInstallCancelStatus.ts | 5 + .../typescript/v2/RuntimeInstallManifest.ts | 5 - .../v2/RuntimeInstallManifestParams.ts | 5 + .../typescript/v2/RuntimeInstallParams.ts | 4 +- .../v2/RuntimeInstallProgressNotification.ts | 6 + .../v2/RuntimeInstallProgressPhase.ts | 5 + .../schema/typescript/v2/index.ts | 6 +- .../src/protocol/common.rs | 6 + .../src/protocol/v2/runtime.rs | 48 ++- codex-rs/app-server/Cargo.toml | 1 + codex-rs/app-server/README.md | 3 + codex-rs/app-server/src/lib.rs | 1 + codex-rs/app-server/src/message_processor.rs | 44 +-- codex-rs/app-server/src/request_processors.rs | 2 + .../runtime_install_processor.rs | 156 ++++++++ .../app-server/src/runtime_install_worker.rs | 143 +++++++ codex-rs/arg0/Cargo.toml | 1 + codex-rs/arg0/src/lib.rs | 4 + codex-rs/exec-server/Cargo.toml | 2 - codex-rs/exec-server/src/client.rs | 77 +++- codex-rs/exec-server/src/environment.rs | 72 ++-- codex-rs/exec-server/src/lib.rs | 2 - codex-rs/exec-server/src/protocol.rs | 2 +- codex-rs/exec-server/src/rpc.rs | 41 +- codex-rs/exec-server/src/server/handler.rs | 18 +- codex-rs/exec-server/src/server/processor.rs | 1 - codex-rs/exec-server/src/server/registry.rs | 8 - codex-rs/exec-server/tests/http_client.rs | 1 + codex-rs/runtime-install/BUILD.bazel | 6 + codex-rs/runtime-install/Cargo.toml | 38 ++ codex-rs/runtime-install/src/errors.rs | 20 + codex-rs/runtime-install/src/helper.rs | 117 ++++++ .../src/installer.rs} | 364 +++++++++++++++--- codex-rs/runtime-install/src/lib.rs | 8 + 46 files changed, 1452 insertions(+), 201 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallCancelResponse.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallProgressNotification.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallCancelResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallCancelStatus.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallManifest.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallManifestParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallProgressNotification.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallProgressPhase.ts create mode 100644 codex-rs/app-server/src/request_processors/runtime_install_processor.rs create mode 100644 codex-rs/app-server/src/runtime_install_worker.rs create mode 100644 codex-rs/runtime-install/BUILD.bazel create mode 100644 codex-rs/runtime-install/Cargo.toml create mode 100644 codex-rs/runtime-install/src/errors.rs create mode 100644 codex-rs/runtime-install/src/helper.rs rename codex-rs/{exec-server/src/runtime_install.rs => runtime-install/src/installer.rs} (75%) create mode 100644 codex-rs/runtime-install/src/lib.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 479bdbf036..dec4f9893d 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1917,6 +1917,7 @@ dependencies = [ "codex-protocol", "codex-rmcp-client", "codex-rollout", + "codex-runtime-install", "codex-sandboxing", "codex-shell-command", "codex-state", @@ -2120,6 +2121,7 @@ dependencies = [ "codex-apply-patch", "codex-exec-server", "codex-linux-sandbox", + "codex-runtime-install", "codex-sandboxing", "codex-shell-escalation", "codex-utils-absolute-path", @@ -2770,7 +2772,6 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2", "tempfile", "test-case", "thiserror 2.0.18", @@ -2781,7 +2782,6 @@ dependencies = [ "tracing", "uuid", "wiremock", - "zip 2.4.2", ] [[package]] @@ -3586,6 +3586,25 @@ dependencies = [ "uuid", ] +[[package]] +name = "codex-runtime-install" +version = "0.0.0" +dependencies = [ + "codex-app-server-protocol", + "codex-utils-absolute-path", + "futures", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "tokio-util", + "tracing", + "zip 2.4.2", +] + [[package]] name = "codex-sandboxing" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index df169504f0..845e4881a0 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -42,6 +42,7 @@ members = [ "exec", "file-system", "exec-server", + "runtime-install", "execpolicy", "execpolicy-legacy", "ext/extension-api", @@ -161,6 +162,7 @@ codex-core-skills = { path = "core-skills" } codex-exec = { path = "exec" } codex-file-system = { path = "file-system" } codex-exec-server = { path = "exec-server" } +codex-runtime-install = { path = "runtime-install" } codex-execpolicy = { path = "execpolicy" } codex-extension-api = { path = "ext/extension-api" } codex-goal-extension = { path = "ext/goal" } diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index eb9ef2ae72..21291a62ec 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2765,7 +2765,7 @@ } ] }, - "RuntimeInstallManifest": { + "RuntimeInstallManifestParams": { "properties": { "archiveName": { "type": [ @@ -2829,7 +2829,7 @@ ] }, "manifest": { - "$ref": "#/definitions/RuntimeInstallManifest" + "$ref": "#/definitions/RuntimeInstallManifestParams" }, "release": { "type": "string" @@ -5409,6 +5409,29 @@ "title": "Runtime/installRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "runtime/install/cancel" + ], + "title": "Runtime/install/cancelRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Runtime/install/cancelRequest", + "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 90899cb152..e50e56e972 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2946,6 +2946,51 @@ }, "type": "object" }, + "RuntimeInstallProgressNotification": { + "properties": { + "bundleVersion": { + "type": [ + "string", + "null" + ] + }, + "downloadedBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "phase": { + "$ref": "#/definitions/RuntimeInstallProgressPhase" + }, + "totalBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "phase" + ], + "type": "object" + }, + "RuntimeInstallProgressPhase": { + "enum": [ + "checking", + "downloading", + "verifying", + "extracting", + "validating", + "installed", + "configuring" + ], + "type": "string" + }, "SandboxPolicy": { "oneOf": [ { @@ -5397,6 +5442,26 @@ "title": "Skills/changedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "runtime/install/progress" + ], + "title": "Runtime/install/progressNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/RuntimeInstallProgressNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Runtime/install/progressNotification", + "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 60cc12bbab..c649b03da3 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 @@ -1333,6 +1333,29 @@ "title": "Runtime/installRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "runtime/install/cancel" + ], + "title": "Runtime/install/cancelRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Runtime/install/cancelRequest", + "type": "object" + }, { "properties": { "id": { @@ -4091,6 +4114,26 @@ "title": "Skills/changedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "runtime/install/progress" + ], + "title": "Runtime/install/progressNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/RuntimeInstallProgressNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Runtime/install/progressNotification", + "type": "object" + }, { "properties": { "method": { @@ -14637,7 +14680,27 @@ } ] }, - "RuntimeInstallManifest": { + "RuntimeInstallCancelResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/v2/RuntimeInstallCancelStatus" + } + }, + "required": [ + "status" + ], + "title": "RuntimeInstallCancelResponse", + "type": "object" + }, + "RuntimeInstallCancelStatus": { + "enum": [ + "canceled", + "not-found" + ], + "type": "string" + }, + "RuntimeInstallManifestParams": { "properties": { "archiveName": { "type": [ @@ -14702,7 +14765,7 @@ ] }, "manifest": { - "$ref": "#/definitions/v2/RuntimeInstallManifest" + "$ref": "#/definitions/v2/RuntimeInstallManifestParams" }, "release": { "type": "string" @@ -14755,6 +14818,53 @@ ], "type": "object" }, + "RuntimeInstallProgressNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "bundleVersion": { + "type": [ + "string", + "null" + ] + }, + "downloadedBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "phase": { + "$ref": "#/definitions/v2/RuntimeInstallProgressPhase" + }, + "totalBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "phase" + ], + "title": "RuntimeInstallProgressNotification", + "type": "object" + }, + "RuntimeInstallProgressPhase": { + "enum": [ + "checking", + "downloading", + "verifying", + "extracting", + "validating", + "installed", + "configuring" + ], + "type": "string" + }, "RuntimeInstallResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index dd49a575eb..29d164f69d 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 @@ -2059,6 +2059,29 @@ "title": "Runtime/installRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "runtime/install/cancel" + ], + "title": "Runtime/install/cancelRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Runtime/install/cancelRequest", + "type": "object" + }, { "properties": { "id": { @@ -11166,7 +11189,27 @@ } ] }, - "RuntimeInstallManifest": { + "RuntimeInstallCancelResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/RuntimeInstallCancelStatus" + } + }, + "required": [ + "status" + ], + "title": "RuntimeInstallCancelResponse", + "type": "object" + }, + "RuntimeInstallCancelStatus": { + "enum": [ + "canceled", + "not-found" + ], + "type": "string" + }, + "RuntimeInstallManifestParams": { "properties": { "archiveName": { "type": [ @@ -11231,7 +11274,7 @@ ] }, "manifest": { - "$ref": "#/definitions/RuntimeInstallManifest" + "$ref": "#/definitions/RuntimeInstallManifestParams" }, "release": { "type": "string" @@ -11284,6 +11327,53 @@ ], "type": "object" }, + "RuntimeInstallProgressNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "bundleVersion": { + "type": [ + "string", + "null" + ] + }, + "downloadedBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "phase": { + "$ref": "#/definitions/RuntimeInstallProgressPhase" + }, + "totalBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "phase" + ], + "title": "RuntimeInstallProgressNotification", + "type": "object" + }, + "RuntimeInstallProgressPhase": { + "enum": [ + "checking", + "downloading", + "verifying", + "extracting", + "validating", + "installed", + "configuring" + ], + "type": "string" + }, "RuntimeInstallResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -11616,6 +11706,26 @@ "title": "Skills/changedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "runtime/install/progress" + ], + "title": "Runtime/install/progressNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/RuntimeInstallProgressNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Runtime/install/progressNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallCancelResponse.json b/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallCancelResponse.json new file mode 100644 index 0000000000..c7ba89ef08 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallCancelResponse.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "RuntimeInstallCancelStatus": { + "enum": [ + "canceled", + "not-found" + ], + "type": "string" + } + }, + "properties": { + "status": { + "$ref": "#/definitions/RuntimeInstallCancelStatus" + } + }, + "required": [ + "status" + ], + "title": "RuntimeInstallCancelResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallParams.json b/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallParams.json index 975795f178..6fa843ec9e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallParams.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "RuntimeInstallManifest": { + "RuntimeInstallManifestParams": { "properties": { "archiveName": { "type": [ @@ -65,7 +65,7 @@ ] }, "manifest": { - "$ref": "#/definitions/RuntimeInstallManifest" + "$ref": "#/definitions/RuntimeInstallManifestParams" }, "release": { "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallProgressNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallProgressNotification.json new file mode 100644 index 0000000000..34f5071617 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/RuntimeInstallProgressNotification.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "RuntimeInstallProgressPhase": { + "enum": [ + "checking", + "downloading", + "verifying", + "extracting", + "validating", + "installed", + "configuring" + ], + "type": "string" + } + }, + "properties": { + "bundleVersion": { + "type": [ + "string", + "null" + ] + }, + "downloadedBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "phase": { + "$ref": "#/definitions/RuntimeInstallProgressPhase" + }, + "totalBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "phase" + ], + "title": "RuntimeInstallProgressNotification", + "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 f668815797..37779a802d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -86,4 +86,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/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "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/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "runtime/install", id: RequestId, params: RuntimeInstallParams, } | { "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": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "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": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "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/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "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/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "runtime/install", id: RequestId, params: RuntimeInstallParams, } | { "method": "runtime/install/cancel", id: RequestId, params: undefined, } | { "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": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "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": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "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/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index 3ed710efc0..26d43d8b6a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -38,6 +38,7 @@ import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummar import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification"; import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification"; import type { RemoteControlStatusChangedNotification } from "./v2/RemoteControlStatusChangedNotification"; +import type { RuntimeInstallProgressNotification } from "./v2/RuntimeInstallProgressNotification"; import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification"; import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification"; import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; @@ -70,4 +71,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "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": "model/verification", "params": ModelVerificationNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "runtime/install/progress", "params": RuntimeInstallProgressNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "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": "model/verification", "params": ModelVerificationNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallCancelResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallCancelResponse.ts new file mode 100644 index 0000000000..47dd42e9e4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallCancelResponse.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 { RuntimeInstallCancelStatus } from "./RuntimeInstallCancelStatus"; + +export type RuntimeInstallCancelResponse = { status: RuntimeInstallCancelStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallCancelStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallCancelStatus.ts new file mode 100644 index 0000000000..8c530d3912 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallCancelStatus.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 RuntimeInstallCancelStatus = "canceled" | "not-found"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallManifest.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallManifest.ts deleted file mode 100644 index 7bf37ad8c5..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallManifest.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RuntimeInstallManifest = { archiveName: string | null, archiveSha256: string, archiveSizeBytes: bigint | null, archiveUrl: string, bundleFormatVersion: number | null, bundleVersion: string | null, format: string | null, runtimeRootDirectoryName: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallManifestParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallManifestParams.ts new file mode 100644 index 0000000000..e0ee37ddbb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallManifestParams.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 RuntimeInstallManifestParams = { archiveName?: string | null, archiveSha256: string, archiveSizeBytes?: bigint | null, archiveUrl: string, bundleFormatVersion?: number | null, bundleVersion?: string | null, format?: string | null, runtimeRootDirectoryName?: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallParams.ts index d3386dea72..f0cb81cb74 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallParams.ts @@ -1,6 +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 { RuntimeInstallManifest } from "./RuntimeInstallManifest"; +import type { RuntimeInstallManifestParams } from "./RuntimeInstallManifestParams"; -export type RuntimeInstallParams = { environmentId?: string | null, manifest: RuntimeInstallManifest, release: string, }; +export type RuntimeInstallParams = { environmentId?: string | null, manifest: RuntimeInstallManifestParams, release: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallProgressNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallProgressNotification.ts new file mode 100644 index 0000000000..16bd7467ec --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallProgressNotification.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 { RuntimeInstallProgressPhase } from "./RuntimeInstallProgressPhase"; + +export type RuntimeInstallProgressNotification = { bundleVersion: string | null, downloadedBytes: bigint | null, phase: RuntimeInstallProgressPhase, totalBytes: bigint | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallProgressPhase.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallProgressPhase.ts new file mode 100644 index 0000000000..561d3b1cd4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RuntimeInstallProgressPhase.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 RuntimeInstallProgressPhase = "checking" | "downloading" | "verifying" | "extracting" | "validating" | "installed" | "configuring"; 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 6920e05bd7..98bdfff12d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -325,9 +325,13 @@ export type { ReviewDelivery } from "./ReviewDelivery"; export type { ReviewStartParams } from "./ReviewStartParams"; export type { ReviewStartResponse } from "./ReviewStartResponse"; export type { ReviewTarget } from "./ReviewTarget"; -export type { RuntimeInstallManifest } from "./RuntimeInstallManifest"; +export type { RuntimeInstallCancelResponse } from "./RuntimeInstallCancelResponse"; +export type { RuntimeInstallCancelStatus } from "./RuntimeInstallCancelStatus"; +export type { RuntimeInstallManifestParams } from "./RuntimeInstallManifestParams"; export type { RuntimeInstallParams } from "./RuntimeInstallParams"; export type { RuntimeInstallPaths } from "./RuntimeInstallPaths"; +export type { RuntimeInstallProgressNotification } from "./RuntimeInstallProgressNotification"; +export type { RuntimeInstallProgressPhase } from "./RuntimeInstallProgressPhase"; export type { RuntimeInstallResponse } from "./RuntimeInstallResponse"; export type { RuntimeInstallStatus } from "./RuntimeInstallStatus"; export type { SandboxMode } from "./SandboxMode"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index ea63e3e0fc..668cd1f20e 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -742,6 +742,11 @@ client_request_definitions! { serialization: global("runtime-install"), response: v2::RuntimeInstallResponse, }, + RuntimeInstallCancel => "runtime/install/cancel" { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + serialization: None, + response: v2::RuntimeInstallCancelResponse, + }, PluginUninstall => "plugin/uninstall" { params: v2::PluginUninstallParams, serialization: global("config"), @@ -1480,6 +1485,7 @@ server_notification_definitions! { ThreadUnarchived => "thread/unarchived" (v2::ThreadUnarchivedNotification), ThreadClosed => "thread/closed" (v2::ThreadClosedNotification), SkillsChanged => "skills/changed" (v2::SkillsChangedNotification), + RuntimeInstallProgress => "runtime/install/progress" (v2::RuntimeInstallProgressNotification), ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification), ThreadGoalUpdated => "thread/goal/updated" (v2::ThreadGoalUpdatedNotification), ThreadGoalCleared => "thread/goal/cleared" (v2::ThreadGoalClearedNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/runtime.rs b/codex-rs/app-server-protocol/src/protocol/v2/runtime.rs index a6831bd639..40fe0deb22 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/runtime.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/runtime.rs @@ -7,14 +7,20 @@ use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct RuntimeInstallManifest { +pub struct RuntimeInstallManifestParams { + #[ts(optional = nullable)] pub archive_name: Option, pub archive_sha256: String, + #[ts(optional = nullable)] pub archive_size_bytes: Option, pub archive_url: String, + #[ts(optional = nullable)] pub bundle_format_version: Option, + #[ts(optional = nullable)] pub bundle_version: Option, + #[ts(optional = nullable)] pub format: Option, + #[ts(optional = nullable)] pub runtime_root_directory_name: Option, } @@ -24,10 +30,25 @@ pub struct RuntimeInstallManifest { pub struct RuntimeInstallParams { #[ts(optional = nullable)] pub environment_id: Option, - pub manifest: Box, + pub manifest: Box, pub release: String, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(export_to = "v2/")] +pub enum RuntimeInstallCancelStatus { + Canceled, + NotFound, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RuntimeInstallCancelResponse { + pub status: RuntimeInstallCancelStatus, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "kebab-case")] #[ts(export_to = "v2/")] @@ -56,3 +77,26 @@ pub struct RuntimeInstallResponse { pub paths: RuntimeInstallPaths, pub status: RuntimeInstallStatus, } + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(export_to = "v2/")] +pub enum RuntimeInstallProgressPhase { + Checking, + Downloading, + Verifying, + Extracting, + Validating, + Installed, + Configuring, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RuntimeInstallProgressNotification { + pub bundle_version: Option, + pub downloaded_bytes: Option, + pub phase: RuntimeInstallProgressPhase, + pub total_bytes: Option, +} diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 95baac4e9e..4385a1210b 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -65,6 +65,7 @@ codex-app-server-transport = { workspace = true } codex-feedback = { workspace = true } codex-rmcp-client = { workspace = true } codex-rollout = { workspace = true } +codex-runtime-install = { workspace = true } codex-sandboxing = { workspace = true } codex-state = { workspace = true } codex-thread-store = { workspace = true } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 2ceffc86fe..41f6f04498 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -212,6 +212,9 @@ Example with notification opt-out: - `remoteControl/status/changed` — notification emitted when the remote-control status or client-visible environment id changes. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `serverName` is the local machine name used by this app-server process; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled. Newly initialized app-server clients always receive the current status snapshot. - `skills/config/write` — write user-level skill config by name or absolute path. - `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**). +- `runtime/install` — install the selected primary runtime bundle and return its executable, dependency, bundled-skill, and bundled-marketplace paths after app-server config synchronization. Runtime installs are process-wide and serialized. +- `runtime/install/progress` — notification sent to the connection that requested `runtime/install` as the active install moves through checking, downloading, verifying, extracting, validating, installed, and configuring phases. Download notifications include byte counts when available. +- `runtime/install/cancel` — cancel the one active `runtime/install` request for this app-server process and report whether an install was found. - `plugin/uninstall` — uninstall a local plugin by `pluginId` in `@` form by removing its cached files and clearing its user-level config entry, or uninstall a remote ChatGPT plugin by backend `pluginId` by forwarding the uninstall to the ChatGPT plugin backend and removing any downloaded remote-plugin cache (**under development; do not call from production clients yet**). - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes. - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 9393330c1d..b490374e0d 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -96,6 +96,7 @@ mod outgoing_message; mod request_processors; mod request_serialization; mod runtime_install; +mod runtime_install_worker; mod server_request_error; mod skills_watcher; mod thread_state; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index e135c9b245..de380c548d 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -7,7 +7,6 @@ use std::sync::atomic::AtomicBool; use crate::attestation::app_server_attestation_provider; use crate::config_manager::ConfigManager; use crate::connection_rpc_gate::ConnectionRpcGate; -use crate::error_code::internal_error; use crate::error_code::invalid_request; use crate::extensions::guardian_agent_spawner; use crate::extensions::thread_extensions; @@ -32,6 +31,7 @@ use crate::request_processors::McpRequestProcessor; use crate::request_processors::PluginRequestProcessor; use crate::request_processors::ProcessExecRequestProcessor; use crate::request_processors::RemoteControlRequestProcessor; +use crate::request_processors::RuntimeInstallRequestProcessor; use crate::request_processors::SearchRequestProcessor; use crate::request_processors::ThreadGoalRequestProcessor; use crate::request_processors::ThreadRequestProcessor; @@ -168,7 +168,6 @@ pub(crate) struct MessageProcessor { command_exec_processor: CommandExecRequestProcessor, process_exec_processor: ProcessExecRequestProcessor, config_processor: ConfigRequestProcessor, - environment_manager: Arc, environment_processor: EnvironmentRequestProcessor, external_agent_config_processor: ExternalAgentConfigRequestProcessor, feedback_processor: FeedbackRequestProcessor, @@ -179,8 +178,8 @@ pub(crate) struct MessageProcessor { mcp_processor: McpRequestProcessor, plugin_processor: PluginRequestProcessor, remote_control_processor: RemoteControlRequestProcessor, + runtime_install_processor: RuntimeInstallRequestProcessor, search_processor: SearchRequestProcessor, - thread_manager: Arc, thread_goal_processor: ThreadGoalRequestProcessor, thread_processor: ThreadRequestProcessor, turn_processor: TurnRequestProcessor, @@ -405,6 +404,11 @@ impl MessageProcessor { workspace_settings_cache, ); let remote_control_processor = RemoteControlRequestProcessor::new(remote_control_handle); + let runtime_install_processor = RuntimeInstallRequestProcessor::new( + Arc::clone(&environment_manager_for_requests), + outgoing.clone(), + Arc::clone(&thread_manager), + ); let search_processor = SearchRequestProcessor::new(outgoing.clone()); let thread_goal_processor = ThreadGoalRequestProcessor::new( Arc::clone(&thread_manager), @@ -491,7 +495,6 @@ impl MessageProcessor { command_exec_processor, process_exec_processor, config_processor, - environment_manager: thread_manager.environment_manager(), environment_processor, external_agent_config_processor, feedback_processor, @@ -502,8 +505,8 @@ impl MessageProcessor { mcp_processor, plugin_processor, remote_control_processor, + runtime_install_processor, search_processor, - thread_manager, thread_goal_processor, thread_processor, turn_processor, @@ -974,29 +977,14 @@ impl MessageProcessor { .await .map(|response| Some(response.into())), ClientRequest::RuntimeInstall { params, .. } => { - let mut params = params; - let environment = if let Some(environment_id) = params.environment_id.take() { - self.environment_manager - .get_environment(&environment_id) - .ok_or_else(|| { - invalid_request(format!( - "unknown runtime install environment id `{environment_id}`" - )) - })? - } else { - self.environment_manager - .default_or_local_environment() - .ok_or_else(|| { - internal_error("runtime install environment is not configured") - })? - }; - let response = environment.install_runtime(params).await?; - let response = - crate::runtime_install::finalize_runtime_install(&environment, response) - .await?; - self.thread_manager.plugins_manager().clear_cache(); - self.thread_manager.skills_manager().clear_cache(); - Ok(Some(response.into())) + self.runtime_install_processor + .install_runtime(connection_id, params) + .await + } + ClientRequest::RuntimeInstallCancel { .. } => { + self.runtime_install_processor + .cancel_runtime_install() + .await } ClientRequest::ThreadStart { params, .. } => { self.thread_processor diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index d0958e80ab..94d4a468dc 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -466,6 +466,7 @@ mod mcp_processor; mod plugins; mod process_exec_processor; mod remote_control_processor; +mod runtime_install_processor; mod search; mod thread_processor; mod token_usage_replay; @@ -488,6 +489,7 @@ pub(crate) use mcp_processor::McpRequestProcessor; pub(crate) use plugins::PluginRequestProcessor; pub(crate) use process_exec_processor::ProcessExecRequestProcessor; pub(crate) use remote_control_processor::RemoteControlRequestProcessor; +pub(crate) use runtime_install_processor::RuntimeInstallRequestProcessor; pub(crate) use search::SearchRequestProcessor; pub(crate) use thread_goal_processor::ThreadGoalRequestProcessor; pub(crate) use thread_processor::ThreadRequestProcessor; diff --git a/codex-rs/app-server/src/request_processors/runtime_install_processor.rs b/codex-rs/app-server/src/request_processors/runtime_install_processor.rs new file mode 100644 index 0000000000..01de9047a5 --- /dev/null +++ b/codex-rs/app-server/src/request_processors/runtime_install_processor.rs @@ -0,0 +1,156 @@ +use super::*; +use codex_app_server_protocol::RuntimeInstallCancelResponse; +use codex_app_server_protocol::RuntimeInstallCancelStatus; +use codex_app_server_protocol::RuntimeInstallParams; +use codex_app_server_protocol::RuntimeInstallProgressNotification; +use codex_app_server_protocol::RuntimeInstallProgressPhase; +use std::sync::Mutex as StdMutex; + +#[derive(Clone)] +pub(crate) struct RuntimeInstallRequestProcessor { + environment_manager: Arc, + outgoing: Arc, + thread_manager: Arc, + active_install: Arc>>, +} + +impl RuntimeInstallRequestProcessor { + pub(crate) fn new( + environment_manager: Arc, + outgoing: Arc, + thread_manager: Arc, + ) -> Self { + Self { + environment_manager, + outgoing, + thread_manager, + active_install: Arc::new(StdMutex::new(None)), + } + } + + pub(crate) async fn install_runtime( + &self, + connection_id: ConnectionId, + mut params: RuntimeInstallParams, + ) -> Result, JSONRPCErrorError> { + let (cancellation, _active_install) = self.begin_install()?; + let environment = if let Some(environment_id) = params.environment_id.take() { + self.environment_manager + .get_environment(&environment_id) + .ok_or_else(|| { + invalid_request(format!( + "unknown runtime install environment id `{environment_id}`" + )) + })? + } else { + self.environment_manager + .default_or_local_environment() + .ok_or_else(|| internal_error("runtime install environment is not configured"))? + }; + + let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel(); + let outgoing = Arc::clone(&self.outgoing); + let progress_forwarder = tokio::spawn(async move { + while let Some(progress) = progress_rx.recv().await { + outgoing + .send_server_notification_to_connections( + &[connection_id], + ServerNotification::RuntimeInstallProgress(progress), + ) + .await; + } + }); + + let install_result = crate::runtime_install_worker::install_runtime_with_progress( + &environment, + params, + progress_tx, + cancellation, + ) + .await; + if let Err(error) = progress_forwarder.await { + warn!("runtime install progress forwarder failed: {error}"); + } + + let response = install_result?; + self.send_progress( + connection_id, + RuntimeInstallProgressNotification { + bundle_version: response.bundle_version.clone(), + downloaded_bytes: None, + phase: RuntimeInstallProgressPhase::Configuring, + total_bytes: None, + }, + ) + .await; + let response = + crate::runtime_install::finalize_runtime_install(&environment, response).await?; + self.thread_manager.plugins_manager().clear_cache(); + self.thread_manager.skills_manager().clear_cache(); + Ok(Some(response.into())) + } + + pub(crate) async fn cancel_runtime_install( + &self, + ) -> Result, JSONRPCErrorError> { + let status = { + let active_install = self.active_install(); + match active_install.as_ref() { + Some(cancellation) => { + cancellation.cancel(); + RuntimeInstallCancelStatus::Canceled + } + None => RuntimeInstallCancelStatus::NotFound, + } + }; + Ok(Some(RuntimeInstallCancelResponse { status }.into())) + } + + fn begin_install(&self) -> Result<(CancellationToken, ActiveInstallGuard), JSONRPCErrorError> { + let cancellation = CancellationToken::new(); + let mut active_install = self.active_install(); + if active_install.is_some() { + return Err(invalid_request("runtime install is already in progress")); + } + *active_install = Some(cancellation.clone()); + drop(active_install); + Ok(( + cancellation, + ActiveInstallGuard { + active_install: Arc::clone(&self.active_install), + }, + )) + } + + fn active_install(&self) -> std::sync::MutexGuard<'_, Option> { + self.active_install + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + } + + async fn send_progress( + &self, + connection_id: ConnectionId, + progress: RuntimeInstallProgressNotification, + ) { + self.outgoing + .send_server_notification_to_connections( + &[connection_id], + ServerNotification::RuntimeInstallProgress(progress), + ) + .await; + } +} + +struct ActiveInstallGuard { + active_install: Arc>>, +} + +impl Drop for ActiveInstallGuard { + fn drop(&mut self) { + self.active_install + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take(); + } +} diff --git a/codex-rs/app-server/src/runtime_install_worker.rs b/codex-rs/app-server/src/runtime_install_worker.rs new file mode 100644 index 0000000000..f68ce4a796 --- /dev/null +++ b/codex-rs/app-server/src/runtime_install_worker.rs @@ -0,0 +1,143 @@ +use std::collections::HashMap; + +use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::RuntimeInstallParams; +use codex_app_server_protocol::RuntimeInstallProgressNotification; +use codex_app_server_protocol::RuntimeInstallResponse; +use codex_exec_server::Environment; +use codex_exec_server::ExecEnvPolicy; +use codex_exec_server::ExecOutputStream; +use codex_exec_server::ExecParams; +use codex_exec_server::ExecProcessEvent; +use codex_exec_server::ProcessId; +use codex_exec_server::WriteStatus; +use codex_protocol::config_types::ShellEnvironmentPolicyInherit; +use codex_runtime_install::CODEX_RUNTIME_INSTALL_HELPER_ARG1; +use codex_runtime_install::RuntimeInstallHelperMessage; +use codex_runtime_install::RuntimeInstallHelperRequest; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use uuid::Uuid; + +use crate::error_code::internal_error; + +pub(crate) async fn install_runtime_with_progress( + environment: &Environment, + params: RuntimeInstallParams, + progress: mpsc::UnboundedSender, + cancellation: CancellationToken, +) -> Result { + let executable = environment.codex_self_exe().await?; + let cwd = environment.codex_home().await?; + let started = environment + .get_exec_backend() + .start(ExecParams { + process_id: ProcessId::from(format!("runtime-install-{}", Uuid::now_v7())), + argv: vec![ + executable.as_path().to_string_lossy().into_owned(), + CODEX_RUNTIME_INSTALL_HELPER_ARG1.to_string(), + ], + cwd: cwd.as_path().to_path_buf(), + env_policy: Some(ExecEnvPolicy { + inherit: ShellEnvironmentPolicyInherit::All, + ignore_default_excludes: true, + exclude: Vec::new(), + r#set: HashMap::new(), + include_only: Vec::new(), + }), + env: HashMap::new(), + tty: false, + pipe_stdin: true, + arg0: None, + }) + .await + .map_err(|err| internal_error(format!("failed to start runtime install helper: {err}")))?; + write_helper_request( + started.process.as_ref(), + &RuntimeInstallHelperRequest::Install { params }, + ) + .await?; + + let mut events = started.process.subscribe_events(); + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let mut exit_code = None; + let mut cancellation_sent = false; + loop { + tokio::select! { + _ = cancellation.cancelled(), if !cancellation_sent => { + write_helper_request(started.process.as_ref(), &RuntimeInstallHelperRequest::Cancel).await?; + cancellation_sent = true; + } + event = events.recv() => { + let event = event.map_err(|err| { + internal_error(format!("runtime install helper output stream failed: {err}")) + })?; + match event { + ExecProcessEvent::Output(chunk) => match chunk.stream { + ExecOutputStream::Stdout => { + stdout.extend_from_slice(&chunk.chunk.0); + while let Some(line_end) = stdout.iter().position(|byte| *byte == b'\n') { + let line = stdout.drain(..=line_end).collect::>(); + let message: RuntimeInstallHelperMessage = serde_json::from_slice( + line.strip_suffix(b"\n").unwrap_or(line.as_slice()), + ) + .map_err(|err| { + internal_error(format!("runtime install helper returned invalid output: {err}")) + })?; + match message { + RuntimeInstallHelperMessage::Progress { progress: update } => { + let _ = progress.send(update); + } + RuntimeInstallHelperMessage::Complete { response } => { + return Ok(response); + } + RuntimeInstallHelperMessage::Error { error } => return Err(error), + } + } + } + ExecOutputStream::Stderr => stderr.extend_from_slice(&chunk.chunk.0), + ExecOutputStream::Pty => { + return Err(internal_error("runtime install helper unexpectedly used a pty")); + } + }, + ExecProcessEvent::Exited { exit_code: code, .. } => exit_code = Some(code), + ExecProcessEvent::Closed { .. } => { + let stderr = String::from_utf8_lossy(&stderr); + return Err(internal_error(format!( + "runtime install helper exited without a result (exit code {}; stderr: {stderr})", + exit_code.unwrap_or(-1) + ))); + } + ExecProcessEvent::Failed(message) => { + return Err(internal_error(format!("runtime install helper process failed: {message}"))); + } + } + } + } + } +} + +async fn write_helper_request( + process: &dyn codex_exec_server::ExecProcess, + request: &RuntimeInstallHelperRequest, +) -> Result<(), JSONRPCErrorError> { + let mut encoded = serde_json::to_vec(request).map_err(|err| { + internal_error(format!( + "failed to serialize runtime install request: {err}" + )) + })?; + encoded.push(b'\n'); + let response = process.write(encoded).await.map_err(|err| { + internal_error(format!( + "failed to write runtime install helper input: {err}" + )) + })?; + if response.status != WriteStatus::Accepted { + return Err(internal_error(format!( + "runtime install helper rejected stdin: {:?}", + response.status + ))); + } + Ok(()) +} diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index 7ee21a770e..f4416372a9 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -17,6 +17,7 @@ anyhow = { workspace = true } codex-apply-patch = { workspace = true } codex-exec-server = { workspace = true } codex-linux-sandbox = { workspace = true } +codex-runtime-install = { workspace = true } codex-sandboxing = { workspace = true } codex-shell-escalation = { workspace = true } codex-utils-absolute-path = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 2f6ae4653c..94563802b6 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; use codex_exec_server::CODEX_FS_HELPER_ARG1; +use codex_runtime_install::CODEX_RUNTIME_INSTALL_HELPER_ARG1; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_utils_home_dir::find_codex_home; #[cfg(unix)] @@ -97,6 +98,9 @@ pub fn arg0_dispatch() -> Option { if argv1 == CODEX_FS_HELPER_ARG1 { codex_exec_server::run_fs_helper_main(); } + if argv1 == CODEX_RUNTIME_INSTALL_HELPER_ARG1 { + codex_runtime_install::run_runtime_install_helper_main(); + } if argv1 == CODEX_CORE_APPLY_PATCH_ARG1 { let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned)); let exit_code = match patch_arg { diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index b238187496..6ebe8f24fa 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -30,7 +30,6 @@ reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] } prost = "0.14.3" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -sha2 = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } @@ -49,7 +48,6 @@ tokio-util = { workspace = true, features = ["rt"] } tokio-tungstenite = { workspace = true } tracing = { workspace = true } uuid = { workspace = true, features = ["v4"] } -zip = { workspace = true } [dev-dependencies] anyhow = { workspace = true } diff --git a/codex-rs/exec-server/src/client.rs b/codex-rs/exec-server/src/client.rs index cbe6a0fc79..82446257ca 100644 --- a/codex-rs/exec-server/src/client.rs +++ b/codex-rs/exec-server/src/client.rs @@ -8,8 +8,6 @@ use std::time::Duration; use arc_swap::ArcSwap; use codex_app_server_protocol::JSONRPCNotification; -use codex_app_server_protocol::RuntimeInstallParams; -use codex_app_server_protocol::RuntimeInstallResponse; use codex_utils_absolute_path::AbsolutePathBuf; use futures::FutureExt; use futures::future::BoxFuture; @@ -72,7 +70,6 @@ use crate::protocol::INITIALIZED_METHOD; use crate::protocol::InitializeParams; use crate::protocol::InitializeResponse; use crate::protocol::ProcessOutputChunk; -use crate::protocol::RUNTIME_INSTALL_METHOD; use crate::protocol::ReadParams; use crate::protocol::ReadResponse; use crate::protocol::TerminateParams; @@ -82,6 +79,7 @@ use crate::protocol::WriteResponse; use crate::rpc::RpcCallError; use crate::rpc::RpcClient; use crate::rpc::RpcClientEvent; +use crate::rpc::RpcPendingResponse; pub(crate) mod http_client; @@ -180,6 +178,7 @@ struct Inner { http_body_stream_next_id: AtomicU64, session_id: std::sync::RwLock>, codex_home: std::sync::RwLock>, + codex_self_exe: std::sync::RwLock>, reader_task: tokio::task::JoinHandle<()>, } @@ -354,6 +353,14 @@ impl ExecServerClient { .unwrap_or_else(std::sync::PoisonError::into_inner); *codex_home = Some(response.codex_home.clone()); } + { + let mut codex_self_exe = self + .inner + .codex_self_exe + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *codex_self_exe = Some(response.codex_self_exe.clone()); + } self.notify_initialized().await?; Ok(response) }) @@ -445,13 +452,6 @@ impl ExecServerClient { self.call(FS_COPY_METHOD, ¶ms).await } - pub async fn runtime_install( - &self, - params: RuntimeInstallParams, - ) -> Result { - self.call(RUNTIME_INSTALL_METHOD, ¶ms).await - } - pub(crate) async fn register_session( &self, process_id: &ProcessId, @@ -491,6 +491,14 @@ impl ExecServerClient { .clone() } + pub fn codex_self_exe(&self) -> Option { + self.inner + .codex_self_exe + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + } + pub(crate) async fn connect( connection: JsonRpcConnection, options: ExecServerClientConnectOptions, @@ -539,6 +547,7 @@ impl ExecServerClient { http_body_stream_next_id: AtomicU64::new(1), session_id: std::sync::RwLock::new(None), codex_home: std::sync::RwLock::new(None), + codex_self_exe: std::sync::RwLock::new(None), reader_task, } }); @@ -560,6 +569,18 @@ impl ExecServerClient { where P: serde::Serialize, T: serde::de::DeserializeOwned, + { + let response = self.start_call(method, params).await?; + self.finish_call(response).await + } + + async fn start_call

( + &self, + method: &str, + params: &P, + ) -> Result + where + P: serde::Serialize, { // Reject new work before allocating a JSON-RPC request id. MCP tool // calls, process writes, and fs operations all pass through here, so @@ -568,7 +589,17 @@ impl ExecServerClient { return Err(error); } - match self.inner.client.call(method, params).await { + match self.inner.client.start_call(method, params).await { + Ok(response) => Ok(response), + Err(error) => Err(ExecServerError::from(error)), + } + } + + async fn finish_call(&self, response: RpcPendingResponse) -> Result + where + T: serde::de::DeserializeOwned, + { + match response.response().await { Ok(response) => Ok(response), Err(error) => { let error = ExecServerError::from(error); @@ -1081,6 +1112,10 @@ mod tests { std::env::current_dir().expect("current dir"), ) .expect("absolute current dir"), + codex_self_exe: AbsolutePathBuf::try_from( + std::env::current_exe().expect("current exe"), + ) + .expect("absolute current exe"), }) .expect("initialize response should serialize"), }), @@ -1116,7 +1151,7 @@ mod tests { program: "sh".to_string(), args: vec![ "-c".to_string(), - "read _line; printf '%s\\n' '{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\",\"codexHome\":\"/tmp\"}}'; read _line; sleep 60".to_string(), + "read _line; printf '%s\\n' '{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\",\"codexHome\":\"/tmp\",\"codexSelfExe\":\"/tmp/codex\"}}'; read _line; sleep 60".to_string(), ], env: HashMap::new(), cwd: None, @@ -1140,7 +1175,7 @@ mod tests { program: "sh".to_string(), args: vec![ "-c".to_string(), - "read _line; printf '%s\\n' '{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\",\"codexHome\":\"/tmp\"}}'; read _line; sleep 60".to_string(), + "read _line; printf '%s\\n' '{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\",\"codexHome\":\"/tmp\",\"codexSelfExe\":\"/tmp/codex\"}}'; read _line; sleep 60".to_string(), ], env: HashMap::new(), cwd: None, @@ -1163,7 +1198,7 @@ mod tests { args: vec![ "-NoProfile".to_string(), "-Command".to_string(), - "$null = [Console]::In.ReadLine(); [Console]::Out.WriteLine('{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\",\"codexHome\":\"C:\\\\Users\\\\codex\\\\.codex\"}}'); $null = [Console]::In.ReadLine(); Start-Sleep -Seconds 60".to_string(), + "$null = [Console]::In.ReadLine(); [Console]::Out.WriteLine('{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\",\"codexHome\":\"C:\\\\Users\\\\codex\\\\.codex\",\"codexSelfExe\":\"C:\\\\codex\\\\codex.exe\"}}'); $null = [Console]::In.ReadLine(); Start-Sleep -Seconds 60".to_string(), ], env: HashMap::new(), cwd: None, @@ -1188,7 +1223,7 @@ mod tests { "read _line; \ echo \"$$\" > {}; \ sleep 60 >/dev/null 2>&1 & echo \"$!\" > {}; \ - printf '%s\\n' '{{\"id\":1,\"result\":{{\"sessionId\":\"stdio-test\",\"codexHome\":\"/tmp\"}}}}'; \ + printf '%s\\n' '{{\"id\":1,\"result\":{{\"sessionId\":\"stdio-test\",\"codexHome\":\"/tmp\",\"codexSelfExe\":\"/tmp/codex\"}}}}'; \ read _line; \ wait", shell_quote(pid_file.as_path()), @@ -1318,6 +1353,10 @@ mod tests { std::env::current_dir().expect("current dir"), ) .expect("absolute current dir"), + codex_self_exe: AbsolutePathBuf::try_from( + std::env::current_exe().expect("current exe"), + ) + .expect("absolute current exe"), }) .expect("initialize response should serialize"), }), @@ -1465,6 +1504,10 @@ mod tests { std::env::current_dir().expect("current dir"), ) .expect("absolute current dir"), + codex_self_exe: AbsolutePathBuf::try_from( + std::env::current_exe().expect("current exe"), + ) + .expect("absolute current exe"), }) .expect("initialize response should serialize"), }), @@ -1606,6 +1649,10 @@ mod tests { std::env::current_dir().expect("current dir"), ) .expect("absolute current dir"), + codex_self_exe: AbsolutePathBuf::try_from( + std::env::current_exe().expect("current exe"), + ) + .expect("absolute current exe"), }) .expect("initialize response should serialize"), }), diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 5566acb1a2..2ce6bd8247 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -3,8 +3,6 @@ use std::sync::Arc; use std::sync::RwLock; use codex_app_server_protocol::JSONRPCErrorError; -use codex_app_server_protocol::RuntimeInstallParams; -use codex_app_server_protocol::RuntimeInstallResponse; use codex_utils_absolute_path::AbsolutePathBuf; use crate::ExecServerError; @@ -317,35 +315,11 @@ pub struct Environment { exec_backend: Arc, filesystem: Arc, http_client: Arc, - runtime_installer: RuntimeInstaller, + remote_client: Option, local_runtime_paths: Option, codex_home: Option, } -#[derive(Clone)] -enum RuntimeInstaller { - Local, - Remote(LazyRemoteExecServerClient), -} - -impl RuntimeInstaller { - async fn install_runtime( - &self, - params: RuntimeInstallParams, - ) -> Result { - match self { - RuntimeInstaller::Local => crate::runtime_install::install_runtime(params).await, - RuntimeInstaller::Remote(client) => { - let client = client.get().await.map_err(exec_server_error_to_jsonrpc)?; - client - .runtime_install(params) - .await - .map_err(exec_server_error_to_jsonrpc) - } - } - } -} - fn exec_server_error_to_jsonrpc(err: ExecServerError) -> JSONRPCErrorError { match err { ExecServerError::Server { code, message } => JSONRPCErrorError { @@ -366,7 +340,7 @@ impl Environment { exec_backend: Arc::new(LocalProcess::default()), filesystem: Arc::new(LocalFileSystem::unsandboxed()), http_client: Arc::new(ReqwestHttpClient), - runtime_installer: RuntimeInstaller::Local, + remote_client: None, local_runtime_paths: None, codex_home: default_local_codex_home(), } @@ -433,7 +407,7 @@ impl Environment { local_runtime_paths.clone(), )), http_client: Arc::new(ReqwestHttpClient), - runtime_installer: RuntimeInstaller::Local, + remote_client: None, local_runtime_paths: Some(local_runtime_paths), codex_home, } @@ -472,7 +446,7 @@ impl Environment { exec_backend, filesystem, http_client: Arc::new(http_client), - runtime_installer: RuntimeInstaller::Remote(client), + remote_client: Some(client), local_runtime_paths, codex_home: None, } @@ -503,28 +477,32 @@ impl Environment { Arc::clone(&self.filesystem) } - pub async fn install_runtime( - &self, - params: RuntimeInstallParams, - ) -> Result { - self.runtime_installer.install_runtime(params).await - } - pub async fn codex_home(&self) -> Result { if let Some(codex_home) = self.codex_home.clone() { return Ok(codex_home); } - match &self.runtime_installer { - RuntimeInstaller::Local => default_local_codex_home().ok_or_else(|| { - internal_error("failed to locate local codex home for runtime install") - }), - RuntimeInstaller::Remote(client) => { - let client = client.get().await.map_err(exec_server_error_to_jsonrpc)?; - client - .codex_home() - .ok_or_else(|| internal_error("remote exec-server did not report a codex home")) - } + let client = self.remote_client.as_ref().ok_or_else(|| { + internal_error("failed to locate local codex home for runtime install") + })?; + let client = client.get().await.map_err(exec_server_error_to_jsonrpc)?; + client + .codex_home() + .ok_or_else(|| internal_error("remote exec-server did not report a codex home")) + } + + pub async fn codex_self_exe(&self) -> Result { + if let Some(client) = self.remote_client.as_ref() { + let client = client.get().await.map_err(exec_server_error_to_jsonrpc)?; + return client.codex_self_exe().ok_or_else(|| { + internal_error("remote exec-server did not report its Codex executable") + }); } + self.local_runtime_paths + .as_ref() + .map(|runtime_paths| runtime_paths.codex_self_exe.clone()) + .ok_or_else(|| { + internal_error("failed to locate local Codex executable for runtime install") + }) } } diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 9b65d85f73..f091f14170 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -20,7 +20,6 @@ mod remote; mod remote_file_system; mod remote_process; mod rpc; -mod runtime_install; mod runtime_paths; mod sandboxed_file_system; mod server; @@ -86,7 +85,6 @@ pub use protocol::HttpRequestResponse; pub use protocol::InitializeParams; pub use protocol::InitializeResponse; pub use protocol::ProcessOutputChunk; -pub use protocol::RUNTIME_INSTALL_METHOD; pub use protocol::ReadParams; pub use protocol::ReadResponse; pub use protocol::TerminateParams; diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index 5ecd6a34d6..18729f462a 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -30,7 +30,6 @@ pub const FS_COPY_METHOD: &str = "fs/copy"; pub const HTTP_REQUEST_METHOD: &str = "http/request"; /// JSON-RPC notification method for streamed executor HTTP response bodies. pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta"; -pub const RUNTIME_INSTALL_METHOD: &str = "runtime/install"; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] @@ -61,6 +60,7 @@ pub struct InitializeParams { pub struct InitializeResponse { pub session_id: String, pub codex_home: AbsolutePathBuf, + pub codex_self_exe: AbsolutePathBuf, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/codex-rs/exec-server/src/rpc.rs b/codex-rs/exec-server/src/rpc.rs index 4195bc1833..132131e93e 100644 --- a/codex-rs/exec-server/src/rpc.rs +++ b/codex-rs/exec-server/src/rpc.rs @@ -49,6 +49,22 @@ pub(crate) enum RpcClientEvent { Disconnected { reason: Option }, } +pub(crate) struct RpcPendingResponse { + response_rx: oneshot::Receiver>, +} + +impl RpcPendingResponse { + pub(crate) async fn response(self) -> Result + where + T: DeserializeOwned, + { + let result: Result = + self.response_rx.await.map_err(|_| RpcCallError::Closed)?; + let response = result?; + serde_json::from_value(response).map_err(RpcCallError::Json) + } +} + #[derive(Debug, Clone, PartialEq)] pub(crate) enum RpcServerOutboundMessage { Response { @@ -320,6 +336,17 @@ impl RpcClient { where P: Serialize, T: DeserializeOwned, + { + self.start_call(method, params).await?.response().await + } + + pub(crate) async fn start_call

( + &self, + method: &str, + params: &P, + ) -> Result + where + P: Serialize, { let request_id = RequestId::Integer(self.next_request_id.fetch_add(1, Ordering::SeqCst)); let (response_tx, response_rx) = oneshot::channel(); @@ -356,19 +383,7 @@ impl RpcClient { return Err(RpcCallError::Closed); } - // Do not race in-flight requests directly against the transport-close - // watch value. The connection reader receives JSON-RPC messages and - // the terminal disconnect event on one ordered queue, then drains any - // still-pending requests. Awaiting this receiver preserves that order: - // responses already read before EOF still win, and truly pending calls - // are failed once the reader observes the disconnect. - let result: Result = - response_rx.await.map_err(|_| RpcCallError::Closed)?; - let response = match result { - Ok(response) => response, - Err(error) => return Err(error), - }; - serde_json::from_value(response).map_err(RpcCallError::Json) + Ok(RpcPendingResponse { response_rx }) } #[cfg(test)] diff --git a/codex-rs/exec-server/src/server/handler.rs b/codex-rs/exec-server/src/server/handler.rs index 5dd6ea537a..a322b71db7 100644 --- a/codex-rs/exec-server/src/server/handler.rs +++ b/codex-rs/exec-server/src/server/handler.rs @@ -1,12 +1,9 @@ use std::sync::Arc; -use std::sync::Mutex as StdMutex; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::RequestId; -use codex_app_server_protocol::RuntimeInstallParams; -use codex_app_server_protocol::RuntimeInstallResponse; use serde_json::to_value; use std::collections::HashSet; use tokio::sync::Mutex; @@ -52,11 +49,12 @@ use crate::server::session_registry::SessionRegistry; pub(crate) struct ExecServerHandler { session_registry: Arc, notifications: RpcNotificationSender, - session: StdMutex>, + session: std::sync::Mutex>, active_body_stream_ids: Mutex>, background_task_shutdown: CancellationToken, background_tasks: TaskTracker, file_system: FileSystemHandler, + codex_self_exe: codex_utils_absolute_path::AbsolutePathBuf, initialize_requested: AtomicBool, initialized: AtomicBool, } @@ -70,10 +68,11 @@ impl ExecServerHandler { Self { session_registry, notifications, - session: StdMutex::new(None), + session: std::sync::Mutex::new(None), active_body_stream_ids: Mutex::new(HashSet::new()), background_task_shutdown: CancellationToken::new(), background_tasks: TaskTracker::new(), + codex_self_exe: runtime_paths.codex_self_exe.clone(), file_system: FileSystemHandler::new(runtime_paths), initialize_requested: AtomicBool::new(false), initialized: AtomicBool::new(false), @@ -128,6 +127,7 @@ impl ExecServerHandler { Ok(InitializeResponse { session_id, codex_home: crate::codex_home::default_codex_home()?, + codex_self_exe: self.codex_self_exe.clone(), }) } @@ -269,14 +269,6 @@ impl ExecServerHandler { self.file_system.copy(params).await } - pub(crate) async fn runtime_install( - &self, - params: RuntimeInstallParams, - ) -> Result { - self.require_initialized_for("runtime")?; - crate::runtime_install::install_runtime(params).await - } - fn require_initialized_for( &self, method_family: &str, diff --git a/codex-rs/exec-server/src/server/processor.rs b/codex-rs/exec-server/src/server/processor.rs index 6fc0723f0c..c03b20278c 100644 --- a/codex-rs/exec-server/src/server/processor.rs +++ b/codex-rs/exec-server/src/server/processor.rs @@ -1,5 +1,4 @@ use std::sync::Arc; - use tokio::sync::mpsc; use tracing::debug; use tracing::warn; diff --git a/codex-rs/exec-server/src/server/registry.rs b/codex-rs/exec-server/src/server/registry.rs index eafeffbcb0..87dee6aa58 100644 --- a/codex-rs/exec-server/src/server/registry.rs +++ b/codex-rs/exec-server/src/server/registry.rs @@ -24,13 +24,11 @@ use crate::protocol::HttpRequestParams; use crate::protocol::INITIALIZE_METHOD; use crate::protocol::INITIALIZED_METHOD; use crate::protocol::InitializeParams; -use crate::protocol::RUNTIME_INSTALL_METHOD; use crate::protocol::ReadParams; use crate::protocol::TerminateParams; use crate::protocol::WriteParams; use crate::rpc::RpcRouter; use crate::server::ExecServerHandler; -use codex_app_server_protocol::RuntimeInstallParams; pub(crate) fn build_router() -> RpcRouter { let mut router = RpcRouter::new(); @@ -116,11 +114,5 @@ pub(crate) fn build_router() -> RpcRouter { handler.fs_copy(params).await }, ); - router.request( - RUNTIME_INSTALL_METHOD, - |handler: Arc, params: RuntimeInstallParams| async move { - handler.runtime_install(params).await - }, - ); router } diff --git a/codex-rs/exec-server/tests/http_client.rs b/codex-rs/exec-server/tests/http_client.rs index 5d473d162a..8442dc273c 100644 --- a/codex-rs/exec-server/tests/http_client.rs +++ b/codex-rs/exec-server/tests/http_client.rs @@ -1015,6 +1015,7 @@ impl JsonRpcPeer { InitializeResponse { session_id: "session-1".to_string(), codex_home: AbsolutePathBuf::try_from(std::env::current_dir()?)?, + codex_self_exe: AbsolutePathBuf::try_from(std::env::current_exe()?)?, }, ) .await?; diff --git a/codex-rs/runtime-install/BUILD.bazel b/codex-rs/runtime-install/BUILD.bazel new file mode 100644 index 0000000000..763798360f --- /dev/null +++ b/codex-rs/runtime-install/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "runtime-install", + crate_name = "codex_runtime_install", +) diff --git a/codex-rs/runtime-install/Cargo.toml b/codex-rs/runtime-install/Cargo.toml new file mode 100644 index 0000000000..f664c90101 --- /dev/null +++ b/codex-rs/runtime-install/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "codex-runtime-install" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +name = "codex_runtime_install" +path = "src/lib.rs" +doctest = false + +[lints] +workspace = true + +[dependencies] +codex-app-server-protocol = { workspace = true } +codex-utils-absolute-path = { workspace = true } +futures = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls", "stream"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = [ + "fs", + "io-std", + "io-util", + "macros", + "process", + "rt", + "sync", +] } +tokio-util = { workspace = true, features = ["rt"] } +tracing = { workspace = true } +zip = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/runtime-install/src/errors.rs b/codex-rs/runtime-install/src/errors.rs new file mode 100644 index 0000000000..d88e81eea9 --- /dev/null +++ b/codex-rs/runtime-install/src/errors.rs @@ -0,0 +1,20 @@ +use codex_app_server_protocol::JSONRPCErrorError; + +const INVALID_PARAMS_ERROR_CODE: i64 = -32602; +const INTERNAL_ERROR_CODE: i64 = -32603; + +pub(crate) fn invalid_params(message: impl Into) -> JSONRPCErrorError { + error(INVALID_PARAMS_ERROR_CODE, message) +} + +pub(crate) fn internal_error(message: impl Into) -> JSONRPCErrorError { + error(INTERNAL_ERROR_CODE, message) +} + +fn error(code: i64, message: impl Into) -> JSONRPCErrorError { + JSONRPCErrorError { + code, + message: message.into(), + data: None, + } +} diff --git a/codex-rs/runtime-install/src/helper.rs b/codex-rs/runtime-install/src/helper.rs new file mode 100644 index 0000000000..16d9baa771 --- /dev/null +++ b/codex-rs/runtime-install/src/helper.rs @@ -0,0 +1,117 @@ +use std::error::Error; + +use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::RuntimeInstallParams; +use codex_app_server_protocol::RuntimeInstallProgressNotification; +use codex_app_server_protocol::RuntimeInstallResponse; +use serde::Deserialize; +use serde::Serialize; +use tokio::io; +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncWriteExt; +use tokio::io::BufReader; +use tokio_util::sync::CancellationToken; + +use crate::installer::install_runtime_with_progress; + +pub const CODEX_RUNTIME_INSTALL_HELPER_ARG1: &str = "--codex-run-as-runtime-install-helper"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum RuntimeInstallHelperRequest { + Install { params: RuntimeInstallParams }, + Cancel, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum RuntimeInstallHelperMessage { + Progress { + progress: RuntimeInstallProgressNotification, + }, + Complete { + response: RuntimeInstallResponse, + }, + Error { + error: JSONRPCErrorError, + }, +} + +pub fn main() -> ! { + let exit_code = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(runtime) => match runtime.block_on(run_main()) { + Ok(()) => 0, + Err(err) => { + eprintln!("runtime install helper failed: {err}"); + 1 + } + }, + Err(err) => { + eprintln!("failed to start runtime install helper runtime: {err}"); + 1 + } + }; + std::process::exit(exit_code); +} + +async fn run_main() -> Result<(), Box> { + let mut input = BufReader::new(io::stdin()).lines(); + let request = input + .next_line() + .await? + .ok_or("runtime install helper requires an install request")?; + let RuntimeInstallHelperRequest::Install { params } = serde_json::from_str(&request)? else { + return Err("runtime install helper first request must be install".into()); + }; + + let cancellation = CancellationToken::new(); + let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel(); + let install = install_runtime_with_progress(params, progress_tx, cancellation.clone()); + tokio::pin!(install); + let mut stdout = io::stdout(); + let mut stdin_closed = false; + + loop { + tokio::select! { + request = input.next_line(), if !stdin_closed => { + match request? { + Some(request) => match serde_json::from_str(&request)? { + RuntimeInstallHelperRequest::Cancel => cancellation.cancel(), + RuntimeInstallHelperRequest::Install { .. } => { + return Err("runtime install helper accepts one install request".into()); + } + }, + None => stdin_closed = true + } + } + progress = progress_rx.recv() => { + if let Some(progress) = progress { + write_message(&mut stdout, RuntimeInstallHelperMessage::Progress { progress }).await?; + } + } + response = &mut install => { + let message = match response { + Ok(response) => RuntimeInstallHelperMessage::Complete { response }, + Err(error) => RuntimeInstallHelperMessage::Error { error }, + }; + write_message(&mut stdout, message).await?; + return Ok(()); + } + } + } +} + +async fn write_message( + stdout: &mut io::Stdout, + message: RuntimeInstallHelperMessage, +) -> Result<(), Box> { + stdout + .write_all(serde_json::to_string(&message)?.as_bytes()) + .await?; + stdout.write_all(b"\n").await?; + stdout.flush().await?; + Ok(()) +} diff --git a/codex-rs/exec-server/src/runtime_install.rs b/codex-rs/runtime-install/src/installer.rs similarity index 75% rename from codex-rs/exec-server/src/runtime_install.rs rename to codex-rs/runtime-install/src/installer.rs index 565d566029..647a0aa1fc 100644 --- a/codex-rs/exec-server/src/runtime_install.rs +++ b/codex-rs/runtime-install/src/installer.rs @@ -5,9 +5,11 @@ use std::path::PathBuf; use std::process::Stdio; use codex_app_server_protocol::JSONRPCErrorError; -use codex_app_server_protocol::RuntimeInstallManifest; +use codex_app_server_protocol::RuntimeInstallManifestParams; use codex_app_server_protocol::RuntimeInstallParams; use codex_app_server_protocol::RuntimeInstallPaths; +use codex_app_server_protocol::RuntimeInstallProgressNotification; +use codex_app_server_protocol::RuntimeInstallProgressPhase; use codex_app_server_protocol::RuntimeInstallResponse; use codex_app_server_protocol::RuntimeInstallStatus; use codex_utils_absolute_path::AbsolutePathBuf; @@ -19,12 +21,14 @@ use tokio::fs; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; use tokio::process::Command; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; -use crate::rpc::internal_error; -use crate::rpc::invalid_params; +use crate::errors::internal_error; +use crate::errors::invalid_params; const PUBLISHED_ARTIFACT_NAME: &str = "codex-primary-runtime"; -const USER_AGENT: &str = "codex-exec-server-runtime-installer"; +const USER_AGENT: &str = "codex-runtime-installer"; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum RuntimeArchiveFormat { @@ -42,16 +46,68 @@ struct InstalledRuntimeMetadata { skills_to_remove: Option>, } -pub(crate) async fn install_runtime( - params: RuntimeInstallParams, -) -> Result { - let install_root = default_install_root()?; - install_runtime_with_root(params, install_root).await +pub type RuntimeInstallProgressSender = mpsc::UnboundedSender; + +#[derive(Clone)] +struct RuntimeInstallProgressReporter { + bundle_version: Option, + sender: Option, } -async fn install_runtime_with_root( +impl RuntimeInstallProgressReporter { + fn new(bundle_version: Option, sender: Option) -> Self { + Self { + bundle_version, + sender, + } + } + + fn phase(&self, phase: RuntimeInstallProgressPhase) { + self.send( + phase, /*downloaded_bytes*/ None, /*total_bytes*/ None, + ); + } + + fn download_progress(&self, downloaded_bytes: u64, total_bytes: Option) { + self.send( + RuntimeInstallProgressPhase::Downloading, + Some(downloaded_bytes), + total_bytes, + ); + } + + fn send( + &self, + phase: RuntimeInstallProgressPhase, + downloaded_bytes: Option, + total_bytes: Option, + ) { + let Some(sender) = self.sender.as_ref() else { + return; + }; + let _ = sender.send(RuntimeInstallProgressNotification { + bundle_version: self.bundle_version.clone(), + downloaded_bytes, + phase, + total_bytes, + }); + } +} + +pub(crate) async fn install_runtime_with_progress( + params: RuntimeInstallParams, + progress: RuntimeInstallProgressSender, + cancellation: CancellationToken, +) -> Result { + let install_root = default_install_root()?; + install_runtime_with_root_and_control(params, install_root, Some(progress), cancellation).await +} + +async fn install_runtime_with_root_and_control( params: RuntimeInstallParams, install_root: PathBuf, + progress: Option, + cancellation: CancellationToken, ) -> Result { validate_manifest(¶ms.manifest)?; let archive_format = runtime_archive_format(¶ms.manifest)?; @@ -62,11 +118,38 @@ async fn install_runtime_with_root( .unwrap_or_else(|| default_archive_name(archive_format).to_string()); validate_path_segment(&archive_name, "archiveName")?; + let progress = + RuntimeInstallProgressReporter::new(params.manifest.bundle_version.clone(), progress); + progress.phase(RuntimeInstallProgressPhase::Checking); + ensure_not_cancelled(&cancellation)?; + if let Some(response) = + reuse_current_runtime(¶ms.manifest, &install_root, &progress, &cancellation).await? + { + return Ok(response); + } let staging_dir = make_staging_dir(&install_root).await?; let archive_path = staging_dir.join(archive_name); let result = async { - download_archive(¶ms.manifest.archive_url, &archive_path).await?; - install_runtime_from_archive(¶ms.manifest, &archive_path, &install_root).await + progress.download_progress( + /*downloaded_bytes*/ 0, + params.manifest.archive_size_bytes, + ); + download_archive( + ¶ms.manifest.archive_url, + &archive_path, + params.manifest.archive_size_bytes, + &progress, + &cancellation, + ) + .await?; + install_runtime_from_archive_with_control( + ¶ms.manifest, + &archive_path, + &install_root, + &progress, + &cancellation, + ) + .await } .await; let cleanup_result = fs::remove_dir_all(&staging_dir).await; @@ -81,44 +164,54 @@ async fn install_runtime_with_root( result } +#[cfg(test)] async fn install_runtime_from_archive( - manifest: &RuntimeInstallManifest, + manifest: &RuntimeInstallManifestParams, archive_path: &Path, install_root: &Path, +) -> Result { + install_runtime_from_archive_with_control( + manifest, + archive_path, + install_root, + &RuntimeInstallProgressReporter::new(manifest.bundle_version.clone(), None), + &CancellationToken::new(), + ) + .await +} + +async fn install_runtime_from_archive_with_control( + manifest: &RuntimeInstallManifestParams, + archive_path: &Path, + install_root: &Path, + progress: &RuntimeInstallProgressReporter, + cancellation: &CancellationToken, ) -> Result { let runtime_root_directory_name = runtime_root_directory_name(manifest)?; let installed_runtime_root = install_root.join(&runtime_root_directory_name); let target_platform = target_platform(); - if let Some(bundle_version) = manifest.bundle_version.as_ref() - && let Ok(Some(metadata)) = read_installed_runtime_metadata(&installed_runtime_root).await - && metadata.bundle_version.as_ref() == Some(bundle_version) - && let Ok(paths) = validate_runtime_root( - &installed_runtime_root, - manifest.bundle_format_version, - target_platform, - ) - .await + if let Some(response) = + reuse_current_runtime(manifest, install_root, progress, cancellation).await? { - return Ok(RuntimeInstallResponse { - bundle_version: Some(bundle_version.clone()), - paths, - status: RuntimeInstallStatus::AlreadyCurrent, - }); + return Ok(response); } fs::create_dir_all(install_root) .await .map_err(|err| internal_error(format!("failed to create runtime install root: {err}")))?; + progress.phase(RuntimeInstallProgressPhase::Verifying); verify_archive_checksum( archive_path, &manifest.archive_sha256, &manifest.archive_url, + cancellation, ) .await?; let archive_format = runtime_archive_format(manifest)?; + ensure_not_cancelled(cancellation)?; let staging_dir = make_staging_dir(install_root).await?; let result = async { let extract_dir = staging_dir.join("payload"); @@ -126,17 +219,23 @@ async fn install_runtime_from_archive( internal_error(format!("failed to create runtime extract dir: {err}")) })?; + progress.phase(RuntimeInstallProgressPhase::Extracting); + ensure_not_cancelled(cancellation)?; let entries = list_archive_entries(archive_format, archive_path).await?; assert_archive_entries_stay_within_directory(&entries, &extract_dir)?; + ensure_not_cancelled(cancellation)?; extract_archive(archive_format, archive_path, &extract_dir).await?; let extracted_runtime_root = extract_dir.join(&runtime_root_directory_name); + progress.phase(RuntimeInstallProgressPhase::Validating); + ensure_not_cancelled(cancellation)?; validate_runtime_root( &extracted_runtime_root, manifest.bundle_format_version, target_platform, ) .await?; + ensure_not_cancelled(cancellation)?; let previous_runtime_root = install_root.join(format!("{runtime_root_directory_name}.previous")); @@ -193,9 +292,40 @@ async fn install_runtime_from_archive( staging_dir.display() ); } + if result.is_ok() { + progress.phase(RuntimeInstallProgressPhase::Installed); + } result } +async fn reuse_current_runtime( + manifest: &RuntimeInstallManifestParams, + install_root: &Path, + progress: &RuntimeInstallProgressReporter, + cancellation: &CancellationToken, +) -> Result, JSONRPCErrorError> { + let installed_runtime_root = install_root.join(runtime_root_directory_name(manifest)?); + ensure_not_cancelled(cancellation)?; + if let Some(bundle_version) = manifest.bundle_version.as_ref() + && let Ok(Some(metadata)) = read_installed_runtime_metadata(&installed_runtime_root).await + && metadata.bundle_version.as_ref() == Some(bundle_version) + && let Ok(paths) = validate_runtime_root( + &installed_runtime_root, + manifest.bundle_format_version, + target_platform(), + ) + .await + { + progress.phase(RuntimeInstallProgressPhase::Installed); + return Ok(Some(RuntimeInstallResponse { + bundle_version: Some(bundle_version.clone()), + paths, + status: RuntimeInstallStatus::AlreadyCurrent, + })); + } + Ok(None) +} + fn default_install_root() -> Result { let home = std::env::var_os("HOME") .or_else(|| std::env::var_os("USERPROFILE")) @@ -219,7 +349,7 @@ async fn make_staging_dir(install_root: &Path) -> Result Result<(), JSONRPCErrorError> { +fn validate_manifest(manifest: &RuntimeInstallManifestParams) -> Result<(), JSONRPCErrorError> { if manifest.archive_url.trim().is_empty() { return Err(invalid_params( "runtime manifest archiveUrl must not be empty", @@ -259,7 +389,7 @@ fn validate_path_segment(value: &str, field_name: &str) -> Result<(), JSONRPCErr } fn runtime_root_directory_name( - manifest: &RuntimeInstallManifest, + manifest: &RuntimeInstallManifestParams, ) -> Result { let runtime_root_directory_name = manifest .runtime_root_directory_name @@ -270,7 +400,7 @@ fn runtime_root_directory_name( } fn runtime_archive_format( - manifest: &RuntimeInstallManifest, + manifest: &RuntimeInstallManifestParams, ) -> Result { if let Some(format) = manifest.format.as_deref() { match format.to_ascii_lowercase().as_str() { @@ -301,13 +431,21 @@ fn default_archive_name(format: RuntimeArchiveFormat) -> &'static str { } } -async fn download_archive(url: &str, destination: &Path) -> Result<(), JSONRPCErrorError> { - let response = reqwest::Client::new() - .get(url) - .header(reqwest::header::USER_AGENT, USER_AGENT) - .send() - .await - .map_err(|err| internal_error(format!("failed to download runtime archive: {err}")))?; +async fn download_archive( + url: &str, + destination: &Path, + expected_size_bytes: Option, + progress: &RuntimeInstallProgressReporter, + cancellation: &CancellationToken, +) -> Result<(), JSONRPCErrorError> { + let response = tokio::select! { + _ = cancellation.cancelled() => return Err(runtime_install_canceled()), + response = reqwest::Client::new() + .get(url) + .header(reqwest::header::USER_AGENT, USER_AGENT) + .send() => response + } + .map_err(|err| internal_error(format!("failed to download runtime archive: {err}")))?; if !response.status().is_success() { return Err(internal_error(format!( "failed to download runtime archive ({} {})", @@ -322,14 +460,27 @@ async fn download_archive(url: &str, destination: &Path) -> Result<(), JSONRPCEr let mut file = fs::File::create(destination) .await .map_err(|err| internal_error(format!("failed to create runtime archive file: {err}")))?; + let total_bytes = response.content_length().or(expected_size_bytes); + let mut downloaded_bytes = 0_u64; let mut stream = response.bytes_stream(); - while let Some(chunk) = stream.next().await { + loop { + let chunk = tokio::select! { + _ = cancellation.cancelled() => return Err(runtime_install_canceled()), + chunk = stream.next() => chunk + }; + let Some(chunk) = chunk else { + break; + }; let chunk = chunk.map_err(|err| { internal_error(format!("failed to read runtime archive bytes: {err}")) })?; - file.write_all(&chunk) - .await - .map_err(|err| internal_error(format!("failed to write runtime archive: {err}")))?; + tokio::select! { + _ = cancellation.cancelled() => return Err(runtime_install_canceled()), + result = file.write_all(&chunk) => result + } + .map_err(|err| internal_error(format!("failed to write runtime archive: {err}")))?; + downloaded_bytes += chunk.len() as u64; + progress.download_progress(downloaded_bytes, total_bytes); } file.flush() .await @@ -341,8 +492,9 @@ async fn verify_archive_checksum( archive_path: &Path, expected_sha256: &str, source_url: &str, + cancellation: &CancellationToken, ) -> Result<(), JSONRPCErrorError> { - let actual_sha256 = compute_sha256(archive_path).await?; + let actual_sha256 = compute_sha256_with_cancellation(archive_path, cancellation).await?; if !actual_sha256.eq_ignore_ascii_case(expected_sha256) { return Err(invalid_params(format!( "checksum mismatch for '{source_url}': expected {expected_sha256}, got {actual_sha256}" @@ -351,17 +503,26 @@ async fn verify_archive_checksum( Ok(()) } +#[cfg(test)] async fn compute_sha256(path: &Path) -> Result { + compute_sha256_with_cancellation(path, &CancellationToken::new()).await +} + +async fn compute_sha256_with_cancellation( + path: &Path, + cancellation: &CancellationToken, +) -> Result { let mut file = fs::File::open(path) .await .map_err(|err| internal_error(format!("failed to open runtime archive: {err}")))?; let mut digest = Sha256::new(); let mut buffer = [0_u8; 64 * 1024]; loop { - let bytes_read = file - .read(&mut buffer) - .await - .map_err(|err| internal_error(format!("failed to read runtime archive: {err}")))?; + let bytes_read = tokio::select! { + _ = cancellation.cancelled() => return Err(runtime_install_canceled()), + bytes_read = file.read(&mut buffer) => bytes_read + } + .map_err(|err| internal_error(format!("failed to read runtime archive: {err}")))?; if bytes_read == 0 { break; } @@ -370,6 +531,18 @@ async fn compute_sha256(path: &Path) -> Result { Ok(format!("{:x}", digest.finalize())) } +fn ensure_not_cancelled(cancellation: &CancellationToken) -> Result<(), JSONRPCErrorError> { + if cancellation.is_cancelled() { + Err(runtime_install_canceled()) + } else { + Ok(()) + } +} + +fn runtime_install_canceled() -> JSONRPCErrorError { + internal_error("runtime install canceled") +} + async fn list_archive_entries( format: RuntimeArchiveFormat, archive_path: &Path, @@ -788,6 +961,35 @@ mod tests { ); } + #[tokio::test] + async fn install_runtime_reuses_current_runtime_without_downloading_archive() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let install_root = temp_dir.path().join("install"); + let runtime_root = install_root.join(PUBLISHED_ARTIFACT_NAME); + create_runtime_root(&runtime_root, "v1").await; + let archive_path = temp_dir.path().join("unused.tar.xz"); + fs::write(&archive_path, b"not used") + .await + .expect("write archive"); + let mut manifest = manifest_for_archive(&archive_path, "v1").await; + manifest.archive_url = "not a valid archive URL".to_string(); + + let response = install_runtime_with_root_and_control( + RuntimeInstallParams { + environment_id: None, + manifest: Box::new(manifest), + release: "primary".to_string(), + }, + install_root, + /*progress*/ None, + CancellationToken::new(), + ) + .await + .expect("installed runtime should be reused without downloading"); + + assert_eq!(response.status, RuntimeInstallStatus::AlreadyCurrent); + } + #[tokio::test] async fn install_from_archive_uses_runtime_metadata_bundle_format_when_manifest_omits_it() { let temp_dir = tempfile::tempdir().expect("tempdir"); @@ -900,7 +1102,7 @@ mod tests { fs::write(&archive_path, b"archive") .await .expect("write archive"); - let manifest = RuntimeInstallManifest { + let manifest = RuntimeInstallManifestParams { archive_name: None, archive_sha256: "0".repeat(64), archive_size_bytes: None, @@ -955,6 +1157,70 @@ mod tests { assert_eq!(metadata.bundle_version.as_deref(), Some("old")); } + #[tokio::test] + async fn install_from_archive_reports_install_progress() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let payload_root = temp_dir + .path() + .join("payload") + .join(PUBLISHED_ARTIFACT_NAME); + create_runtime_root(&payload_root, "v1").await; + let archive_path = temp_dir.path().join("archive.tar.xz"); + create_tar_xz(temp_dir.path().join("payload").as_path(), &archive_path).await; + let manifest = manifest_for_archive(&archive_path, "v1").await; + let (progress_tx, mut progress_rx) = mpsc::unbounded_channel(); + let progress = + RuntimeInstallProgressReporter::new(manifest.bundle_version.clone(), Some(progress_tx)); + + install_runtime_from_archive_with_control( + &manifest, + &archive_path, + &temp_dir.path().join("install"), + &progress, + &CancellationToken::new(), + ) + .await + .expect("install should succeed"); + + let mut phases = Vec::new(); + while let Ok(notification) = progress_rx.try_recv() { + phases.push(notification.phase); + } + assert_eq!( + phases, + vec![ + RuntimeInstallProgressPhase::Verifying, + RuntimeInstallProgressPhase::Extracting, + RuntimeInstallProgressPhase::Validating, + RuntimeInstallProgressPhase::Installed, + ] + ); + } + + #[tokio::test] + async fn install_from_archive_stops_when_canceled() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let archive_path = temp_dir.path().join("unused.tar.xz"); + fs::write(&archive_path, b"unused") + .await + .expect("write archive"); + let manifest = manifest_for_archive(&archive_path, "v1").await; + let cancellation = CancellationToken::new(); + cancellation.cancel(); + + let error = install_runtime_from_archive_with_control( + &manifest, + &archive_path, + &temp_dir.path().join("install"), + &RuntimeInstallProgressReporter::new(manifest.bundle_version.clone(), None), + &cancellation, + ) + .await + .expect_err("canceled install should fail"); + + assert_eq!(error.message, "runtime install canceled"); + } + async fn create_runtime_root(runtime_root: &Path, bundle_version: &str) { let node_bin = runtime_root.join("dependencies").join("node").join("bin"); let python_bin = runtime_root.join("dependencies").join("python").join("bin"); @@ -991,8 +1257,8 @@ mod tests { async fn manifest_for_archive( archive_path: &Path, bundle_version: &str, - ) -> RuntimeInstallManifest { - RuntimeInstallManifest { + ) -> RuntimeInstallManifestParams { + RuntimeInstallManifestParams { archive_name: None, archive_sha256: compute_sha256(archive_path).await.expect("sha256"), archive_size_bytes: None, diff --git a/codex-rs/runtime-install/src/lib.rs b/codex-rs/runtime-install/src/lib.rs new file mode 100644 index 0000000000..13d9f2b406 --- /dev/null +++ b/codex-rs/runtime-install/src/lib.rs @@ -0,0 +1,8 @@ +mod errors; +mod helper; +mod installer; + +pub use helper::CODEX_RUNTIME_INSTALL_HELPER_ARG1; +pub use helper::RuntimeInstallHelperMessage; +pub use helper::RuntimeInstallHelperRequest; +pub use helper::main as run_runtime_install_helper_main;