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 872a688d7e..4bf7632083 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 @@ -20251,6 +20251,18 @@ }, "UpdateInstallResponse": { "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "installedVersion": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "installedVersion", + "success" + ], "title": "UpdateInstallResponse", "type": "object" }, 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 223a253ff6..2167a61c1d 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 @@ -18030,6 +18030,18 @@ }, "UpdateInstallResponse": { "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "installedVersion": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "installedVersion", + "success" + ], "title": "UpdateInstallResponse", "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/UpdateInstallResponse.json b/codex-rs/app-server-protocol/schema/json/v2/UpdateInstallResponse.json index 0b083b57f6..535628dd18 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/UpdateInstallResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/UpdateInstallResponse.json @@ -1,5 +1,17 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "installedVersion": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "installedVersion", + "success" + ], "title": "UpdateInstallResponse", "type": "object" } \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/UpdateInstallResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/UpdateInstallResponse.ts index 4a9e363d55..9190b9e5cb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/UpdateInstallResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/UpdateInstallResponse.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type UpdateInstallResponse = Record; +export type UpdateInstallResponse = { installedVersion: string, success: boolean, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/update.rs b/codex-rs/app-server-protocol/src/protocol/v2/update.rs index b420677e48..711ff3c3ff 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/update.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/update.rs @@ -28,4 +28,7 @@ pub struct UpdateCheckResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct UpdateInstallResponse {} +pub struct UpdateInstallResponse { + pub installed_version: String, + pub success: bool, +} diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 867c7488e7..d3252ce5ff 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -202,7 +202,7 @@ Example with notification opt-out: - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with model-advertised string reasoning effort options in the catalog's intended progression order, `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. Clients should preserve the `supportedReasoningEfforts` array order rather than deriving order from the effort names. - `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider. - `update/check` — force a fresh update check and return the current version, latest version available through the detected distribution channel, installation method, and whether app-server can apply the update automatically. -- `update/install` — run the updater associated with the detected installation method; returns `{}` after the updater exits successfully. +- `update/install` — run the updater associated with the detected installation method; returns the installed version and whether installation succeeded. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`. - `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text and an `allowed` flag reflecting effective requirements, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. - `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for currently supported feature keys. For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. Invalid keys will be ignored. @@ -1920,7 +1920,8 @@ request has no params: `installationMethod` is one of `npm`, `bun`, `brew`, `standaloneUnix`, `standaloneWindows`, or `other`. npm and Bun checks require the latest GitHub release to be ready under npm's `latest` tag. Homebrew checks the cask version; -standalone and other installations check the latest GitHub release. +standalone and other installations check the latest GitHub release. The +`latestVersion` field is the version that an available update would install. When `canAutoUpdate` is `true`, call `update/install` to run the corresponding npm, Bun, Homebrew, or standalone installer. The updater's output @@ -1930,11 +1931,15 @@ installed version. ```json { "method": "update/install", "id": 51 } -{ "id": 51, "result": {} } +{ "id": 51, "result": { + "installedVersion": "1.3.0", + "success": true +} } ``` For `installationMethod: "other"`, `update/install` returns an invalid-request -error because app-server cannot determine a safe updater command. +error because app-server cannot determine a safe updater command. Installer or +post-install version-check failures return a JSON-RPC error instead of a result. ## Auth endpoints diff --git a/codex-rs/app-server/src/request_processors/update_processor.rs b/codex-rs/app-server/src/request_processors/update_processor.rs index a462e77905..64f0ab0664 100644 --- a/codex-rs/app-server/src/request_processors/update_processor.rs +++ b/codex-rs/app-server/src/request_processors/update_processor.rs @@ -79,7 +79,44 @@ impl UpdateRequestProcessor { ))); } - Ok(Some(UpdateInstallResponse {}.into())) + let version_output = Command::new("codex") + .arg("--version") + .output() + .await + .map_err(|err| { + internal_error(format!( + "Codex update succeeded, but the installed version could not be read: {err}" + )) + })?; + if !version_output.status.success() { + return Err(internal_error(format!( + "Codex update succeeded, but `codex --version` failed with status {}", + version_output.status + ))); + } + let version_stdout = String::from_utf8(version_output.stdout).map_err(|err| { + internal_error(format!( + "Codex update succeeded, but `codex --version` returned invalid UTF-8: {err}" + )) + })?; + let installed_version = version_stdout + .trim() + .strip_prefix("codex-cli ") + .filter(|version| !version.is_empty()) + .ok_or_else(|| { + internal_error(format!( + "Codex update succeeded, but `codex --version` returned an unexpected value: {version_stdout:?}" + )) + })? + .to_string(); + + Ok(Some( + UpdateInstallResponse { + installed_version, + success: true, + } + .into(), + )) } } diff --git a/codex-rs/app-server/tests/suite/v2/update.rs b/codex-rs/app-server/tests/suite/v2/update.rs index 1d41154453..d261fb201f 100644 --- a/codex-rs/app-server/tests/suite/v2/update.rs +++ b/codex-rs/app-server/tests/suite/v2/update.rs @@ -15,20 +15,27 @@ use tokio::time::timeout; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); +fn write_executable(path: &std::path::Path, contents: &str) -> Result<()> { + fs::write(path, contents)?; + let mut permissions = fs::metadata(path)?.permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions)?; + Ok(()) +} + #[tokio::test] async fn update_install_runs_the_detected_npm_updater() -> Result<()> { let codex_home = TempDir::new()?; let bin_dir = codex_home.path().join("bin"); fs::create_dir_all(&bin_dir)?; let npm_path = bin_dir.join("npm"); + let codex_path = bin_dir.join("codex"); let invocation_path = codex_home.path().join("npm-invocation.txt"); - fs::write( + write_executable( &npm_path, "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$CODEX_TEST_UPDATE_INVOCATION\"\n", )?; - let mut permissions = fs::metadata(&npm_path)?.permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&npm_path, permissions)?; + write_executable(&codex_path, "#!/bin/sh\nprintf 'codex-cli 1.2.3\\n'\n")?; let path = format!( "{}:{}", @@ -55,7 +62,13 @@ async fn update_install_runs_the_detected_npm_updater() -> Result<()> { .await??; let response = to_response::(response)?; - assert_eq!(response, UpdateInstallResponse {}); + assert_eq!( + response, + UpdateInstallResponse { + installed_version: "1.2.3".to_string(), + success: true, + } + ); assert_eq!( fs::read_to_string(invocation_path)?, "install\n-g\n@openai/codex\n"