Files
openclaw/scripts/check-codex-app-server-protocol.ts

555 lines
23 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 {
codexAppServerSharedDefinitionsSchema,
compactCodexAppServerProtocolJsonSchemas,
expandCodexAppServerProtocolJsonSchema,
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/AppSummary.ts",
snippets: [
"description: string | null",
"installUrl: string | null",
"category: string | null",
],
},
{
file: "v2/AppsInstalledParams.ts",
snippets: ["threadId?: string | null", "forceRefresh?: boolean"],
},
{
file: "v2/AppsInstalledResponse.ts",
snippets: ["apps: Array<InstalledApp>"],
},
{
file: "v2/AppsReadParams.ts",
snippets: ["appIds: Array<string>", "includeTools?: boolean"],
},
{
file: "v2/AppsReadResponse.ts",
snippets: ["apps: Array<ConnectorMetadata>", "missingAppIds: Array<string>"],
},
{
file: "v2/CommandExecParams.ts",
snippets: [
"command: Array<string>",
"outputBytesCap?: number | null",
"timeoutMs?: number | null",
"env?: { [key in string]?: string | null } | null",
],
},
{
file: "v2/CommandExecResponse.ts",
snippets: ["exitCode: number", "stdout: string", "stderr: string"],
},
{
file: "v2/ConfigBatchWriteParams.ts",
snippets: [
"edits: Array<ConfigEdit>",
"filePath?: string | null",
"expectedVersion?: string | null",
"reloadUserConfig?: boolean",
],
},
{
file: "v2/ConfigEdit.ts",
snippets: ["keyPath: string", "value: JsonValue", "mergeStrategy: MergeStrategy"],
},
{
file: "v2/ConfigValueWriteParams.ts",
snippets: [
"keyPath: string",
"value: JsonValue",
"mergeStrategy: MergeStrategy",
"filePath?: string | null",
"expectedVersion?: string | null",
],
},
{
file: "v2/ConfigWriteResponse.ts",
snippets: [
"status: WriteStatus",
"version: string",
"filePath: AbsolutePathBuf",
"overriddenMetadata: OverriddenMetadata | null",
],
},
{
file: "v2/InstalledApp.ts",
snippets: ["runtimeName: string | null", "enabled: boolean", "callable: boolean"],
},
{
file: "v2/MarketplaceLoadErrorInfo.ts",
snippets: ["marketplacePath: AbsolutePathBuf", "message: string"],
},
{
file: "v2/MergeStrategy.ts",
snippets: ['"replace"', '"upsert"'],
},
{
file: "v2/OverriddenMetadata.ts",
snippets: [
"message: string",
"overridingLayer: ConfigLayerMetadata",
"effectiveValue: JsonValue",
],
},
{
file: "v2/PluginSummary.ts",
snippets: ["remotePluginId: string | null"],
},
{
file: "v2/PluginListParams.ts",
snippets: ["forceRefetch?: boolean"],
},
{
file: "v2/PluginInstalledParams.ts",
snippets: [
"cwds?: Array<AbsolutePathBuf> | null",
"installSuggestionPluginNames?: Array<string> | null",
],
},
{
file: "v2/PluginInstalledResponse.ts",
snippets: [
"marketplaces: Array<PluginMarketplaceEntry>",
"marketplaceLoadErrors: Array<MarketplaceLoadErrorInfo>",
],
},
{
file: "v2/PluginListResponse.ts",
snippets: [
"marketplaces: Array<PluginMarketplaceEntry>",
"marketplaceLoadErrors: Array<MarketplaceLoadErrorInfo>",
"featuredPluginIds: Array<string>",
],
},
{
file: "v2/PluginReadParams.ts",
snippets: ["pluginName: string"],
},
{
file: "v2/PluginReadResponse.ts",
snippets: ["plugin: PluginDetail"],
},
{
file: "v2/PluginInstallParams.ts",
snippets: ["pluginName: string"],
},
{
file: "v2/PluginInstallResponse.ts",
snippets: ["appsNeedingAuth: Array<AppSummary>"],
},
{
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: "v2/WriteStatus.ts",
snippets: ['"ok"', '"okOverridden"'],
},
{
file: "ReviewDecision.ts",
snippets: ['"approved"', '"approved_for_session"', "denied: { rejection: string }", '"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 {
CodexAppServerRequestParams,
CodexAppServerRequestResult,
CodexConfigEdit,
CodexDynamicToolSpec,
CodexDynamicToolCallParams,
CodexErrorNotification,
CodexGetAccountResponse,
CodexModelListResponse,
CodexThreadForkParams,
CodexThreadForkResponse,
CodexThreadResumeParams,
CodexThreadResumeResponse,
CodexThreadStartParams,
CodexThreadStartResponse,
CodexTurnEnvironmentParams,
CodexTurnStartParams,
v2,
} from ${JSON.stringify(protocolImport)};
import type { AppSummary } from ${JSON.stringify(generatedImport("v2/AppSummary.ts"))};
import type { AppsInstalledParams } from ${JSON.stringify(generatedImport("v2/AppsInstalledParams.ts"))};
import type { AppsInstalledResponse } from ${JSON.stringify(generatedImport("v2/AppsInstalledResponse.ts"))};
import type { AppsListParams } from ${JSON.stringify(generatedImport("v2/AppsListParams.ts"))};
import type { AppsListResponse } from ${JSON.stringify(generatedImport("v2/AppsListResponse.ts"))};
import type { AppsReadParams } from ${JSON.stringify(generatedImport("v2/AppsReadParams.ts"))};
import type { AppsReadResponse } from ${JSON.stringify(generatedImport("v2/AppsReadResponse.ts"))};
import type { CommandExecParams } from ${JSON.stringify(generatedImport("v2/CommandExecParams.ts"))};
import type { CommandExecResponse } from ${JSON.stringify(generatedImport("v2/CommandExecResponse.ts"))};
import type { ConfigBatchWriteParams } from ${JSON.stringify(generatedImport("v2/ConfigBatchWriteParams.ts"))};
import type { ConfigEdit } from ${JSON.stringify(generatedImport("v2/ConfigEdit.ts"))};
import type { ConfigValueWriteParams } from ${JSON.stringify(generatedImport("v2/ConfigValueWriteParams.ts"))};
import type { ConfigWriteResponse } from ${JSON.stringify(generatedImport("v2/ConfigWriteResponse.ts"))};
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 { GetAccountResponse } from ${JSON.stringify(generatedImport("v2/GetAccountResponse.ts"))};
import type { MarketplaceLoadErrorInfo } from ${JSON.stringify(generatedImport("v2/MarketplaceLoadErrorInfo.ts"))};
import type { ModelListResponse } from ${JSON.stringify(generatedImport("v2/ModelListResponse.ts"))};
import type { PluginInstalledParams } from ${JSON.stringify(generatedImport("v2/PluginInstalledParams.ts"))};
import type { PluginInstalledResponse } from ${JSON.stringify(generatedImport("v2/PluginInstalledResponse.ts"))};
import type { PluginInstallParams } from ${JSON.stringify(generatedImport("v2/PluginInstallParams.ts"))};
import type { PluginInstallResponse } from ${JSON.stringify(generatedImport("v2/PluginInstallResponse.ts"))};
import type { PluginListParams } from ${JSON.stringify(generatedImport("v2/PluginListParams.ts"))};
import type { PluginListResponse } from ${JSON.stringify(generatedImport("v2/PluginListResponse.ts"))};
import type { PluginReadParams } from ${JSON.stringify(generatedImport("v2/PluginReadParams.ts"))};
import type { PluginReadResponse } from ${JSON.stringify(generatedImport("v2/PluginReadResponse.ts"))};
import type { ThreadDeleteParams } from ${JSON.stringify(generatedImport("v2/ThreadDeleteParams.ts"))};
import type { ThreadDeleteResponse } from ${JSON.stringify(generatedImport("v2/ThreadDeleteResponse.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 openClawAppsInstalledParams: CodexAppServerRequestParams<"app/installed">;
const generatedAppsInstalledParams: AppsInstalledParams = openClawAppsInstalledParams;
declare const openClawAppsListParams: CodexAppServerRequestParams<"app/list">;
const generatedAppsListParams: AppsListParams = openClawAppsListParams;
declare const openClawAppsReadParams: CodexAppServerRequestParams<"app/read">;
const generatedAppsReadParams: AppsReadParams = openClawAppsReadParams;
declare const openClawAppSummary: v2.AppSummary;
const generatedAppSummary: AppSummary = openClawAppSummary;
declare const openClawCommandExecParams: CodexAppServerRequestParams<"command/exec">;
const generatedCommandExecParams: CommandExecParams = openClawCommandExecParams;
declare const generatedNullableCommandExecParams: CommandExecParams;
const openClawNullableCommandExecParams: CodexAppServerRequestParams<"command/exec"> =
generatedNullableCommandExecParams;
declare const openClawConfigBatchWriteParams: CodexAppServerRequestParams<"config/batchWrite">;
const generatedConfigBatchWriteParams: ConfigBatchWriteParams = openClawConfigBatchWriteParams;
declare const openClawConfigEdit: CodexConfigEdit;
const generatedConfigEdit: ConfigEdit = openClawConfigEdit;
declare const openClawConfigValueWriteParams: CodexAppServerRequestParams<"config/value/write">;
const generatedConfigValueWriteParams: ConfigValueWriteParams = openClawConfigValueWriteParams;
declare const openClawPluginInstalledParams: CodexAppServerRequestParams<"plugin/installed">;
const generatedPluginInstalledParams: PluginInstalledParams = openClawPluginInstalledParams;
declare const openClawPluginInstallParams: CodexAppServerRequestParams<"plugin/install">;
const generatedPluginInstallParams: PluginInstallParams = openClawPluginInstallParams;
declare const openClawPluginListParams: CodexAppServerRequestParams<"plugin/list">;
const generatedPluginListParams: PluginListParams = openClawPluginListParams;
declare const openClawPluginReadParams: CodexAppServerRequestParams<"plugin/read">;
const generatedPluginReadParams: PluginReadParams = openClawPluginReadParams;
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 openClawThreadDeleteParams: CodexAppServerRequestParams<"thread/delete">;
const generatedThreadDeleteParams: ThreadDeleteParams = openClawThreadDeleteParams;
declare const openClawTurnInterruptParams: CodexAppServerRequestParams<"turn/interrupt">;
const generatedTurnInterruptParams: TurnInterruptParams = openClawTurnInterruptParams;
declare const openClawTurnStartParams: CodexTurnStartParams;
const generatedTurnStartParams: TurnStartParams = openClawTurnStartParams;
declare const generatedAppsInstalledResponse: AppsInstalledResponse;
const openClawAppsInstalledResponse: CodexAppServerRequestResult<"app/installed"> =
generatedAppsInstalledResponse;
declare const generatedAppsListResponse: AppsListResponse;
const openClawAppsListResponse: CodexAppServerRequestResult<"app/list"> =
generatedAppsListResponse;
declare const generatedAppsReadResponse: AppsReadResponse;
const openClawAppsReadResponse: CodexAppServerRequestResult<"app/read"> =
generatedAppsReadResponse;
declare const generatedAppSummaryResponse: AppSummary;
const openClawAppSummaryResponse: v2.AppSummary = generatedAppSummaryResponse;
declare const generatedCommandExecResponse: CommandExecResponse;
const openClawCommandExecResponse: CodexAppServerRequestResult<"command/exec"> =
generatedCommandExecResponse;
declare const generatedConfigWriteResponse: ConfigWriteResponse;
const openClawConfigBatchWriteResponse: CodexAppServerRequestResult<"config/batchWrite"> =
generatedConfigWriteResponse;
const openClawConfigValueWriteResponse: CodexAppServerRequestResult<"config/value/write"> =
generatedConfigWriteResponse;
const generatedExactConfigBatchWriteResponse: ConfigWriteResponse =
openClawConfigBatchWriteResponse;
const generatedExactConfigValueWriteResponse: ConfigWriteResponse =
openClawConfigValueWriteResponse;
declare const generatedPluginInstalledResponse: PluginInstalledResponse;
const openClawPluginInstalledResponse: CodexAppServerRequestResult<"plugin/installed"> =
generatedPluginInstalledResponse;
const generatedPluginInstalledMarketplaceLoadErrors: MarketplaceLoadErrorInfo[] =
openClawPluginInstalledResponse.marketplaceLoadErrors;
type InstalledPluginResponseHasNoFeaturedCatalog =
"featuredPluginIds" extends keyof v2.PluginInstalledResponse ? never : true;
const installedPluginResponseHasNoFeaturedCatalog: InstalledPluginResponseHasNoFeaturedCatalog =
true;
declare const generatedPluginInstallResponse: PluginInstallResponse;
const openClawPluginInstallResponse: CodexAppServerRequestResult<"plugin/install"> =
generatedPluginInstallResponse;
declare const generatedPluginListResponse: PluginListResponse;
const openClawPluginListResponse: CodexAppServerRequestResult<"plugin/list"> =
generatedPluginListResponse;
const generatedPluginListMarketplaceLoadErrors: MarketplaceLoadErrorInfo[] =
openClawPluginListResponse.marketplaceLoadErrors;
const generatedPluginListFeaturedPluginIds: string[] = openClawPluginListResponse.featuredPluginIds;
declare const generatedPluginReadResponse: PluginReadResponse;
const openClawPluginReadResponse: CodexAppServerRequestResult<"plugin/read"> =
generatedPluginReadResponse;
declare const generatedDynamicToolCallParams: Omit<DynamicToolCallParams, "arguments">;
const openClawDynamicToolCallParams: Omit<CodexDynamicToolCallParams, "arguments"> =
generatedDynamicToolCallParams;
declare const generatedErrorNotification: ErrorNotification;
const openClawErrorNotification: CodexErrorNotification = generatedErrorNotification;
declare const generatedGetAccountResponse: GetAccountResponse;
const openClawGetAccountResponse: CodexGetAccountResponse = generatedGetAccountResponse;
declare const generatedModelListResponse: ModelListResponse;
const openClawModelListResponse: CodexModelListResponse = generatedModelListResponse;
declare const generatedThreadDeleteResponse: ThreadDeleteResponse;
const openClawThreadDeleteResponse: CodexAppServerRequestResult<"thread/delete"> =
generatedThreadDeleteResponse;
// 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> {
const sourceSchemas = new Map<string, unknown>();
for (const schema of selectedCodexAppServerJsonSchemas) {
const sourcePath = path.join(sourceJsonRoot, schema);
try {
sourceSchemas.set(schema, JSON.parse(await fs.readFile(sourcePath, "utf8")));
} catch (error) {
failures.push(
`protocol-generated/json/${schema}: missing upstream schema (${String(error)})`,
);
}
}
if (sourceSchemas.size !== selectedCodexAppServerJsonSchemas.length) {
return;
}
const expected = compactCodexAppServerProtocolJsonSchemas(sourceSchemas);
const local = new Map<string, unknown>();
for (const [schema, expectedValue] of expected) {
const targetPath = path.join(generatedRoot, "json", schema);
try {
const target = await fs.readFile(targetPath, "utf8");
local.set(schema, JSON.parse(target));
if (normalizeJsonSchema(JSON.stringify(expectedValue)) !== normalizeJsonSchema(target)) {
failures.push(`protocol-generated/json/${schema}: differs from compacted source schema`);
}
} catch (error) {
failures.push(`protocol-generated/json/${schema}: missing local schema (${String(error)})`);
}
}
const sharedSchema = local.get(codexAppServerSharedDefinitionsSchema);
if (sharedSchema === undefined) {
return;
}
for (const schema of selectedCodexAppServerJsonSchemas) {
const compactSchema = local.get(schema);
const sourceSchema = sourceSchemas.get(schema);
if (compactSchema === undefined || sourceSchema === undefined) {
continue;
}
try {
const expanded = expandCodexAppServerProtocolJsonSchema({
schema: compactSchema,
schemaPath: schema,
sharedSchema,
});
if (
normalizeJsonSchema(JSON.stringify(expanded)) !==
normalizeJsonSchema(JSON.stringify(sourceSchema))
) {
failures.push(
`protocol-generated/json/${schema}: compact schema does not expand to its source schema`,
);
}
} catch (error) {
failures.push(`protocol-generated/json/${schema}: cannot expand (${String(error)})`);
}
}
}
function normalizeJsonSchema(sourceLocal: string): string {
return normalizeCodexAppServerProtocolJsonText(sourceLocal);
}