fix: canonicalize codex protocol JSON assets (#91507)

This commit is contained in:
Dallin Romney
2026-06-08 18:59:51 -07:00
committed by GitHub
parent 27189b3e74
commit 5097749de3
9 changed files with 290 additions and 131 deletions
@@ -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"
}
]
},
@@ -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"
}
]
},
@@ -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"
}
]
},
@@ -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"
}
]
},
@@ -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"
}
]
},
+2 -1
View File
@@ -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<v
}
function normalizeJsonSchema(sourceLocal: string): string {
return JSON.stringify(JSON.parse(sourceLocal));
return normalizeCodexAppServerProtocolJsonText(sourceLocal);
}
@@ -383,3 +383,83 @@ export function normalizeGeneratedTypeScript(text: string): string {
.replace('export * as v2 from "./v2.js";', 'export * as v2 from "./v2/index.js";')
.replaceAll("| null | null", "| null");
}
export function canonicalizeCodexAppServerProtocolJson(value: unknown): unknown {
if (Array.isArray(value)) {
const items = value.map(canonicalizeCodexAppServerProtocolJson);
return sortCodexProtocolJsonArrayByType(items);
}
if (!isPlainObject(value)) {
return value;
}
const sorted: Record<string, unknown> = {};
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<string, unknown>; 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<string, unknown>, key: string): string | undefined {
const value = record[key];
return typeof value === "string" ? value : undefined;
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
+2 -1
View File
@@ -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<void> {
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 {
@@ -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,