diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 30090089c6..e7fc2c5ef2 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -611,7 +611,22 @@ jobs: fi echo "+ ${publish_cmd[*]}" - "${publish_cmd[@]}" + set +e + publish_output="$("${publish_cmd[@]}" 2>&1)" + publish_status=$? + set -e + + echo "${publish_output}" + if [[ ${publish_status} -eq 0 ]]; then + continue + fi + + if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then + echo "Skipping already-published package version for ${filename}" + continue + fi + + exit "${publish_status}" done update-branch: diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 6f15063691..80e889a031 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -608,7 +608,7 @@ "anyhow_1.0.101": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.51\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "arbitrary_1.4.2": "{\"dependencies\":[{\"name\":\"derive_arbitrary\",\"optional\":true,\"req\":\"~1.4.0\"},{\"kind\":\"dev\",\"name\":\"exhaustigen\",\"req\":\"^0.1.0\"}],\"features\":{\"derive\":[\"derive_arbitrary\"]}}", "arboard_3.6.1": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"clipboard-win\",\"req\":\"^5.3.1\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"features\":[\"png\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"default_features\":false,\"features\":[\"tiff\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"png\",\"bmp\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(windows)\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(windows)\"},{\"name\":\"objc2\",\"req\":\"^0.6.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"objc2-core-graphics\",\"NSPasteboard\",\"NSPasteboardItem\",\"NSImage\"],\"name\":\"objc2-app-kit\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CFCGTypes\"],\"name\":\"objc2-core-foundation\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CGImage\",\"CGColorSpace\",\"CGDataProvider\"],\"name\":\"objc2-core-graphics\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"NSArray\",\"NSString\",\"NSEnumerator\",\"NSGeometry\",\"NSValue\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_DataExchange\",\"Win32_System_Memory\",\"Win32_System_Ole\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.61.0\",\"target\":\"cfg(windows)\"},{\"name\":\"wl-clipboard-rs\",\"optional\":true,\"req\":\"^0.9.0\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"x11rb\",\"req\":\"^0.13\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"}],\"features\":{\"core-graphics\":[\"dep:objc2-core-graphics\"],\"default\":[\"image-data\"],\"image\":[\"dep:image\"],\"image-data\":[\"dep:objc2-core-graphics\",\"dep:objc2-core-foundation\",\"image\",\"windows-sys\",\"core-graphics\"],\"wayland-data-control\":[\"wl-clipboard-rs\"],\"windows-sys\":[\"windows-sys/Win32_Graphics_Gdi\"],\"wl-clipboard-rs\":[\"dep:wl-clipboard-rs\"]}}", - "arc-swap_1.8.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adaptive-barrier\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"~0.7\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"~0.8\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"~0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.177\"}],\"features\":{\"experimental-strategies\":[],\"experimental-thread-local\":[],\"internal-test-strategies\":[],\"weak\":[]}}", + "arc-swap_1.8.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adaptive-barrier\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"~0.7\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"~0.8\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"~0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.177\"}],\"features\":{\"experimental-strategies\":[],\"experimental-thread-local\":[],\"internal-test-strategies\":[],\"weak\":[]}}", "arrayvec_0.7.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"matches\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "ascii-canvas_3.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"diff\",\"req\":\"^0.1\"},{\"name\":\"term\",\"req\":\"^0.7\"}],\"features\":{}}", "ascii_1.1.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"name\":\"serde_test\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 3456f9d731..189d9468d2 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -423,16 +423,16 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.52.0", "wl-clipboard-rs", "x11rb", ] [[package]] name = "arc-swap" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" dependencies = [ "rustversion", ] @@ -1227,9 +1227,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.56" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -1237,9 +1237,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -1271,9 +1271,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clipboard-win" @@ -3293,7 +3293,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3489,9 +3489,9 @@ checksum = "dbfd0e7fc632dec5e6c9396a27bc9f9975b4e039720e1fd3e34021d3ce28c415" [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -3505,9 +3505,9 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -3538,7 +3538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4931,7 +4931,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5639,7 +5639,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6205,7 +6205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -6766,7 +6766,7 @@ dependencies = [ "once_cell", "socket2 0.6.2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -7592,7 +7592,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8969,7 +8969,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -10329,7 +10329,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 75b3d18d3e..7ad0566efd 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -170,7 +170,7 @@ dotenvy = "0.15.7" dunce = "1.0.4" encoding_rs = "0.8.35" env-flags = "0.1.1" -env_logger = "0.11.5" +env_logger = "0.11.9" eventsource-stream = "0.2.3" futures = { version = "0.3", default-features = false } globset = "0.4" diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a8b66a202b..1e8ffa9dd8 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -711,6 +711,15 @@ ], "type": "object" }, + "HazelnutScope": { + "enum": [ + "example", + "workspace-shared", + "all-shared", + "personal" + ], + "type": "string" + }, "InitializeCapabilities": { "description": "Client-declared capabilities negotiated during initialize.", "properties": { @@ -1250,6 +1259,15 @@ ], "type": "string" }, + "ProductSurface": { + "enum": [ + "chatgpt", + "codex", + "api", + "atlas" + ], + "type": "string" + }, "ReadOnlyAccess": { "oneOf": [ { @@ -2421,20 +2439,38 @@ "type": "object" }, "SkillsRemoteReadParams": { + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "hazelnutScope": { + "allOf": [ + { + "$ref": "#/definitions/HazelnutScope" + } + ], + "default": "example" + }, + "productSurface": { + "allOf": [ + { + "$ref": "#/definitions/ProductSurface" + } + ], + "default": "codex" + } + }, "type": "object" }, "SkillsRemoteWriteParams": { "properties": { "hazelnutId": { "type": "string" - }, - "isPreload": { - "type": "boolean" } }, "required": [ - "hazelnutId", - "isPreload" + "hazelnutId" ], "type": "object" }, @@ -3599,9 +3635,9 @@ }, "method": { "enum": [ - "skills/remote/read" + "skills/remote/list" ], - "title": "Skills/remote/readRequestMethod", + "title": "Skills/remote/listRequestMethod", "type": "string" }, "params": { @@ -3613,7 +3649,7 @@ "method", "params" ], - "title": "Skills/remote/readRequest", + "title": "Skills/remote/listRequest", "type": "object" }, { @@ -3623,9 +3659,9 @@ }, "method": { "enum": [ - "skills/remote/write" + "skills/remote/export" ], - "title": "Skills/remote/writeRequestMethod", + "title": "Skills/remote/exportRequestMethod", "type": "string" }, "params": { @@ -3637,7 +3673,7 @@ "method", "params" ], - "title": "Skills/remote/writeRequest", + "title": "Skills/remote/exportRequest", "type": "object" }, { diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 21d8784c34..5ec61285ca 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -473,6 +473,35 @@ "title": "WarningEventMsg", "type": "object" }, + { + "description": "Model routing changed from the requested model to a different model.", + "properties": { + "from_model": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "to_model": { + "type": "string" + }, + "type": { + "enum": [ + "model_reroute" + ], + "title": "ModelRerouteEventMsgType", + "type": "string" + } + }, + "required": [ + "from_model", + "reason", + "to_model", + "type" + ], + "title": "ModelRerouteEventMsg", + "type": "object" + }, { "description": "Conversation history was compacted (either automatically or manually).", "properties": { @@ -3318,6 +3347,12 @@ ], "type": "string" }, + "ModelRerouteReason": { + "enum": [ + "high_risk_cyber_activity" + ], + "type": "string" + }, "NetworkAccess": { "description": "Represents whether outbound network access is available to the agent.", "enum": [ @@ -5378,6 +5413,35 @@ "title": "WarningEventMsg", "type": "object" }, + { + "description": "Model routing changed from the requested model to a different model.", + "properties": { + "from_model": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "to_model": { + "type": "string" + }, + "type": { + "enum": [ + "model_reroute" + ], + "title": "ModelRerouteEventMsgType", + "type": "string" + } + }, + "required": [ + "from_model", + "reason", + "to_model", + "type" + ], + "title": "ModelRerouteEventMsg", + "type": "object" + }, { "description": "Conversation history was compacted (either automatically or manually).", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 6b12cee9cf..540e037563 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -165,9 +165,71 @@ } ] }, + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/AppBranding" + }, + { + "type": "null" + } + ] + }, "description": { "type": [ "string", @@ -198,6 +260,15 @@ "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", "type": "boolean" }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "logoUrl": { "type": [ "string", @@ -235,6 +306,130 @@ ], "type": "object" }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "firstPartyType": { + "type": [ + "string", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" + }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", "oneOf": [ @@ -1094,6 +1289,35 @@ "title": "WarningEventMsg", "type": "object" }, + { + "description": "Model routing changed from the requested model to a different model.", + "properties": { + "from_model": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason2" + }, + "to_model": { + "type": "string" + }, + "type": { + "enum": [ + "model_reroute" + ], + "title": "ModelRerouteEventMsgType", + "type": "string" + } + }, + "required": [ + "from_model", + "reason", + "to_model", + "type" + ], + "title": "ModelRerouteEventMsg", + "type": "object" + }, { "description": "Conversation history was compacted (either automatically or manually).", "properties": { @@ -4210,6 +4434,45 @@ ], "type": "string" }, + "ModelRerouteReason": { + "enum": [ + "highRiskCyberActivity" + ], + "type": "string" + }, + "ModelRerouteReason2": { + "enum": [ + "high_risk_cyber_activity" + ], + "type": "string" + }, + "ModelReroutedNotification": { + "properties": { + "fromModel": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "threadId": { + "type": "string" + }, + "toModel": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "fromModel", + "reason", + "threadId", + "toModel", + "turnId" + ], + "type": "object" + }, "NetworkAccess": { "description": "Represents whether outbound network access is available to the agent.", "enum": [ @@ -6276,6 +6539,17 @@ ], "type": "object" }, + "ThreadArchivedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "ThreadId": { "type": "string" }, @@ -6820,6 +7094,17 @@ ], "type": "object" }, + "ThreadUnarchivedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "TokenUsage": { "properties": { "cached_input_tokens": { @@ -7813,6 +8098,46 @@ "title": "Thread/startedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/archived" + ], + "title": "Thread/archivedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadArchivedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/archivedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/unarchived" + ], + "title": "Thread/unarchivedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUnarchivedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/unarchivedNotification", + "type": "object" + }, { "properties": { "method": { @@ -8276,6 +8601,26 @@ "title": "Thread/compactedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "model/rerouted" + ], + "title": "Model/reroutedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelReroutedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Model/reroutedNotification", + "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 5f352004cb..82f5c3b7dc 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 @@ -755,9 +755,9 @@ }, "method": { "enum": [ - "skills/remote/read" + "skills/remote/list" ], - "title": "Skills/remote/readRequestMethod", + "title": "Skills/remote/listRequestMethod", "type": "string" }, "params": { @@ -769,7 +769,7 @@ "method", "params" ], - "title": "Skills/remote/readRequest", + "title": "Skills/remote/listRequest", "type": "object" }, { @@ -779,9 +779,9 @@ }, "method": { "enum": [ - "skills/remote/write" + "skills/remote/export" ], - "title": "Skills/remote/writeRequestMethod", + "title": "Skills/remote/exportRequestMethod", "type": "string" }, "params": { @@ -793,7 +793,7 @@ "method", "params" ], - "title": "Skills/remote/writeRequest", + "title": "Skills/remote/exportRequest", "type": "object" }, { @@ -2486,6 +2486,35 @@ "title": "WarningEventMsg", "type": "object" }, + { + "description": "Model routing changed from the requested model to a different model.", + "properties": { + "from_model": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/v2/ModelRerouteReason" + }, + "to_model": { + "type": "string" + }, + "type": { + "enum": [ + "model_reroute" + ], + "title": "ModelRerouteEventMsgType", + "type": "string" + } + }, + "required": [ + "from_model", + "reason", + "to_model", + "type" + ], + "title": "ModelRerouteEventMsg", + "type": "object" + }, { "description": "Conversation history was compacted (either automatically or manually).", "properties": { @@ -6266,6 +6295,12 @@ ], "type": "string" }, + "ModelRerouteReason": { + "enum": [ + "high_risk_cyber_activity" + ], + "type": "string" + }, "NetworkAccess": { "description": "Represents whether outbound network access is available to the agent.", "enum": [ @@ -8028,6 +8063,46 @@ "title": "Thread/startedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/archived" + ], + "title": "Thread/archivedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadArchivedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/archivedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/unarchived" + ], + "title": "Thread/unarchivedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadUnarchivedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/unarchivedNotification", + "type": "object" + }, { "properties": { "method": { @@ -8491,6 +8566,26 @@ "title": "Thread/compactedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "model/rerouted" + ], + "title": "Model/reroutedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ModelReroutedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Model/reroutedNotification", + "type": "object" + }, { "properties": { "method": { @@ -10216,6 +10311,48 @@ }, "type": "object" }, + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, "AppConfig": { "properties": { "disabled_reason": { @@ -10245,6 +10382,26 @@ "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppBranding" + }, + { + "type": "null" + } + ] + }, "description": { "type": [ "string", @@ -10275,6 +10432,15 @@ "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", "type": "boolean" }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "logoUrl": { "type": [ "string", @@ -10314,6 +10480,130 @@ "title": "AppListUpdatedNotification", "type": "object" }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "firstPartyType": { + "type": [ + "string", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/v2/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" + }, "AppsConfig": { "type": "object" }, @@ -12114,6 +12404,15 @@ }, "type": "object" }, + "HazelnutScope": { + "enum": [ + "example", + "workspace-shared", + "all-shared", + "personal" + ], + "type": "string" + }, "InputModality": { "description": "Canonical user-input modality tags advertised by a model.", "oneOf": [ @@ -12750,6 +13049,41 @@ "title": "ModelListResponse", "type": "object" }, + "ModelRerouteReason": { + "enum": [ + "highRiskCyberActivity" + ], + "type": "string" + }, + "ModelReroutedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "fromModel": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/v2/ModelRerouteReason" + }, + "threadId": { + "type": "string" + }, + "toModel": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "fromModel", + "reason", + "threadId", + "toModel", + "turnId" + ], + "title": "ModelReroutedNotification", + "type": "object" + }, "NetworkAccess": { "enum": [ "restricted", @@ -12967,6 +13301,15 @@ ], "type": "string" }, + "ProductSurface": { + "enum": [ + "chatgpt", + "codex", + "api", + "atlas" + ], + "type": "string" + }, "ProfileV2": { "additionalProperties": true, "properties": { @@ -14498,6 +14841,28 @@ }, "SkillsRemoteReadParams": { "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "hazelnutScope": { + "allOf": [ + { + "$ref": "#/definitions/v2/HazelnutScope" + } + ], + "default": "example" + }, + "productSurface": { + "allOf": [ + { + "$ref": "#/definitions/v2/ProductSurface" + } + ], + "default": "codex" + } + }, "title": "SkillsRemoteReadParams", "type": "object" }, @@ -14522,14 +14887,10 @@ "properties": { "hazelnutId": { "type": "string" - }, - "isPreload": { - "type": "boolean" } }, "required": [ - "hazelnutId", - "isPreload" + "hazelnutId" ], "title": "SkillsRemoteWriteParams", "type": "object" @@ -14540,16 +14901,12 @@ "id": { "type": "string" }, - "name": { - "type": "string" - }, "path": { "type": "string" } }, "required": [ "id", - "name", "path" ], "title": "SkillsRemoteWriteResponse", @@ -14790,6 +15147,19 @@ "title": "ThreadArchiveResponse", "type": "object" }, + "ThreadArchivedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadArchivedNotification", + "type": "object" + }, "ThreadCompactStartParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -15998,6 +16368,19 @@ "title": "ThreadUnarchiveResponse", "type": "object" }, + "ThreadUnarchivedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUnarchivedNotification", + "type": "object" + }, "TokenUsageBreakdown": { "properties": { "cachedInputTokens": { diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index 225ea3f42a..a73c550f69 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -473,6 +473,35 @@ "title": "WarningEventMsg", "type": "object" }, + { + "description": "Model routing changed from the requested model to a different model.", + "properties": { + "from_model": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "to_model": { + "type": "string" + }, + "type": { + "enum": [ + "model_reroute" + ], + "title": "ModelRerouteEventMsgType", + "type": "string" + } + }, + "required": [ + "from_model", + "reason", + "to_model", + "type" + ], + "title": "ModelRerouteEventMsg", + "type": "object" + }, { "description": "Conversation history was compacted (either automatically or manually).", "properties": { @@ -3318,6 +3347,12 @@ ], "type": "string" }, + "ModelRerouteReason": { + "enum": [ + "high_risk_cyber_activity" + ], + "type": "string" + }, "NetworkAccess": { "description": "Represents whether outbound network access is available to the agent.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index 2b305281c8..6dc02934d3 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -473,6 +473,35 @@ "title": "WarningEventMsg", "type": "object" }, + { + "description": "Model routing changed from the requested model to a different model.", + "properties": { + "from_model": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "to_model": { + "type": "string" + }, + "type": { + "enum": [ + "model_reroute" + ], + "title": "ModelRerouteEventMsgType", + "type": "string" + } + }, + "required": [ + "from_model", + "reason", + "to_model", + "type" + ], + "title": "ModelRerouteEventMsg", + "type": "object" + }, { "description": "Conversation history was compacted (either automatically or manually).", "properties": { @@ -3318,6 +3347,12 @@ ], "type": "string" }, + "ModelRerouteReason": { + "enum": [ + "high_risk_cyber_activity" + ], + "type": "string" + }, "NetworkAccess": { "description": "Represents whether outbound network access is available to the agent.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index d4bbf908d5..69967e6853 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -473,6 +473,35 @@ "title": "WarningEventMsg", "type": "object" }, + { + "description": "Model routing changed from the requested model to a different model.", + "properties": { + "from_model": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "to_model": { + "type": "string" + }, + "type": { + "enum": [ + "model_reroute" + ], + "title": "ModelRerouteEventMsgType", + "type": "string" + } + }, + "required": [ + "from_model", + "reason", + "to_model", + "type" + ], + "title": "ModelRerouteEventMsg", + "type": "object" + }, { "description": "Conversation history was compacted (either automatically or manually).", "properties": { @@ -3318,6 +3347,12 @@ ], "type": "string" }, + "ModelRerouteReason": { + "enum": [ + "high_risk_cyber_activity" + ], + "type": "string" + }, "NetworkAccess": { "description": "Represents whether outbound network access is available to the agent.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json index 1638472c4e..0813ed6f56 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json @@ -1,9 +1,71 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/AppBranding" + }, + { + "type": "null" + } + ] + }, "description": { "type": [ "string", @@ -34,6 +96,15 @@ "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", "type": "boolean" }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "logoUrl": { "type": [ "string", @@ -55,6 +126,130 @@ "name" ], "type": "object" + }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "firstPartyType": { + "type": [ + "string", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" } }, "description": "EXPERIMENTAL - notification emitted when the app list changes.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json index cec3da5ccc..4697b34e12 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json @@ -1,9 +1,71 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/AppBranding" + }, + { + "type": "null" + } + ] + }, "description": { "type": [ "string", @@ -34,6 +96,15 @@ "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", "type": "boolean" }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "logoUrl": { "type": [ "string", @@ -55,6 +126,130 @@ "name" ], "type": "object" + }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "firstPartyType": { + "type": [ + "string", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" } }, "description": "EXPERIMENTAL - app list response.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelReroutedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ModelReroutedNotification.json new file mode 100644 index 0000000000..b9bcc491b0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ModelReroutedNotification.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ModelRerouteReason": { + "enum": [ + "highRiskCyberActivity" + ], + "type": "string" + } + }, + "properties": { + "fromModel": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "threadId": { + "type": "string" + }, + "toModel": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "fromModel", + "reason", + "threadId", + "toModel", + "turnId" + ], + "title": "ModelReroutedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json index ace2fb5ba7..f99e53d894 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json @@ -1,5 +1,47 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "HazelnutScope": { + "enum": [ + "example", + "workspace-shared", + "all-shared", + "personal" + ], + "type": "string" + }, + "ProductSurface": { + "enum": [ + "chatgpt", + "codex", + "api", + "atlas" + ], + "type": "string" + } + }, + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "hazelnutScope": { + "allOf": [ + { + "$ref": "#/definitions/HazelnutScope" + } + ], + "default": "example" + }, + "productSurface": { + "allOf": [ + { + "$ref": "#/definitions/ProductSurface" + } + ], + "default": "codex" + } + }, "title": "SkillsRemoteReadParams", "type": "object" } \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json index 871a5a428a..f1a70eeeb0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json @@ -3,14 +3,10 @@ "properties": { "hazelnutId": { "type": "string" - }, - "isPreload": { - "type": "boolean" } }, "required": [ - "hazelnutId", - "isPreload" + "hazelnutId" ], "title": "SkillsRemoteWriteParams", "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json index 1a9473d054..b732732bdc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json @@ -4,16 +4,12 @@ "id": { "type": "string" }, - "name": { - "type": "string" - }, "path": { "type": "string" } }, "required": [ "id", - "name", "path" ], "title": "SkillsRemoteWriteResponse", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadArchivedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadArchivedNotification.json new file mode 100644 index 0000000000..cd24f957d4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadArchivedNotification.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadArchivedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchivedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchivedNotification.json new file mode 100644 index 0000000000..7e4bcd566e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchivedNotification.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUnarchivedNotification", + "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 3b5f6787b1..40757d9d95 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -57,4 +57,4 @@ import type { TurnSteerParams } from "./v2/TurnSteerParams"; /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/read", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/write", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts index 2c0514870f..5ed3ca2529 100644 --- a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts +++ b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts @@ -42,6 +42,7 @@ import type { McpStartupCompleteEvent } from "./McpStartupCompleteEvent"; import type { McpStartupUpdateEvent } from "./McpStartupUpdateEvent"; import type { McpToolCallBeginEvent } from "./McpToolCallBeginEvent"; import type { McpToolCallEndEvent } from "./McpToolCallEndEvent"; +import type { ModelRerouteEvent } from "./ModelRerouteEvent"; import type { PatchApplyBeginEvent } from "./PatchApplyBeginEvent"; import type { PatchApplyEndEvent } from "./PatchApplyEndEvent"; import type { PlanDeltaEvent } from "./PlanDeltaEvent"; @@ -74,4 +75,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent"; * Response event from the agent * NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen. */ -export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent; +export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent; diff --git a/codex-rs/app-server-protocol/schema/typescript/ModelRerouteEvent.ts b/codex-rs/app-server-protocol/schema/typescript/ModelRerouteEvent.ts new file mode 100644 index 0000000000..23a4e1efb6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/ModelRerouteEvent.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 { ModelRerouteReason } from "./ModelRerouteReason"; + +export type ModelRerouteEvent = { from_model: string, to_model: string, reason: ModelRerouteReason, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ModelRerouteReason.ts b/codex-rs/app-server-protocol/schema/typescript/ModelRerouteReason.ts new file mode 100644 index 0000000000..f5e1abf1e3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/ModelRerouteReason.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 ModelRerouteReason = "high_risk_cyber_activity"; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index ffc39598b6..8ac8322e9e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -21,15 +21,18 @@ import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification"; import type { ItemStartedNotification } from "./v2/ItemStartedNotification"; import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification"; import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification"; +import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification"; import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification"; import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification"; import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification"; import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification"; import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification"; import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; +import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification"; import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; +import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification"; import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification"; import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification"; @@ -39,4 +42,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/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 3c0826712a..2a626d3638 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -125,6 +125,8 @@ export type { McpToolCallBeginEvent } from "./McpToolCallBeginEvent"; export type { McpToolCallEndEvent } from "./McpToolCallEndEvent"; export type { MessagePhase } from "./MessagePhase"; export type { ModeKind } from "./ModeKind"; +export type { ModelRerouteEvent } from "./ModelRerouteEvent"; +export type { ModelRerouteReason } from "./ModelRerouteReason"; export type { NetworkAccess } from "./NetworkAccess"; export type { NetworkApprovalContext } from "./NetworkApprovalContext"; export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts new file mode 100644 index 0000000000..873398db67 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * EXPERIMENTAL - app metadata returned by app-list APIs. + */ +export type AppBranding = { category: string | null, developer: string | null, website: string | null, privacyPolicy: string | null, termsOfService: string | null, isDiscoverableApp: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts index 2f65c18327..0c9a13b124 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts @@ -1,11 +1,13 @@ // 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 { AppBranding } from "./AppBranding"; +import type { AppMetadata } from "./AppMetadata"; /** * EXPERIMENTAL - app metadata returned by app-list APIs. */ -export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, distributionChannel: string | null, installUrl: string | null, isAccessible: boolean, +export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, distributionChannel: string | null, branding: AppBranding | null, appMetadata: AppMetadata | null, labels: { [key in string]?: string } | null, installUrl: string | null, isAccessible: boolean, /** * Whether this app is enabled in config.toml. * Example: diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts new file mode 100644 index 0000000000..f1a5001eb1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts @@ -0,0 +1,7 @@ +// 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 { AppReview } from "./AppReview"; +import type { AppScreenshot } from "./AppScreenshot"; + +export type AppMetadata = { review: AppReview | null, categories: Array | null, subCategories: Array | null, seoDescription: string | null, screenshots: Array | null, developer: string | null, version: string | null, versionId: string | null, versionNotes: string | null, firstPartyType: string | null, firstPartyRequiresInstall: boolean | null, showInComposerWhenUnlinked: boolean | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.ts new file mode 100644 index 0000000000..10fd95f09d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.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 AppReview = { status: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.ts new file mode 100644 index 0000000000..0d264246f3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.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 AppScreenshot = { url: string | null, fileId: string | null, userPrompt: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts new file mode 100644 index 0000000000..e623f1860b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.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 HazelnutScope = "example" | "workspace-shared" | "all-shared" | "personal"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelRerouteReason.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelRerouteReason.ts new file mode 100644 index 0000000000..e780e7f95d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelRerouteReason.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 ModelRerouteReason = "highRiskCyberActivity"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelReroutedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelReroutedNotification.ts new file mode 100644 index 0000000000..9b6b2e524a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelReroutedNotification.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 { ModelRerouteReason } from "./ModelRerouteReason"; + +export type ModelReroutedNotification = { threadId: string, turnId: string, fromModel: string, toModel: string, reason: ModelRerouteReason, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts new file mode 100644 index 0000000000..9998c727a8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.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 ProductSurface = "chatgpt" | "codex" | "api" | "atlas"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts index 9f91787686..1257f0d791 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts @@ -1,5 +1,7 @@ // 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 { HazelnutScope } from "./HazelnutScope"; +import type { ProductSurface } from "./ProductSurface"; -export type SkillsRemoteReadParams = Record; +export type SkillsRemoteReadParams = { hazelnutScope: HazelnutScope, productSurface: ProductSurface, enabled: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts index 857b609ef1..ea42595bfd 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.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 SkillsRemoteWriteParams = { hazelnutId: string, isPreload: boolean, }; +export type SkillsRemoteWriteParams = { hazelnutId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts index cf1665ab97..228b723b19 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.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 SkillsRemoteWriteResponse = { id: string, name: string, path: string, }; +export type SkillsRemoteWriteResponse = { id: string, path: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadArchivedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadArchivedNotification.ts new file mode 100644 index 0000000000..cca1890791 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadArchivedNotification.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 ThreadArchivedNotification = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnarchivedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnarchivedNotification.ts new file mode 100644 index 0000000000..e2c1617172 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnarchivedNotification.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 ThreadUnarchivedNotification = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 500b1d930f..8b9453c170 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -6,9 +6,13 @@ export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUp export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification"; export type { AnalyticsConfig } from "./AnalyticsConfig"; +export type { AppBranding } from "./AppBranding"; export type { AppDisabledReason } from "./AppDisabledReason"; export type { AppInfo } from "./AppInfo"; export type { AppListUpdatedNotification } from "./AppListUpdatedNotification"; +export type { AppMetadata } from "./AppMetadata"; +export type { AppReview } from "./AppReview"; +export type { AppScreenshot } from "./AppScreenshot"; export type { AppsConfig } from "./AppsConfig"; export type { AppsListParams } from "./AppsListParams"; export type { AppsListResponse } from "./AppsListResponse"; @@ -70,6 +74,7 @@ export type { GetAccountParams } from "./GetAccountParams"; export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; export type { GetAccountResponse } from "./GetAccountResponse"; export type { GitInfo } from "./GitInfo"; +export type { HazelnutScope } from "./HazelnutScope"; export type { ItemCompletedNotification } from "./ItemCompletedNotification"; export type { ItemStartedNotification } from "./ItemStartedNotification"; export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams"; @@ -91,12 +96,15 @@ export type { MergeStrategy } from "./MergeStrategy"; export type { Model } from "./Model"; export type { ModelListParams } from "./ModelListParams"; export type { ModelListResponse } from "./ModelListResponse"; +export type { ModelRerouteReason } from "./ModelRerouteReason"; +export type { ModelReroutedNotification } from "./ModelReroutedNotification"; export type { NetworkAccess } from "./NetworkAccess"; export type { NetworkRequirements } from "./NetworkRequirements"; export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; export type { PatchChangeKind } from "./PatchChangeKind"; export type { PlanDeltaNotification } from "./PlanDeltaNotification"; +export type { ProductSurface } from "./ProductSurface"; export type { ProfileV2 } from "./ProfileV2"; export type { RateLimitSnapshot } from "./RateLimitSnapshot"; export type { RateLimitWindow } from "./RateLimitWindow"; @@ -139,6 +147,7 @@ export type { TextRange } from "./TextRange"; export type { Thread } from "./Thread"; export type { ThreadArchiveParams } from "./ThreadArchiveParams"; export type { ThreadArchiveResponse } from "./ThreadArchiveResponse"; +export type { ThreadArchivedNotification } from "./ThreadArchivedNotification"; export type { ThreadCompactStartParams } from "./ThreadCompactStartParams"; export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; export type { ThreadForkParams } from "./ThreadForkParams"; @@ -166,6 +175,7 @@ export type { ThreadTokenUsage } from "./ThreadTokenUsage"; export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification"; export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams"; export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse"; +export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification"; export type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer"; export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 6c58d3b90c..64e5d2c682 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -239,11 +239,11 @@ client_request_definitions! { params: v2::SkillsListParams, response: v2::SkillsListResponse, }, - SkillsRemoteRead => "skills/remote/read" { + SkillsRemoteList => "skills/remote/list" { params: v2::SkillsRemoteReadParams, response: v2::SkillsRemoteReadResponse, }, - SkillsRemoteWrite => "skills/remote/write" { + SkillsRemoteExport => "skills/remote/export" { params: v2::SkillsRemoteWriteParams, response: v2::SkillsRemoteWriteResponse, }, @@ -769,6 +769,8 @@ server_notification_definitions! { /// NEW NOTIFICATIONS Error => "error" (v2::ErrorNotification), ThreadStarted => "thread/started" (v2::ThreadStartedNotification), + ThreadArchived => "thread/archived" (v2::ThreadArchivedNotification), + ThreadUnarchived => "thread/unarchived" (v2::ThreadUnarchivedNotification), ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification), ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification), TurnStarted => "turn/started" (v2::TurnStartedNotification), @@ -795,6 +797,7 @@ server_notification_definitions! { ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification), /// Deprecated: Use `ContextCompaction` item type instead. ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification), + ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification), DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification), ConfigWarning => "configWarning" (v2::ConfigWarningNotification), FuzzyFileSearchSessionUpdated => "fuzzyFileSearch/sessionUpdated" (FuzzyFileSearchSessionUpdatedNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 9a184d1754..96c14ef5f1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -30,6 +30,7 @@ use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus; +use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason; use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; @@ -229,6 +230,12 @@ v2_enum_from_core!( } ); +v2_enum_from_core!( + pub enum ModelRerouteReason from CoreModelRerouteReason { + HighRiskCyberActivity + } +); + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] @@ -1280,6 +1287,55 @@ pub struct AppsListParams { pub force_refetch: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - app metadata returned by app-list APIs. +pub struct AppBranding { + pub category: Option, + pub developer: Option, + pub website: Option, + pub privacy_policy: Option, + pub terms_of_service: Option, + pub is_discoverable_app: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppReview { + pub status: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppScreenshot { + pub url: Option, + #[serde(alias = "file_id")] + pub file_id: Option, + #[serde(alias = "user_prompt")] + pub user_prompt: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppMetadata { + pub review: Option, + pub categories: Option>, + pub sub_categories: Option>, + pub seo_description: Option, + pub screenshots: Option>, + pub developer: Option, + pub version: Option, + pub version_id: Option, + pub version_notes: Option, + pub first_party_type: Option, + pub first_party_requires_install: Option, + pub show_in_composer_when_unlinked: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1291,6 +1347,9 @@ pub struct AppInfo { pub logo_url: Option, pub logo_url_dark: Option, pub distribution_channel: Option, + pub branding: Option, + pub app_metadata: Option, + pub labels: Option>, pub install_url: Option, #[serde(default)] pub is_accessible: bool, @@ -1830,7 +1889,38 @@ pub struct SkillsListResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct SkillsRemoteReadParams {} +pub struct SkillsRemoteReadParams { + #[serde(default)] + pub hazelnut_scope: HazelnutScope, + #[serde(default)] + pub product_surface: ProductSurface, + #[serde(default)] + pub enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case")] +#[ts(export_to = "v2/")] +pub enum HazelnutScope { + #[default] + Example, + WorkspaceShared, + AllShared, + Personal, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(rename_all = "lowercase")] +#[ts(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum ProductSurface { + Chatgpt, + #[default] + Codex, + Api, + Atlas, +} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -1853,7 +1943,6 @@ pub struct SkillsRemoteReadResponse { #[ts(export_to = "v2/")] pub struct SkillsRemoteWriteParams { pub hazelnut_id: String, - pub is_preload: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -1861,7 +1950,6 @@ pub struct SkillsRemoteWriteParams { #[ts(export_to = "v2/")] pub struct SkillsRemoteWriteResponse { pub id: String, - pub name: String, pub path: PathBuf, } @@ -2825,6 +2913,20 @@ pub struct ThreadStartedNotification { pub thread: Thread, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadArchivedNotification { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUnarchivedNotification { + pub thread_id: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3305,6 +3407,17 @@ pub struct AccountLoginCompletedNotification { pub error: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelReroutedNotification { + pub thread_id: String, + pub turn_id: String, + pub from_model: String, + pub to_model: String, + pub reason: ModelRerouteReason, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 27bb78e49e..e414f1da6e 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -120,9 +120,9 @@ Example with notification opt-out: - `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, and `cwd` filters. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. -- `thread/archive` — move a thread’s rollout file into the archived directory; returns `{}` on success. +- `thread/archive` — move a thread’s rollout file into the archived directory; returns `{}` on success and emits `thread/archived`. - `thread/name/set` — set or update a thread’s user-facing name; returns `{}` on success. Thread names are not required to be unique; name lookups resolve to the most recently updated thread. -- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success. +- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`. - `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. - `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted. - `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. @@ -135,8 +135,8 @@ Example with notification opt-out: - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). -- `skills/remote/read` — list public remote skills (**under development; do not call from production clients yet**). -- `skills/remote/write` — download a public remote skill by `hazelnutId`; `isPreload=true` writes to `.codex/vendor_imports/skills` under `codex_home` (**under development; do not call from production clients yet**). +- `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**). +- `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**). - `app/list` — list available apps. - `skills/config/write` — write user-level skill config by path. - `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. @@ -274,6 +274,7 @@ Use `thread/archive` to move the persisted rollout (stored as a JSONL file on di ```json { "method": "thread/archive", "id": 21, "params": { "threadId": "thr_b" } } { "id": 21, "result": {} } +{ "method": "thread/archived", "params": { "threadId": "thr_b" } } ``` An archived thread will not appear in `thread/list` unless `archived` is set to `true`. @@ -285,6 +286,7 @@ Use `thread/unarchive` to move an archived rollout back into the sessions direct ```json { "method": "thread/unarchive", "id": 24, "params": { "threadId": "thr_b" } } { "id": 24, "result": { "thread": { "id": "thr_b" } } } +{ "method": "thread/unarchived", "params": { "threadId": "thr_b" } } ``` ### Example: Trigger thread compaction @@ -519,7 +521,7 @@ Notes: ## Events -Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `turn/*`, and `item/*` notifications. +Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `thread/archived`, `thread/unarchived`, `turn/*`, and `item/*` notifications. ### Notification opt-out @@ -550,6 +552,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn - `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`. - `turn/diff/updated` — `{ threadId, turnId, diff }` represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item. `diff` is the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individual `fileChange` items. - `turn/plan/updated` — `{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`. +- `model/rerouted` — `{ threadId, turnId, fromModel, toModel, reason }` when the backend reroutes a request to a different model (for example, due to high-risk cyber safety checks). Today both notifications carry an empty `items` array even when item events were streamed; rely on `item/*` notifications for the canonical item list until this is fixed. @@ -557,7 +560,7 @@ Today both notifications carry an empty `items` array even when item events were `ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items: -- `userMessage` — `{id, content}` where `content` is a list of user inputs (`text`, `image`, or `localImage`). Cyber model-routing warnings are surfaced as synthetic `userMessage` items with `text` prefixed by `Warning:`. +- `userMessage` — `{id, content}` where `content` is a list of user inputs (`text`, `image`, or `localImage`). - `agentMessage` — `{id, text}` containing the accumulated agent reply. - `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental). - `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models). @@ -771,7 +774,7 @@ To enable or disable a skill by path: ## Apps -Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, whether it is currently accessible, and whether it is enabled in config. +Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, `branding`, `appMetadata`, `labels`, whether it is currently accessible, and whether it is enabled in config. ```json { "method": "app/list", "id": 50, "params": { @@ -789,6 +792,9 @@ Use `app/list` to fetch available apps (connectors). Each entry includes metadat "logoUrl": "https://example.com/demo-app.png", "logoUrlDark": null, "distributionChannel": null, + "branding": null, + "appMetadata": null, + "labels": null, "installUrl": "https://chatgpt.com/apps/demo-app/demo-app", "isAccessible": true, "isEnabled": true @@ -816,6 +822,9 @@ The server also emits `app/list/updated` notifications whenever either source (a "logoUrl": "https://example.com/demo-app.png", "logoUrlDark": null, "distributionChannel": null, + "branding": null, + "appMetadata": null, + "labels": null, "installUrl": "https://chatgpt.com/apps/demo-app/demo-app", "isAccessible": true, "isEnabled": true diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 87ddf7e18a..3acdefc731 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -41,6 +41,7 @@ use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::McpToolCallError; use codex_app_server_protocol::McpToolCallResult; use codex_app_server_protocol::McpToolCallStatus; +use codex_app_server_protocol::ModelReroutedNotification; use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::PatchChangeKind as V2PatchChangeKind; use codex_app_server_protocol::PlanDeltaNotification; @@ -68,7 +69,6 @@ use codex_app_server_protocol::TurnInterruptResponse; use codex_app_server_protocol::TurnPlanStep; use codex_app_server_protocol::TurnPlanUpdatedNotification; use codex_app_server_protocol::TurnStatus; -use codex_app_server_protocol::UserInput as V2UserInput; use codex_app_server_protocol::build_turns_from_rollout_items; use codex_core::CodexThread; use codex_core::parse_command::shlex_join; @@ -96,8 +96,6 @@ use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUse use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse; use std::collections::HashMap; use std::convert::TryFrom; -use std::hash::Hash; -use std::hash::Hasher; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; @@ -125,32 +123,18 @@ pub(crate) async fn apply_bespoke_event_handling( EventMsg::TurnComplete(_ev) => { handle_turn_complete(conversation_id, event_turn_id, &outgoing, &thread_state).await; } - EventMsg::Warning(warning_event) => { - if matches!(api_version, ApiVersion::V2) - && is_safety_check_downgrade_warning(&warning_event.message) - { - let item = ThreadItem::UserMessage { - id: warning_item_id(&event_turn_id, &warning_event.message), - content: vec![V2UserInput::Text { - text: format!("Warning: {}", warning_event.message), - text_elements: Vec::new(), - }], - }; - let started = ItemStartedNotification { + EventMsg::Warning(_warning_event) => {} + EventMsg::ModelReroute(event) => { + if let ApiVersion::V2 = api_version { + let notification = ModelReroutedNotification { thread_id: conversation_id.to_string(), turn_id: event_turn_id.clone(), - item: item.clone(), + from_model: event.from_model, + to_model: event.to_model, + reason: event.reason.into(), }; outgoing - .send_server_notification(ServerNotification::ItemStarted(started)) - .await; - let completed = ItemCompletedNotification { - thread_id: conversation_id.to_string(), - turn_id: event_turn_id.clone(), - item, - }; - outgoing - .send_server_notification(ServerNotification::ItemCompleted(completed)) + .send_server_notification(ServerNotification::ModelRerouted(notification)) .await; } } @@ -1318,18 +1302,6 @@ async fn complete_command_execution_item( .await; } -fn is_safety_check_downgrade_warning(message: &str) -> bool { - message.contains("Your account was flagged for potentially high-risk cyber activity") - && message.contains("apply for trusted access: https://chatgpt.com/cyber") -} - -fn warning_item_id(turn_id: &str, message: &str) -> String { - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - message.hash(&mut hasher); - let digest = hasher.finish(); - format!("{turn_id}-warning-{digest:x}") -} - async fn maybe_emit_raw_response_item_completed( api_version: ApiVersion, conversation_id: ThreadId, @@ -2060,18 +2032,6 @@ mod tests { assert_eq!(item, expected); } - #[test] - fn safety_check_downgrade_warning_detection_matches_expected_message() { - let warning = "Your account was flagged for potentially high-risk cyber activity and this request was routed to gpt-5.2 as a fallback. To regain access to gpt-5.3-codex, apply for trusted access: https://chatgpt.com/cyber\nLearn more: https://developers.openai.com/codex/concepts/cyber-safety"; - assert!(is_safety_check_downgrade_warning(warning)); - } - - #[test] - fn safety_check_downgrade_warning_detection_ignores_other_warnings() { - let warning = "Model metadata for `mock-model` not found. Defaulting to fallback metadata; this can degrade performance and cause issues."; - assert!(!is_safety_check_downgrade_warning(warning)); - } - #[tokio::test] async fn test_handle_error_records_message() -> Result<()> { let conversation_id = ThreadId::new(); diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 0439068db9..5a6a246a3d 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -66,6 +66,7 @@ use codex_app_server_protocol::GetUserAgentResponse; use codex_app_server_protocol::GetUserSavedConfigResponse; use codex_app_server_protocol::GitDiffToRemoteResponse; use codex_app_server_protocol::GitInfo as ApiGitInfo; +use codex_app_server_protocol::HazelnutScope as ApiHazelnutScope; use codex_app_server_protocol::InputItem as WireInputItem; use codex_app_server_protocol::InterruptConversationParams; use codex_app_server_protocol::JSONRPCErrorError; @@ -92,6 +93,7 @@ use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; use codex_app_server_protocol::NewConversationParams; use codex_app_server_protocol::NewConversationResponse; +use codex_app_server_protocol::ProductSurface as ApiProductSurface; use codex_app_server_protocol::RemoveConversationListenerParams; use codex_app_server_protocol::RemoveConversationSubscriptionResponse; use codex_app_server_protocol::ResumeConversationParams; @@ -120,6 +122,7 @@ use codex_app_server_protocol::SkillsRemoteWriteResponse; use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadArchiveParams; use codex_app_server_protocol::ThreadArchiveResponse; +use codex_app_server_protocol::ThreadArchivedNotification; use codex_app_server_protocol::ThreadBackgroundTerminalsCleanParams; use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse; use codex_app_server_protocol::ThreadCompactStartParams; @@ -145,6 +148,7 @@ use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStartedNotification; use codex_app_server_protocol::ThreadUnarchiveParams; use codex_app_server_protocol::ThreadUnarchiveResponse; +use codex_app_server_protocol::ThreadUnarchivedNotification; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnStartParams; @@ -207,7 +211,7 @@ use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; use codex_core::rollout_date_parts; use codex_core::sandboxing::SandboxPermissions; -use codex_core::skills::remote::download_remote_skill; +use codex_core::skills::remote::export_remote_skill; use codex_core::skills::remote::list_remote_skills; use codex_core::state_db::StateDbHandle; use codex_core::state_db::get_state_db; @@ -229,6 +233,8 @@ use codex_protocol::protocol::GitInfo as CoreGitInfo; use codex_protocol::protocol::McpAuthStatus as CoreMcpAuthStatus; use codex_protocol::protocol::McpServerRefreshConfig; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; +use codex_protocol::protocol::RemoteSkillHazelnutScope; +use codex_protocol::protocol::RemoteSkillProductSurface; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::USER_MESSAGE_BEGIN; @@ -291,6 +297,24 @@ enum AppListLoadResult { Directory(Result, String>), } +fn convert_remote_scope(scope: ApiHazelnutScope) -> RemoteSkillHazelnutScope { + match scope { + ApiHazelnutScope::WorkspaceShared => RemoteSkillHazelnutScope::WorkspaceShared, + ApiHazelnutScope::AllShared => RemoteSkillHazelnutScope::AllShared, + ApiHazelnutScope::Personal => RemoteSkillHazelnutScope::Personal, + ApiHazelnutScope::Example => RemoteSkillHazelnutScope::Example, + } +} + +fn convert_remote_product_surface(product_surface: ApiProductSurface) -> RemoteSkillProductSurface { + match product_surface { + ApiProductSurface::Chatgpt => RemoteSkillProductSurface::Chatgpt, + ApiProductSurface::Codex => RemoteSkillProductSurface::Codex, + ApiProductSurface::Api => RemoteSkillProductSurface::Api, + ApiProductSurface::Atlas => RemoteSkillProductSurface::Atlas, + } +} + impl Drop for ActiveLogin { fn drop(&mut self) { self.shutdown_handle.shutdown(); @@ -549,12 +573,12 @@ impl CodexMessageProcessor { self.skills_list(to_connection_request_id(request_id), params) .await; } - ClientRequest::SkillsRemoteRead { request_id, params } => { - self.skills_remote_read(to_connection_request_id(request_id), params) + ClientRequest::SkillsRemoteList { request_id, params } => { + self.skills_remote_list(to_connection_request_id(request_id), params) .await; } - ClientRequest::SkillsRemoteWrite { request_id, params } => { - self.skills_remote_write(to_connection_request_id(request_id), params) + ClientRequest::SkillsRemoteExport { request_id, params } => { + self.skills_remote_export(to_connection_request_id(request_id), params) .await; } ClientRequest::AppsList { request_id, params } => { @@ -599,11 +623,10 @@ impl CodexMessageProcessor { ClientRequest::ModelList { request_id, params } => { let outgoing = self.outgoing.clone(); let thread_manager = self.thread_manager.clone(); - let config = self.config.clone(); let request_id = to_connection_request_id(request_id); tokio::spawn(async move { - Self::list_models(outgoing, thread_manager, config, request_id, params).await; + Self::list_models(outgoing, thread_manager, request_id, params).await; }); } ClientRequest::ExperimentalFeatureList { request_id, params } => { @@ -2105,10 +2128,17 @@ impl CodexMessageProcessor { } }; + let thread_id_str = thread_id.to_string(); match self.archive_thread_common(thread_id, &rollout_path).await { Ok(()) => { let response = ThreadArchiveResponse {}; self.outgoing.send_response(request_id, response).await; + let notification = ThreadArchivedNotification { + thread_id: thread_id_str, + }; + self.outgoing + .send_server_notification(ServerNotification::ThreadArchived(notification)) + .await; } Err(err) => { self.outgoing.send_error(request_id, err).await; @@ -2314,8 +2344,13 @@ impl CodexMessageProcessor { match result { Ok(thread) => { + let thread_id = thread.id.clone(); let response = ThreadUnarchiveResponse { thread }; self.outgoing.send_response(request_id, response).await; + let notification = ThreadUnarchivedNotification { thread_id }; + self.outgoing + .send_server_notification(ServerNotification::ThreadUnarchived(notification)) + .await; } Err(err) => { self.outgoing.send_error(request_id, err).await; @@ -3599,7 +3634,6 @@ impl CodexMessageProcessor { async fn list_models( outgoing: Arc, thread_manager: Arc, - config: Arc, request_id: ConnectionRequestId, params: ModelListParams, ) { @@ -3608,10 +3642,7 @@ impl CodexMessageProcessor { cursor, include_hidden, } = params; - let mut config = (*config).clone(); - config.features.enable(Feature::RemoteModels); - let models = - supported_models(thread_manager, &config, include_hidden.unwrap_or(false)).await; + let models = supported_models(thread_manager, include_hidden.unwrap_or(false)).await; let total = models.len(); if total == 0 { @@ -5048,12 +5079,25 @@ impl CodexMessageProcessor { .await; } - async fn skills_remote_read( + async fn skills_remote_list( &self, request_id: ConnectionRequestId, - _params: SkillsRemoteReadParams, + params: SkillsRemoteReadParams, ) { - match list_remote_skills(&self.config).await { + let hazelnut_scope = convert_remote_scope(params.hazelnut_scope); + let product_surface = convert_remote_product_surface(params.product_surface); + let enabled = if params.enabled { Some(true) } else { None }; + + let auth = self.auth_manager.auth().await; + match list_remote_skills( + &self.config, + auth.as_ref(), + hazelnut_scope, + product_surface, + enabled, + ) + .await + { Ok(skills) => { let data = skills .into_iter() @@ -5070,23 +5114,21 @@ impl CodexMessageProcessor { Err(err) => { self.send_internal_error( request_id, - format!("failed to read remote skills: {err}"), + format!("failed to list remote skills: {err}"), ) .await; } } } - async fn skills_remote_write( + async fn skills_remote_export( &self, request_id: ConnectionRequestId, params: SkillsRemoteWriteParams, ) { - let SkillsRemoteWriteParams { - hazelnut_id, - is_preload, - } = params; - let response = download_remote_skill(&self.config, hazelnut_id.as_str(), is_preload).await; + let SkillsRemoteWriteParams { hazelnut_id } = params; + let auth = self.auth_manager.auth().await; + let response = export_remote_skill(&self.config, auth.as_ref(), hazelnut_id.as_str()).await; match response { Ok(downloaded) => { @@ -5095,7 +5137,6 @@ impl CodexMessageProcessor { request_id, SkillsRemoteWriteResponse { id: downloaded.id, - name: downloaded.name, path: downloaded.path, }, ) diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index d31e005210..1594c66229 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -3,18 +3,16 @@ use std::sync::Arc; use codex_app_server_protocol::Model; use codex_app_server_protocol::ReasoningEffortOption; use codex_core::ThreadManager; -use codex_core::config::Config; use codex_core::models_manager::manager::RefreshStrategy; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; pub async fn supported_models( thread_manager: Arc, - config: &Config, include_hidden: bool, ) -> Vec { thread_manager - .list_models(config, RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached) .await .into_iter() .filter(|preset| include_hidden || preset.show_in_picker) diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 2d8b748c90..810182db0a 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -399,6 +399,8 @@ mod tests { use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::LoginChatGptCompleteNotification; + use codex_app_server_protocol::ModelRerouteReason; + use codex_app_server_protocol::ModelReroutedNotification; use codex_app_server_protocol::RateLimitSnapshot; use codex_app_server_protocol::RateLimitWindow; use codex_protocol::ThreadId; @@ -546,6 +548,34 @@ mod tests { ); } + #[test] + fn verify_model_rerouted_notification_serialization() { + let notification = ServerNotification::ModelRerouted(ModelReroutedNotification { + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + from_model: "gpt-5.3-codex".to_string(), + to_model: "gpt-5.2".to_string(), + reason: ModelRerouteReason::HighRiskCyberActivity, + }); + + let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification); + assert_eq!( + json!({ + "method": "model/rerouted", + "params": { + "threadId": "thread-1", + "turnId": "turn-1", + "fromModel": "gpt-5.3-codex", + "toModel": "gpt-5.2", + "reason": "highRiskCyberActivity", + }, + }), + serde_json::to_value(jsonrpc_notification) + .expect("ensure the notification serializes correctly"), + "ensure the notification serializes correctly" + ); + } + #[tokio::test] async fn send_response_routes_to_target_connection() { let (tx, mut rx) = mpsc::channel::(4); diff --git a/codex-rs/app-server/tests/common/config.rs b/codex-rs/app-server/tests/common/config.rs index 09471b4a69..bffb35aa02 100644 --- a/codex-rs/app-server/tests/common/config.rs +++ b/codex-rs/app-server/tests/common/config.rs @@ -13,7 +13,7 @@ pub fn write_mock_responses_config_toml( compact_prompt: &str, ) -> std::io::Result<()> { // Phase 1: build the features block for config.toml. - let mut features = BTreeMap::from([(Feature::RemoteModels, false)]); + let mut features = BTreeMap::new(); for (feature, enabled) in feature_flags { features.insert(*feature, *enabled); } diff --git a/codex-rs/app-server/tests/suite/v2/app_list.rs b/codex-rs/app-server/tests/suite/v2/app_list.rs index 079eba4794..1fd3f5a447 100644 --- a/codex-rs/app-server/tests/suite/v2/app_list.rs +++ b/codex-rs/app-server/tests/suite/v2/app_list.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex as StdMutex; use std::time::Duration; @@ -16,8 +17,12 @@ use axum::http::HeaderMap; use axum::http::StatusCode; use axum::http::header::AUTHORIZATION; use axum::routing::get; +use codex_app_server_protocol::AppBranding; use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppListUpdatedNotification; +use codex_app_server_protocol::AppMetadata; +use codex_app_server_protocol::AppReview; +use codex_app_server_protocol::AppScreenshot; use codex_app_server_protocol::AppsListParams; use codex_app_server_protocol::AppsListResponse; use codex_app_server_protocol::JSONRPCError; @@ -85,6 +90,9 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -184,6 +192,9 @@ async fn list_apps_reports_is_enabled_from_config() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -249,6 +260,39 @@ enabled = false #[tokio::test] async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<()> { + let alpha_branding = Some(AppBranding { + category: Some("PRODUCTIVITY".to_string()), + developer: Some("Acme".to_string()), + website: Some("https://acme.example".to_string()), + privacy_policy: Some("https://acme.example/privacy".to_string()), + terms_of_service: Some("https://acme.example/terms".to_string()), + is_discoverable_app: true, + }); + let alpha_app_metadata = Some(AppMetadata { + review: Some(AppReview { + status: "APPROVED".to_string(), + }), + categories: Some(vec!["PRODUCTIVITY".to_string()]), + sub_categories: Some(vec!["WRITING".to_string()]), + seo_description: Some("Alpha connector".to_string()), + screenshots: Some(vec![AppScreenshot { + url: Some("https://example.com/alpha-screenshot.png".to_string()), + file_id: Some("file_123".to_string()), + user_prompt: "Summarize this draft".to_string(), + }]), + developer: Some("Acme".to_string()), + version: Some("1.2.3".to_string()), + version_id: Some("version_123".to_string()), + version_notes: Some("Fixes and improvements".to_string()), + first_party_type: Some("internal".to_string()), + first_party_requires_install: Some(true), + show_in_composer_when_unlinked: Some(true), + }); + let alpha_labels = Some(HashMap::from([ + ("feature".to_string(), "beta".to_string()), + ("source".to_string(), "directory".to_string()), + ])); + let connectors = vec![ AppInfo { id: "alpha".to_string(), @@ -257,6 +301,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: alpha_branding.clone(), + app_metadata: alpha_app_metadata.clone(), + labels: alpha_labels.clone(), install_url: None, is_accessible: false, is_enabled: true, @@ -268,6 +315,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -313,6 +363,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), is_accessible: true, is_enabled: true, @@ -329,6 +382,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()), is_accessible: true, is_enabled: true, @@ -340,6 +396,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: alpha_branding, + app_metadata: alpha_app_metadata, + labels: alpha_labels, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -377,6 +436,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -388,6 +450,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -437,6 +502,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -448,6 +516,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()), is_accessible: false, is_enabled: true, @@ -463,6 +534,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()), is_accessible: true, is_enabled: true, @@ -474,6 +548,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -506,6 +583,9 @@ async fn list_apps_paginates_results() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -517,6 +597,9 @@ async fn list_apps_paginates_results() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -571,6 +654,9 @@ async fn list_apps_paginates_results() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()), is_accessible: true, is_enabled: true, @@ -611,6 +697,9 @@ async fn list_apps_paginates_results() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -632,6 +721,9 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -733,6 +825,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -744,6 +839,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -790,6 +888,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), is_accessible: true, is_enabled: true, @@ -807,6 +908,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), is_accessible: true, is_enabled: true, @@ -818,6 +922,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -844,6 +951,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -870,6 +980,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -881,6 +994,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), is_accessible: false, is_enabled: true, @@ -895,6 +1011,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, diff --git a/codex-rs/app-server/tests/suite/v2/plan_item.rs b/codex-rs/app-server/tests/suite/v2/plan_item.rs index d138954ac4..881bbb54c8 100644 --- a/codex-rs/app-server/tests/suite/v2/plan_item.rs +++ b/codex-rs/app-server/tests/suite/v2/plan_item.rs @@ -215,10 +215,7 @@ async fn collect_turn_notifications( } fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { - let features = BTreeMap::from([ - (Feature::RemoteModels, false), - (Feature::CollaborationModes, true), - ]); + let features = BTreeMap::from([(Feature::CollaborationModes, true)]); let feature_entries = features .into_iter() .map(|(feature, enabled)| { diff --git a/codex-rs/app-server/tests/suite/v2/review.rs b/codex-rs/app-server/tests/suite/v2/review.rs index 05397d485d..219812d6b1 100644 --- a/codex-rs/app-server/tests/suite/v2/review.rs +++ b/codex-rs/app-server/tests/suite/v2/review.rs @@ -437,7 +437,6 @@ sandbox_mode = "read-only" model_provider = "mock_provider" [features] -remote_models = false shell_snapshot = false [model_providers.mock_provider] diff --git a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs index 20a7c60236..53acd492b6 100644 --- a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs +++ b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs @@ -3,8 +3,10 @@ use app_test_support::McpProcess; use app_test_support::to_response; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; -use codex_app_server_protocol::JSONRPCNotification; +use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::ModelRerouteReason; +use codex_app_server_protocol::ModelReroutedNotification; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadStartParams; @@ -23,7 +25,7 @@ const REQUESTED_MODEL: &str = "gpt-5.1-codex-max"; const SERVER_MODEL: &str = "gpt-5.2-codex"; #[tokio::test] -async fn openai_model_header_mismatch_emits_warning_item_v2() -> Result<()> { +async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -64,64 +66,30 @@ async fn openai_model_header_mismatch_emits_warning_item_v2() -> Result<()> { ..Default::default() }) .await?; - let _turn_resp: JSONRPCResponse = timeout( + let turn_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), ) .await??; - let _turn_start: TurnStartResponse = to_response(_turn_resp)?; + let turn_start: TurnStartResponse = to_response(turn_resp)?; - let warning_started = timeout(DEFAULT_READ_TIMEOUT, async { - loop { - let notification: JSONRPCNotification = mcp - .read_stream_until_notification_message("item/started") - .await?; - let params = notification.params.expect("item/started params"); - let started: ItemStartedNotification = - serde_json::from_value(params).expect("deserialize item/started"); - if warning_text_from_item(&started.item).is_some_and(is_cyber_model_warning_text) { - return Ok::(started); - } - } - }) - .await??; - - let warning_text = - warning_text_from_item(&warning_started.item).expect("expected warning user message item"); - assert!(warning_text.contains("Warning:")); - assert!(warning_text.contains("gpt-5.2 as a fallback")); - assert!(warning_text.contains("regain access to gpt-5.3-codex")); - - let warning_completed = timeout(DEFAULT_READ_TIMEOUT, async { - loop { - let notification: JSONRPCNotification = mcp - .read_stream_until_notification_message("item/completed") - .await?; - let params = notification.params.expect("item/completed params"); - let completed: ItemCompletedNotification = - serde_json::from_value(params).expect("deserialize item/completed"); - if warning_text_from_item(&completed.item).is_some_and(is_cyber_model_warning_text) { - return Ok::(completed); - } - } - }) - .await??; + let rerouted = collect_turn_notifications_and_validate_no_warning_item(&mut mcp).await?; assert_eq!( - warning_text_from_item(&warning_completed.item), - warning_text_from_item(&warning_started.item) + rerouted, + ModelReroutedNotification { + thread_id: thread.id, + turn_id: turn_start.turn.id, + from_model: REQUESTED_MODEL.to_string(), + to_model: SERVER_MODEL.to_string(), + reason: ModelRerouteReason::HighRiskCyberActivity, + } ); - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - Ok(()) } #[tokio::test] -async fn response_model_field_mismatch_emits_warning_item_v2_when_header_matches_requested() +async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when_header_matches_requested() -> Result<()> { skip_if_no_network!(Ok(())); @@ -174,54 +142,65 @@ async fn response_model_field_mismatch_emits_warning_item_v2_when_header_matches mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), ) .await??; - let _turn_start: TurnStartResponse = to_response(turn_resp)?; + let turn_start: TurnStartResponse = to_response(turn_resp)?; - let warning_started = timeout(DEFAULT_READ_TIMEOUT, async { - loop { - let notification: JSONRPCNotification = mcp - .read_stream_until_notification_message("item/started") - .await?; - let params = notification.params.expect("item/started params"); - let started: ItemStartedNotification = - serde_json::from_value(params).expect("deserialize item/started"); - if warning_text_from_item(&started.item).is_some_and(is_cyber_model_warning_text) { - return Ok::(started); - } - } - }) - .await??; - let warning_text = - warning_text_from_item(&warning_started.item).expect("expected warning user message item"); - assert!(warning_text.contains("gpt-5.2 as a fallback")); - - let warning_completed = timeout(DEFAULT_READ_TIMEOUT, async { - loop { - let notification: JSONRPCNotification = mcp - .read_stream_until_notification_message("item/completed") - .await?; - let params = notification.params.expect("item/completed params"); - let completed: ItemCompletedNotification = - serde_json::from_value(params).expect("deserialize item/completed"); - if warning_text_from_item(&completed.item).is_some_and(is_cyber_model_warning_text) { - return Ok::(completed); - } - } - }) - .await??; + let rerouted = collect_turn_notifications_and_validate_no_warning_item(&mut mcp).await?; assert_eq!( - warning_text_from_item(&warning_completed.item), - warning_text_from_item(&warning_started.item) + rerouted, + ModelReroutedNotification { + thread_id: thread.id, + turn_id: turn_start.turn.id, + from_model: REQUESTED_MODEL.to_string(), + to_model: SERVER_MODEL.to_string(), + reason: ModelRerouteReason::HighRiskCyberActivity, + } ); - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - Ok(()) } +async fn collect_turn_notifications_and_validate_no_warning_item( + mcp: &mut McpProcess, +) -> Result { + let mut rerouted = None; + + loop { + let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??; + let JSONRPCMessage::Notification(notification) = message else { + continue; + }; + match notification.method.as_str() { + "model/rerouted" => { + let params = notification.params.ok_or_else(|| { + anyhow::anyhow!("model/rerouted notifications must include params") + })?; + let payload: ModelReroutedNotification = serde_json::from_value(params)?; + rerouted = Some(payload); + } + "item/started" => { + let params = notification.params.ok_or_else(|| { + anyhow::anyhow!("item/started notifications must include params") + })?; + let payload: ItemStartedNotification = serde_json::from_value(params)?; + assert!(!is_warning_user_message_item(&payload.item)); + } + "item/completed" => { + let params = notification.params.ok_or_else(|| { + anyhow::anyhow!("item/completed notifications must include params") + })?; + let payload: ItemCompletedNotification = serde_json::from_value(params)?; + assert!(!is_warning_user_message_item(&payload.item)); + } + "turn/completed" => { + return rerouted.ok_or_else(|| { + anyhow::anyhow!("expected model/rerouted notification before turn/completed") + }); + } + _ => {} + } + } +} + fn warning_text_from_item(item: &ThreadItem) -> Option<&str> { let ThreadItem::UserMessage { content, .. } = item else { return None; @@ -233,9 +212,8 @@ fn warning_text_from_item(item: &ThreadItem) -> Option<&str> { }) } -fn is_cyber_model_warning_text(text: &str) -> bool { - text.contains("flagged for potentially high-risk cyber activity") - && text.contains("apply for trusted access: https://chatgpt.com/cyber") +fn is_warning_user_message_item(item: &ThreadItem) -> bool { + warning_text_from_item(item).is_some() } fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> { diff --git a/codex-rs/app-server/tests/suite/v2/thread_archive.rs b/codex-rs/app-server/tests/suite/v2/thread_archive.rs index 8fccd2227b..d7aa60f2bb 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_archive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_archive.rs @@ -7,6 +7,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadArchiveParams; use codex_app_server_protocol::ThreadArchiveResponse; +use codex_app_server_protocol::ThreadArchivedNotification; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -14,6 +15,7 @@ use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput; use codex_core::ARCHIVED_SESSIONS_SUBDIR; use codex_core::find_thread_path_by_id_str; +use pretty_assertions::assert_eq; use std::path::Path; use tempfile::TempDir; use tokio::time::timeout; @@ -122,6 +124,17 @@ async fn thread_archive_requires_materialized_rollout() -> Result<()> { ) .await??; let _: ThreadArchiveResponse = to_response::(archive_resp)?; + let archive_notification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("thread/archived"), + ) + .await??; + let archived_notification: ThreadArchivedNotification = serde_json::from_value( + archive_notification + .params + .expect("thread/archived notification params"), + )?; + assert_eq!(archived_notification.thread_id, thread.id); // Verify file moved. let archived_directory = codex_home.path().join(ARCHIVED_SESSIONS_SUBDIR); diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index e001d4a7fd..3976d3eea6 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -1008,7 +1008,6 @@ sandbox_mode = "read-only" model_provider = "mock_provider" [features] -remote_models = false personality = true [model_providers.mock_provider] @@ -1038,7 +1037,6 @@ sandbox_mode = "read-only" model_provider = "mock_provider" [features] -remote_models = false personality = true [model_providers.mock_provider] diff --git a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs index a6c81728d5..ba3b740e24 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs @@ -10,11 +10,13 @@ use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadUnarchiveParams; use codex_app_server_protocol::ThreadUnarchiveResponse; +use codex_app_server_protocol::ThreadUnarchivedNotification; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput; use codex_core::find_archived_thread_path_by_id_str; use codex_core::find_thread_path_by_id_str; +use pretty_assertions::assert_eq; use std::fs::FileTimes; use std::fs::OpenOptions; use std::path::Path; @@ -120,6 +122,17 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result let ThreadUnarchiveResponse { thread: unarchived_thread, } = to_response::(unarchive_resp)?; + let unarchive_notification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("thread/unarchived"), + ) + .await??; + let unarchived_notification: ThreadUnarchivedNotification = serde_json::from_value( + unarchive_notification + .params + .expect("thread/unarchived notification params"), + )?; + assert_eq!(unarchived_notification.thread_id, thread.id); assert!( unarchived_thread.updated_at > old_timestamp, "expected updated_at to be bumped on unarchive" diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 97cb14f445..2eecfc5590 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1871,7 +1871,7 @@ fn create_config_toml_with_sandbox( feature_flags: &BTreeMap, sandbox_mode: &str, ) -> std::io::Result<()> { - let mut features = BTreeMap::from([(Feature::RemoteModels, false)]); + let mut features = BTreeMap::new(); for (feature, enabled) in feature_flags { features.insert(*feature, *enabled); } diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index bafb0273a4..8955fbf84a 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -14,7 +14,9 @@ use crate::chatgpt_client::chatgpt_get_request_with_timeout; use crate::chatgpt_token::get_chatgpt_token_data; use crate::chatgpt_token::init_chatgpt_token_from_auth; +use codex_core::connectors::AppBranding; pub use codex_core::connectors::AppInfo; +use codex_core::connectors::AppMetadata; use codex_core::connectors::CONNECTORS_CACHE_TTL; pub use codex_core::connectors::connector_display_label; use codex_core::connectors::connector_install_url; @@ -36,6 +38,10 @@ struct DirectoryApp { id: String, name: String, description: Option, + #[serde(alias = "appMetadata")] + app_metadata: Option, + branding: Option, + labels: Option>, #[serde(alias = "logoUrl")] logo_url: Option, #[serde(alias = "logoUrlDark")] @@ -269,6 +275,9 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { id: _, name, description, + app_metadata, + branding, + labels, logo_url, logo_url_dark, distribution_channel, @@ -302,6 +311,108 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { if existing.distribution_channel.is_none() && distribution_channel.is_some() { existing.distribution_channel = distribution_channel; } + + if let Some(incoming_branding) = branding { + if let Some(existing_branding) = existing.branding.as_mut() { + if existing_branding.category.is_none() && incoming_branding.category.is_some() { + existing_branding.category = incoming_branding.category; + } + if existing_branding.developer.is_none() && incoming_branding.developer.is_some() { + existing_branding.developer = incoming_branding.developer; + } + if existing_branding.website.is_none() && incoming_branding.website.is_some() { + existing_branding.website = incoming_branding.website; + } + if existing_branding.privacy_policy.is_none() + && incoming_branding.privacy_policy.is_some() + { + existing_branding.privacy_policy = incoming_branding.privacy_policy; + } + if existing_branding.terms_of_service.is_none() + && incoming_branding.terms_of_service.is_some() + { + existing_branding.terms_of_service = incoming_branding.terms_of_service; + } + if !existing_branding.is_discoverable_app && incoming_branding.is_discoverable_app { + existing_branding.is_discoverable_app = true; + } + } else { + existing.branding = Some(incoming_branding); + } + } + + if let Some(incoming_app_metadata) = app_metadata { + if let Some(existing_app_metadata) = existing.app_metadata.as_mut() { + if existing_app_metadata.review.is_none() && incoming_app_metadata.review.is_some() { + existing_app_metadata.review = incoming_app_metadata.review; + } + if existing_app_metadata.categories.is_none() + && incoming_app_metadata.categories.is_some() + { + existing_app_metadata.categories = incoming_app_metadata.categories; + } + if existing_app_metadata.sub_categories.is_none() + && incoming_app_metadata.sub_categories.is_some() + { + existing_app_metadata.sub_categories = incoming_app_metadata.sub_categories; + } + if existing_app_metadata.seo_description.is_none() + && incoming_app_metadata.seo_description.is_some() + { + existing_app_metadata.seo_description = incoming_app_metadata.seo_description; + } + if existing_app_metadata.screenshots.is_none() + && incoming_app_metadata.screenshots.is_some() + { + existing_app_metadata.screenshots = incoming_app_metadata.screenshots; + } + if existing_app_metadata.developer.is_none() + && incoming_app_metadata.developer.is_some() + { + existing_app_metadata.developer = incoming_app_metadata.developer; + } + if existing_app_metadata.version.is_none() && incoming_app_metadata.version.is_some() { + existing_app_metadata.version = incoming_app_metadata.version; + } + if existing_app_metadata.version_id.is_none() + && incoming_app_metadata.version_id.is_some() + { + existing_app_metadata.version_id = incoming_app_metadata.version_id; + } + if existing_app_metadata.version_notes.is_none() + && incoming_app_metadata.version_notes.is_some() + { + existing_app_metadata.version_notes = incoming_app_metadata.version_notes; + } + if existing_app_metadata.first_party_type.is_none() + && incoming_app_metadata.first_party_type.is_some() + { + existing_app_metadata.first_party_type = incoming_app_metadata.first_party_type; + } + if existing_app_metadata.first_party_requires_install.is_none() + && incoming_app_metadata.first_party_requires_install.is_some() + { + existing_app_metadata.first_party_requires_install = + incoming_app_metadata.first_party_requires_install; + } + if existing_app_metadata + .show_in_composer_when_unlinked + .is_none() + && incoming_app_metadata + .show_in_composer_when_unlinked + .is_some() + { + existing_app_metadata.show_in_composer_when_unlinked = + incoming_app_metadata.show_in_composer_when_unlinked; + } + } else { + existing.app_metadata = Some(incoming_app_metadata); + } + } + + if existing.labels.is_none() && labels.is_some() { + existing.labels = labels; + } } fn is_hidden_directory_app(app: &DirectoryApp) -> bool { @@ -316,6 +427,9 @@ fn directory_app_to_app_info(app: DirectoryApp) -> AppInfo { logo_url: app.logo_url, logo_url_dark: app.logo_url_dark, distribution_channel: app.distribution_channel, + branding: app.branding, + app_metadata: app.app_metadata, + labels: app.labels, install_url: None, is_accessible: false, is_enabled: true, @@ -342,6 +456,8 @@ const DISALLOWED_CONNECTOR_IDS: &[&str] = &[ "asdk_app_6938a94a61d881918ef32cb999ff937c", "connector_2b0a9009c9c64bf9933a3dae3f2b1254", "connector_68de829bf7648191acd70a907364c67c", + "connector_68e004f14af881919eb50893d3d9f523", + "connector_69272cb413a081919685ec3c88d1744e", ]; const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_"; @@ -375,6 +491,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -429,6 +548,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some(connector_install_url(id, id)), is_accessible, is_enabled: true, diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 208ba1c3a6..64b7330c3a 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -19,7 +19,7 @@ workspace = true [dependencies] anyhow = { workspace = true } -arc-swap = "1.8.0" +arc-swap = "1.8.2" async-channel = { workspace = true } async-trait = { workspace = true } askama = { workspace = true } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index b05d11edc9..e3f7456c95 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -214,6 +214,9 @@ "child_agents_md": { "type": "boolean" }, + "codex_git_commit": { + "type": "boolean" + }, "collab": { "type": "boolean" }, @@ -1308,6 +1311,10 @@ "default": null, "description": "Preferred backend for storing CLI auth credentials. file (default): Use a file in the Codex home directory. keyring: Use an OS-specific keyring service. auto: Use the keyring if available, otherwise use a file." }, + "commit_attribution": { + "description": "Optional commit attribution text for commit message co-author trailers.\n\nSet to an empty string to disable automatic commit attribution.", + "type": "string" + }, "compact_prompt": { "description": "Compact prompt used for history compaction.", "type": "string" @@ -1347,6 +1354,9 @@ "child_agents_md": { "type": "boolean" }, + "codex_git_commit": { + "type": "boolean" + }, "collab": { "type": "boolean" }, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 5eb80de2ec..6fa5ae3b80 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -17,6 +17,7 @@ use crate::analytics_client::AnalyticsEventsClient; use crate::analytics_client::AppInvocation; use crate::analytics_client::build_track_events_context; use crate::apps::render_apps_section; +use crate::commit_attribution::commit_message_trailer_instruction; use crate::compact; use crate::compact::run_inline_auto_compact_task; use crate::compact::should_use_remote_compact_task; @@ -172,6 +173,8 @@ use crate::protocol::Event; use crate::protocol::EventMsg; use crate::protocol::ExecApprovalRequestEvent; use crate::protocol::McpServerRefreshConfig; +use crate::protocol::ModelRerouteEvent; +use crate::protocol::ModelRerouteReason; use crate::protocol::NetworkApprovalContext; use crate::protocol::Op; use crate::protocol::PlanDeltaEvent; @@ -327,15 +330,11 @@ impl Codex { let config = Arc::new(config); let _ = models_manager - .list_models( - &config, - crate::models_manager::manager::RefreshStrategy::OnlineIfUncached, - ) + .list_models(crate::models_manager::manager::RefreshStrategy::OnlineIfUncached) .await; let model = models_manager .get_default_model( &config.model, - &config, crate::models_manager::manager::RefreshStrategy::OnlineIfUncached, ) .await; @@ -2440,7 +2439,7 @@ impl Session { turn_context: &Arc, server_model: String, ) -> bool { - let requested_model = turn_context.model_info.slug.as_str(); + let requested_model = turn_context.model_info.slug.clone(); if server_model == requested_model { info!("server reported model {server_model} (matches requested model)"); return false; @@ -2452,6 +2451,16 @@ impl Session { "Your account was flagged for potentially high-risk cyber activity and this request was routed to gpt-5.2 as a fallback. To regain access to gpt-5.3-codex, apply for trusted access: {CYBER_VERIFY_URL} or learn more: {CYBER_SAFETY_URL}" ); + self.send_event( + turn_context, + EventMsg::ModelReroute(ModelRerouteEvent { + from_model: requested_model.clone(), + to_model: server_model.clone(), + reason: ModelRerouteReason::HighRiskCyberActivity, + }), + ) + .await; + self.send_event( turn_context, EventMsg::Warning(WarningEvent { @@ -2575,6 +2584,13 @@ impl Session { if turn_context.features.enabled(Feature::Apps) { items.push(DeveloperInstructions::new(render_apps_section()).into()); } + if turn_context.features.enabled(Feature::CodexGitCommit) + && let Some(commit_message_instruction) = commit_message_trailer_instruction( + turn_context.config.commit_attribution.as_deref(), + ) + { + items.push(DeveloperInstructions::new(commit_message_instruction).into()); + } if let Some(user_instructions) = turn_context.user_instructions.as_deref() { items.push( UserInstructions { @@ -3162,22 +3178,24 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv Op::ListSkills { cwds, force_reload } => { handlers::list_skills(&sess, sub.id.clone(), cwds, force_reload).await; } - Op::ListRemoteSkills => { - handlers::list_remote_skills(&sess, &config, sub.id.clone()).await; - } - Op::DownloadRemoteSkill { - hazelnut_id, - is_preload, + Op::ListRemoteSkills { + hazelnut_scope, + product_surface, + enabled, } => { - handlers::download_remote_skill( + handlers::list_remote_skills( &sess, &config, sub.id.clone(), - hazelnut_id, - is_preload, + hazelnut_scope, + product_surface, + enabled, ) .await; } + Op::DownloadRemoteSkill { hazelnut_id } => { + handlers::export_remote_skill(&sess, &config, sub.id.clone(), hazelnut_id).await; + } Op::Undo => { handlers::undo(&sess, sub.id.clone()).await; } @@ -3257,6 +3275,8 @@ mod handlers { use codex_protocol::protocol::McpServerRefreshConfig; use codex_protocol::protocol::Op; use codex_protocol::protocol::RemoteSkillDownloadedEvent; + use codex_protocol::protocol::RemoteSkillHazelnutScope; + use codex_protocol::protocol::RemoteSkillProductSurface; use codex_protocol::protocol::RemoteSkillSummary; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; @@ -3653,19 +3673,33 @@ mod handlers { sess.send_event_raw(event).await; } - pub async fn list_remote_skills(sess: &Session, config: &Arc, sub_id: String) { - let response = crate::skills::remote::list_remote_skills(config) - .await - .map(|skills| { - skills - .into_iter() - .map(|skill| RemoteSkillSummary { - id: skill.id, - name: skill.name, - description: skill.description, - }) - .collect::>() - }); + pub async fn list_remote_skills( + sess: &Session, + config: &Arc, + sub_id: String, + hazelnut_scope: RemoteSkillHazelnutScope, + product_surface: RemoteSkillProductSurface, + enabled: Option, + ) { + let auth = sess.services.auth_manager.auth().await; + let response = crate::skills::remote::list_remote_skills( + config, + auth.as_ref(), + hazelnut_scope, + product_surface, + enabled, + ) + .await + .map(|skills| { + skills + .into_iter() + .map(|skill| RemoteSkillSummary { + id: skill.id, + name: skill.name, + description: skill.description, + }) + .collect::>() + }); match response { Ok(skills) => { @@ -3690,22 +3724,27 @@ mod handlers { } } - pub async fn download_remote_skill( + pub async fn export_remote_skill( sess: &Session, config: &Arc, sub_id: String, hazelnut_id: String, - is_preload: bool, ) { - match crate::skills::remote::download_remote_skill(config, hazelnut_id.as_str(), is_preload) - .await + let auth = sess.services.auth_manager.auth().await; + match crate::skills::remote::export_remote_skill( + config, + auth.as_ref(), + hazelnut_id.as_str(), + ) + .await { Ok(result) => { + let id = result.id; let event = Event { id: sub_id, msg: EventMsg::RemoteSkillDownloaded(RemoteSkillDownloadedEvent { - id: result.id, - name: result.name, + id: id.clone(), + name: id, path: result.path, }), }; @@ -3715,7 +3754,7 @@ mod handlers { let event = Event { id: sub_id, msg: EventMsg::Error(ErrorEvent { - message: format!("failed to download remote skill {hazelnut_id}: {err}"), + message: format!("failed to export remote skill {hazelnut_id}: {err}"), codex_error_info: Some(CodexErrorInfo::Other), }), }; @@ -5548,11 +5587,7 @@ async fn try_run_sampling_request( } ResponseEvent::ModelsEtag(etag) => { // Update internal state with latest models etag - let config = sess.get_config().await; - sess.services - .models_manager - .refresh_if_new_etag(etag, &config) - .await; + sess.services.models_manager.refresh_if_new_etag(etag).await; } ResponseEvent::Completed { response_id: _, @@ -5796,6 +5831,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: true, is_enabled: true, diff --git a/codex-rs/core/src/commit_attribution.rs b/codex-rs/core/src/commit_attribution.rs new file mode 100644 index 0000000000..a2c9104b25 --- /dev/null +++ b/codex-rs/core/src/commit_attribution.rs @@ -0,0 +1,76 @@ +const DEFAULT_ATTRIBUTION_VALUE: &str = "Codex "; + +fn build_commit_message_trailer(config_attribution: Option<&str>) -> Option { + let value = resolve_attribution_value(config_attribution)?; + Some(format!("Co-authored-by: {value}")) +} + +pub(crate) fn commit_message_trailer_instruction( + config_attribution: Option<&str>, +) -> Option { + let trailer = build_commit_message_trailer(config_attribution)?; + Some(format!( + "When you write or edit a git commit message, ensure the message ends with this trailer exactly once:\n{trailer}\n\nRules:\n- Keep existing trailers and append this trailer at the end if missing.\n- Do not duplicate this trailer if it already exists.\n- Keep one blank line between the commit body and trailer block." + )) +} + +fn resolve_attribution_value(config_attribution: Option<&str>) -> Option { + match config_attribution { + Some(value) => { + let trimmed = value.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + } + None => Some(DEFAULT_ATTRIBUTION_VALUE.to_string()), + } +} + +#[cfg(test)] +mod tests { + use super::build_commit_message_trailer; + use super::commit_message_trailer_instruction; + use super::resolve_attribution_value; + + #[test] + fn blank_attribution_disables_trailer_prompt() { + assert_eq!(build_commit_message_trailer(Some("")), None); + assert_eq!(commit_message_trailer_instruction(Some(" ")), None); + } + + #[test] + fn default_attribution_uses_codex_trailer() { + assert_eq!( + build_commit_message_trailer(None).as_deref(), + Some("Co-authored-by: Codex ") + ); + } + + #[test] + fn resolve_value_handles_default_custom_and_blank() { + assert_eq!( + resolve_attribution_value(None), + Some("Codex ".to_string()) + ); + assert_eq!( + resolve_attribution_value(Some("MyAgent ")), + Some("MyAgent ".to_string()) + ); + assert_eq!( + resolve_attribution_value(Some("MyAgent")), + Some("MyAgent".to_string()) + ); + assert_eq!(resolve_attribution_value(Some(" ")), None); + } + + #[test] + fn instruction_mentions_trailer_and_omits_generated_with() { + let instruction = commit_message_trailer_instruction(Some("AgentX ")) + .expect("instruction expected"); + assert!(instruction.contains("Co-authored-by: AgentX ")); + assert!(instruction.contains("exactly once")); + assert!(!instruction.contains("Generated-with")); + } +} diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index e69f57737e..9874cc2774 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -206,6 +206,13 @@ pub struct Config { /// Compact prompt override. pub compact_prompt: Option, + /// Optional commit attribution text for commit message co-author trailers. + /// + /// - `None`: use default attribution (`Codex `) + /// - `Some("")` or whitespace-only: disable commit attribution + /// - `Some("...")`: use the provided attribution text verbatim + pub commit_attribution: Option, + /// Optional external notifier command. When set, Codex will spawn this /// program after each completed *turn* (i.e. when the agent finishes /// processing a user submission). The value must be the full command @@ -917,6 +924,11 @@ pub struct ConfigToml { /// Compact prompt used for history compaction. pub compact_prompt: Option, + /// Optional commit attribution text for commit message co-author trailers. + /// + /// Set to an empty string to disable automatic commit attribution. + pub commit_attribution: Option, + /// When set, restricts ChatGPT login to a specific workspace identifier. #[serde(default)] pub forced_chatgpt_workspace_id: Option, @@ -1696,6 +1708,8 @@ impl Config { } }); + let commit_attribution = cfg.commit_attribution; + // Load base instructions override from a file if specified. If the // path is relative, resolve it against the effective cwd so the // behaviour matches other path-like config values. @@ -1816,6 +1830,7 @@ impl Config { personality, developer_instructions, compact_prompt, + commit_attribution, // The config.toml omits "_mode" because it's a config file. However, "_mode" // is important in code to differentiate the mode from the store implementation. cli_auth_credentials_store_mode: cfg.cli_auth_credentials_store.unwrap_or_default(), @@ -4192,6 +4207,7 @@ model_verbosity = "high" base_instructions: None, developer_instructions: None, compact_prompt: None, + commit_attribution: None, forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, @@ -4304,6 +4320,7 @@ model_verbosity = "high" base_instructions: None, developer_instructions: None, compact_prompt: None, + commit_attribution: None, forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, @@ -4414,6 +4431,7 @@ model_verbosity = "high" base_instructions: None, developer_instructions: None, compact_prompt: None, + commit_attribution: None, forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, @@ -4510,6 +4528,7 @@ model_verbosity = "high" base_instructions: None, developer_instructions: None, compact_prompt: None, + commit_attribution: None, forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, diff --git a/codex-rs/core/src/config/service.rs b/codex-rs/core/src/config/service.rs index db6963a2f8..02a9da23fd 100644 --- a/codex-rs/core/src/config/service.rs +++ b/codex-rs/core/src/config/service.rs @@ -777,7 +777,7 @@ unified_exec = true service .write_value(ConfigValueWriteParams { file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), - key_path: "features.remote_models".to_string(), + key_path: "features.personality".to_string(), value: serde_json::json!(true), merge_strategy: MergeStrategy::Replace, expected_version: None, @@ -797,7 +797,7 @@ hide_full_access_warning = true [features] unified_exec = true -remote_models = true +personality = true "#; assert_eq!(updated, expected); Ok(()) diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 35c2dc5ceb..5a8930231b 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -7,7 +7,9 @@ use std::time::Duration; use std::time::Instant; use async_channel::unbounded; +pub use codex_app_server_protocol::AppBranding; pub use codex_app_server_protocol::AppInfo; +pub use codex_app_server_protocol::AppMetadata; use codex_protocol::protocol::SandboxPolicy; use serde::Deserialize; use tokio_util::sync::CancellationToken; @@ -320,6 +322,9 @@ where logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some(connector_install_url(&connector_name, &connector_id)), is_accessible: true, is_enabled: true, diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 7ef7485ace..49363641bb 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -101,10 +101,12 @@ pub enum Feature { WindowsSandbox, /// Use the elevated Windows sandbox pipeline (setup + runner). WindowsSandboxElevated, - /// Refresh remote models and emit AppReady once the list is available. + /// Legacy remote models flag kept for backward compatibility. RemoteModels, /// Experimental shell snapshotting. ShellSnapshot, + /// Enable git commit attribution guidance via model instructions. + CodexGitCommit, /// Enable runtime metrics snapshots via a manual reader. RuntimeMetrics, /// Persist rollout metadata to a local SQLite database. @@ -466,6 +468,12 @@ pub const FEATURES: &[FeatureSpec] = &[ default_enabled: false, }, // Experimental program. Rendered in the `/experimental` menu for users. + FeatureSpec { + id: Feature::CodexGitCommit, + key: "codex_git_commit", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::RuntimeMetrics, key: "runtime_metrics", @@ -530,8 +538,8 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::RemoteModels, key: "remote_models", - stage: Stage::Stable, - default_enabled: true, + stage: Stage::Removed, + default_enabled: false, }, FeatureSpec { id: Feature::PowershellUtf8, diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 91bbe042ef..bbfef7f8fa 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -21,6 +21,7 @@ pub use codex_thread::ThreadConfigSnapshot; mod agent; mod codex_delegate; mod command_canonicalization; +mod commit_attribution; pub mod config; pub mod config_loader; pub mod connectors; diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index dd769da2e8..457351995f 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -7,7 +7,6 @@ use crate::config::Config; use crate::default_client::build_reqwest_client; use crate::error::CodexErr; use crate::error::Result as CoreResult; -use crate::features::Feature; use crate::model_provider_info::ModelProviderInfo; use crate::models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets; use crate::models_manager::model_info; @@ -74,18 +73,11 @@ impl ModelsManager { /// List all available models, refreshing according to the specified strategy. /// /// Returns model presets sorted by priority and filtered by auth mode and visibility. - pub async fn list_models( - &self, - config: &Config, - refresh_strategy: RefreshStrategy, - ) -> Vec { - if let Err(err) = self - .refresh_available_models(config, refresh_strategy) - .await - { + pub async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec { + if let Err(err) = self.refresh_available_models(refresh_strategy).await { error!("failed to refresh available models: {err}"); } - let remote_models = self.get_remote_models(config).await; + let remote_models = self.get_remote_models().await; self.build_available_models(remote_models) } @@ -99,8 +91,8 @@ impl ModelsManager { /// Attempt to list models without blocking, using the current cached state. /// /// Returns an error if the internal lock cannot be acquired. - pub fn try_list_models(&self, config: &Config) -> Result, TryLockError> { - let remote_models = self.try_get_remote_models(config)?; + pub fn try_list_models(&self) -> Result, TryLockError> { + let remote_models = self.try_get_remote_models()?; Ok(self.build_available_models(remote_models)) } @@ -112,19 +104,15 @@ impl ModelsManager { pub async fn get_default_model( &self, model: &Option, - config: &Config, refresh_strategy: RefreshStrategy, ) -> String { if let Some(model) = model.as_ref() { return model.to_string(); } - if let Err(err) = self - .refresh_available_models(config, refresh_strategy) - .await - { + if let Err(err) = self.refresh_available_models(refresh_strategy).await { error!("failed to refresh available models: {err}"); } - let remote_models = self.get_remote_models(config).await; + let remote_models = self.get_remote_models().await; let available = self.build_available_models(remote_models); available .iter() @@ -137,9 +125,7 @@ impl ModelsManager { // todo(aibrahim): look if we can tighten it to pub(crate) /// Look up model metadata, applying remote overrides and config adjustments. pub async fn get_model_info(&self, model: &str, config: &Config) -> ModelInfo { - let remote = self - .find_remote_model_by_longest_prefix(model, config) - .await; + let remote = self.find_remote_model_by_longest_prefix(model).await; let model_info = if let Some(remote) = remote { ModelInfo { slug: model.to_string(), @@ -152,13 +138,9 @@ impl ModelsManager { model_info::with_config_overrides(model_info, config) } - async fn find_remote_model_by_longest_prefix( - &self, - model: &str, - config: &Config, - ) -> Option { + async fn find_remote_model_by_longest_prefix(&self, model: &str) -> Option { let mut best: Option = None; - for candidate in self.get_remote_models(config).await { + for candidate in self.get_remote_models().await { if !model.starts_with(&candidate.slug) { continue; } @@ -177,7 +159,7 @@ impl ModelsManager { /// Refresh models if the provided ETag differs from the cached ETag. /// /// Uses `Online` strategy to fetch latest models when ETags differ. - pub(crate) async fn refresh_if_new_etag(&self, etag: String, config: &Config) { + pub(crate) async fn refresh_if_new_etag(&self, etag: String) { let current_etag = self.get_etag().await; if current_etag.clone().is_some() && current_etag.as_deref() == Some(etag.as_str()) { if let Err(err) = self.cache_manager.renew_cache_ttl().await { @@ -185,23 +167,13 @@ impl ModelsManager { } return; } - if let Err(err) = self - .refresh_available_models(config, RefreshStrategy::Online) - .await - { + if let Err(err) = self.refresh_available_models(RefreshStrategy::Online).await { error!("failed to refresh available models: {err}"); } } /// Refresh available models according to the specified strategy. - async fn refresh_available_models( - &self, - config: &Config, - refresh_strategy: RefreshStrategy, - ) -> CoreResult<()> { - if !config.features.enabled(Feature::RemoteModels) { - return Ok(()); - } + async fn refresh_available_models(&self, refresh_strategy: RefreshStrategy) -> CoreResult<()> { if self.auth_manager.auth_mode() != Some(AuthMode::Chatgpt) { if matches!( refresh_strategy, @@ -336,20 +308,12 @@ impl ModelsManager { merged_presets } - async fn get_remote_models(&self, config: &Config) -> Vec { - if config.features.enabled(Feature::RemoteModels) { - self.remote_models.read().await.clone() - } else { - Vec::new() - } + async fn get_remote_models(&self) -> Vec { + self.remote_models.read().await.clone() } - fn try_get_remote_models(&self, config: &Config) -> Result, TryLockError> { - if config.features.enabled(Feature::RemoteModels) { - Ok(self.remote_models.try_read()?.clone()) - } else { - Ok(Vec::new()) - } + fn try_get_remote_models(&self) -> Result, TryLockError> { + Ok(self.remote_models.try_read()?.clone()) } /// Construct a manager with a specific provider for testing. @@ -399,7 +363,6 @@ mod tests { use crate::CodexAuth; use crate::auth::AuthCredentialsStoreMode; use crate::config::ConfigBuilder; - use crate::features::Feature; use crate::model_provider_info::WireApi; use chrono::Utc; use codex_protocol::openai_models::ModelsResponse; @@ -476,17 +439,16 @@ mod tests { #[tokio::test] async fn get_model_info_tracks_fallback_usage() { let codex_home = tempdir().expect("temp dir"); - let mut config = ConfigBuilder::default() + let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .build() .await .expect("load default test config"); - config.features.enable(Feature::RemoteModels); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager); let known_slug = manager - .get_remote_models(&config) + .get_remote_models() .await .first() .expect("bundled models should include at least one model") @@ -520,12 +482,6 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); - config.features.enable(Feature::RemoteModels); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); @@ -536,15 +492,13 @@ mod tests { ); manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("refresh succeeds"); - let cached_remote = manager.get_remote_models(&config).await; + let cached_remote = manager.get_remote_models().await; assert_models_contain(&cached_remote, &remote_models); - let available = manager - .list_models(&config, RefreshStrategy::OnlineIfUncached) - .await; + let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; let high_idx = available .iter() .position(|model| model.model == "priority-high") @@ -577,12 +531,6 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); - config.features.enable(Feature::RemoteModels); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); @@ -593,17 +541,17 @@ mod tests { ); manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("first refresh succeeds"); - assert_models_contain(&manager.get_remote_models(&config).await, &remote_models); + assert_models_contain(&manager.get_remote_models().await, &remote_models); // Second call should read from cache and avoid the network. manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("cached refresh succeeds"); - assert_models_contain(&manager.get_remote_models(&config).await, &remote_models); + assert_models_contain(&manager.get_remote_models().await, &remote_models); assert_eq!( models_mock.requests().len(), 1, @@ -624,12 +572,6 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); - config.features.enable(Feature::RemoteModels); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); @@ -640,7 +582,7 @@ mod tests { ); manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("initial refresh succeeds"); @@ -664,10 +606,10 @@ mod tests { .await; manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("second refresh succeeds"); - assert_models_contain(&manager.get_remote_models(&config).await, &updated_models); + assert_models_contain(&manager.get_remote_models().await, &updated_models); assert_eq!( initial_mock.requests().len(), 1, @@ -693,12 +635,6 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); - config.features.enable(Feature::RemoteModels); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); @@ -709,7 +645,7 @@ mod tests { ); manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("initial refresh succeeds"); @@ -733,10 +669,10 @@ mod tests { .await; manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("second refresh succeeds"); - assert_models_contain(&manager.get_remote_models(&config).await, &updated_models); + assert_models_contain(&manager.get_remote_models().await, &updated_models); assert_eq!( initial_mock.requests().len(), 1, @@ -762,12 +698,6 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); - config.features.enable(Feature::RemoteModels); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); @@ -779,7 +709,7 @@ mod tests { manager.cache_manager.set_ttl(Duration::ZERO); manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("initial refresh succeeds"); @@ -794,12 +724,12 @@ mod tests { .await; manager - .refresh_available_models(&config, RefreshStrategy::OnlineIfUncached) + .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("second refresh succeeds"); let available = manager - .try_list_models(&config) + .try_list_models() .expect("models should be available"); assert!( available.iter().any(|preset| preset.model == "remote-new"), @@ -834,12 +764,6 @@ mod tests { .await; let codex_home = tempdir().expect("temp dir"); - let mut config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); - config.features.enable(Feature::RemoteModels); let auth_manager = Arc::new(AuthManager::new( codex_home.path().to_path_buf(), false, @@ -853,10 +777,10 @@ mod tests { ); manager - .refresh_available_models(&config, RefreshStrategy::Online) + .refresh_available_models(RefreshStrategy::Online) .await .expect("refresh should no-op without chatgpt auth"); - let cached_remote = manager.get_remote_models(&config).await; + let cached_remote = manager.get_remote_models().await; assert!( !cached_remote .iter() diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index dc3e111a67..f83433efb2 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -122,6 +122,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::CollabCloseEnd(_) | EventMsg::CollabResumeEnd(_) => Some(EventPersistenceMode::Extended), EventMsg::Warning(_) + | EventMsg::ModelReroute(_) | EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_) | EventMsg::AgentReasoningRawContentDelta(_) diff --git a/codex-rs/core/src/skills/remote.rs b/codex-rs/core/src/skills/remote.rs index af6dd5593a..6abd402584 100644 --- a/codex-rs/core/src/skills/remote.rs +++ b/codex-rs/core/src/skills/remote.rs @@ -1,18 +1,49 @@ use anyhow::Context; use anyhow::Result; use serde::Deserialize; -use std::collections::HashMap; -use std::collections::HashSet; use std::path::Component; use std::path::Path; use std::path::PathBuf; use std::time::Duration; +use crate::auth::CodexAuth; use crate::config::Config; use crate::default_client::build_reqwest_client; +use codex_protocol::protocol::RemoteSkillHazelnutScope; +use codex_protocol::protocol::RemoteSkillProductSurface; const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30); +fn as_query_hazelnut_scope(scope: RemoteSkillHazelnutScope) -> Option<&'static str> { + match scope { + RemoteSkillHazelnutScope::WorkspaceShared => Some("workspace-shared"), + RemoteSkillHazelnutScope::AllShared => Some("all-shared"), + RemoteSkillHazelnutScope::Personal => Some("personal"), + RemoteSkillHazelnutScope::Example => Some("example"), + } +} + +fn as_query_product_surface(product_surface: RemoteSkillProductSurface) -> &'static str { + match product_surface { + RemoteSkillProductSurface::Chatgpt => "chatgpt", + RemoteSkillProductSurface::Codex => "codex", + RemoteSkillProductSurface::Api => "api", + RemoteSkillProductSurface::Atlas => "atlas", + } +} + +fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth> { + let Some(auth) = auth else { + anyhow::bail!("chatgpt authentication required for hazelnut scopes"); + }; + if !auth.is_chatgpt_auth() { + anyhow::bail!( + "chatgpt authentication required for hazelnut scopes; api key auth is not supported" + ); + } + Ok(auth) +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct RemoteSkillSummary { pub id: String, @@ -20,27 +51,12 @@ pub struct RemoteSkillSummary { pub description: String, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RemoteSkillDownload { - pub id: String, - pub name: String, - pub base_sediment_id: String, - pub files: HashMap, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct RemoteSkillDownloadResult { pub id: String, - pub name: String, pub path: PathBuf, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RemoteSkillFileRange { - pub start: u64, - pub length: u64, -} - #[derive(Debug, Deserialize)] struct RemoteSkillsResponse { hazelnuts: Vec, @@ -53,36 +69,40 @@ struct RemoteSkill { description: String, } -#[derive(Debug, Deserialize)] -struct RemoteSkillsDownloadResponse { - hazelnuts: Vec, -} - -#[derive(Debug, Deserialize)] -struct RemoteSkillDownloadPayload { - id: String, - name: String, - #[serde(rename = "base_sediment_id")] - base_sediment_id: String, - files: HashMap, -} - -#[derive(Debug, Deserialize)] -struct RemoteSkillFileRangePayload { - start: u64, - length: u64, -} - -pub async fn list_remote_skills(config: &Config) -> Result> { +pub async fn list_remote_skills( + config: &Config, + auth: Option<&CodexAuth>, + hazelnut_scope: RemoteSkillHazelnutScope, + product_surface: RemoteSkillProductSurface, + enabled: Option, +) -> Result> { let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url); - let url = format!("{base_url}/public-api/hazelnuts/"); + let auth = ensure_chatgpt_auth(auth)?; + + let url = format!("{base_url}/hazelnuts"); + let product_surface = as_query_product_surface(product_surface); + let mut query_params = vec![("product_surface", product_surface)]; + if let Some(scope) = as_query_hazelnut_scope(hazelnut_scope) { + query_params.push(("scope", scope)); + } + if let Some(enabled) = enabled { + let enabled = if enabled { "true" } else { "false" }; + query_params.push(("enabled", enabled)); + } let client = build_reqwest_client(); - let response = client + let mut request = client .get(&url) .timeout(REMOTE_SKILLS_API_TIMEOUT) - .query(&[("product_surface", "codex")]) + .query(&query_params); + let token = auth + .get_token() + .context("Failed to read auth token for remote skills")?; + request = request.bearer_auth(token); + if let Some(account_id) = auth.get_account_id() { + request = request.header("chatgpt-account-id", account_id); + } + let response = request .send() .await .with_context(|| format!("Failed to send request to {url}"))?; @@ -107,20 +127,27 @@ pub async fn list_remote_skills(config: &Config) -> Result, hazelnut_id: &str, - is_preload: bool, ) -> Result { - let hazelnut = fetch_remote_skill(config, hazelnut_id).await?; + let auth = ensure_chatgpt_auth(auth)?; let client = build_reqwest_client(); let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url); - let url = format!("{base_url}/public-api/hazelnuts/{hazelnut_id}/export"); - let response = client - .get(&url) - .timeout(REMOTE_SKILLS_API_TIMEOUT) + let url = format!("{base_url}/hazelnuts/{hazelnut_id}/export"); + let mut request = client.get(&url).timeout(REMOTE_SKILLS_API_TIMEOUT); + + let token = auth + .get_token() + .context("Failed to read auth token for remote skills")?; + request = request.bearer_auth(token); + if let Some(account_id) = auth.get_account_id() { + request = request.header("chatgpt-account-id", account_id); + } + + let response = request .send() .await .with_context(|| format!("Failed to send download request to {url}"))?; @@ -136,48 +163,22 @@ pub async fn download_remote_skill( anyhow::bail!("Downloaded remote skill payload is not a zip archive"); } - let preferred_dir_name = if hazelnut.name.trim().is_empty() { - None - } else { - Some(hazelnut.name.as_str()) - }; - let dir_name = preferred_dir_name - .and_then(validate_dir_name_format) - .or_else(|| validate_dir_name_format(&hazelnut.id)) - .ok_or_else(|| anyhow::anyhow!("Remote skill has no valid directory name"))?; - let output_root = if is_preload { - config - .codex_home - .join("vendor_imports") - .join("skills") - .join("skills") - .join(".curated") - } else { - config.codex_home.join("skills").join("downloaded") - }; - let output_dir = output_root.join(dir_name); + let output_dir = config.codex_home.join("skills").join(hazelnut_id); tokio::fs::create_dir_all(&output_dir) .await .context("Failed to create downloaded skills directory")?; - let allowed_files = hazelnut.files.keys().cloned().collect::>(); let zip_bytes = body.to_vec(); let output_dir_clone = output_dir.clone(); - let prefix_candidates = vec![hazelnut.name.clone(), hazelnut.id.clone()]; + let prefix_candidates = vec![hazelnut_id.to_string()]; tokio::task::spawn_blocking(move || { - extract_zip_to_dir( - zip_bytes, - &output_dir_clone, - &allowed_files, - &prefix_candidates, - ) + extract_zip_to_dir(zip_bytes, &output_dir_clone, &prefix_candidates) }) .await .context("Zip extraction task failed")??; Ok(RemoteSkillDownloadResult { - id: hazelnut.id, - name: hazelnut.name, + id: hazelnut_id.to_string(), path: output_dir, }) } @@ -195,17 +196,6 @@ fn safe_join(base: &Path, name: &str) -> Result { Ok(base.join(path)) } -fn validate_dir_name_format(name: &str) -> Option { - let mut components = Path::new(name).components(); - match (components.next(), components.next()) { - (Some(Component::Normal(component)), None) => { - let value = component.to_string_lossy().to_string(); - if value.is_empty() { None } else { Some(value) } - } - _ => None, - } -} - fn is_zip_payload(bytes: &[u8]) -> bool { bytes.starts_with(b"PK\x03\x04") || bytes.starts_with(b"PK\x05\x06") @@ -215,7 +205,6 @@ fn is_zip_payload(bytes: &[u8]) -> bool { fn extract_zip_to_dir( bytes: Vec, output_dir: &Path, - allowed_files: &HashSet, prefix_candidates: &[String], ) -> Result<()> { let cursor = std::io::Cursor::new(bytes); @@ -230,9 +219,6 @@ fn extract_zip_to_dir( let Some(normalized) = normalized else { continue; }; - if !allowed_files.contains(&normalized) { - continue; - } let file_path = safe_join(output_dir, &normalized)?; if let Some(parent) = file_path.parent() { std::fs::create_dir_all(parent) @@ -264,51 +250,3 @@ fn normalize_zip_name(name: &str, prefix_candidates: &[String]) -> Option Result { - let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url); - let url = format!("{base_url}/public-api/hazelnuts/"); - - let client = build_reqwest_client(); - let response = client - .get(&url) - .timeout(REMOTE_SKILLS_API_TIMEOUT) - .query(&[("product_surface", "codex")]) - .send() - .await - .with_context(|| format!("Failed to send request to {url}"))?; - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - if !status.is_success() { - anyhow::bail!("Request failed with status {status} from {url}: {body}"); - } - - let parsed: RemoteSkillsDownloadResponse = - serde_json::from_str(&body).context("Failed to parse skills response")?; - let hazelnut = parsed - .hazelnuts - .into_iter() - .find(|hazelnut| hazelnut.id == hazelnut_id) - .ok_or_else(|| anyhow::anyhow!("Remote skill {hazelnut_id} not found"))?; - - Ok(RemoteSkillDownload { - id: hazelnut.id, - name: hazelnut.name, - base_sediment_id: hazelnut.base_sediment_id, - files: hazelnut - .files - .into_iter() - .map(|(name, range)| { - ( - name, - RemoteSkillFileRange { - start: range.start, - length: range.length, - }, - ) - }) - .collect(), - }) -} diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index b13ef8c61a..c16a883852 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -230,12 +230,11 @@ impl ThreadManager { pub async fn list_models( &self, - config: &Config, refresh_strategy: crate::models_manager::manager::RefreshStrategy, ) -> Vec { self.state .models_manager - .list_models(config, refresh_strategy) + .list_models(refresh_strategy) .await } diff --git a/codex-rs/core/src/tools/handlers/search_tool_bm25.rs b/codex-rs/core/src/tools/handlers/search_tool_bm25.rs index e535abac91..c5e267cb49 100644 --- a/codex-rs/core/src/tools/handlers/search_tool_bm25.rs +++ b/codex-rs/core/src/tools/handlers/search_tool_bm25.rs @@ -261,6 +261,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: true, is_enabled: enabled, diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 25278073ec..22a6c72076 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -5,7 +5,6 @@ use codex_core::built_in_model_providers; use codex_core::compact::SUMMARIZATION_PROMPT; use codex_core::compact::SUMMARY_PREFIX; use codex_core::config::Config; -use codex_core::features::Feature; use codex_core::protocol::AskForApproval; use codex_core::protocol::EventMsg; use codex_core::protocol::ItemCompletedEvent; @@ -1708,7 +1707,6 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { .with_config(move |config| { config.model_provider = model_provider; set_test_compact_prompt(config); - config.features.enable(Feature::RemoteModels); }); let test = builder.build(&server).await.expect("build test codex"); @@ -1831,7 +1829,6 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { .with_config(move |config| { config.model_provider = model_provider; set_test_compact_prompt(config); - config.features.enable(Feature::RemoteModels); }); let initial = initial_builder .build(&server) @@ -1885,7 +1882,6 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { .with_config(move |config| { config.model_provider = model_provider; set_test_compact_prompt(config); - config.features.enable(Feature::RemoteModels); }); let resumed = resumed_builder .resume(&server, home, rollout_path) diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/core/tests/suite/model_info_overrides.rs index a9e18f9605..2b085a004f 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -1,5 +1,4 @@ use codex_core::CodexAuth; -use codex_core::features::Feature; use codex_core::models_manager::manager::ModelsManager; use codex_protocol::openai_models::TruncationPolicyConfig; use core_test_support::load_default_config_for_test; @@ -9,8 +8,7 @@ use tempfile::TempDir; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn offline_model_info_without_tool_output_override() { let codex_home = TempDir::new().expect("create temp dir"); - let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); + let config = load_default_config_for_test(&codex_home).await; let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); @@ -28,7 +26,6 @@ async fn offline_model_info_without_tool_output_override() { async fn offline_model_info_with_tool_output_override() { let codex_home = TempDir::new().expect("create temp dir"); let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); config.tool_output_token_limit = Some(123); let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 44d0d8a2fc..4d4abafeb6 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -266,13 +266,12 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(move |config| { - config.features.enable(Feature::RemoteModels); config.model = Some(image_model_slug.to_string()); }); let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); let _ = models_manager - .list_models(&test.config, RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached) .await; let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=" .to_string(); @@ -434,15 +433,12 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model = Some(large_model_slug.to_string()); }); let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); - let available_models = models_manager - .list_models(&test.config, RefreshStrategy::Online) - .await; + let available_models = models_manager.list_models(RefreshStrategy::Online).await; assert!( available_models .iter() diff --git a/codex-rs/core/tests/suite/model_tools.rs b/codex-rs/core/tests/suite/model_tools.rs index 836807881b..026be98dab 100644 --- a/codex-rs/core/tests/suite/model_tools.rs +++ b/codex-rs/core/tests/suite/model_tools.rs @@ -36,7 +36,6 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec { .with_model(model) // Keep tool expectations stable when the default web_search mode changes. .with_config(|config| { - config.features.enable(Feature::RemoteModels); config .web_search_mode .set(WebSearchMode::Cached) diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 780937c111..e9f0a73f98 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -6,7 +6,6 @@ use chrono::DateTime; use chrono::TimeZone; use chrono::Utc; use codex_core::CodexAuth; -use codex_core::features::Feature; use codex_core::models_manager::manager::RefreshStrategy; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; @@ -57,7 +56,6 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { let mut builder = test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()); builder = builder.with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model = Some("gpt-5".to_string()); config.model_provider.request_max_retries = Some(0); config.model_provider.stream_max_retries = Some(1); @@ -70,7 +68,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { // Populate cache via initial refresh. let models_manager = test.thread_manager.get_models_manager(); let _ = models_manager - .list_models(&config, RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached) .await; let cache_path = config.codex_home.join(CACHE_FILE); @@ -123,7 +121,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { // Cached models remain usable offline. let offline_models = test .thread_manager - .list_models(&config, RefreshStrategy::Offline) + .list_models(RefreshStrategy::Offline) .await; assert!( offline_models @@ -160,14 +158,13 @@ async fn uses_cache_when_version_matches() -> Result<()> { write_cache_sync(&cache_path, &cache).expect("write cache"); }) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model_provider.request_max_retries = Some(0); }); let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); let models = models_manager - .list_models(&test.config, RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached) .await; assert!( @@ -208,14 +205,13 @@ async fn refreshes_when_cache_version_missing() -> Result<()> { write_cache_sync(&cache_path, &cache).expect("write cache"); }) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model_provider.request_max_retries = Some(0); }); let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); let models = models_manager - .list_models(&test.config, RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached) .await; assert!( @@ -257,14 +253,13 @@ async fn refreshes_when_cache_version_differs() -> Result<()> { write_cache_sync(&cache_path, &cache).expect("write cache"); }) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model_provider.request_max_retries = Some(0); }); let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); let models = models_manager - .list_models(&test.config, RefreshStrategy::OnlineIfUncached) + .list_models(RefreshStrategy::OnlineIfUncached) .await; assert!( diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index 413616b9dc..13c31cd7f9 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use anyhow::Result; use codex_core::CodexAuth; -use codex_core::features::Feature; use codex_core::protocol::AskForApproval; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; @@ -48,7 +47,6 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch .with_auth(auth) .with_model("gpt-5") .with_config(|config| { - config.features.enable(Feature::RemoteModels); // Keep this test deterministic: no request retries, and a small stream retry budget. config.model_provider.request_max_retries = Some(0); config.model_provider.stream_max_retries = Some(1); diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 51f99720aa..d15147ed36 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -81,7 +81,6 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res let mut builder = test_codex() .with_model("gpt-5.2-codex") .with_config(|config| { - config.features.disable(Feature::RemoteModels); config.features.enable(Feature::Personality); }); let test = builder.build(&server).await?; @@ -127,7 +126,6 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< let mut builder = test_codex() .with_model("gpt-5.2-codex") .with_config(|config| { - config.features.disable(Feature::RemoteModels); config.features.enable(Feature::Personality); config.personality = Some(Personality::Friendly); }); @@ -181,7 +179,6 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { let mut builder = test_codex() .with_model("gpt-5.2-codex") .with_config(|config| { - config.features.disable(Feature::RemoteModels); config.features.enable(Feature::Personality); config.personality = Some(Personality::None); }); @@ -242,7 +239,6 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul let mut builder = test_codex() .with_model("gpt-5.2-codex") .with_config(|config| { - config.features.disable(Feature::RemoteModels); config.features.enable(Feature::Personality); }); let test = builder.build(&server).await?; @@ -290,7 +286,6 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> let mut builder = test_codex() .with_model("exp-codex-personality") .with_config(|config| { - config.features.disable(Feature::RemoteModels); config.features.enable(Feature::Personality); }); let test = builder.build(&server).await?; @@ -386,7 +381,6 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho let mut builder = test_codex() .with_model("exp-codex-personality") .with_config(|config| { - config.features.disable(Feature::RemoteModels); config.features.enable(Feature::Personality); config.personality = Some(Personality::Pragmatic); }); @@ -494,7 +488,6 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> let mut builder = test_codex() .with_model("exp-codex-personality") .with_config(|config| { - config.features.disable(Feature::RemoteModels); config.features.disable(Feature::Personality); }); let test = builder.build(&server).await?; @@ -570,123 +563,6 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn ignores_remote_personality_if_remote_models_disabled() -> anyhow::Result<()> { - skip_if_no_network!(Ok(())); - - let server = MockServer::builder() - .body_print_limit(BodyPrintLimit::Limited(80_000)) - .start() - .await; - - let remote_slug = "gpt-5.2-codex"; - let remote_personality_message = "Friendly from remote template"; - let remote_model = ModelInfo { - slug: remote_slug.to_string(), - display_name: "Remote personality test".to_string(), - description: Some("Remote model with personality template".to_string()), - default_reasoning_level: Some(ReasoningEffort::Medium), - supported_reasoning_levels: vec![ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: ReasoningEffort::Medium.to_string(), - }], - shell_type: ConfigShellToolType::UnifiedExec, - visibility: ModelVisibility::List, - supported_in_api: true, - priority: 1, - upgrade: None, - base_instructions: "base instructions".to_string(), - model_messages: Some(ModelMessages { - instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()), - instructions_variables: Some(ModelInstructionsVariables { - personality_default: None, - personality_friendly: Some(remote_personality_message.to_string()), - personality_pragmatic: None, - }), - }), - supports_reasoning_summaries: false, - support_verbosity: false, - default_verbosity: None, - apply_patch_tool_type: None, - truncation_policy: TruncationPolicyConfig::bytes(10_000), - supports_parallel_tool_calls: false, - context_window: Some(128_000), - auto_compact_token_limit: None, - effective_context_window_percent: 95, - experimental_supported_tools: Vec::new(), - input_modalities: default_input_modalities(), - prefer_websockets: false, - used_fallback_model_metadata: false, - }; - - let _models_mock = mount_models_once( - &server, - ModelsResponse { - models: vec![remote_model], - }, - ) - .await; - - let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await; - - let mut builder = test_codex() - .with_auth(codex_core::CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(|config| { - config.features.disable(Feature::RemoteModels); - config.features.enable(Feature::Personality); - config.model = Some(remote_slug.to_string()); - config.personality = Some(Personality::Friendly); - }); - let test = builder.build(&server).await?; - - wait_for_model_available( - &test.thread_manager.get_models_manager(), - remote_slug, - &test.config, - ) - .await; - - test.codex - .submit(Op::UserTurn { - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - cwd: test.cwd_path().to_path_buf(), - approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::new_read_only_policy(), - model: remote_slug.to_string(), - effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, - collaboration_mode: None, - personality: None, - }) - .await?; - - wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - - let request = resp_mock.single_request(); - let instructions_text = request.instructions_text(); - - assert!( - instructions_text.contains("You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals."), - "expected instructions to use the template instructions, got: {instructions_text:?}" - ); - assert!( - instructions_text.contains( - "You optimize for team morale and being a supportive teammate as much as code quality." - ), - "expected instructions to include the local friendly personality template, got: {instructions_text:?}" - ); - assert!( - !instructions_text.contains("{{ personality }}"), - "expected legacy personality placeholder to be replaced, got: {instructions_text:?}" - ); - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); @@ -750,19 +626,13 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow let mut builder = test_codex() .with_auth(codex_core::CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.features.enable(Feature::Personality); config.model = Some(remote_slug.to_string()); config.personality = Some(Personality::Friendly); }); let test = builder.build(&server).await?; - wait_for_model_available( - &test.thread_manager.get_models_manager(), - remote_slug, - &test.config, - ) - .await; + wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await; test.codex .submit(Op::UserTurn { @@ -867,18 +737,12 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - let mut builder = test_codex() .with_auth(codex_core::CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.features.enable(Feature::Personality); config.model = Some("gpt-5.2-codex".to_string()); }); let test = builder.build(&server).await?; - wait_for_model_available( - &test.thread_manager.get_models_manager(), - remote_slug, - &test.config, - ) - .await; + wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await; test.codex .submit(Op::UserTurn { @@ -957,16 +821,10 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - Ok(()) } -async fn wait_for_model_available( - manager: &Arc, - slug: &str, - config: &codex_core::config::Config, -) { +async fn wait_for_model_available(manager: &Arc, slug: &str) { let deadline = Instant::now() + Duration::from_secs(2); loop { - let models = manager - .list_models(config, RefreshStrategy::OnlineIfUncached) - .await; + let models = manager.list_models(RefreshStrategy::OnlineIfUncached).await; if models.iter().any(|model| model.model == slug) { return; } diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 71a565540b..cd3b4272a0 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -194,7 +194,6 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an let TestCodex { codex, .. } = test_codex() .with_config(|config| { config.user_instructions = Some("be consistent and helpful".to_string()); - config.features.enable(Feature::RemoteModels); config.features.disable(Feature::ApplyPatchFreeform); config.features.enable(Feature::CollaborationModes); config.model = Some("gpt-5".to_string()); diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 29ba047d01..110150d447 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -7,8 +7,6 @@ use anyhow::Result; use codex_core::CodexAuth; use codex_core::ModelProviderInfo; use codex_core::built_in_model_providers; -use codex_core::config::Config; -use codex_core::features::Feature; use codex_core::models_manager::manager::ModelsManager; use codex_core::models_manager::manager::RefreshStrategy; use codex_core::protocol::AskForApproval; @@ -92,8 +90,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<( .await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); + let config = load_default_config_for_test(&codex_home).await; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let provider = ModelProviderInfo { @@ -106,9 +103,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<( provider, ); - manager - .list_models(&config, RefreshStrategy::OnlineIfUncached) - .await; + manager.list_models(RefreshStrategy::OnlineIfUncached).await; let model_info = manager.get_model_info("gpt-5.3-codex-test", &config).await; @@ -163,7 +158,6 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( } = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model = Some(requested_model.to_string()); }) .build(&server) @@ -253,7 +247,6 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model = Some("gpt-5.1".to_string()); }); let TestCodex { @@ -265,8 +258,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { } = builder.build(&server).await?; let models_manager = thread_manager.get_models_manager(); - let available_model = - wait_for_model_available(&models_manager, REMOTE_MODEL_SLUG, &config).await; + let available_model = wait_for_model_available(&models_manager, REMOTE_MODEL_SLUG).await; assert_eq!(available_model.model, REMOTE_MODEL_SLUG); @@ -375,13 +367,12 @@ async fn remote_models_truncation_policy_without_override_preserves_remote() -> let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model = Some("gpt-5.1".to_string()); }); let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); - wait_for_model_available(&models_manager, slug, &test.config).await; + wait_for_model_available(&models_manager, slug).await; let model_info = models_manager.get_model_info(slug, &test.config).await; assert_eq!( @@ -420,14 +411,13 @@ async fn remote_models_truncation_policy_with_tool_output_override() -> Result<( let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model = Some("gpt-5.1".to_string()); config.tool_output_token_limit = Some(50); }); let test = builder.build(&server).await?; let models_manager = test.thread_manager.get_models_manager(); - wait_for_model_available(&models_manager, slug, &test.config).await; + wait_for_model_available(&models_manager, slug).await; let model_info = models_manager.get_model_info(slug, &test.config).await; assert_eq!( @@ -502,7 +492,6 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model = Some("gpt-5.1".to_string()); }); let TestCodex { @@ -514,7 +503,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { } = builder.build(&server).await?; let models_manager = thread_manager.get_models_manager(); - wait_for_model_available(&models_manager, model, &config).await; + wait_for_model_available(&models_manager, model).await; codex .submit(Op::OverrideTurnContext { @@ -574,8 +563,6 @@ async fn remote_models_preserve_builtin_presets() -> Result<()> { .await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let provider = ModelProviderInfo { @@ -588,9 +575,7 @@ async fn remote_models_preserve_builtin_presets() -> Result<()> { provider, ); - let available = manager - .list_models(&config, RefreshStrategy::OnlineIfUncached) - .await; + let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; let remote = available .iter() .find(|model| model.model == "remote-alpha") @@ -639,8 +624,6 @@ async fn remote_models_merge_adds_new_high_priority_first() -> Result<()> { .await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let provider = ModelProviderInfo { @@ -653,9 +636,7 @@ async fn remote_models_merge_adds_new_high_priority_first() -> Result<()> { provider, ); - let available = manager - .list_models(&config, RefreshStrategy::OnlineIfUncached) - .await; + let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; assert_eq!( available.first().map(|model| model.model.as_str()), Some("remote-top") @@ -690,8 +671,6 @@ async fn remote_models_merge_replaces_overlapping_model() -> Result<()> { .await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let provider = ModelProviderInfo { @@ -704,9 +683,7 @@ async fn remote_models_merge_replaces_overlapping_model() -> Result<()> { provider, ); - let available = manager - .list_models(&config, RefreshStrategy::OnlineIfUncached) - .await; + let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; let overridden = available .iter() .find(|model| model.model == slug) @@ -738,8 +715,6 @@ async fn remote_models_merge_preserves_bundled_models_on_empty_response() -> Res let _models_mock = mount_models_once(&server, ModelsResponse { models: Vec::new() }).await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let provider = ModelProviderInfo { @@ -752,9 +727,7 @@ async fn remote_models_merge_preserves_bundled_models_on_empty_response() -> Res provider, ); - let available = manager - .list_models(&config, RefreshStrategy::OnlineIfUncached) - .await; + let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; let bundled_slug = bundled_model_slug(); assert!( available.iter().any(|model| model.model == bundled_slug), @@ -783,8 +756,6 @@ async fn remote_models_request_times_out_after_5s() -> Result<()> { .await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let provider = ModelProviderInfo { @@ -800,7 +771,7 @@ async fn remote_models_request_times_out_after_5s() -> Result<()> { let start = Instant::now(); let model = timeout( Duration::from_secs(7), - manager.get_default_model(&None, &config, RefreshStrategy::OnlineIfUncached), + manager.get_default_model(&None, RefreshStrategy::OnlineIfUncached), ) .await; let elapsed = start.elapsed(); @@ -850,8 +821,6 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { .await; let codex_home = TempDir::new()?; - let mut config = load_default_config_for_test(&codex_home).await; - config.features.enable(Feature::RemoteModels); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let provider = ModelProviderInfo { @@ -865,13 +834,11 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { ); let selected = manager - .get_default_model(&None, &config, RefreshStrategy::OnlineIfUncached) + .get_default_model(&None, RefreshStrategy::OnlineIfUncached) .await; assert_eq!(selected, "gpt-5.2-codex"); - let available = manager - .list_models(&config, RefreshStrategy::OnlineIfUncached) - .await; + let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; let hidden = available .iter() .find(|model| model.model == "codex-auto-balanced") @@ -888,17 +855,11 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { Ok(()) } -async fn wait_for_model_available( - manager: &Arc, - slug: &str, - config: &Config, -) -> ModelPreset { +async fn wait_for_model_available(manager: &Arc, slug: &str) -> ModelPreset { let deadline = Instant::now() + Duration::from_secs(2); loop { if let Some(model) = { - let guard = manager - .list_models(config, RefreshStrategy::OnlineIfUncached) - .await; + let guard = manager.list_models(RefreshStrategy::OnlineIfUncached).await; guard.iter().find(|model| model.model == slug).cloned() } { return model; diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 467f4e354d..d1394f7a74 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -11,7 +11,6 @@ use std::time::UNIX_EPOCH; use codex_core::CodexAuth; use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; -use codex_core::features::Feature; use codex_core::models_manager::manager::RefreshStrategy; use codex_core::protocol::AskForApproval; @@ -441,8 +440,6 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re let fixture = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(move |config| { - config.features.enable(Feature::RemoteModels); - let mut servers = config.mcp_servers.get().clone(); servers.insert( server_name.to_string(), @@ -478,7 +475,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re fixture .thread_manager .get_models_manager() - .list_models(&fixture.config, RefreshStrategy::Online) + .list_models(RefreshStrategy::Online) .await; assert_eq!(models_mock.requests().len(), 1); diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index a22fead90f..62a46bf1de 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -1,6 +1,7 @@ use anyhow::Result; use codex_core::protocol::AskForApproval; use codex_core::protocol::EventMsg; +use codex_core::protocol::ModelRerouteReason; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; use codex_protocol::config_types::ReasoningSummary; @@ -54,6 +55,17 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> }) .await?; + let reroute = wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::ModelReroute(_)) + }) + .await; + let EventMsg::ModelReroute(reroute) = reroute else { + panic!("expected model reroute event"); + }; + assert_eq!(reroute.from_model, REQUESTED_MODEL); + assert_eq!(reroute.to_model, SERVER_MODEL); + assert_eq!(reroute.reason, ModelRerouteReason::HighRiskCyberActivity); + let warning = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Warning(_))).await; let EventMsg::Warning(warning) = warning else { panic!("expected warning event"); @@ -138,6 +150,17 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request }) .await?; + let reroute = wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::ModelReroute(_)) + }) + .await; + let EventMsg::ModelReroute(reroute) = reroute else { + panic!("expected model reroute event"); + }; + assert_eq!(reroute.from_model, REQUESTED_MODEL); + assert_eq!(reroute.to_model, SERVER_MODEL); + assert_eq!(reroute.reason, ModelRerouteReason::HighRiskCyberActivity); + let warning = wait_for_event(&test.codex, |event| { matches!( event, @@ -151,7 +174,8 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request let EventMsg::Warning(warning) = warning else { panic!("expected warning event"); }; - assert!(warning.message.contains("gpt-5.2 as a fallback")); + assert!(warning.message.contains(REQUESTED_MODEL)); + assert!(warning.message.contains(SERVER_MODEL)); let _ = wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index bd5bd6a037..bad9efa2d0 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -698,7 +698,6 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an let TestCodex { codex, cwd, .. } = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - config.features.enable(Feature::RemoteModels); config.model = Some(model_slug.to_string()); }) .build(&server) diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index d7b95e2882..26861af7c8 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -188,6 +188,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { "warning:".style(self.yellow).style(self.bold) ); } + EventMsg::ModelReroute(_) => {} EventMsg::DeprecationNotice(DeprecationNoticeEvent { summary, details }) => { ts_msg!( self, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 68c88f0000..49163c900b 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -374,7 +374,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any )); let default_model = thread_manager .get_models_manager() - .get_default_model(&config.model, &config, RefreshStrategy::OnlineIfUncached) + .get_default_model(&config.model, RefreshStrategy::OnlineIfUncached) .await; // Handle resume subcommand by resolving a rollout path and using explicit resume API. diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 5af2191fcc..04fa658017 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -254,6 +254,9 @@ async fn run_codex_tool_session_inner( EventMsg::Warning(_) => { continue; } + EventMsg::ModelReroute(_) => { + continue; + } EventMsg::ElicitationRequest(_) => { // TODO: forward elicitation requests to the client? continue; diff --git a/codex-rs/mcp-server/tests/suite/codex_tool.rs b/codex-rs/mcp-server/tests/suite/codex_tool.rs index c898b8f0d7..de8a9306d4 100644 --- a/codex-rs/mcp-server/tests/suite/codex_tool.rs +++ b/codex-rs/mcp-server/tests/suite/codex_tool.rs @@ -507,7 +507,6 @@ request_max_retries = 0 stream_max_retries = 0 [features] -remote_models = false "# ), ) diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 7ffed9008d..925d7147bc 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -62,6 +62,8 @@ For HTTP(S) traffic: ```bash export HTTP_PROXY="http://127.0.0.1:3128" export HTTPS_PROXY="http://127.0.0.1:3128" +export WS_PROXY="http://127.0.0.1:3128" +export WSS_PROXY="http://127.0.0.1:3128" ``` For SOCKS5 traffic (when `enable_socks5 = true`): @@ -83,6 +85,9 @@ When a request is blocked, the proxy responds with `403` and includes: In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. HTTPS `CONNECT` and SOCKS5 are blocked because they would bypass method enforcement. +Websocket clients typically tunnel `wss://` through HTTPS `CONNECT`; those CONNECT targets still go +through the same host allowlist/denylist checks. + ## Library API `codex-network-proxy` can be embedded as a library with a thin API: diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index eed0ad9da6..7be1b81f47 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -828,6 +828,51 @@ mod tests { ); } + #[tokio::test] + async fn http_connect_accept_allows_allowlisted_host_in_full_mode() { + let policy = NetworkProxySettings { + allowed_domains: vec!["example.com".to_string()], + ..Default::default() + }; + let state = Arc::new(network_proxy_state_for_policy(policy)); + + let mut req = Request::builder() + .method(Method::CONNECT) + .uri("https://example.com:443") + .header("host", "example.com:443") + .body(Body::empty()) + .unwrap(); + req.extensions_mut().insert(state); + + let (response, _request) = http_connect_accept(None, req).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn http_connect_accept_denies_denylisted_host() { + let policy = NetworkProxySettings { + allowed_domains: vec!["**.openai.com".to_string()], + denied_domains: vec!["api.openai.com".to_string()], + ..Default::default() + }; + let state = Arc::new(network_proxy_state_for_policy(policy)); + + let mut req = Request::builder() + .method(Method::CONNECT) + .uri("https://api.openai.com:443") + .header("host", "api.openai.com:443") + .body(Body::empty()) + .unwrap(); + req.extensions_mut().insert(state); + + let response = http_connect_accept(None, req).await.unwrap_err(); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.headers().get("x-proxy-error").unwrap(), + "blocked-by-denylist" + ); + } + #[test] fn request_network_attempt_id_reads_proxy_authorization_header() { let encoded = STANDARD.encode("codex-net-attempt-attempt-1:"); diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 55a0eaa165..2e93fb7a1b 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -272,6 +272,8 @@ impl Eq for NetworkProxy {} pub const PROXY_URL_ENV_KEYS: &[&str] = &[ "HTTP_PROXY", "HTTPS_PROXY", + "WS_PROXY", + "WSS_PROXY", "ALL_PROXY", "FTP_PROXY", "YARN_HTTP_PROXY", @@ -290,6 +292,7 @@ pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"]; pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING"; const FTP_PROXY_ENV_KEYS: &[&str] = &["FTP_PROXY", "ftp_proxy"]; +const WEBSOCKET_PROXY_ENV_KEYS: &[&str] = &["WS_PROXY", "WSS_PROXY", "ws_proxy", "wss_proxy"]; pub const NO_PROXY_ENV_KEYS: &[&str] = &[ "NO_PROXY", @@ -375,6 +378,9 @@ fn apply_proxy_env_overrides( ], &http_proxy_url, ); + // Some websocket clients look for dedicated WS/WSS proxy environment variables instead of + // HTTP(S)_PROXY. Keep them aligned with the managed HTTP proxy endpoint. + set_env_keys(env, WEBSOCKET_PROXY_ENV_KEYS, &http_proxy_url); // Keep local/private targets direct so local IPC and metadata endpoints avoid the proxy. set_env_keys(env, NO_PROXY_ENV_KEYS, DEFAULT_NO_PROXY_VALUE); @@ -728,6 +734,14 @@ mod tests { assert_eq!(has_proxy_url_env_vars(&env), true); } + #[test] + fn has_proxy_url_env_vars_detects_websocket_proxy_keys() { + let mut env = HashMap::new(); + env.insert("wss_proxy".to_string(), "http://127.0.0.1:3128".to_string()); + + assert_eq!(has_proxy_url_env_vars(&env), true); + } + #[test] fn apply_proxy_env_overrides_sets_common_tool_vars() { let mut env = HashMap::new(); @@ -744,6 +758,14 @@ mod tests { env.get("HTTP_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); + assert_eq!( + env.get("WS_PROXY"), + Some(&"http://127.0.0.1:3128".to_string()) + ); + assert_eq!( + env.get("WSS_PROXY"), + Some(&"http://127.0.0.1:3128".to_string()) + ); assert_eq!( env.get("npm_config_proxy"), Some(&"http://127.0.0.1:3128".to_string()) @@ -810,6 +832,14 @@ mod tests { env.get("HTTPS_PROXY"), Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string()) ); + assert_eq!( + env.get("WS_PROXY"), + Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string()) + ); + assert_eq!( + env.get("WSS_PROXY"), + Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string()) + ); assert_eq!( env.get("ALL_PROXY"), Some(&"http://codex-net-attempt-attempt-123@127.0.0.1:3128".to_string()) diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 8a7fa9c382..f9c56c5bc4 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -285,13 +285,14 @@ pub enum Op { }, /// Request the list of remote skills available via ChatGPT sharing. - ListRemoteSkills, + ListRemoteSkills { + hazelnut_scope: RemoteSkillHazelnutScope, + product_surface: RemoteSkillProductSurface, + enabled: Option, + }, /// Download a remote skill by id into the local skills cache. - DownloadRemoteSkill { - hazelnut_id: String, - is_preload: bool, - }, + DownloadRemoteSkill { hazelnut_id: String }, /// Request the agent to summarize the current conversation context. /// The agent will use its existing context (either conversation history or previous response id) @@ -883,6 +884,9 @@ pub enum EventMsg { /// indicates the turn continued but the user should still be notified. Warning(WarningEvent), + /// Model routing changed from the requested model to a different model. + ModelReroute(ModelRerouteEvent), + /// Conversation history was compacted (either automatically or manually). ContextCompacted(ContextCompactedEvent), @@ -1342,6 +1346,20 @@ pub struct WarningEvent { pub message: String, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case")] +pub enum ModelRerouteReason { + HighRiskCyberActivity, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +pub struct ModelRerouteEvent { + pub from_model: String, + pub to_model: String, + pub reason: ModelRerouteReason, +} + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ContextCompactedEvent; @@ -2405,6 +2423,26 @@ pub struct RemoteSkillSummary { pub description: String, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case")] +pub enum RemoteSkillHazelnutScope { + WorkspaceShared, + AllShared, + Personal, + Example, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(rename_all = "lowercase")] +pub enum RemoteSkillProductSurface { + Chatgpt, + Codex, + Api, + Atlas, +} + /// Response payload for `Op::ListRemoteSkills`. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ListRemoteSkillsResponseEvent { diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index b23b5f1a13..dd019946f2 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1029,11 +1029,11 @@ impl App { )); let mut model = thread_manager .get_models_manager() - .get_default_model(&config.model, &config, RefreshStrategy::Offline) + .get_default_model(&config.model, RefreshStrategy::Offline) .await; let available_models = thread_manager .get_models_manager() - .list_models(&config, RefreshStrategy::Offline) + .list_models(RefreshStrategy::Offline) .await; let exit_info = handle_model_migration_prompt_if_needed( tui, @@ -1452,8 +1452,11 @@ impl App { ) .await? { - Some(cwd) => cwd, - None => current_cwd.clone(), + crate::ResolveCwdOutcome::Continue(Some(cwd)) => cwd, + crate::ResolveCwdOutcome::Continue(None) => current_cwd.clone(), + crate::ResolveCwdOutcome::Exit => { + return Ok(AppRunControl::Exit(ExitReason::UserRequested)); + } }; let mut resume_config = if crate::cwds_differ(¤t_cwd, &resume_cwd) { match self.rebuild_config_for_cwd(resume_cwd).await { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 36142cbaed..91dfa00a52 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -4596,6 +4596,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4633,6 +4636,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: false, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index dcbee999bc..0a6da6a5ae 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4021,6 +4021,7 @@ impl ChatWidget { self.on_rate_limit_snapshot(ev.rate_limits); } EventMsg::Warning(WarningEvent { message }) => self.on_warning(message), + EventMsg::ModelReroute(_) => {} EventMsg::Error(ErrorEvent { message, codex_error_info, @@ -4698,7 +4699,7 @@ impl ChatWidget { } fn lower_cost_preset(&self) -> Option { - let models = self.models_manager.try_list_models(&self.config).ok()?; + let models = self.models_manager.try_list_models().ok()?; models .iter() .find(|preset| preset.show_in_picker && preset.model == NUDGE_MODEL_SLUG) @@ -4815,7 +4816,7 @@ impl ChatWidget { return; } - let presets: Vec = match self.models_manager.try_list_models(&self.config) { + let presets: Vec = match self.models_manager.try_list_models() { Ok(models) => models, Err(_) => { self.add_info_message( @@ -6137,7 +6138,7 @@ impl ChatWidget { fn current_model_supports_personality(&self) -> bool { let model = self.current_model(); self.models_manager - .try_list_models(&self.config) + .try_list_models() .ok() .and_then(|models| { models @@ -6155,7 +6156,7 @@ impl ChatWidget { fn current_model_supports_images(&self) -> bool { let model = self.current_model(); self.models_manager - .try_list_models(&self.config) + .try_list_models() .ok() .and_then(|models| { models diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index f069db9160..bfae9581b2 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -2749,7 +2749,7 @@ fn active_blob(chat: &ChatWidget) -> String { fn get_available_model(chat: &ChatWidget, model: &str) -> ModelPreset { let models = chat .models_manager - .try_list_models(&chat.config) + .try_list_models() .expect("models lock available"); models .iter() @@ -4424,6 +4424,9 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4457,6 +4460,9 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4468,6 +4474,9 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/linear".to_string()), is_accessible: true, is_enabled: true, @@ -4504,6 +4513,9 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4515,6 +4527,9 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/linear".to_string()), is_accessible: false, is_enabled: true, @@ -4536,6 +4551,9 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4572,6 +4590,9 @@ async fn apps_partial_refresh_uses_same_filtering_as_full_refresh() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4583,6 +4604,9 @@ async fn apps_partial_refresh_uses_same_filtering_as_full_refresh() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/linear".to_string()), is_accessible: false, is_enabled: true, @@ -4606,6 +4630,9 @@ async fn apps_partial_refresh_uses_same_filtering_as_full_refresh() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4617,6 +4644,9 @@ async fn apps_partial_refresh_uses_same_filtering_as_full_refresh() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/hidden-openai".to_string()), is_accessible: true, is_enabled: true, @@ -4657,6 +4687,9 @@ async fn apps_popup_shows_disabled_status_for_installed_but_disabled_apps() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: false, @@ -4704,6 +4737,9 @@ async fn apps_initial_load_applies_enabled_state_from_config() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4738,6 +4774,9 @@ async fn apps_refresh_preserves_toggled_enabled_state() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4756,6 +4795,9 @@ async fn apps_refresh_preserves_toggled_enabled_state() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4797,6 +4839,9 @@ async fn apps_popup_for_not_installed_app_uses_install_only_selected_description logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/linear".to_string()), is_accessible: false, is_enabled: true, diff --git a/codex-rs/tui/src/cwd_prompt.rs b/codex-rs/tui/src/cwd_prompt.rs index 2a9c016a1e..cb04aa0b4a 100644 --- a/codex-rs/tui/src/cwd_prompt.rs +++ b/codex-rs/tui/src/cwd_prompt.rs @@ -51,6 +51,12 @@ pub(crate) enum CwdSelection { Session, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CwdPromptOutcome { + Selection(CwdSelection), + Exit, +} + impl CwdSelection { fn next(self) -> Self { match self { @@ -72,7 +78,7 @@ pub(crate) async fn run_cwd_selection_prompt( action: CwdPromptAction, current_cwd: &Path, session_cwd: &Path, -) -> Result { +) -> Result { let mut screen = CwdPromptScreen::new( tui.frame_requester(), action, @@ -102,7 +108,13 @@ pub(crate) async fn run_cwd_selection_prompt( } } - Ok(screen.selection().unwrap_or(CwdSelection::Session)) + if screen.should_exit { + Ok(CwdPromptOutcome::Exit) + } else { + Ok(CwdPromptOutcome::Selection( + screen.selection().unwrap_or(CwdSelection::Session), + )) + } } struct CwdPromptScreen { @@ -112,6 +124,7 @@ struct CwdPromptScreen { session_cwd: String, highlighted: CwdSelection, selection: Option, + should_exit: bool, } impl CwdPromptScreen { @@ -128,6 +141,7 @@ impl CwdPromptScreen { session_cwd, highlighted: CwdSelection::Session, selection: None, + should_exit: false, } } @@ -138,7 +152,9 @@ impl CwdPromptScreen { if key_event.modifiers.contains(KeyModifiers::CONTROL) && matches!(key_event.code, KeyCode::Char('c') | KeyCode::Char('d')) { - self.select(CwdSelection::Session); + self.selection = None; + self.should_exit = true; + self.request_frame.schedule_frame(); return; } match key_event.code { @@ -166,7 +182,7 @@ impl CwdPromptScreen { } fn is_done(&self) -> bool { - self.selection.is_some() + self.should_exit || self.selection.is_some() } fn selection(&self) -> Option { @@ -283,4 +299,12 @@ mod tests { screen.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_eq!(screen.selection(), Some(CwdSelection::Current)); } + + #[test] + fn cwd_prompt_ctrl_c_exits_instead_of_selecting() { + let mut screen = new_prompt(); + screen.handle_key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); + assert_eq!(screen.selection(), None); + assert!(screen.is_done()); + } } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index cb4e311576..20c9fc21b0 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -44,6 +44,7 @@ use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_oss::ensure_oss_provider_ready; use codex_utils_oss::get_default_model_for_oss_provider; use cwd_prompt::CwdPromptAction; +use cwd_prompt::CwdPromptOutcome; use cwd_prompt::CwdSelection; use std::fs::OpenOptions; use std::path::Path; @@ -672,8 +673,22 @@ async fn run_ratatui_app( }; let fallback_cwd = match action_and_path_if_resume_or_fork { Some((action, path)) => { - resolve_cwd_for_resume_or_fork(&mut tui, ¤t_cwd, path, action, allow_prompt) + match resolve_cwd_for_resume_or_fork(&mut tui, ¤t_cwd, path, action, allow_prompt) .await? + { + ResolveCwdOutcome::Continue(cwd) => cwd, + ResolveCwdOutcome::Exit => { + restore(); + session_log::log_session_end(); + return Ok(AppExitInfo { + token_usage: codex_core::protocol::TokenUsage::default(), + thread_id: None, + thread_name: None, + update_action: None, + exit_reason: ExitReason::UserRequested, + }); + } + } } None => None, }; @@ -780,25 +795,35 @@ pub(crate) fn cwds_differ(current_cwd: &Path, session_cwd: &Path) -> bool { } } +pub(crate) enum ResolveCwdOutcome { + Continue(Option), + Exit, +} + pub(crate) async fn resolve_cwd_for_resume_or_fork( tui: &mut Tui, current_cwd: &Path, path: &Path, action: CwdPromptAction, allow_prompt: bool, -) -> color_eyre::Result> { +) -> color_eyre::Result { let Some(history_cwd) = read_session_cwd(path).await else { - return Ok(None); + return Ok(ResolveCwdOutcome::Continue(None)); }; if allow_prompt && cwds_differ(current_cwd, &history_cwd) { - let selection = + let selection_outcome = cwd_prompt::run_cwd_selection_prompt(tui, action, current_cwd, &history_cwd).await?; - return Ok(Some(match selection { - CwdSelection::Current => current_cwd.to_path_buf(), - CwdSelection::Session => history_cwd, - })); + return Ok(match selection_outcome { + CwdPromptOutcome::Selection(CwdSelection::Current) => { + ResolveCwdOutcome::Continue(Some(current_cwd.to_path_buf())) + } + CwdPromptOutcome::Selection(CwdSelection::Session) => { + ResolveCwdOutcome::Continue(Some(history_cwd)) + } + CwdPromptOutcome::Exit => ResolveCwdOutcome::Exit, + }); } - Ok(Some(history_cwd)) + Ok(ResolveCwdOutcome::Continue(Some(history_cwd))) } #[expect(