Merge branch 'main' into kkumar/realtime-session-create-codex

This commit is contained in:
Ahmed Ibrahim
2026-02-17 14:54:27 -08:00
committed by GitHub
97 changed files with 2803 additions and 822 deletions
+16 -1
View File
@@ -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:
+1 -1
View File
@@ -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\"]}}",
+22 -22
View File
@@ -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]]
+1 -1
View File
@@ -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"
@@ -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"
},
{
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": [
@@ -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": [
@@ -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": [
@@ -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.",
@@ -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.",
@@ -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"
}
@@ -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"
}
@@ -3,14 +3,10 @@
"properties": {
"hazelnutId": {
"type": "string"
},
"isPreload": {
"type": "boolean"
}
},
"required": [
"hazelnutId",
"isPreload"
"hazelnutId"
],
"title": "SkillsRemoteWriteParams",
"type": "object"
@@ -4,16 +4,12 @@
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"id",
"name",
"path"
],
"title": "SkillsRemoteWriteResponse",
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadArchivedNotification",
"type": "object"
}
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadUnarchivedNotification",
"type": "object"
}
@@ -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, };
@@ -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;
@@ -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, };
@@ -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";
@@ -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 };
@@ -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";
@@ -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, };
@@ -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:
@@ -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<string> | null, subCategories: Array<string> | null, seoDescription: string | null, screenshots: Array<AppScreenshot> | null, developer: string | null, version: string | null, versionId: string | null, versionNotes: string | null, firstPartyType: string | null, firstPartyRequiresInstall: boolean | null, showInComposerWhenUnlinked: boolean | null, };
@@ -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, };
@@ -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, };
@@ -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";
@@ -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";
@@ -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, };
@@ -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";
@@ -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<string, never>;
export type SkillsRemoteReadParams = { hazelnutScope: HazelnutScope, productSurface: ProductSurface, enabled: boolean, };
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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";
@@ -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),
+116 -3
View File
@@ -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<String>,
pub developer: Option<String>,
pub website: Option<String>,
pub privacy_policy: Option<String>,
pub terms_of_service: Option<String>,
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<String>,
#[serde(alias = "file_id")]
pub file_id: Option<String>,
#[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<AppReview>,
pub categories: Option<Vec<String>>,
pub sub_categories: Option<Vec<String>>,
pub seo_description: Option<String>,
pub screenshots: Option<Vec<AppScreenshot>>,
pub developer: Option<String>,
pub version: Option<String>,
pub version_id: Option<String>,
pub version_notes: Option<String>,
pub first_party_type: Option<String>,
pub first_party_requires_install: Option<bool>,
pub show_in_composer_when_unlinked: Option<bool>,
}
#[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<String>,
pub logo_url_dark: Option<String>,
pub distribution_channel: Option<String>,
pub branding: Option<AppBranding>,
pub app_metadata: Option<AppMetadata>,
pub labels: Option<HashMap<String, String>>,
pub install_url: Option<String>,
#[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<String>,
}
#[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/")]
+16 -7
View File
@@ -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
@@ -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();
@@ -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<Vec<AppInfo>, 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<OutgoingMessageSender>,
thread_manager: Arc<ThreadManager>,
config: Arc<Config>,
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,
},
)
+1 -3
View File
@@ -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<ThreadManager>,
config: &Config,
include_hidden: bool,
) -> Vec<Model> {
thread_manager
.list_models(config, RefreshStrategy::OnlineIfUncached)
.list_models(RefreshStrategy::OnlineIfUncached)
.await
.into_iter()
.filter(|preset| include_hidden || preset.show_in_picker)
@@ -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::<OutgoingEnvelope>(4);
+1 -1
View File
@@ -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);
}
@@ -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,
@@ -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)| {
@@ -437,7 +437,6 @@ sandbox_mode = "read-only"
model_provider = "mock_provider"
[features]
remote_models = false
shell_snapshot = false
[model_providers.mock_provider]
@@ -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::<ItemStartedNotification, anyhow::Error>(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::<ItemCompletedNotification, anyhow::Error>(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::<ItemStartedNotification, anyhow::Error>(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::<ItemCompletedNotification, anyhow::Error>(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<ModelReroutedNotification> {
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<()> {
@@ -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::<ThreadArchiveResponse>(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);
@@ -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]
@@ -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::<ThreadUnarchiveResponse>(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"
@@ -1871,7 +1871,7 @@ fn create_config_toml_with_sandbox(
feature_flags: &BTreeMap<Feature, bool>,
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);
}
+122
View File
@@ -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<String>,
#[serde(alias = "appMetadata")]
app_metadata: Option<AppMetadata>,
branding: Option<AppBranding>,
labels: Option<HashMap<String, String>>,
#[serde(alias = "logoUrl")]
logo_url: Option<String>,
#[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,
+1 -1
View File
@@ -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 }
+10
View File
@@ -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"
},
+78 -40
View File
@@ -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<TurnContext>,
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<Session>, config: Arc<Config>, 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<Config>, 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::<Vec<_>>()
});
pub async fn list_remote_skills(
sess: &Session,
config: &Arc<Config>,
sub_id: String,
hazelnut_scope: RemoteSkillHazelnutScope,
product_surface: RemoteSkillProductSurface,
enabled: Option<bool>,
) {
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::<Vec<_>>()
});
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<Config>,
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,
+76
View File
@@ -0,0 +1,76 @@
const DEFAULT_ATTRIBUTION_VALUE: &str = "Codex <noreply@openai.com>";
fn build_commit_message_trailer(config_attribution: Option<&str>) -> Option<String> {
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<String> {
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<String> {
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 <noreply@openai.com>")
);
}
#[test]
fn resolve_value_handles_default_custom_and_blank() {
assert_eq!(
resolve_attribution_value(None),
Some("Codex <noreply@openai.com>".to_string())
);
assert_eq!(
resolve_attribution_value(Some("MyAgent <me@example.com>")),
Some("MyAgent <me@example.com>".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 <agent@example.com>"))
.expect("instruction expected");
assert!(instruction.contains("Co-authored-by: AgentX <agent@example.com>"));
assert!(instruction.contains("exactly once"));
assert!(!instruction.contains("Generated-with"));
}
}
+19
View File
@@ -206,6 +206,13 @@ pub struct Config {
/// Compact prompt override.
pub compact_prompt: Option<String>,
/// Optional commit attribution text for commit message co-author trailers.
///
/// - `None`: use default attribution (`Codex <noreply@openai.com>`)
/// - `Some("")` or whitespace-only: disable commit attribution
/// - `Some("...")`: use the provided attribution text verbatim
pub commit_attribution: Option<String>,
/// 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<String>,
/// Optional commit attribution text for commit message co-author trailers.
///
/// Set to an empty string to disable automatic commit attribution.
pub commit_attribution: Option<String>,
/// When set, restricts ChatGPT login to a specific workspace identifier.
#[serde(default)]
pub forced_chatgpt_workspace_id: Option<String>,
@@ -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,
+2 -2
View File
@@ -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(())
+5
View File
@@ -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,
+11 -3
View File
@@ -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,
+1
View File
@@ -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;
+37 -113
View File
@@ -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<ModelPreset> {
if let Err(err) = self
.refresh_available_models(config, refresh_strategy)
.await
{
pub async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec<ModelPreset> {
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<Vec<ModelPreset>, TryLockError> {
let remote_models = self.try_get_remote_models(config)?;
pub fn try_list_models(&self) -> Result<Vec<ModelPreset>, 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<String>,
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<ModelInfo> {
async fn find_remote_model_by_longest_prefix(&self, model: &str) -> Option<ModelInfo> {
let mut best: Option<ModelInfo> = 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<ModelInfo> {
if config.features.enabled(Feature::RemoteModels) {
self.remote_models.read().await.clone()
} else {
Vec::new()
}
async fn get_remote_models(&self) -> Vec<ModelInfo> {
self.remote_models.read().await.clone()
}
fn try_get_remote_models(&self, config: &Config) -> Result<Vec<ModelInfo>, 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<Vec<ModelInfo>, 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()
+1
View File
@@ -122,6 +122,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
| EventMsg::CollabCloseEnd(_)
| EventMsg::CollabResumeEnd(_) => Some(EventPersistenceMode::Extended),
EventMsg::Warning(_)
| EventMsg::ModelReroute(_)
| EventMsg::AgentMessageDelta(_)
| EventMsg::AgentReasoningDelta(_)
| EventMsg::AgentReasoningRawContentDelta(_)
+81 -143
View File
@@ -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<String, RemoteSkillFileRange>,
}
#[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<RemoteSkill>,
@@ -53,36 +69,40 @@ struct RemoteSkill {
description: String,
}
#[derive(Debug, Deserialize)]
struct RemoteSkillsDownloadResponse {
hazelnuts: Vec<RemoteSkillDownloadPayload>,
}
#[derive(Debug, Deserialize)]
struct RemoteSkillDownloadPayload {
id: String,
name: String,
#[serde(rename = "base_sediment_id")]
base_sediment_id: String,
files: HashMap<String, RemoteSkillFileRangePayload>,
}
#[derive(Debug, Deserialize)]
struct RemoteSkillFileRangePayload {
start: u64,
length: u64,
}
pub async fn list_remote_skills(config: &Config) -> Result<Vec<RemoteSkillSummary>> {
pub async fn list_remote_skills(
config: &Config,
auth: Option<&CodexAuth>,
hazelnut_scope: RemoteSkillHazelnutScope,
product_surface: RemoteSkillProductSurface,
enabled: Option<bool>,
) -> Result<Vec<RemoteSkillSummary>> {
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<Vec<RemoteSkillSummar
.collect())
}
pub async fn download_remote_skill(
pub async fn export_remote_skill(
config: &Config,
auth: Option<&CodexAuth>,
hazelnut_id: &str,
is_preload: bool,
) -> Result<RemoteSkillDownloadResult> {
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::<HashSet<String>>();
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<PathBuf> {
Ok(base.join(path))
}
fn validate_dir_name_format(name: &str) -> Option<String> {
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<u8>,
output_dir: &Path,
allowed_files: &HashSet<String>,
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<String
Some(trimmed.to_string())
}
}
async fn fetch_remote_skill(config: &Config, hazelnut_id: &str) -> Result<RemoteSkillDownload> {
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(),
})
}
+1 -2
View File
@@ -230,12 +230,11 @@ impl ThreadManager {
pub async fn list_models(
&self,
config: &Config,
refresh_strategy: crate::models_manager::manager::RefreshStrategy,
) -> Vec<ModelPreset> {
self.state
.models_manager
.list_models(config, refresh_strategy)
.list_models(refresh_strategy)
.await
}
@@ -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,
-4
View File
@@ -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)
@@ -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(),
+2 -6
View File
@@ -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()
-1
View File
@@ -36,7 +36,6 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec<String> {
.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)
+5 -10
View File
@@ -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!(
@@ -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);
+4 -146
View File
@@ -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<ModelsManager>,
slug: &str,
config: &codex_core::config::Config,
) {
async fn wait_for_model_available(manager: &Arc<ModelsManager>, 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;
}
@@ -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());
+15 -54
View File
@@ -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<ModelsManager>,
slug: &str,
config: &Config,
) -> ModelPreset {
async fn wait_for_model_available(manager: &Arc<ModelsManager>, 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;
+1 -4
View File
@@ -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);
@@ -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(_))
-1
View File
@@ -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)
@@ -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,
+1 -1
View File
@@ -374,7 +374,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> 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.
@@ -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;
@@ -507,7 +507,6 @@ request_max_retries = 0
stream_max_retries = 0
[features]
remote_models = false
"#
),
)
+5
View File
@@ -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:
+45
View File
@@ -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:");
+30
View File
@@ -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())
+43 -5
View File
@@ -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<bool>,
},
/// 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 {
+7 -4
View File
@@ -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(&current_cwd, &resume_cwd) {
match self.rebuild_config_for_cwd(resume_cwd).await {
@@ -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,
+5 -4
View File
@@ -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<ModelPreset> {
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<ModelPreset> = match self.models_manager.try_list_models(&self.config) {
let presets: Vec<ModelPreset> = 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
+46 -1
View File
@@ -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,
+28 -4
View File
@@ -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<CwdSelection> {
) -> Result<CwdPromptOutcome> {
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<CwdSelection>,
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<CwdSelection> {
@@ -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());
}
}
+34 -9
View File
@@ -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, &current_cwd, path, action, allow_prompt)
match resolve_cwd_for_resume_or_fork(&mut tui, &current_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<PathBuf>),
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<Option<PathBuf>> {
) -> color_eyre::Result<ResolveCwdOutcome> {
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(