mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 10:04:05 +00:00
* feat: add codex supervisor plugin * fix: restore merged branch checks * fix: unblock supervisor extension CI * fix: restore merged agent checks
202 lines
6.3 KiB
TypeScript
202 lines
6.3 KiB
TypeScript
import { Type, type Static } from "typebox";
|
|
import type { CodexSupervisorEndpoint } from "./types.js";
|
|
|
|
const ENDPOINTS_ENV = "OPENCLAW_CODEX_SUPERVISOR_ENDPOINTS";
|
|
|
|
const StdioEndpointSchema = Type.Object(
|
|
{
|
|
id: Type.Optional(Type.String()),
|
|
label: Type.Optional(Type.String()),
|
|
transport: Type.Optional(Type.Literal("stdio-proxy")),
|
|
command: Type.Optional(Type.String()),
|
|
args: Type.Optional(Type.Array(Type.String())),
|
|
cwd: Type.Optional(Type.String()),
|
|
},
|
|
{ additionalProperties: false },
|
|
);
|
|
|
|
const WebSocketEndpointSchema = Type.Object(
|
|
{
|
|
id: Type.Optional(Type.String()),
|
|
label: Type.Optional(Type.String()),
|
|
transport: Type.Literal("websocket"),
|
|
url: Type.String(),
|
|
authTokenEnv: Type.Optional(Type.String()),
|
|
},
|
|
{ additionalProperties: false },
|
|
);
|
|
|
|
export const CodexSupervisorPluginConfigSchema = Type.Object(
|
|
{
|
|
endpoints: Type.Optional(
|
|
Type.Array(Type.Union([StdioEndpointSchema, WebSocketEndpointSchema])),
|
|
),
|
|
allowRawTranscripts: Type.Optional(Type.Boolean({ default: false })),
|
|
allowWriteControls: Type.Optional(Type.Boolean({ default: false })),
|
|
},
|
|
{ additionalProperties: false },
|
|
);
|
|
|
|
export type CodexSupervisorPluginConfig = Static<typeof CodexSupervisorPluginConfigSchema>;
|
|
|
|
export type ResolvedCodexSupervisorPluginConfig = {
|
|
endpoints: CodexSupervisorEndpoint[];
|
|
allowRawTranscripts: boolean;
|
|
allowWriteControls: boolean;
|
|
};
|
|
|
|
function normalizeEndpointId(value: string, index: number): string {
|
|
const trimmed = value.trim();
|
|
if (trimmed) {
|
|
return trimmed.replace(/[^a-zA-Z0-9_.:-]/g, "-");
|
|
}
|
|
return `endpoint-${index + 1}`;
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function parseEndpointRecord(value: unknown, index: number): CodexSupervisorEndpoint | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const transport = typeof value.transport === "string" ? value.transport : undefined;
|
|
const id =
|
|
typeof value.id === "string"
|
|
? normalizeEndpointId(value.id, index)
|
|
: normalizeEndpointId(typeof value.label === "string" ? value.label : "", index);
|
|
const label = typeof value.label === "string" ? value.label : undefined;
|
|
if (transport === "websocket" && typeof value.url === "string") {
|
|
return {
|
|
id,
|
|
transport,
|
|
url: value.url,
|
|
...(label ? { label } : {}),
|
|
...(typeof value.authTokenEnv === "string" ? { authTokenEnv: value.authTokenEnv } : {}),
|
|
};
|
|
}
|
|
if (transport === "stdio-proxy" || transport === undefined) {
|
|
const args = Array.isArray(value.args)
|
|
? value.args.filter((entry): entry is string => typeof entry === "string")
|
|
: undefined;
|
|
return {
|
|
id,
|
|
transport: "stdio-proxy",
|
|
...(label ? { label } : {}),
|
|
...(typeof value.command === "string" ? { command: value.command } : {}),
|
|
...(args && args.length > 0 ? { args } : {}),
|
|
...(typeof value.cwd === "string" ? { cwd: value.cwd } : {}),
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function requireUniqueEndpointIds(endpoints: CodexSupervisorEndpoint[]): CodexSupervisorEndpoint[] {
|
|
const seen = new Set<string>();
|
|
for (const endpoint of endpoints) {
|
|
if (seen.has(endpoint.id)) {
|
|
throw new Error(`duplicate Codex supervisor endpoint id: ${endpoint.id}`);
|
|
}
|
|
seen.add(endpoint.id);
|
|
}
|
|
return endpoints;
|
|
}
|
|
|
|
function endpointFromToken(token: string, index: number): CodexSupervisorEndpoint | undefined {
|
|
const trimmed = token.trim();
|
|
if (!trimmed) {
|
|
return undefined;
|
|
}
|
|
if (
|
|
trimmed.startsWith("ws://") ||
|
|
trimmed.startsWith("wss://") ||
|
|
trimmed.startsWith("unix://")
|
|
) {
|
|
return {
|
|
id: normalizeEndpointId("", index),
|
|
transport: "websocket",
|
|
url: trimmed,
|
|
};
|
|
}
|
|
if (trimmed === "local" || trimmed === "proxy" || trimmed === "stdio") {
|
|
return {
|
|
id: "local",
|
|
label: "local Codex app-server daemon",
|
|
transport: "websocket",
|
|
url: "unix://",
|
|
};
|
|
}
|
|
const separatorIndex = trimmed.indexOf("=");
|
|
const id = separatorIndex >= 0 ? trimmed.slice(0, separatorIndex) : trimmed;
|
|
const url = separatorIndex >= 0 ? trimmed.slice(separatorIndex + 1) : undefined;
|
|
if (url?.startsWith("ws://") || url?.startsWith("wss://") || url?.startsWith("unix://")) {
|
|
return {
|
|
id: normalizeEndpointId(id ?? "", index),
|
|
transport: "websocket",
|
|
url,
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function loadCodexSupervisorEndpoints(
|
|
env: Pick<NodeJS.ProcessEnv, string> = process.env,
|
|
): CodexSupervisorEndpoint[] {
|
|
const raw = env[ENDPOINTS_ENV]?.trim();
|
|
if (!raw) {
|
|
return requireUniqueEndpointIds([
|
|
{
|
|
id: "local",
|
|
label: "local Codex app-server daemon",
|
|
transport: "websocket",
|
|
url: "unix://",
|
|
},
|
|
]);
|
|
}
|
|
if (raw.startsWith("[")) {
|
|
const parsed = JSON.parse(raw) as unknown;
|
|
if (!Array.isArray(parsed)) {
|
|
throw new Error(`${ENDPOINTS_ENV} must be a JSON array`);
|
|
}
|
|
return requireUniqueEndpointIds(
|
|
parsed
|
|
.map((entry, index) => parseEndpointRecord(entry, index))
|
|
.filter((entry): entry is CodexSupervisorEndpoint => Boolean(entry)),
|
|
);
|
|
}
|
|
return requireUniqueEndpointIds(
|
|
raw
|
|
.split(",")
|
|
.map(endpointFromToken)
|
|
.filter((entry): entry is CodexSupervisorEndpoint => Boolean(entry)),
|
|
);
|
|
}
|
|
|
|
function normalizeConfiguredEndpoints(
|
|
endpoints: CodexSupervisorPluginConfig["endpoints"],
|
|
): CodexSupervisorEndpoint[] | undefined {
|
|
if (!endpoints || endpoints.length === 0) {
|
|
return undefined;
|
|
}
|
|
const normalized = endpoints
|
|
.map((entry, index) => parseEndpointRecord(entry, index))
|
|
.filter((entry): entry is CodexSupervisorEndpoint => Boolean(entry));
|
|
return normalized.length > 0 ? requireUniqueEndpointIds(normalized) : undefined;
|
|
}
|
|
|
|
export function resolveCodexSupervisorPluginConfig(
|
|
rawConfig: unknown,
|
|
env: Pick<NodeJS.ProcessEnv, string> = process.env,
|
|
): ResolvedCodexSupervisorPluginConfig {
|
|
const config =
|
|
rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig)
|
|
? (rawConfig as CodexSupervisorPluginConfig)
|
|
: {};
|
|
return {
|
|
endpoints: normalizeConfiguredEndpoints(config.endpoints) ?? loadCodexSupervisorEndpoints(env),
|
|
allowRawTranscripts: config.allowRawTranscripts === true,
|
|
allowWriteControls: config.allowWriteControls === true,
|
|
};
|
|
}
|