From 90469d0a230a4cbc05fa8e457a4341c322c75feb Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Fri, 6 Mar 2026 17:33:46 -0800 Subject: [PATCH] feat(app-server-protocol): address naming conflicts in json schema exporter (#13819) This fixes a schema export bug where two different `WebSearchAction` types were getting merged under the same name in the app-server v2 JSON schema bundle. The problem was that v2 thread items use the app-server API's `WebSearchAction` with camelCase variants like `openPage`, while `ThreadResumeParams.history` and `RawResponseItemCompletedNotification.item` pull in the upstream `ResponseItem` graph, which uses the Responses API snake_case shape like `open_page`. During bundle generation we were flattening nested definitions into the v2 namespace by plain name, so the later definition could silently overwrite the earlier one. That meant clients generating code from the bundled schema could end up with the wrong `WebSearchAction` definition for v2 thread history. In practice this shows up on web search items reconstructed from rollout files with persisted extended history. This change does two things: - Gives the upstream Responses API schema a distinct JSON schema name: `ResponsesApiWebSearchAction` - Makes namespace-level schema definition collisions fail loudly instead of silently overwriting --- .../schema/json/ClientRequest.json | 204 ++++++++--------- .../schema/json/EventMsg.json | 210 +++++++++--------- .../codex_app_server_protocol.schemas.json | 111 ++++++++- .../codex_app_server_protocol.v2.schemas.json | 111 ++++++++- .../RawResponseItemCompletedNotification.json | 20 +- .../schema/json/v2/ThreadResumeParams.json | 50 ++--- codex-rs/app-server-protocol/src/export.rs | 73 +++++- codex-rs/protocol/src/models.rs | 1 + 8 files changed, 526 insertions(+), 254 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index cbcf8b886f..c181b8bcf2 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1558,7 +1558,7 @@ "action": { "anyOf": [ { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, { "type": "null" @@ -1684,6 +1684,107 @@ } ] }, + "ResponsesApiWebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SearchResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "open_page" + ], + "title": "OpenPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "OpenPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "find_in_page" + ], + "title": "FindInPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "FindInPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "other" + ], + "title": "OtherResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "OtherResponsesApiWebSearchAction", + "type": "object" + } + ] + }, "ReviewDelivery": { "enum": [ "inline", @@ -2930,107 +3031,6 @@ } ] }, - "WebSearchAction": { - "oneOf": [ - { - "properties": { - "queries": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "query": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "search" - ], - "title": "SearchWebSearchActionType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "SearchWebSearchAction", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "open_page" - ], - "title": "OpenPageWebSearchActionType", - "type": "string" - }, - "url": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "type" - ], - "title": "OpenPageWebSearchAction", - "type": "object" - }, - { - "properties": { - "pattern": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "find_in_page" - ], - "title": "FindInPageWebSearchActionType", - "type": "string" - }, - "url": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "type" - ], - "title": "FindInPageWebSearchAction", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "other" - ], - "title": "OtherWebSearchActionType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "OtherWebSearchAction", - "type": "object" - } - ] - }, "WindowsSandboxSetupMode": { "enum": [ "elevated", diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index e17940056b..70ddde458e 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -1414,7 +1414,7 @@ { "properties": { "action": { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, "call_id": { "type": "string" @@ -5072,7 +5072,7 @@ "action": { "anyOf": [ { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, { "type": "null" @@ -5198,6 +5198,107 @@ } ] }, + "ResponsesApiWebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SearchResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "open_page" + ], + "title": "OpenPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "OpenPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "find_in_page" + ], + "title": "FindInPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "FindInPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "other" + ], + "title": "OtherResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "OtherResponsesApiWebSearchAction", + "type": "object" + } + ] + }, "Result_of_CallToolResult_or_String": { "oneOf": [ { @@ -6095,7 +6196,7 @@ { "properties": { "action": { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, "id": { "type": "string" @@ -6306,107 +6407,6 @@ "type": "object" } ] - }, - "WebSearchAction": { - "oneOf": [ - { - "properties": { - "queries": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "query": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "search" - ], - "title": "SearchWebSearchActionType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "SearchWebSearchAction", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "open_page" - ], - "title": "OpenPageWebSearchActionType", - "type": "string" - }, - "url": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "type" - ], - "title": "OpenPageWebSearchAction", - "type": "object" - }, - { - "properties": { - "pattern": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "find_in_page" - ], - "title": "FindInPageWebSearchActionType", - "type": "string" - }, - "url": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "type" - ], - "title": "FindInPageWebSearchAction", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "other" - ], - "title": "OtherWebSearchActionType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "OtherWebSearchAction", - "type": "object" - } - ] } }, "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", @@ -7226,7 +7226,7 @@ { "properties": { "action": { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, "call_id": { "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 3cc179456c..495339982b 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -2730,7 +2730,7 @@ { "properties": { "action": { - "$ref": "#/definitions/v2/WebSearchAction" + "$ref": "#/definitions/v2/ResponsesApiWebSearchAction" }, "call_id": { "type": "string" @@ -8236,7 +8236,7 @@ { "properties": { "action": { - "$ref": "#/definitions/v2/WebSearchAction" + "$ref": "#/definitions/v2/ResponsesApiWebSearchAction" }, "id": { "type": "string" @@ -13191,7 +13191,7 @@ "action": { "anyOf": [ { - "$ref": "#/definitions/v2/WebSearchAction" + "$ref": "#/definitions/v2/ResponsesApiWebSearchAction" }, { "type": "null" @@ -13317,6 +13317,107 @@ } ] }, + "ResponsesApiWebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SearchResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "open_page" + ], + "title": "OpenPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "OpenPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "find_in_page" + ], + "title": "FindInPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "FindInPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "other" + ], + "title": "OtherResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "OtherResponsesApiWebSearchAction", + "type": "object" + } + ] + }, "ReviewDelivery": { "enum": [ "inline", @@ -16722,7 +16823,7 @@ "properties": { "type": { "enum": [ - "open_page" + "openPage" ], "title": "OpenPageWebSearchActionType", "type": "string" @@ -16750,7 +16851,7 @@ }, "type": { "enum": [ - "find_in_page" + "findInPage" ], "title": "FindInPageWebSearchActionType", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 76f1d19d93..cbc28994bc 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -4543,7 +4543,7 @@ { "properties": { "action": { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, "call_id": { "type": "string" @@ -10052,7 +10052,7 @@ "action": { "anyOf": [ { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, { "type": "null" @@ -10178,6 +10178,107 @@ } ] }, + "ResponsesApiWebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SearchResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "open_page" + ], + "title": "OpenPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "OpenPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "find_in_page" + ], + "title": "FindInPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "FindInPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "other" + ], + "title": "OtherResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "OtherResponsesApiWebSearchAction", + "type": "object" + } + ] + }, "Result_of_CallToolResult_or_String": { "oneOf": [ { @@ -14485,7 +14586,7 @@ { "properties": { "action": { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, "id": { "type": "string" @@ -14978,7 +15079,7 @@ "properties": { "type": { "enum": [ - "open_page" + "openPage" ], "title": "OpenPageWebSearchActionType", "type": "string" @@ -15006,7 +15107,7 @@ }, "type": { "enum": [ - "find_in_page" + "findInPage" ], "title": "FindInPageWebSearchActionType", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index cb14542607..94a6c8ba77 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -607,7 +607,7 @@ "action": { "anyOf": [ { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, { "type": "null" @@ -733,7 +733,7 @@ } ] }, - "WebSearchAction": { + "ResponsesApiWebSearchAction": { "oneOf": [ { "properties": { @@ -756,14 +756,14 @@ "enum": [ "search" ], - "title": "SearchWebSearchActionType", + "title": "SearchResponsesApiWebSearchActionType", "type": "string" } }, "required": [ "type" ], - "title": "SearchWebSearchAction", + "title": "SearchResponsesApiWebSearchAction", "type": "object" }, { @@ -772,7 +772,7 @@ "enum": [ "open_page" ], - "title": "OpenPageWebSearchActionType", + "title": "OpenPageResponsesApiWebSearchActionType", "type": "string" }, "url": { @@ -785,7 +785,7 @@ "required": [ "type" ], - "title": "OpenPageWebSearchAction", + "title": "OpenPageResponsesApiWebSearchAction", "type": "object" }, { @@ -800,7 +800,7 @@ "enum": [ "find_in_page" ], - "title": "FindInPageWebSearchActionType", + "title": "FindInPageResponsesApiWebSearchActionType", "type": "string" }, "url": { @@ -813,7 +813,7 @@ "required": [ "type" ], - "title": "FindInPageWebSearchAction", + "title": "FindInPageResponsesApiWebSearchAction", "type": "object" }, { @@ -822,14 +822,14 @@ "enum": [ "other" ], - "title": "OtherWebSearchActionType", + "title": "OtherResponsesApiWebSearchActionType", "type": "string" } }, "required": [ "type" ], - "title": "OtherWebSearchAction", + "title": "OtherResponsesApiWebSearchAction", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 3dce8b5df1..75c8a39675 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -657,7 +657,7 @@ "action": { "anyOf": [ { - "$ref": "#/definitions/WebSearchAction" + "$ref": "#/definitions/ResponsesApiWebSearchAction" }, { "type": "null" @@ -783,22 +783,7 @@ } ] }, - "SandboxMode": { - "enum": [ - "read-only", - "workspace-write", - "danger-full-access" - ], - "type": "string" - }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, - "WebSearchAction": { + "ResponsesApiWebSearchAction": { "oneOf": [ { "properties": { @@ -821,14 +806,14 @@ "enum": [ "search" ], - "title": "SearchWebSearchActionType", + "title": "SearchResponsesApiWebSearchActionType", "type": "string" } }, "required": [ "type" ], - "title": "SearchWebSearchAction", + "title": "SearchResponsesApiWebSearchAction", "type": "object" }, { @@ -837,7 +822,7 @@ "enum": [ "open_page" ], - "title": "OpenPageWebSearchActionType", + "title": "OpenPageResponsesApiWebSearchActionType", "type": "string" }, "url": { @@ -850,7 +835,7 @@ "required": [ "type" ], - "title": "OpenPageWebSearchAction", + "title": "OpenPageResponsesApiWebSearchAction", "type": "object" }, { @@ -865,7 +850,7 @@ "enum": [ "find_in_page" ], - "title": "FindInPageWebSearchActionType", + "title": "FindInPageResponsesApiWebSearchActionType", "type": "string" }, "url": { @@ -878,7 +863,7 @@ "required": [ "type" ], - "title": "FindInPageWebSearchAction", + "title": "FindInPageResponsesApiWebSearchAction", "type": "object" }, { @@ -887,17 +872,32 @@ "enum": [ "other" ], - "title": "OtherWebSearchActionType", + "title": "OtherResponsesApiWebSearchActionType", "type": "string" } }, "required": [ "type" ], - "title": "OtherWebSearchAction", + "title": "OtherResponsesApiWebSearchAction", "type": "object" } ] + }, + "SandboxMode": { + "enum": [ + "read-only", + "workspace-write", + "danger-full-access" + ], + "type": "string" + }, + "ServiceTier": { + "enum": [ + "fast", + "flex" + ], + "type": "string" } }, "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index aad5ab7233..ba151046a1 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -1154,13 +1154,40 @@ fn insert_into_namespace( .or_insert_with(|| Value::Object(Map::new())); match entry { Value::Object(map) => { - map.insert(name, schema); - Ok(()) + insert_definition(map, name, schema, &format!("namespace `{namespace}`")) } _ => Err(anyhow!("expected namespace {namespace} to be an object")), } } +fn insert_definition( + definitions: &mut Map, + name: String, + schema: Value, + location: &str, +) -> Result<()> { + if let Some(existing) = definitions.get(&name) { + if existing == &schema { + return Ok(()); + } + + let existing_title = existing + .get("title") + .and_then(Value::as_str) + .unwrap_or(""); + let new_title = schema + .get("title") + .and_then(Value::as_str) + .unwrap_or(""); + return Err(anyhow!( + "schema definition collision in {location}: {name} (existing title: {existing_title}, new title: {new_title}); use #[schemars(rename = \"...\")] to rename one of the conflicting schema definitions" + )); + } + + definitions.insert(name, schema); + Ok(()) +} + fn write_json_schema_with_return(out_dir: &Path, name: &str) -> Result where T: JsonSchema, @@ -2291,6 +2318,48 @@ mod tests { Ok(()) } + #[test] + fn build_schema_bundle_rejects_conflicting_duplicate_definitions() { + let err = build_schema_bundle(vec![ + GeneratedSchema { + namespace: Some("v2".to_string()), + logical_name: "First".to_string(), + in_v1_dir: false, + value: serde_json::json!({ + "title": "First", + "type": "object", + "definitions": { + "Shared": { + "title": "SharedString", + "type": "string" + } + } + }), + }, + GeneratedSchema { + namespace: Some("v2".to_string()), + logical_name: "Second".to_string(), + in_v1_dir: false, + value: serde_json::json!({ + "title": "Second", + "type": "object", + "definitions": { + "Shared": { + "title": "SharedInteger", + "type": "integer" + } + } + }), + }, + ]) + .expect_err("conflicting schema definitions should be rejected"); + + assert_eq!( + err.to_string(), + "schema definition collision in namespace `v2`: Shared (existing title: SharedString, new title: SharedInteger); use #[schemars(rename = \"...\")] to rename one of the conflicting schema definitions" + ); + } + #[test] fn build_flat_v2_schema_keeps_shared_root_schemas_and_dependencies() -> Result<()> { let bundle = serde_json::json!({ diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index f211e4eff0..a4a2aa2cb3 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -879,6 +879,7 @@ pub struct LocalShellExecAction { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] +#[schemars(rename = "ResponsesApiWebSearchAction")] pub enum WebSearchAction { Search { #[serde(default, skip_serializing_if = "Option::is_none")]