From 5097749de32c5afd2be80c4464c532020cc97a39 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Mon, 8 Jun 2026 18:59:51 -0700 Subject: [PATCH] fix: canonicalize codex protocol JSON assets (#91507) --- .../json/v2/ErrorNotification.json | 30 +++---- .../json/v2/ThreadResumeResponse.json | 84 +++++++++---------- .../json/v2/ThreadStartResponse.json | 84 +++++++++---------- .../json/v2/TurnCompletedNotification.json | 30 +++---- .../json/v2/TurnStartResponse.json | 30 +++---- scripts/check-codex-app-server-protocol.ts | 3 +- .../lib/codex-app-server-protocol-source.ts | 80 ++++++++++++++++++ scripts/sync-codex-app-server-protocol.ts | 3 +- .../codex-app-server-protocol-source.test.ts | 77 +++++++++++++++++ 9 files changed, 290 insertions(+), 131 deletions(-) diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json b/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json index 4ab0aaa341d6..9fb2c42ae1bf 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json @@ -4,21 +4,6 @@ "CodexErrorInfo": { "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", "oneOf": [ - { - "enum": [ - "contextWindowExceeded", - "usageLimitExceeded", - "serverOverloaded", - "cyberPolicy", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -135,6 +120,21 @@ ], "title": "ActiveTurnNotSteerableCodexErrorInfo", "type": "object" + }, + { + "enum": [ + "contextWindowExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" } ] }, diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json b/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json index 038995bf27e3..9a4881c87c5c 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json @@ -39,15 +39,6 @@ }, "AskForApproval": { "oneOf": [ - { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -84,6 +75,15 @@ ], "title": "GranularAskForApproval", "type": "object" + }, + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" } ] }, @@ -109,21 +109,6 @@ "CodexErrorInfo": { "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", "oneOf": [ - { - "enum": [ - "contextWindowExceeded", - "usageLimitExceeded", - "serverOverloaded", - "cyberPolicy", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -240,6 +225,21 @@ ], "title": "ActiveTurnNotSteerableCodexErrorInfo", "type": "object" + }, + { + "enum": [ + "contextWindowExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" } ] }, @@ -830,16 +830,6 @@ }, "SessionSource": { "oneOf": [ - { - "enum": [ - "cli", - "vscode", - "exec", - "appServer", - "unknown" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -865,19 +855,21 @@ ], "title": "SubAgentSessionSource", "type": "object" + }, + { + "enum": [ + "cli", + "vscode", + "exec", + "appServer", + "unknown" + ], + "type": "string" } ] }, "SubAgentSource": { "oneOf": [ - { - "enum": [ - "review", - "compact", - "memory_consolidation" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -941,6 +933,14 @@ ], "title": "OtherSubAgentSource", "type": "object" + }, + { + "enum": [ + "review", + "compact", + "memory_consolidation" + ], + "type": "string" } ] }, diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json b/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json index aa718b0b664a..0cc1438f39a2 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json @@ -39,15 +39,6 @@ }, "AskForApproval": { "oneOf": [ - { - "enum": [ - "untrusted", - "on-failure", - "on-request", - "never" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -84,6 +75,15 @@ ], "title": "GranularAskForApproval", "type": "object" + }, + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" } ] }, @@ -109,21 +109,6 @@ "CodexErrorInfo": { "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", "oneOf": [ - { - "enum": [ - "contextWindowExceeded", - "usageLimitExceeded", - "serverOverloaded", - "cyberPolicy", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -240,6 +225,21 @@ ], "title": "ActiveTurnNotSteerableCodexErrorInfo", "type": "object" + }, + { + "enum": [ + "contextWindowExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" } ] }, @@ -830,16 +830,6 @@ }, "SessionSource": { "oneOf": [ - { - "enum": [ - "cli", - "vscode", - "exec", - "appServer", - "unknown" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -865,19 +855,21 @@ ], "title": "SubAgentSessionSource", "type": "object" + }, + { + "enum": [ + "cli", + "vscode", + "exec", + "appServer", + "unknown" + ], + "type": "string" } ] }, "SubAgentSource": { "oneOf": [ - { - "enum": [ - "review", - "compact", - "memory_consolidation" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -941,6 +933,14 @@ ], "title": "OtherSubAgentSource", "type": "object" + }, + { + "enum": [ + "review", + "compact", + "memory_consolidation" + ], + "type": "string" } ] }, diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json b/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json index 563f3f59d8c3..08f8a08dbc84 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json @@ -27,21 +27,6 @@ "CodexErrorInfo": { "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", "oneOf": [ - { - "enum": [ - "contextWindowExceeded", - "usageLimitExceeded", - "serverOverloaded", - "cyberPolicy", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -158,6 +143,21 @@ ], "title": "ActiveTurnNotSteerableCodexErrorInfo", "type": "object" + }, + { + "enum": [ + "contextWindowExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" } ] }, diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json b/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json index 457528d3b6b0..768fde278dda 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json @@ -27,21 +27,6 @@ "CodexErrorInfo": { "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", "oneOf": [ - { - "enum": [ - "contextWindowExceeded", - "usageLimitExceeded", - "serverOverloaded", - "cyberPolicy", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other" - ], - "type": "string" - }, { "additionalProperties": false, "properties": { @@ -158,6 +143,21 @@ ], "title": "ActiveTurnNotSteerableCodexErrorInfo", "type": "object" + }, + { + "enum": [ + "contextWindowExceeded", + "usageLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" } ] }, diff --git a/scripts/check-codex-app-server-protocol.ts b/scripts/check-codex-app-server-protocol.ts index 2fb5884f13af..d0fa520c6778 100644 --- a/scripts/check-codex-app-server-protocol.ts +++ b/scripts/check-codex-app-server-protocol.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { generateExperimentalCodexAppServerProtocolSource, + normalizeCodexAppServerProtocolJsonText, selectedCodexAppServerJsonSchemas, } from "./lib/codex-app-server-protocol-source.js"; @@ -142,5 +143,5 @@ async function compareGeneratedProtocolMirror(sourceJsonRoot: string): Promise = {}; + const entries = Object.entries(value) + .map(([key, child]) => [key, canonicalizeCodexAppServerProtocolJson(child)] as const) + .toSorted(([left], [right]) => { + if (left < right) { + return -1; + } + if (left > right) { + return 1; + } + return 0; + }); + for (const [key, child] of entries) { + sorted[key] = child; + } + return sorted; +} + +export function normalizeCodexAppServerProtocolJsonText(text: string): string { + return JSON.stringify(canonicalizeCodexAppServerProtocolJson(JSON.parse(text))); +} + +export function formatCodexAppServerProtocolJsonText(text: string): string { + return `${JSON.stringify(canonicalizeCodexAppServerProtocolJson(JSON.parse(text)), null, 2)}\n`; +} + +function sortCodexProtocolJsonArrayByType(items: unknown[]): unknown[] { + if (!items.every(isPlainObject)) { + return items; + } + + const typed = items + .map((item, index) => ({ index, item, type: stringRecordValue(item, "type") })) + .filter( + (entry): entry is { index: number; item: Record; type: string } => + entry.type !== undefined, + ); + if (typed.length < 2) { + return items; + } + + const sortedTyped = typed.toSorted((left, right) => { + if (left.type < right.type) { + return -1; + } + if (left.type > right.type) { + return 1; + } + return left.index - right.index; + }); + const sortedByOriginalIndex = new Map( + typed.map((entry, index) => [entry.index, sortedTyped[index]?.item]), + ); + + return items.map((item, index) => sortedByOriginalIndex.get(index) ?? item); +} + +function stringRecordValue(record: Record, key: string): string | undefined { + const value = record[key]; + return typeof value === "string" ? value : undefined; +} + +function isPlainObject(value: unknown): value is Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} diff --git a/scripts/sync-codex-app-server-protocol.ts b/scripts/sync-codex-app-server-protocol.ts index 4c612e226be7..0871552b315a 100644 --- a/scripts/sync-codex-app-server-protocol.ts +++ b/scripts/sync-codex-app-server-protocol.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { + formatCodexAppServerProtocolJsonText, generateExperimentalCodexAppServerProtocolSource, selectedCodexAppServerJsonSchemas, } from "./lib/codex-app-server-protocol-source.js"; @@ -27,7 +28,7 @@ async function main(): Promise { const schemaSource = await fs.readFile(path.join(source.jsonRoot, schema), "utf8"); await fs.writeFile( path.join(targetRoot, "json", schema), - `${JSON.stringify(JSON.parse(schemaSource), null, 2)}\n`, + formatCodexAppServerProtocolJsonText(schemaSource), ); } } finally { diff --git a/test/scripts/codex-app-server-protocol-source.test.ts b/test/scripts/codex-app-server-protocol-source.test.ts index 9198ac41364f..4118bf7f2a35 100644 --- a/test/scripts/codex-app-server-protocol-source.test.ts +++ b/test/scripts/codex-app-server-protocol-source.test.ts @@ -4,6 +4,8 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { buildCodexProtocolExportArgs, + canonicalizeCodexAppServerProtocolJson, + formatCodexAppServerProtocolJsonText, resolveCodexAppServerProtocolSource, resolveCodexProtocolCargoTargetDir, resolveCodexProtocolMinFreeBytes, @@ -162,6 +164,81 @@ describe("codex app-server protocol source resolver", () => { }); }); +describe("Codex app-server protocol JSON canonicalizer", () => { + it("sorts object keys recursively before formatting", () => { + const source = JSON.stringify({ + z: { + d: 1, + b: { + y: 2, + x: 3, + }, + }, + a: [ + { + z: 4, + a: { + c: 5, + b: 6, + }, + }, + ], + }); + + expect(formatCodexAppServerProtocolJsonText(source)).toBe(`{ + "a": [ + { + "a": { + "b": 6, + "c": 5 + }, + "z": 4 + } + ], + "z": { + "b": { + "x": 3, + "y": 2 + }, + "d": 1 + } +} +`); + }); + + it("sorts arrays only when plain object items expose top-level type values", () => { + expect( + canonicalizeCodexAppServerProtocolJson({ + enum: ["z", "a"], + mixed: [{ type: "b" }, "item", { type: "a" }], + oneOf: [ + { title: "Second", z: true }, + { a: true, title: "First" }, + ], + required: ["z", "a"], + typed: [ + { type: "beta", z: 1 }, + { type: "alpha", z: 2 }, + { type: "beta", z: 3 }, + ], + }), + ).toEqual({ + enum: ["z", "a"], + mixed: [{ type: "b" }, "item", { type: "a" }], + oneOf: [ + { title: "Second", z: true }, + { a: true, title: "First" }, + ], + required: ["z", "a"], + typed: [ + { type: "alpha", z: 2 }, + { type: "beta", z: 1 }, + { type: "beta", z: 3 }, + ], + }); + }); +}); + function createProtocolSchema(codexRepo: string): void { fs.mkdirSync(path.join(codexRepo, "codex-rs/app-server-protocol/schema/typescript"), { recursive: true,