Files
openclaw/scripts/check-codex-app-server-protocol.ts
2026-07-07 15:48:08 +01:00

271 lines
10 KiB
TypeScript

import { spawnSync } from "node:child_process";
// Check Codex App Server Protocol script supports OpenClaw repository automation.
import fs from "node:fs/promises";
import path from "node:path";
import {
generateExperimentalCodexAppServerProtocolSource,
normalizeCodexAppServerProtocolJsonText,
selectedCodexAppServerJsonSchemas,
} from "./lib/codex-app-server-protocol-source.js";
const generatedRoot = path.resolve(
process.cwd(),
"extensions/codex/src/app-server/protocol-generated",
);
const checks: Array<{ file: string; snippets: string[] }> = [
{
file: "ServerRequest.ts",
snippets: [
'"item/commandExecution/requestApproval"',
'"item/fileChange/requestApproval"',
'"item/permissions/requestApproval"',
'"item/tool/call"',
],
},
{
file: "v2/ThreadItem.ts",
snippets: [
'type: "contextCompaction"',
'type: "dynamicToolCall"',
'type: "commandExecution"',
'type: "mcpToolCall"',
],
},
{
file: "v2/DynamicToolSpec.ts",
snippets: [
'"function"',
"& DynamicToolFunctionSpec",
'"namespace"',
"& DynamicToolNamespaceSpec",
],
},
{
file: "v2/DynamicToolFunctionSpec.ts",
snippets: ["name: string", "description: string", "inputSchema: JsonValue"],
},
{
file: "v2/DynamicToolNamespaceSpec.ts",
snippets: ["name: string", "description: string", "tools: Array<DynamicToolNamespaceTool>"],
},
{
file: "v2/CommandExecutionApprovalDecision.ts",
snippets: ['"accept"', '"acceptForSession"', '"decline"', '"cancel"'],
},
{
file: "v2/Account.ts",
snippets: ['type: "apiKey"', 'type: "chatgpt"', 'type: "amazonBedrock"'],
},
{
file: "v2/ThreadStartParams.ts",
snippets: [
"permissions?: string | null",
"dynamicTools?: Array<DynamicToolSpec> | null",
"experimentalRawEvents",
],
},
{
file: "v2/TurnStartParams.ts",
snippets: ["permissions?: string | null", "serviceTier?: string | null"],
},
{
file: "ReviewDecision.ts",
snippets: ['"approved"', '"approved_for_session"', '"denied"', '"abort"'],
},
{
file: "v2/PlanDeltaNotification.ts",
snippets: ["itemId: string", "delta: string"],
},
{
file: "v2/TurnPlanUpdatedNotification.ts",
snippets: ["explanation: string | null", "plan: Array<TurnPlanStep>"],
},
];
const failures: string[] = [];
await main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
async function main(): Promise<void> {
const source = await generateExperimentalCodexAppServerProtocolSource();
try {
await compareGeneratedProtocolMirror(source.jsonRoot);
await checkMaintainedProtocolTypes(source.typescriptRoot);
for (const check of checks) {
const filePath = path.join(source.typescriptRoot, check.file);
let text: string;
try {
text = await fs.readFile(filePath, "utf8");
} catch (error) {
failures.push(`${check.file}: missing (${String(error)})`);
continue;
}
for (const snippet of check.snippets) {
if (!text.includes(snippet)) {
failures.push(`${check.file}: missing ${snippet}`);
}
}
}
} finally {
await source.cleanup();
}
if (failures.length > 0) {
console.error("Codex app-server generated protocol drift:");
for (const failure of failures) {
console.error(`- ${failure}`);
}
console.error(
`Run \`pnpm codex-app-server:protocol:sync\` after refreshing the Codex checkout at ${source.codexRepo}.`,
);
process.exit(1);
}
console.log(
`Codex app-server generated protocol matches OpenClaw bridge assumptions: ${source.codexRepo}`,
);
}
async function checkMaintainedProtocolTypes(sourceRoot: string): Promise<void> {
// Raw requests go to Codex; raw responses flow into OpenClaw. Keep the
// assignability direction explicit so the probe permits deliberate projections.
const probePath = path.join(sourceRoot, "openclaw-protocol-compatibility.ts");
const protocolPath = path.resolve(process.cwd(), "extensions/codex/src/app-server/protocol.ts");
const protocolImport = relativeTypeScriptImport(probePath, protocolPath);
const generatedImport = (file: string) =>
relativeTypeScriptImport(probePath, path.join(sourceRoot, file));
const probe = `
import type {
CodexDynamicToolSpec,
CodexDynamicToolCallParams,
CodexErrorNotification,
CodexModelListResponse,
CodexThreadForkParams,
CodexThreadForkResponse,
CodexThreadResumeParams,
CodexThreadResumeResponse,
CodexThreadStartParams,
CodexThreadStartResponse,
CodexTurnEnvironmentParams,
CodexTurnInterruptParams,
CodexTurnStartParams,
} from ${JSON.stringify(protocolImport)};
import type { DynamicToolCallParams } from ${JSON.stringify(generatedImport("v2/DynamicToolCallParams.ts"))};
import type { DynamicToolSpec } from ${JSON.stringify(generatedImport("v2/DynamicToolSpec.ts"))};
import type { ErrorNotification } from ${JSON.stringify(generatedImport("v2/ErrorNotification.ts"))};
import type { ModelListResponse } from ${JSON.stringify(generatedImport("v2/ModelListResponse.ts"))};
import type { ThreadForkParams } from ${JSON.stringify(generatedImport("v2/ThreadForkParams.ts"))};
import type { ThreadForkResponse } from ${JSON.stringify(generatedImport("v2/ThreadForkResponse.ts"))};
import type { ThreadResumeParams } from ${JSON.stringify(generatedImport("v2/ThreadResumeParams.ts"))};
import type { ThreadResumeResponse } from ${JSON.stringify(generatedImport("v2/ThreadResumeResponse.ts"))};
import type { ThreadStartParams } from ${JSON.stringify(generatedImport("v2/ThreadStartParams.ts"))};
import type { ThreadStartResponse } from ${JSON.stringify(generatedImport("v2/ThreadStartResponse.ts"))};
import type { TurnEnvironmentParams } from ${JSON.stringify(generatedImport("v2/TurnEnvironmentParams.ts"))};
import type { TurnInterruptParams } from ${JSON.stringify(generatedImport("v2/TurnInterruptParams.ts"))};
import type { TurnStartParams } from ${JSON.stringify(generatedImport("v2/TurnStartParams.ts"))};
declare const openClawDynamicToolSpec: CodexDynamicToolSpec;
const generatedDynamicToolSpec: DynamicToolSpec = openClawDynamicToolSpec;
declare const openClawTurnEnvironmentParams: CodexTurnEnvironmentParams;
const generatedTurnEnvironmentParams: TurnEnvironmentParams = openClawTurnEnvironmentParams;
declare const openClawThreadStartParams: CodexThreadStartParams;
const generatedThreadStartParams: ThreadStartParams = openClawThreadStartParams;
declare const openClawThreadResumeParams: CodexThreadResumeParams;
const generatedThreadResumeParams: ThreadResumeParams = openClawThreadResumeParams;
declare const openClawThreadForkParams: CodexThreadForkParams;
const generatedThreadForkParams: ThreadForkParams = openClawThreadForkParams;
declare const openClawTurnInterruptParams: CodexTurnInterruptParams;
const generatedTurnInterruptParams: TurnInterruptParams = openClawTurnInterruptParams;
declare const openClawTurnStartParams: CodexTurnStartParams;
const generatedTurnStartParams: TurnStartParams = openClawTurnStartParams;
declare const generatedDynamicToolCallParams: Omit<DynamicToolCallParams, "arguments">;
const openClawDynamicToolCallParams: Omit<CodexDynamicToolCallParams, "arguments"> =
generatedDynamicToolCallParams;
declare const generatedErrorNotification: ErrorNotification;
const openClawErrorNotification: CodexErrorNotification = generatedErrorNotification;
declare const generatedModelListResponse: ModelListResponse;
const openClawModelListResponse: CodexModelListResponse = generatedModelListResponse;
// Thread and turn bodies are normalized behind checked-in JSON schemas. Their
// raw generated shapes must not be confused with the projector-facing types.
declare const generatedThreadForkResponse: Omit<ThreadForkResponse, "thread">;
const openClawThreadForkResponse: Omit<CodexThreadForkResponse, "thread"> =
generatedThreadForkResponse;
declare const generatedThreadResumeResponse: Omit<ThreadResumeResponse, "thread">;
const openClawThreadResumeResponse: Omit<CodexThreadResumeResponse, "thread"> =
generatedThreadResumeResponse;
declare const generatedThreadStartResponse: Omit<ThreadStartResponse, "thread">;
const openClawThreadStartResponse: Omit<CodexThreadStartResponse, "thread"> =
generatedThreadStartResponse;
export {};
`;
await fs.writeFile(probePath, probe);
const result = spawnSync(
process.execPath,
[
"scripts/run-tsgo.mjs",
"--ignoreConfig",
"--noEmit",
"--allowImportingTsExtensions",
"--strict",
"--skipLibCheck",
"--module",
"nodenext",
"--moduleResolution",
"nodenext",
probePath,
],
{ cwd: process.cwd(), encoding: "utf8" },
);
if (result.error) {
failures.push(`maintained protocol types: failed to start tsgo (${result.error.message})`);
return;
}
if (result.status !== 0) {
const output = `${result.stdout}${result.stderr}`.trim();
failures.push(`maintained protocol types differ from generated Codex types\n${output}`);
}
}
function relativeTypeScriptImport(fromFile: string, toFile: string): string {
const relative = path.relative(path.dirname(fromFile), toFile).replaceAll(path.sep, "/");
return relative.startsWith(".") ? relative : `./${relative}`;
}
async function compareGeneratedProtocolMirror(sourceJsonRoot: string): Promise<void> {
for (const schema of selectedCodexAppServerJsonSchemas) {
const sourcePath = path.join(sourceJsonRoot, schema);
const targetPath = path.join(generatedRoot, "json", schema);
let sourceValue: string;
let target: string;
try {
sourceValue = await fs.readFile(sourcePath, "utf8");
} catch (error) {
failures.push(
`protocol-generated/json/${schema}: missing upstream schema (${String(error)})`,
);
continue;
}
try {
target = await fs.readFile(targetPath, "utf8");
} catch (error) {
failures.push(`protocol-generated/json/${schema}: missing local schema (${String(error)})`);
continue;
}
if (normalizeJsonSchema(sourceValue) !== normalizeJsonSchema(target)) {
failures.push(`protocol-generated/json/${schema}: differs from source schema`);
}
}
}
function normalizeJsonSchema(sourceLocal: string): string {
return normalizeCodexAppServerProtocolJsonText(sourceLocal);
}