Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
/**
* Public Codex Supervisor API barrel for plugin tools, MCP serving, config, and
* session types.
*/
export {
CodexSupervisorPluginConfigSchema,
loadCodexSupervisorEndpoints,
resolveCodexSupervisorPluginConfig,
} from "./config.js";
export { CodexSupervisor } from "./supervisor.js";
export { createCodexSupervisorTools } from "./plugin-tools.js";
export { createCodexSupervisorMcpServer, serveCodexSupervisorMcp } from "./mcp-server.js";
export type { CodexSupervisorPluginConfig, ResolvedCodexSupervisorPluginConfig } from "./config.js";
export type {
CodexJsonRpcConnection,
CodexSupervisorEndpoint,
CodexSupervisorEndpointHealth,
CodexSupervisorSendResult,
CodexSupervisorSession,
CodexSupervisorSessionListResult,
CodexSupervisorThreadStatus,
CodexSupervisorTurnMode,
} from "./types.js";

View File

@@ -0,0 +1,213 @@
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
/**
* Config parsing for Codex Supervisor endpoints and safety gates.
*/
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 },
);
/**
* Plugin config schema accepted by the bundled plugin manifest.
*/
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 },
);
/** Raw plugin config shape accepted from OpenClaw config. */
export type CodexSupervisorPluginConfig = Static<typeof CodexSupervisorPluginConfigSchema>;
/** Normalized config consumed by plugin registration and MCP serving. */
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 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;
}
/**
* Loads endpoint definitions from environment, defaulting to the local Codex
* app-server unix socket.
*/
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;
}
/**
* Resolves raw plugin config and env endpoints into validated runtime config.
*/
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,
};
}

View File

@@ -0,0 +1,370 @@
/**
* JSON-RPC transports for Codex app-server connections over stdio proxies or
* websocket/unix-socket endpoints.
*/
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { randomUUID } from "node:crypto";
import * as net from "node:net";
import * as os from "node:os";
import * as path from "node:path";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import WebSocket from "ws";
import type { CodexJsonRpcConnection, CodexSupervisorEndpoint } from "./types.js";
type PendingRequest = {
reject: (error: Error) => void;
resolve: (value: unknown) => void;
timeout: NodeJS.Timeout;
};
function formatJsonRpcError(message: Record<string, unknown>): Error {
const error = isRecord(message.error) ? message.error : {};
const detail =
typeof error.message === "string" ? error.message : "Codex app-server request failed";
return new Error(detail);
}
function formatMalformedMessageError(error: unknown): Error {
const detail = error instanceof Error ? error.message : String(error);
return new Error(`Malformed Codex app-server message: ${detail}`);
}
/**
* Produces denial responses for app-server approval requests the supervisor
* deliberately cannot grant.
*/
export function resolveSafeApprovalResult(method: string): Record<string, unknown> | undefined {
if (method === "item/tool/call") {
return {
contentItems: [
{
type: "inputText",
text: "OpenClaw Codex supervisor did not register a handler for this app-server tool call.",
},
],
success: false,
};
}
if (method === "item/commandExecution/requestApproval") {
return { decision: "decline" };
}
if (method === "item/fileChange/requestApproval") {
return { decision: "decline" };
}
if (method === "item/permissions/requestApproval") {
return { permissions: {}, scope: "turn" };
}
if (method.endsWith("/requestApproval")) {
return {
decision: "decline",
reason: "OpenClaw Codex supervisor does not grant native approvals.",
};
}
if (method === "item/tool/requestUserInput") {
return { answers: {} };
}
if (method === "mcpServer/elicitation/request") {
return { action: "decline" };
}
return undefined;
}
abstract class BaseCodexJsonRpcConnection implements CodexJsonRpcConnection {
private readonly pending = new Map<string, PendingRequest>();
private closedError: Error | undefined;
abstract close(): Promise<void>;
protected abstract sendRaw(line: string): void;
async initialize(): Promise<void> {
await this.request("initialize", {
clientInfo: {
name: "openclaw-codex-supervisor",
title: "OpenClaw Codex Supervisor",
version: "0.1.0",
},
capabilities: {
experimentalApi: true,
},
});
this.notify("initialized");
}
request(method: string, params?: Record<string, unknown>): Promise<unknown> {
if (this.closedError) {
return Promise.reject(this.closedError);
}
const id = randomUUID();
const payload: Record<string, unknown> = { id, method, params: params ?? {} };
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`Codex app-server request timed out: ${method}`));
}, 60_000);
this.pending.set(id, { resolve, reject, timeout });
try {
this.sendRaw(JSON.stringify(payload));
} catch (error) {
clearTimeout(timeout);
this.pending.delete(id);
reject(error instanceof Error ? error : new Error(String(error)));
}
});
}
notify(method: string, params?: Record<string, unknown>): void {
const payload: Record<string, unknown> = { method, params: params ?? null };
this.sendRaw(JSON.stringify(payload));
}
protected handleMessage(message: unknown): void {
if (!isRecord(message)) {
return;
}
const id =
typeof message.id === "string" || typeof message.id === "number" ? message.id : undefined;
const method = typeof message.method === "string" ? message.method : undefined;
if (id !== undefined && method) {
const result = resolveSafeApprovalResult(method);
// The supervisor is read/steer tooling, not a native approval delegate;
// unknown app-server requests fail closed with either a denial or -32601.
this.sendRaw(
JSON.stringify(
result === undefined
? {
id,
error: {
code: -32601,
message: `OpenClaw Codex supervisor cannot handle app-server request: ${method}`,
},
}
: { id, result },
),
);
return;
}
if (id !== undefined) {
const pending = this.pending.get(String(id));
if (!pending) {
return;
}
clearTimeout(pending.timeout);
this.pending.delete(String(id));
if ("error" in message) {
pending.reject(formatJsonRpcError(message));
return;
}
pending.resolve(message.result);
}
}
protected rejectAll(error: Error): void {
for (const [id, pending] of this.pending) {
clearTimeout(pending.timeout);
this.pending.delete(id);
pending.reject(error);
}
}
protected fail(error: Error): void {
this.closedError ??= error;
this.rejectAll(this.closedError);
}
}
class StdioCodexJsonRpcConnection extends BaseCodexJsonRpcConnection {
private buffer = "";
private readonly proc: ChildProcessWithoutNullStreams;
private readonly stderrTail: string[] = [];
constructor(endpoint: Extract<CodexSupervisorEndpoint, { transport: "stdio-proxy" }>) {
super();
this.proc = spawn(
endpoint.command ?? "codex",
endpoint.args ?? ["app-server", "--listen", "stdio://"],
{
cwd: endpoint.cwd,
stdio: "pipe",
},
);
this.proc.stdout.setEncoding("utf8");
this.proc.stderr.setEncoding("utf8");
this.proc.stdout.on("data", (chunk: string) => this.handleStdout(chunk));
this.proc.stderr.on("data", (chunk: string) => {
this.stderrTail.push(...chunk.split(/\r?\n/).filter(Boolean));
this.stderrTail.splice(0, Math.max(0, this.stderrTail.length - 40));
});
this.proc.stdin.once("error", (error) => this.fail(error));
this.proc.once("error", (error) => this.fail(error));
this.proc.once("close", () =>
this.fail(
new Error(
`Codex app-server stdio transport closed. stderr_tail=${this.stderrTail.join("\n").slice(0, 1200)}`,
),
),
);
}
protected sendRaw(line: string): void {
this.proc.stdin.write(`${line}\n`, (error) => {
if (error) {
this.fail(error);
}
});
}
async close(): Promise<void> {
this.proc.stdin.end();
this.proc.kill("SIGTERM");
}
private handleStdout(chunk: string): void {
this.buffer += chunk;
for (;;) {
const index = this.buffer.indexOf("\n");
if (index < 0) {
return;
}
const line = this.buffer.slice(0, index).trim();
this.buffer = this.buffer.slice(index + 1);
if (!line) {
continue;
}
try {
this.handleMessage(JSON.parse(line) as unknown);
} catch (error) {
this.fail(formatMalformedMessageError(error));
void this.close();
return;
}
}
}
}
function defaultCodexControlSocketPath(): string {
const codexHome = process.env.CODEX_HOME?.trim() || path.join(os.homedir(), ".codex");
return path.join(codexHome, "app-server-control", "app-server-control.sock");
}
function resolveUnixWebSocketPath(url: string): string {
const suffix = url.slice("unix://".length);
return suffix || defaultCodexControlSocketPath();
}
function connectCodexSupervisorUnixSocket(url: string): net.Socket {
return net.createConnection(resolveUnixWebSocketPath(url));
}
function websocketMessageToString(data: WebSocket.RawData): string {
if (typeof data === "string") {
return data;
}
if (Buffer.isBuffer(data)) {
return data.toString("utf8");
}
if (Array.isArray(data)) {
return Buffer.concat(data).toString("utf8");
}
return Buffer.from(data).toString("utf8");
}
class WebSocketCodexJsonRpcConnection extends BaseCodexJsonRpcConnection {
private readonly ws: WebSocket;
private readonly openPromise: Promise<void>;
private closing = false;
constructor(endpoint: Extract<CodexSupervisorEndpoint, { transport: "websocket" }>) {
super();
const headers: Record<string, string> = {};
if (endpoint.authTokenEnv) {
const token = process.env[endpoint.authTokenEnv];
if (token) {
headers.authorization = `Bearer ${token}`;
}
}
this.ws = endpoint.url.startsWith("unix://")
? new WebSocket("ws://localhost/", {
headers,
createConnection: () => connectCodexSupervisorUnixSocket(endpoint.url),
})
: new WebSocket(endpoint.url, { headers });
this.openPromise = new Promise((resolve, reject) => {
this.ws.once("open", resolve);
this.ws.once("error", reject);
});
this.ws.on("message", (data) => {
const text = websocketMessageToString(data);
try {
this.handleMessage(JSON.parse(text) as unknown);
} catch (error) {
this.fail(formatMalformedMessageError(error));
void this.close();
}
});
this.ws.once("error", (error) => this.fail(error));
this.ws.once("close", () => {
if (!this.closing) {
this.fail(new Error("Codex app-server websocket closed"));
}
});
}
async ready(): Promise<void> {
await this.openPromise;
}
protected sendRaw(line: string): void {
this.ws.send(line, (error) => {
if (error) {
this.fail(error);
}
});
}
async close(): Promise<void> {
this.closing = true;
this.fail(new Error("Codex app-server websocket closed"));
if (this.ws.readyState === WebSocket.CLOSED) {
return;
}
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
this.ws.terminate();
resolve();
}, 1000);
this.ws.once("close", () => {
clearTimeout(timeout);
resolve();
});
if (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
} else {
clearTimeout(timeout);
resolve();
}
});
}
}
/**
* Opens, initializes, and returns a JSON-RPC connection for one supervisor
* endpoint.
*/
export async function connectCodexAppServerEndpoint(
endpoint: CodexSupervisorEndpoint,
): Promise<CodexJsonRpcConnection> {
const connection =
endpoint.transport === "websocket"
? new WebSocketCodexJsonRpcConnection(endpoint)
: new StdioCodexJsonRpcConnection(endpoint);
try {
if ("ready" in connection && typeof connection.ready === "function") {
await connection.ready();
}
await connection.initialize();
return connection;
} catch (error) {
await connection.close().catch(() => undefined);
throw error;
}
}

View File

@@ -0,0 +1,18 @@
/**
* Standalone MCP server for OpenClaw Codex supervision.
*
* Run via: node --import tsx extensions/codex-supervisor/src/mcp-serve.ts
*/
import { pathToFileURL } from "node:url";
import { serveCodexSupervisorMcp } from "./mcp-server.js";
function formatErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
serveCodexSupervisorMcp().catch((err: unknown) => {
process.stderr.write(`codex-supervisor-serve: ${formatErrorMessage(err)}\n`);
process.exit(1);
});
}

View File

@@ -0,0 +1,97 @@
/**
* Standalone MCP stdio server for exposing Codex Supervisor tools to trusted
* MCP clients.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { loadCodexSupervisorEndpoints } from "./config.js";
import {
registerCodexSupervisorMcpTools,
type CodexSupervisorMcpToolOptions,
} from "./mcp-tools.js";
import { CodexSupervisor } from "./supervisor.js";
const VERSION = "0.1.0";
function routeLogsToStderr(): void {
const methods = ["log", "info", "warn", "error", "debug"] as const;
for (const method of methods) {
console[method] = (...args: unknown[]) => {
process.stderr.write(`${args.map(String).join(" ")}\n`);
};
}
}
/** Options for creating or serving a Codex Supervisor MCP server. */
export type CodexSupervisorMcpServeOptions = {
supervisor?: CodexSupervisor;
toolOptions?: CodexSupervisorMcpToolOptions;
};
/**
* Creates an MCP server and owns the supervisor instance unless one is supplied.
*/
export function createCodexSupervisorMcpServer(opts: CodexSupervisorMcpServeOptions = {}): {
server: McpServer;
supervisor: CodexSupervisor;
close: () => Promise<void>;
} {
const supervisor = opts.supervisor ?? new CodexSupervisor(loadCodexSupervisorEndpoints());
const server = new McpServer({ name: "openclaw-codex-supervisor", version: VERSION });
registerCodexSupervisorMcpTools(server, supervisor, opts.toolOptions);
return {
server,
supervisor,
close: async () => {
await supervisor.close();
await server.close();
},
};
}
/**
* Serves Codex Supervisor tools over MCP stdio until transport or process
* shutdown.
*/
export async function serveCodexSupervisorMcp(
opts: CodexSupervisorMcpServeOptions = {},
): Promise<void> {
routeLogsToStderr();
const { server, close } = createCodexSupervisorMcpServer(opts);
const transport = new StdioServerTransport();
let shuttingDown = false;
let resolveClosed!: () => void;
const closed = new Promise<void>((resolve) => {
resolveClosed = resolve;
});
const shutdown = () => {
if (shuttingDown) {
return;
}
shuttingDown = true;
process.stdin.off("end", shutdown);
process.stdin.off("close", shutdown);
process.off("SIGINT", shutdown);
process.off("SIGTERM", shutdown);
// The SDK exposes this callback slot but not a stable setter; clear it so
// close() cannot recursively re-enter shutdown.
transport["onclose"] = undefined;
close().then(resolveClosed, resolveClosed);
};
transport["onclose"] = shutdown;
process.stdin.once("end", shutdown);
process.stdin.once("close", shutdown);
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
try {
await server.connect(transport);
await closed;
} finally {
shutdown();
await closed;
}
}

View File

@@ -0,0 +1,105 @@
// Codex Supervisor tests cover mcp tools plugin behavior.
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { describe, expect, it } from "vitest";
import {
redactCodexSupervisorEndpoint,
redactCodexSupervisorValue,
registerCodexSupervisorMcpTools,
sanitizeCodexSupervisorSessionListResult,
} from "./mcp-tools.js";
import type { CodexSupervisor } from "./supervisor.js";
describe("redactCodexSupervisorValue", () => {
it("redacts sensitive keys and common bearer-like secrets", () => {
expect(
redactCodexSupervisorValue({
authorization: "Bearer abcdefghijklmnopqrstuvwxyz012345",
nested: {
apiKey: "sk-abcdefghijklmnopqrstuvwxyz012345",
text: "token ghp_abcdefghijklmnopqrstuvwxyz012345 remains hidden",
},
}),
).toEqual({
authorization: "[redacted]",
nested: {
apiKey: "[redacted]",
text: "token [redacted] remains hidden",
},
});
});
});
describe("redactCodexSupervisorEndpoint", () => {
it("removes websocket credentials and query values", () => {
expect(
redactCodexSupervisorEndpoint({
id: "prod",
transport: "websocket",
url: "wss://user:secret@example.invalid/control?token=a=b",
}),
).toEqual({
id: "prod",
transport: "websocket",
url: "wss://example.invalid/control?[redacted]",
});
});
});
describe("sanitizeCodexSupervisorSessionListResult", () => {
it("omits transcript-derived fields unless explicitly trusted", () => {
const result = {
sessions: [
{
endpointId: "local",
threadId: "thread-1",
status: "idle",
preview: "first prompt",
name: "thread title",
},
],
errors: [{ endpointId: "down", ok: false, detail: "stderr secret" }],
};
expect(sanitizeCodexSupervisorSessionListResult(result, false)).toEqual({
sessions: [{ endpointId: "local", threadId: "thread-1", status: "idle" }],
errors: [{ endpointId: "down", ok: false }],
});
expect(sanitizeCodexSupervisorSessionListResult(result, true)).toEqual(result);
});
});
describe("registerCodexSupervisorMcpTools", () => {
it("uses per-server transcript policy when listing sessions", async () => {
const handlers = new Map<string, (params: Record<string, unknown>) => Promise<unknown>>();
const server = {
tool(name: string, _description: string, _schema: unknown, handler: unknown) {
handlers.set(name, handler as (params: Record<string, unknown>) => Promise<unknown>);
},
} as unknown as McpServer;
const supervisor = {
listSessionSnapshot: async () => ({
sessions: [
{
endpointId: "local",
threadId: "thread-1",
status: "idle",
preview: "first prompt",
name: "thread title",
},
],
errors: [{ endpointId: "down", ok: false, detail: "stderr secret" }],
}),
} as unknown as CodexSupervisor;
registerCodexSupervisorMcpTools(server, supervisor, {
rawTranscriptReadsAllowed: () => false,
});
await expect(handlers.get("codex_sessions_list")?.({})).resolves.toMatchObject({
structuredContent: {
sessions: [{ endpointId: "local", threadId: "thread-1", status: "idle" }],
errors: [{ endpointId: "down", ok: false }],
},
});
});
});

View File

@@ -0,0 +1,281 @@
/**
* MCP tool registration plus redaction helpers for Codex Supervisor sessions
* and endpoint metadata.
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { CodexSupervisor } from "./supervisor.js";
import type {
CodexSupervisorEndpoint,
CodexSupervisorSession,
CodexSupervisorSessionListResult,
} from "./types.js";
/** Env gate for exposing transcript-derived fields through standalone MCP. */
export const RAW_TRANSCRIPTS_ENV = "OPENCLAW_CODEX_SUPERVISOR_ALLOW_RAW_TRANSCRIPTS";
/** Env gate for mutating/steering Codex sessions through standalone MCP. */
export const WRITE_CONTROLS_ENV = "OPENCLAW_CODEX_SUPERVISOR_ALLOW_WRITE_CONTROLS";
/** Optional policy callbacks for standalone MCP tool exposure. */
export type CodexSupervisorMcpToolOptions = {
rawTranscriptReadsAllowed?: () => boolean;
writeControlsAllowed?: () => boolean;
};
function textResult(text: string, structuredContent?: Record<string, unknown>) {
return {
content: [{ type: "text" as const, text }],
...(structuredContent ? { structuredContent } : {}),
};
}
function errorResult(message: string) {
return {
content: [{ type: "text" as const, text: message }],
isError: true,
};
}
function redactString(value: string): string {
return value
.replace(/\b(?:sk|glpat|xox[baprs])-[-_a-zA-Z0-9]{12,}\b/g, "[redacted]")
.replace(/\b(?:ghp|gho|ghu|ghs)_[-_a-zA-Z0-9]{12,}\b/g, "[redacted]")
.replace(/\bBearer\s+[-._~+/a-zA-Z0-9]+=*/g, "Bearer [redacted]");
}
/**
* Redacts common secret-bearing fields and token-like substrings before tool
* results leave the supervisor.
*/
export function redactCodexSupervisorValue(value: unknown, key = ""): unknown {
if (typeof value === "string") {
if (/authorization|password|secret|token|api[-_]?key/i.test(key)) {
return "[redacted]";
}
return redactString(value);
}
if (Array.isArray(value)) {
return value.map((entry) => redactCodexSupervisorValue(entry));
}
if (!value || typeof value !== "object") {
return value;
}
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([entryKey, entryValue]) => [
entryKey,
redactCodexSupervisorValue(entryValue, entryKey),
]),
);
}
function redactEndpointUrl(value: string): string {
if (value.startsWith("unix://")) {
return "unix://";
}
try {
const url = new URL(value);
url.username = "";
url.password = "";
if (url.search) {
url.search = "?[redacted]";
}
return url.toString();
} catch {
return "[redacted]";
}
}
/** Returns endpoint metadata safe for tool results. */
export function redactCodexSupervisorEndpoint(
endpoint: CodexSupervisorEndpoint,
): Record<string, unknown> {
return {
id: endpoint.id,
transport: endpoint.transport,
...(endpoint.label ? { label: endpoint.label } : {}),
...(endpoint.transport === "websocket" ? { url: redactEndpointUrl(endpoint.url) } : {}),
};
}
function rawTranscriptReadsAllowed(): boolean {
return process.env[RAW_TRANSCRIPTS_ENV] === "1";
}
function writeControlsAllowed(): boolean {
return process.env[WRITE_CONTROLS_ENV] === "1";
}
function rawTranscriptReadsAllowedFor(opts: CodexSupervisorMcpToolOptions): boolean {
return opts.rawTranscriptReadsAllowed
? opts.rawTranscriptReadsAllowed()
: rawTranscriptReadsAllowed();
}
function writeControlsAllowedFor(opts: CodexSupervisorMcpToolOptions): boolean {
return opts.writeControlsAllowed ? opts.writeControlsAllowed() : writeControlsAllowed();
}
function sanitizeSessionForMcp(
session: CodexSupervisorSession,
includeTranscriptDerivedFields: boolean,
): Record<string, unknown> {
const sanitized = redactCodexSupervisorValue(session) as Record<string, unknown>;
if (!includeTranscriptDerivedFields) {
delete sanitized.preview;
delete sanitized.name;
}
return sanitized;
}
/**
* Sanitizes session-list output, optionally including transcript-derived
* preview/name fields only when the caller has opted in.
*/
export function sanitizeCodexSupervisorSessionListResult(
result: CodexSupervisorSessionListResult,
includeTranscriptDerivedFields = rawTranscriptReadsAllowed(),
): Record<string, unknown> {
return {
sessions: result.sessions.map((session) =>
sanitizeSessionForMcp(session, includeTranscriptDerivedFields),
),
errors: includeTranscriptDerivedFields
? redactCodexSupervisorValue(result.errors)
: result.errors.map(({ endpointId, ok }) => ({ endpointId, ok })),
};
}
/**
* Registers MCP tools for endpoint probing, session listing, reads, sends, and
* interrupts.
*/
export function registerCodexSupervisorMcpTools(
server: McpServer,
supervisor: CodexSupervisor,
opts: CodexSupervisorMcpToolOptions = {},
): void {
server.tool(
"codex_endpoint_probe",
"Check configured Codex app-server endpoints.",
{},
async () => {
const endpoints = supervisor.listEndpoints().map(redactCodexSupervisorEndpoint);
const health = (await supervisor.probeEndpoints()).map(({ endpointId, ok }) => ({
endpointId,
ok,
}));
return textResult(
`codex endpoints: ${health.filter((entry) => entry.ok).length}/${health.length} ok`,
{
endpoints,
health,
},
);
},
);
server.tool(
"codex_sessions_list",
"List Codex sessions visible to the OpenClaw supervisor.",
{
include_stored: z.boolean().optional(),
max_stored_sessions: z.number().int().min(1).max(1000).optional(),
},
async ({ include_stored, max_stored_sessions }) => {
const result = await supervisor.listSessionSnapshot({
includeStored: include_stored ?? false,
maxStoredSessions: max_stored_sessions,
});
return textResult(
`codex sessions: ${result.sessions.length}`,
sanitizeCodexSupervisorSessionListResult(result, rawTranscriptReadsAllowedFor(opts)),
);
},
);
server.tool(
"codex_session_read",
"Read one Codex session transcript from app-server.",
{
endpoint_id: z.string().optional(),
thread_id: z.string().min(1),
include_turns: z.boolean().optional(),
},
async ({ endpoint_id, thread_id, include_turns }) => {
if (!rawTranscriptReadsAllowedFor(opts)) {
return errorResult(
`Codex session reads are disabled; set ${RAW_TRANSCRIPTS_ENV}=1 for a trusted supervisor-only MCP`,
);
}
const includeTurns = include_turns ?? false;
try {
const response = await supervisor.readSession({
endpointId: endpoint_id,
threadId: thread_id,
includeTurns,
});
return textResult(`codex session: ${thread_id}`, {
response: redactCodexSupervisorValue(response),
});
} catch (error) {
return errorResult(error instanceof Error ? error.message : String(error));
}
},
);
server.tool(
"codex_session_send",
"Send text to a Codex session. Idle sessions start a turn; active sessions are steered.",
{
endpoint_id: z.string().optional(),
thread_id: z.string().min(1),
text: z.string().min(1),
mode: z.enum(["auto", "start", "steer"]).optional(),
},
async ({ endpoint_id, thread_id, text, mode }) => {
if (!writeControlsAllowedFor(opts)) {
return errorResult(
`Codex write controls are disabled; set ${WRITE_CONTROLS_ENV}=1 for a trusted supervisor-only MCP`,
);
}
try {
const result = await supervisor.sendToSession({
endpointId: endpoint_id,
threadId: thread_id,
text,
mode,
});
return textResult(`codex ${result.mode}: ${result.turnId ?? thread_id}`, { result });
} catch (error) {
return errorResult(error instanceof Error ? error.message : String(error));
}
},
);
server.tool(
"codex_session_interrupt",
"Interrupt an active Codex turn.",
{
endpoint_id: z.string().optional(),
thread_id: z.string().min(1),
turn_id: z.string().optional(),
},
async ({ endpoint_id, thread_id, turn_id }) => {
if (!writeControlsAllowedFor(opts)) {
return errorResult(
`Codex write controls are disabled; set ${WRITE_CONTROLS_ENV}=1 for a trusted supervisor-only MCP`,
);
}
try {
const result = await supervisor.interruptSession({
endpointId: endpoint_id,
threadId: thread_id,
turnId: turn_id,
});
return textResult(`codex interrupted: ${result.turnId}`, { result });
} catch (error) {
return errorResult(error instanceof Error ? error.message : String(error));
}
},
);
}

View File

@@ -0,0 +1,174 @@
// Codex Supervisor tests cover plugin tools plugin behavior.
import { describe, expect, it } from "vitest";
import { createCodexSupervisorTools } from "./plugin-tools.js";
import type { CodexSupervisor } from "./supervisor.js";
function createSupervisorStub() {
const calls: string[] = [];
const supervisor = {
listEndpoints: () => [
{
id: "prod",
transport: "websocket",
url: "wss://user:secret@example.invalid/control?token=hidden",
},
],
probeEndpoints: async () => [{ endpointId: "prod", ok: true }],
listSessionSnapshot: async () => ({
sessions: [
{
endpointId: "prod",
threadId: "thread-1",
status: "idle",
preview: "secret prompt",
name: "secret title",
},
],
errors: [{ endpointId: "down", ok: false, detail: "secret stderr" }],
}),
readSession: async () => ({
thread: {
id: "thread-1",
authorization: "Bearer abcdefghijklmnopqrstuvwxyz012345",
},
}),
sendToSession: async (params: { mode?: string }) => {
calls.push(`send:${params.mode ?? "auto"}`);
return {
endpointId: "prod",
threadId: "thread-1",
mode: "start" as const,
turnId: "turn-1",
};
},
interruptSession: async () => {
calls.push("interrupt");
return {
endpointId: "prod",
threadId: "thread-1",
turnId: "turn-1",
};
},
} satisfies Pick<
CodexSupervisor,
| "interruptSession"
| "listEndpoints"
| "listSessionSnapshot"
| "probeEndpoints"
| "readSession"
| "sendToSession"
>;
return { calls, supervisor: supervisor as unknown as CodexSupervisor };
}
function toolByName(tools: ReturnType<typeof createCodexSupervisorTools>, name: string) {
const tool = tools.find((entry) => entry.name === name);
if (!tool) {
throw new Error(`missing tool: ${name}`);
}
return tool;
}
describe("createCodexSupervisorTools", () => {
it("registers redacted read-only supervisor tools by default", async () => {
const { supervisor } = createSupervisorStub();
const tools = createCodexSupervisorTools({
supervisor,
policy: { allowRawTranscripts: false, allowWriteControls: false },
});
const probe = await toolByName(tools, "codex_endpoint_probe").execute("call-1", {});
expect(probe.details).toMatchObject({
summary: "codex endpoints: 1/1 ok",
endpoints: [
{ id: "prod", transport: "websocket", url: "wss://example.invalid/control?[redacted]" },
],
});
const list = await toolByName(tools, "codex_sessions_list").execute("call-2", {});
expect(list.details).toEqual({
summary: "codex sessions: 1",
sessions: [{ endpointId: "prod", threadId: "thread-1", status: "idle" }],
errors: [{ endpointId: "down", ok: false }],
});
});
it("gates transcript reads and write controls", async () => {
const { supervisor } = createSupervisorStub();
const tools = createCodexSupervisorTools({
supervisor,
policy: { allowRawTranscripts: false, allowWriteControls: false },
});
await expect(
toolByName(tools, "codex_session_read").execute("call-1", { thread_id: "thread-1" }),
).rejects.toThrow("Codex session reads are disabled");
await expect(
toolByName(tools, "codex_session_send").execute("call-2", {
thread_id: "thread-1",
text: "continue",
}),
).rejects.toThrow("Codex write controls are disabled");
});
it("rejects stored session limits outside the runtime bounds", async () => {
const { supervisor } = createSupervisorStub();
const tools = createCodexSupervisorTools({
supervisor,
policy: { allowRawTranscripts: false, allowWriteControls: false },
});
await expect(
toolByName(tools, "codex_sessions_list").execute("call-1", {
include_stored: true,
max_stored_sessions: "2",
}),
).rejects.toThrow("max_stored_sessions must be an integer");
await expect(
toolByName(tools, "codex_sessions_list").execute("call-2", {
include_stored: true,
max_stored_sessions: 1001,
}),
).rejects.toThrow("max_stored_sessions must be between 1 and 1000");
await expect(
toolByName(tools, "codex_sessions_list").execute("call-2", {
include_stored: true,
max_stored_sessions: null,
}),
).rejects.toThrow("max_stored_sessions must be an integer");
await expect(
toolByName(tools, "codex_sessions_list").execute("call-3", {
include_stored: true,
max_stored_sessions: Number.MAX_SAFE_INTEGER + 1,
}),
).rejects.toThrow("max_stored_sessions must be between 1 and 1000");
});
it("allows trusted read and write tools when policy enables them", async () => {
const { calls, supervisor } = createSupervisorStub();
const tools = createCodexSupervisorTools({
supervisor,
policy: { allowRawTranscripts: true, allowWriteControls: true },
});
const read = await toolByName(tools, "codex_session_read").execute("call-1", {
thread_id: "thread-1",
});
expect(read.details).toEqual({
summary: "codex session: thread-1",
response: { thread: { id: "thread-1", authorization: "[redacted]" } },
});
const sent = await toolByName(tools, "codex_session_send").execute("call-2", {
thread_id: "thread-1",
text: "continue",
mode: "start",
});
expect(sent.details).toMatchObject({
summary: "codex start: turn-1",
result: { turnId: "turn-1" },
});
expect(calls).toEqual(["send:start"]);
});
});

View File

@@ -0,0 +1,223 @@
/**
* OpenClaw agent-tool definitions for Codex Supervisor endpoint and session
* controls.
*/
import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core";
import { Type } from "typebox";
import {
redactCodexSupervisorEndpoint,
redactCodexSupervisorValue,
sanitizeCodexSupervisorSessionListResult,
} from "./mcp-tools.js";
import type { CodexSupervisor } from "./supervisor.js";
import type { CodexSupervisorTurnMode } from "./types.js";
const EmptyParamsSchema = Type.Object({}, { additionalProperties: false });
const SessionsListParamsSchema = Type.Object(
{
include_stored: Type.Optional(Type.Boolean()),
max_stored_sessions: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
},
{ additionalProperties: false },
);
const SessionReadParamsSchema = Type.Object(
{
endpoint_id: Type.Optional(Type.String()),
thread_id: Type.String(),
include_turns: Type.Optional(Type.Boolean()),
},
{ additionalProperties: false },
);
const SessionSendParamsSchema = Type.Object(
{
endpoint_id: Type.Optional(Type.String()),
thread_id: Type.String(),
text: Type.String(),
mode: Type.Optional(
Type.Union([Type.Literal("auto"), Type.Literal("start"), Type.Literal("steer")]),
),
},
{ additionalProperties: false },
);
const SessionInterruptParamsSchema = Type.Object(
{
endpoint_id: Type.Optional(Type.String()),
thread_id: Type.String(),
turn_id: Type.Optional(Type.String()),
},
{ additionalProperties: false },
);
/** Policy flags controlling transcript reads and write operations. */
export type CodexSupervisorToolPolicy = {
allowRawTranscripts: boolean;
allowWriteControls: boolean;
};
/** Dependencies needed to build OpenClaw agent tools. */
export type CodexSupervisorToolOptions = {
supervisor: CodexSupervisor;
policy: CodexSupervisorToolPolicy;
};
function asRecord(params: unknown): Record<string, unknown> {
return params && typeof params === "object" && !Array.isArray(params)
? (params as Record<string, unknown>)
: {};
}
function readBooleanParam(params: Record<string, unknown>, key: string): boolean {
return params[key] === true;
}
function readIntegerParam(params: Record<string, unknown>, key: string): number | undefined {
const value = params[key];
if (value === undefined) {
return undefined;
}
if (typeof value !== "number" || !Number.isInteger(value)) {
throw new Error(`${key} must be an integer`);
}
if (value < 1 || value > 1000) {
throw new Error(`${key} must be between 1 and 1000`);
}
return value;
}
function readModeParam(params: Record<string, unknown>): CodexSupervisorTurnMode | undefined {
const mode = readStringParam(params, "mode");
if (!mode) {
return undefined;
}
if (mode === "auto" || mode === "start" || mode === "steer") {
return mode;
}
throw new Error("mode must be auto, start, or steer");
}
function requireRawTranscriptAccess(policy: CodexSupervisorToolPolicy): void {
if (!policy.allowRawTranscripts) {
throw new Error("Codex session reads are disabled for this codex-supervisor plugin config.");
}
}
function requireWriteAccess(policy: CodexSupervisorToolPolicy): void {
if (!policy.allowWriteControls) {
throw new Error("Codex write controls are disabled for this codex-supervisor plugin config.");
}
}
/**
* Creates the OpenClaw tools that expose Codex endpoint health and session
* controls.
*/
export function createCodexSupervisorTools({
supervisor,
policy,
}: CodexSupervisorToolOptions): AnyAgentTool[] {
return [
{
name: "codex_endpoint_probe",
label: "Codex Endpoint Probe",
description: "Check configured Codex app-server endpoints.",
parameters: EmptyParamsSchema,
execute: async () => {
const endpoints = supervisor.listEndpoints().map(redactCodexSupervisorEndpoint);
const health = (await supervisor.probeEndpoints()).map(({ endpointId, ok }) => ({
endpointId,
ok,
}));
return jsonResult({
summary: `codex endpoints: ${health.filter((entry) => entry.ok).length}/${health.length} ok`,
endpoints,
health,
});
},
},
{
name: "codex_sessions_list",
label: "Codex Sessions List",
description: "List Codex sessions visible to the OpenClaw supervisor.",
parameters: SessionsListParamsSchema,
execute: async (_toolCallId, rawParams) => {
const params = asRecord(rawParams);
const result = await supervisor.listSessionSnapshot({
includeStored: readBooleanParam(params, "include_stored"),
maxStoredSessions: readIntegerParam(params, "max_stored_sessions"),
});
return jsonResult({
summary: `codex sessions: ${result.sessions.length}`,
...sanitizeCodexSupervisorSessionListResult(result, policy.allowRawTranscripts),
});
},
},
{
name: "codex_session_read",
label: "Codex Session Read",
description: "Read one Codex session transcript from app-server.",
parameters: SessionReadParamsSchema,
execute: async (_toolCallId, rawParams) => {
// Raw transcript access is opt-in because app-server sessions can hold
// secrets, private files, and user-authenticated browser context.
requireRawTranscriptAccess(policy);
const params = asRecord(rawParams);
const threadId = readStringParam(params, "thread_id", { required: true });
const response = await supervisor.readSession({
endpointId: readStringParam(params, "endpoint_id"),
threadId,
includeTurns: readBooleanParam(params, "include_turns"),
});
return jsonResult({
summary: `codex session: ${threadId}`,
response: redactCodexSupervisorValue(response),
});
},
},
{
name: "codex_session_send",
label: "Codex Session Send",
description:
"Send text to a Codex session. Idle sessions start a turn; active sessions are steered.",
parameters: SessionSendParamsSchema,
execute: async (_toolCallId, rawParams) => {
// Session write controls can steer or interrupt a human-visible Codex
// turn, so they remain behind an explicit plugin policy gate.
requireWriteAccess(policy);
const params = asRecord(rawParams);
const result = await supervisor.sendToSession({
endpointId: readStringParam(params, "endpoint_id"),
threadId: readStringParam(params, "thread_id", { required: true }),
text: readStringParam(params, "text", { required: true, allowEmpty: false }),
mode: readModeParam(params),
});
return jsonResult({
summary: `codex ${result.mode}: ${result.turnId ?? result.threadId}`,
result,
});
},
},
{
name: "codex_session_interrupt",
label: "Codex Session Interrupt",
description: "Interrupt an active Codex turn.",
parameters: SessionInterruptParamsSchema,
execute: async (_toolCallId, rawParams) => {
requireWriteAccess(policy);
const params = asRecord(rawParams);
const result = await supervisor.interruptSession({
endpointId: readStringParam(params, "endpoint_id"),
threadId: readStringParam(params, "thread_id", { required: true }),
turnId: readStringParam(params, "turn_id"),
});
return jsonResult({
summary: `codex interrupted: ${result.turnId}`,
result,
});
},
},
];
}

View File

@@ -0,0 +1,951 @@
// Codex Supervisor tests cover supervisor plugin behavior.
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { describe, expect, it } from "vitest";
import { WebSocketServer } from "ws";
import { loadCodexSupervisorEndpoints, resolveCodexSupervisorPluginConfig } from "./config.js";
import { connectCodexAppServerEndpoint, resolveSafeApprovalResult } from "./json-rpc-client.js";
import { CodexSupervisor } from "./supervisor.js";
import type { CodexJsonRpcConnection, CodexSupervisorEndpoint } from "./types.js";
class FakeCodexConnection implements CodexJsonRpcConnection {
readonly calls: Array<{ method: string; params?: Record<string, unknown> }> = [];
closeCount = 0;
constructor(
private thread: Record<string, unknown>,
private readonly failIncludeTurnsUntilMaterialized = false,
) {}
async request(method: string, params?: Record<string, unknown>): Promise<unknown> {
this.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [this.thread.id].filter((id) => typeof id === "string"), nextCursor: null };
}
if (method === "thread/list") {
return { threads: [this.thread] };
}
if (method === "thread/read") {
if (this.failIncludeTurnsUntilMaterialized && params?.includeTurns === true) {
throw new Error(
"thread is not materialized yet; includeTurns is unavailable before first user message",
);
}
return { thread: this.thread };
}
if (method === "turn/start") {
return { turn: { id: "turn-started", status: "inProgress" } };
}
if (method === "turn/steer") {
return {};
}
if (method === "turn/interrupt") {
return {};
}
throw new Error(`unexpected method: ${method}`);
}
notify(): void {}
async close(): Promise<void> {
this.closeCount += 1;
}
}
const endpoint: CodexSupervisorEndpoint = {
id: "local",
transport: "stdio-proxy",
};
describe("loadCodexSupervisorEndpoints", () => {
it("defaults to the local app-server Unix websocket", () => {
expect(loadCodexSupervisorEndpoints({})).toEqual([
{
id: "local",
label: "local Codex app-server daemon",
transport: "websocket",
url: "unix://",
},
]);
});
it("parses websocket shorthand endpoints", () => {
expect(
loadCodexSupervisorEndpoints({
OPENCLAW_CODEX_SUPERVISOR_ENDPOINTS: "crab=ws://127.0.0.1:18080,local",
}),
).toEqual([
{
id: "crab",
transport: "websocket",
url: "ws://127.0.0.1:18080",
},
{
id: "local",
label: "local Codex app-server daemon",
transport: "websocket",
url: "unix://",
},
]);
});
it("keeps equals signs inside endpoint URLs", () => {
expect(
loadCodexSupervisorEndpoints({
OPENCLAW_CODEX_SUPERVISOR_ENDPOINTS: "prod=wss://example.invalid/control?token=a=b&next=c",
}),
).toEqual([
{
id: "prod",
transport: "websocket",
url: "wss://example.invalid/control?token=a=b&next=c",
},
]);
});
it("does not derive generated endpoint ids from secret-bearing URLs", () => {
expect(
loadCodexSupervisorEndpoints({
OPENCLAW_CODEX_SUPERVISOR_ENDPOINTS: "wss://user:secret@example.invalid/control?token=a=b",
}),
).toEqual([
{
id: "endpoint-1",
transport: "websocket",
url: "wss://user:secret@example.invalid/control?token=a=b",
},
]);
expect(
loadCodexSupervisorEndpoints({
OPENCLAW_CODEX_SUPERVISOR_ENDPOINTS: JSON.stringify([
{
transport: "websocket",
url: "wss://example.invalid/control?token=secret",
},
]),
}),
).toEqual([
{
id: "endpoint-1",
transport: "websocket",
url: "wss://example.invalid/control?token=secret",
},
]);
});
it("rejects duplicate normalized endpoint ids", () => {
expect(() =>
loadCodexSupervisorEndpoints({
OPENCLAW_CODEX_SUPERVISOR_ENDPOINTS: "fleet/a=ws://one.invalid,fleet-a=ws://two.invalid",
}),
).toThrow("duplicate Codex supervisor endpoint id: fleet-a");
expect(() =>
resolveCodexSupervisorPluginConfig({
endpoints: [
{ id: "fleet/a", transport: "websocket", url: "ws://one.invalid" },
{ id: "fleet-a", transport: "websocket", url: "ws://two.invalid" },
],
}),
).toThrow("duplicate Codex supervisor endpoint id: fleet-a");
});
it("prefers plugin-configured endpoints over environment defaults", () => {
expect(
resolveCodexSupervisorPluginConfig(
{
endpoints: [
{
id: "fleet",
transport: "websocket",
url: "wss://fleet.example.invalid/codex",
},
],
allowRawTranscripts: true,
allowWriteControls: true,
},
{
OPENCLAW_CODEX_SUPERVISOR_ENDPOINTS: "local",
},
),
).toEqual({
endpoints: [
{
id: "fleet",
transport: "websocket",
url: "wss://fleet.example.invalid/codex",
},
],
allowRawTranscripts: true,
allowWriteControls: true,
});
});
});
describe("CodexSupervisor", () => {
it("does not permanently cache failed endpoint connections", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
let attempts = 0;
const supervisor = new CodexSupervisor([endpoint], async () => {
attempts += 1;
if (attempts === 1) {
throw new Error("daemon unavailable");
}
return fake;
});
await expect(supervisor.probeEndpoints()).resolves.toEqual([
{ endpointId: "local", ok: false, detail: "daemon unavailable" },
]);
await expect(supervisor.probeEndpoints()).resolves.toEqual([{ endpointId: "local", ok: true }]);
expect(attempts).toBe(2);
});
it("lists loaded sessions", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
cwd: "/workspace",
preview: "work",
sessionId: "session-1",
source: "vscode",
status: { type: "idle" },
updatedAt: 10,
turns: [],
});
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(supervisor.listSessions()).resolves.toEqual([
{
endpointId: "local",
threadId: "thread-1",
cwd: "/workspace",
preview: "work",
sessionId: "session-1",
source: "vscode",
status: "idle",
updatedAt: 10,
humanAttached: true,
},
]);
});
it("lists loaded sessions from real app-server data responses", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
cwd: "/workspace",
status: { type: "idle" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: ["thread-1"], nextCursor: null };
}
if (method === "thread/read") {
return {
thread: { id: "thread-1", cwd: "/workspace", status: { type: "idle" }, turns: [] },
};
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(supervisor.listSessions()).resolves.toEqual([
{
endpointId: "local",
threadId: "thread-1",
cwd: "/workspace",
status: "idle",
humanAttached: true,
},
]);
});
it("hydrates loaded-only sessions without stored history", async () => {
const fake = new FakeCodexConnection({
id: "thread-live",
cwd: "/workspace",
status: { type: "active", activeFlags: [] },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: ["thread-live"], nextCursor: null };
}
if (method === "thread/read") {
return {
thread: {
id: "thread-live",
cwd: "/workspace",
status: { type: "active", activeFlags: [] },
turns: [],
},
};
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(supervisor.listSessions()).resolves.toEqual([
{
endpointId: "local",
threadId: "thread-live",
cwd: "/workspace",
status: "active",
humanAttached: true,
},
]);
expect(fake.calls.map((call) => call.method)).toEqual(["thread/loaded/list", "thread/read"]);
});
it("does not enumerate stored sessions unless requested", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "notLoaded" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(supervisor.listSessions()).resolves.toEqual([]);
expect(fake.calls.map((call) => call.method)).toEqual(["thread/loaded/list"]);
});
it("reads stored sessions from real app-server data responses", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
if (method === "thread/list") {
return {
data: [{ id: "thread-1", status: { type: "notLoaded" }, turns: [] }],
nextCursor: null,
};
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(supervisor.listSessions({ includeStored: true })).resolves.toEqual([
{
endpointId: "local",
threadId: "thread-1",
status: "notLoaded",
},
]);
expect(fake.calls.find((call) => call.method === "thread/list")?.params).toMatchObject({
sourceKinds: ["cli", "vscode", "exec", "appServer", "unknown"],
useStateDbOnly: true,
});
});
it("reads every stored session page", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
if (method === "thread/list") {
if (params?.cursor === "page-2") {
return {
data: [{ id: "thread-2", status: { type: "notLoaded" }, turns: [] }],
nextCursor: null,
};
}
return {
data: [{ id: "thread-1", status: { type: "notLoaded" }, turns: [] }],
nextCursor: "page-2",
};
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(supervisor.listSessions({ includeStored: true })).resolves.toEqual([
{
endpointId: "local",
threadId: "thread-1",
status: "notLoaded",
},
{
endpointId: "local",
threadId: "thread-2",
status: "notLoaded",
},
]);
});
it("bounds stored session pagination for large real Codex homes", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
if (method === "thread/list") {
return {
data: [
{ id: "thread-1", status: { type: "notLoaded" }, turns: [] },
{ id: "thread-2", status: { type: "notLoaded" }, turns: [] },
],
nextCursor: "page-2",
};
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.listSessions({ includeStored: true, maxStoredSessions: 1 }),
).resolves.toEqual([
{
endpointId: "local",
threadId: "thread-1",
status: "notLoaded",
},
]);
expect(fake.calls.filter((call) => call.method === "thread/list")).toEqual([
{
method: "thread/list",
params: {
limit: 1,
sourceKinds: ["cli", "vscode", "exec", "appServer", "unknown"],
useStateDbOnly: true,
},
},
]);
});
it("closes settled connections when evicting them", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/read") {
throw new Error("transport closed");
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.readSession({ endpointId: "local", threadId: "thread-1" }),
).rejects.toThrow("transport closed");
await Promise.resolve();
expect(fake.closeCount).toBe(1);
});
it("keeps listing healthy endpoints when one endpoint is down", async () => {
const downEndpoint: CodexSupervisorEndpoint = {
id: "down",
transport: "stdio-proxy",
};
const upEndpoint: CodexSupervisorEndpoint = {
id: "up",
transport: "stdio-proxy",
};
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
const supervisor = new CodexSupervisor([downEndpoint, upEndpoint], async (target) => {
if (target.id === "down") {
throw new Error("host offline");
}
return fake;
});
await expect(supervisor.listSessionSnapshot()).resolves.toEqual({
sessions: [
{
endpointId: "up",
threadId: "thread-1",
status: "idle",
humanAttached: true,
},
],
errors: [{ endpointId: "down", ok: false, detail: "host offline" }],
});
});
it("starts a new turn for idle sessions", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.sendToSession({ endpointId: "local", threadId: "thread-1", text: "continue" }),
).resolves.toMatchObject({
endpointId: "local",
threadId: "thread-1",
mode: "start",
turnId: "turn-started",
});
expect(fake.calls.at(-1)).toEqual({
method: "turn/start",
params: {
threadId: "thread-1",
input: [{ type: "text", text: "continue", text_elements: [] }],
},
});
});
it("resolves omitted endpoint ids from loaded-only sessions", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: ["thread-1"], nextCursor: null };
}
if (method === "thread/read") {
return { thread: { id: "thread-1", status: { type: "idle" }, turns: [] } };
}
if (method === "thread/list") {
return { data: [], nextCursor: null };
}
if (method === "turn/start") {
return { turn: { id: "turn-started", status: "inProgress" } };
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.sendToSession({ threadId: "thread-1", text: "continue" }),
).resolves.toMatchObject({
endpointId: "local",
threadId: "thread-1",
mode: "start",
});
});
it("uses a unique loaded endpoint match even when another endpoint is down", async () => {
const upEndpoint: CodexSupervisorEndpoint = { id: "up", transport: "stdio-proxy" };
const downEndpoint: CodexSupervisorEndpoint = { id: "down", transport: "stdio-proxy" };
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
const supervisor = new CodexSupervisor([upEndpoint, downEndpoint], async (target) => {
if (target.id === "down") {
throw new Error("host offline");
}
return fake;
});
await expect(
supervisor.sendToSession({ threadId: "thread-1", text: "continue" }),
).resolves.toMatchObject({
endpointId: "up",
threadId: "thread-1",
mode: "start",
});
});
it("resolves omitted endpoint ids by exact thread read without scanning stored pages", async () => {
const fake = new FakeCodexConnection({
id: "thread-old",
status: { type: "notLoaded" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
if (method === "thread/read" && params?.threadId === "thread-old") {
return { thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] } };
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(supervisor.readSession({ threadId: "thread-old" })).resolves.toEqual({
thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] },
});
expect(fake.calls.map((call) => call.method)).toEqual([
"thread/loaded/list",
"thread/read",
"thread/read",
]);
});
it("resolves stored threads on healthy endpoints when another endpoint is down", async () => {
const downEndpoint: CodexSupervisorEndpoint = { id: "down", transport: "stdio-proxy" };
const upEndpoint: CodexSupervisorEndpoint = { id: "up", transport: "stdio-proxy" };
const fake = new FakeCodexConnection({
id: "thread-old",
status: { type: "notLoaded" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
if (method === "thread/read" && params?.threadId === "thread-old") {
return { thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] } };
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([downEndpoint, upEndpoint], async (target) => {
if (target.id === "down") {
throw new Error("host offline");
}
return fake;
});
await expect(supervisor.readSession({ threadId: "thread-old" })).resolves.toEqual({
thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] },
});
});
it("steers active sessions when the in-progress turn is readable", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "active", activeFlags: [] },
turns: [
{ id: "turn-old", status: "completed", items: [] },
{ id: "turn-active", status: "inProgress", items: [] },
],
});
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.sendToSession({ endpointId: "local", threadId: "thread-1", text: "heads up" }),
).resolves.toEqual({
endpointId: "local",
threadId: "thread-1",
mode: "steer",
turnId: "turn-active",
status: "active",
});
expect(fake.calls.at(-1)).toEqual({
method: "turn/steer",
params: {
threadId: "thread-1",
expectedTurnId: "turn-active",
input: [{ type: "text", text: "heads up", text_elements: [] }],
},
});
});
it("steers active sessions through the live turns list fallback", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "active", activeFlags: [] },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/list") {
return {
data: [{ id: "thread-1", status: { type: "active", activeFlags: [] }, turns: [] }],
nextCursor: null,
};
}
if (method === "thread/read") {
return {
thread: {
id: "thread-1",
status: { type: "active", activeFlags: [] },
turns: [],
},
};
}
if (method === "thread/turns/list") {
return {
data: [{ id: "turn-active", status: "inProgress", items: [] }],
nextCursor: null,
};
}
if (method === "turn/steer") {
return {};
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.sendToSession({ endpointId: "local", threadId: "thread-1", text: "heads up" }),
).resolves.toEqual({
endpointId: "local",
threadId: "thread-1",
mode: "steer",
turnId: "turn-active",
status: "active",
});
});
it("fails closed when active turn id is not readable", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "active", activeFlags: [] },
turns: [],
});
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.sendToSession({ endpointId: "local", threadId: "thread-1", text: "heads up" }),
).rejects.toThrow("active but no in-progress turn is readable");
});
it("falls back to reading empty unmaterialized threads without turns", async () => {
const fake = new FakeCodexConnection(
{
id: "thread-1",
status: { type: "idle" },
turns: [],
},
true,
);
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.readSession({ endpointId: "local", threadId: "thread-1", includeTurns: true }),
).resolves.toEqual({
thread: {
id: "thread-1",
status: { type: "idle" },
turns: [],
},
});
expect(
fake.calls.filter((call) => call.method === "thread/read").map((call) => call.params),
).toEqual([
{ threadId: "thread-1", includeTurns: true },
{ threadId: "thread-1", includeTurns: false },
]);
});
});
describe("resolveSafeApprovalResult", () => {
it("returns a valid fail-closed permissions response", () => {
expect(resolveSafeApprovalResult("item/permissions/requestApproval")).toEqual({
permissions: {},
scope: "turn",
});
});
it("returns valid fail-closed responses for non-approval server requests", () => {
expect(resolveSafeApprovalResult("item/tool/call")).toEqual({
contentItems: [
{
type: "inputText",
text: "OpenClaw Codex supervisor did not register a handler for this app-server tool call.",
},
],
success: false,
});
expect(resolveSafeApprovalResult("item/tool/requestUserInput")).toEqual({ answers: {} });
expect(resolveSafeApprovalResult("mcpServer/elicitation/request")).toEqual({
action: "decline",
});
expect(resolveSafeApprovalResult("unknown/request")).toBeUndefined();
});
});
async function waitForFile(filePath: string): Promise<string> {
for (let attempt = 0; attempt < 50; attempt += 1) {
try {
return await fs.readFile(filePath, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
}
}
throw new Error(`timed out waiting for ${filePath}`);
}
describe("connectCodexAppServerEndpoint", () => {
it("rejects pending websocket requests when the supervisor closes intentionally", async () => {
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
const port = await new Promise<number>((resolve) => {
server.once("listening", () => {
const address = server.address();
resolve(typeof address === "object" && address ? address.port : 0);
});
});
const sawProbeRequest = new Promise<void>((resolve) => {
server.once("connection", (socket) => {
socket.on("message", (data) => {
const messageText =
typeof data === "string"
? data
: Array.isArray(data)
? Buffer.concat(data).toString("utf8")
: data instanceof ArrayBuffer
? Buffer.from(new Uint8Array(data)).toString("utf8")
: Buffer.from(data).toString("utf8");
const request = JSON.parse(messageText) as Record<string, unknown>;
if (request.method === "initialize") {
socket.send(JSON.stringify({ id: request.id, result: {} }));
}
if (request.method === "thread/loaded/list") {
resolve();
}
});
});
});
const supervisor = new CodexSupervisor(
[{ id: "ws", transport: "websocket", url: `ws://127.0.0.1:${port}` }],
connectCodexAppServerEndpoint,
);
const probe = supervisor.probeEndpoints();
await sawProbeRequest;
await supervisor.close();
await expect(
Promise.race([
probe,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("probe timed out")), 500);
}),
]),
).resolves.toMatchObject([{ endpointId: "ws", ok: false }]);
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
});
it("rejects malformed stdio frames instead of throwing out of band", async () => {
const markerDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-supervisor-malformed-"));
const marker = path.join(markerDir, "closed");
const script = `
const fs = require("node:fs");
const readline = require("node:readline");
process.on("SIGTERM", () => {
fs.writeFileSync(${JSON.stringify(marker)}, "closed");
process.exit(0);
});
readline.createInterface({ input: process.stdin }).on("line", () => {
process.stdout.write("not-json\\n");
});
setTimeout(() => {}, 10_000);
`;
await expect(
connectCodexAppServerEndpoint({
id: "bad",
transport: "stdio-proxy",
command: process.execPath,
args: ["-e", script],
}),
).rejects.toThrow("Malformed Codex app-server message");
await expect(waitForFile(marker)).resolves.toBe("closed");
});
it("closes stdio connections when initialization fails", async () => {
const markerDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-supervisor-init-"));
const marker = path.join(markerDir, "closed");
const script = `
const fs = require("node:fs");
const readline = require("node:readline");
process.on("SIGTERM", () => {
fs.writeFileSync(${JSON.stringify(marker)}, "closed");
process.exit(0);
});
readline.createInterface({ input: process.stdin }).on("line", (line) => {
const request = JSON.parse(line);
process.stdout.write(JSON.stringify({
id: request.id,
error: { code: -32000, message: "init failed" }
}) + "\\n");
});
setTimeout(() => {}, 10_000);
`;
await expect(
connectCodexAppServerEndpoint({
id: "bad",
transport: "stdio-proxy",
command: process.execPath,
args: ["-e", script],
}),
).rejects.toThrow("init failed");
await expect(waitForFile(marker)).resolves.toBe("closed");
});
it("fails a cached stdio connection cleanly after the child exits", async () => {
const script = `
const readline = require("node:readline");
readline.createInterface({ input: process.stdin }).on("line", (line) => {
const request = JSON.parse(line);
if (request.method === "initialize") {
process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n");
return;
}
if (request.method === "thread/loaded/list") {
process.stdout.write(JSON.stringify({ id: request.id, result: { threads: [] } }) + "\\n");
setTimeout(() => process.exit(0), 0);
}
});
`;
const supervisor = new CodexSupervisor(
[
{
id: "exits",
transport: "stdio-proxy",
command: process.execPath,
args: ["-e", script],
},
],
connectCodexAppServerEndpoint,
);
await expect(supervisor.probeEndpoints()).resolves.toEqual([{ endpointId: "exits", ok: true }]);
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
await expect(supervisor.probeEndpoints()).resolves.toMatchObject([
{
endpointId: "exits",
ok: false,
},
]);
await supervisor.close();
});
});

View File

@@ -0,0 +1,536 @@
/**
* Codex app-server supervisor that lists sessions, reads transcripts, and
* starts/steers/interrupts turns across configured endpoints.
*/
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { connectCodexAppServerEndpoint } from "./json-rpc-client.js";
import type {
CodexJsonRpcConnection,
CodexSupervisorEndpoint,
CodexSupervisorEndpointHealth,
CodexSupervisorSendResult,
CodexSupervisorSession,
CodexSupervisorSessionListResult,
CodexSupervisorThreadStatus,
CodexSupervisorTurnMode,
} from "./types.js";
type EndpointConnector = (endpoint: CodexSupervisorEndpoint) => Promise<CodexJsonRpcConnection>;
const ALL_CODEX_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer", "unknown"];
const DEFAULT_MAX_STORED_SESSIONS = 200;
function asRecordArray(value: unknown): Record<string, unknown>[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter(isRecord);
}
function extractThread(value: unknown): Record<string, unknown> | undefined {
if (!isRecord(value)) {
return undefined;
}
if (isRecord(value.thread)) {
return value.thread;
}
return undefined;
}
function extractThreadList(value: unknown): Record<string, unknown>[] {
if (!isRecord(value)) {
return [];
}
if (Array.isArray(value.data)) {
return asRecordArray(value.data);
}
if (Array.isArray(value.threads)) {
return asRecordArray(value.threads);
}
if (Array.isArray(value.loadedThreads)) {
return asRecordArray(value.loadedThreads);
}
return [];
}
function extractStringList(value: unknown): string[] {
if (!isRecord(value) || !Array.isArray(value.data)) {
return [];
}
return value.data.filter((entry) => typeof entry === "string");
}
function getStatusType(thread: Record<string, unknown>): CodexSupervisorThreadStatus {
const status = thread.status;
if (isRecord(status) && typeof status.type === "string") {
return status.type;
}
if (typeof status === "string") {
return status;
}
return "unknown";
}
function toSession(
endpointId: string,
thread: Record<string, unknown>,
humanAttached?: boolean,
): CodexSupervisorSession | undefined {
if (typeof thread.id !== "string") {
return undefined;
}
return {
endpointId,
threadId: thread.id,
status: getStatusType(thread),
...(typeof thread.sessionId === "string" ? { sessionId: thread.sessionId } : {}),
...(typeof thread.cwd === "string" ? { cwd: thread.cwd } : {}),
...(typeof thread.preview === "string" ? { preview: thread.preview } : {}),
...("name" in thread && (typeof thread.name === "string" || thread.name === null)
? { name: thread.name }
: {}),
...(typeof thread.source === "string" ? { source: thread.source } : {}),
...(typeof thread.updatedAt === "number" ? { updatedAt: thread.updatedAt } : {}),
...(humanAttached !== undefined ? { humanAttached } : {}),
};
}
function findInProgressTurnId(thread: Record<string, unknown>): string | undefined {
const turns = asRecordArray(thread.turns);
for (const turn of turns.toReversed()) {
if (turn.status === "inProgress" && typeof turn.id === "string") {
return turn.id;
}
}
return undefined;
}
function isLoadedThreadReadMiss(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return message.includes("thread not found") || message.includes("thread not loaded");
}
/** High-level supervisor facade used by OpenClaw tools and MCP tools. */
export class CodexSupervisor {
private readonly connections = new Map<string, Promise<CodexJsonRpcConnection>>();
constructor(
private readonly endpoints: CodexSupervisorEndpoint[],
private readonly connector: EndpointConnector = connectCodexAppServerEndpoint,
) {}
/** Returns configured endpoint definitions without opening connections. */
listEndpoints(): CodexSupervisorEndpoint[] {
return this.endpoints;
}
/** Closes all open app-server connections owned by this supervisor. */
async close(): Promise<void> {
const settled = await Promise.allSettled(this.connections.values());
this.connections.clear();
await Promise.all(
settled.map(async (entry) => {
if (entry.status === "fulfilled") {
await entry.value.close();
}
}),
);
}
/** Checks whether each endpoint can service a lightweight thread list call. */
async probeEndpoints(): Promise<CodexSupervisorEndpointHealth[]> {
return await Promise.all(
this.endpoints.map(async (endpoint) => {
try {
const connection = await this.connectionFor(endpoint.id);
await connection.request("thread/loaded/list", { limit: 1 });
return { endpointId: endpoint.id, ok: true };
} catch (error) {
this.forgetEndpoint(endpoint.id);
return {
endpointId: endpoint.id,
ok: false,
detail: error instanceof Error ? error.message : String(error),
};
}
}),
);
}
/** Lists sessions, returning only the session array for agent-tool callers. */
async listSessions(
params: { includeStored?: boolean; maxStoredSessions?: number } = {},
): Promise<CodexSupervisorSession[]> {
return (await this.listSessionSnapshot(params)).sessions;
}
/** Lists sessions plus endpoint errors for structured tool output. */
async listSessionSnapshot(
params: { includeStored?: boolean; maxStoredSessions?: number } = {},
): Promise<CodexSupervisorSessionListResult> {
const sessions: CodexSupervisorSession[] = [];
const errors: CodexSupervisorEndpointHealth[] = [];
for (const endpoint of this.endpoints) {
try {
sessions.push(...(await this.listEndpointSessions(endpoint, params)));
} catch (error) {
this.forgetEndpoint(endpoint.id);
errors.push({
endpointId: endpoint.id,
ok: false,
detail: error instanceof Error ? error.message : String(error),
});
}
}
return { sessions, errors };
}
/** Reads a single Codex session transcript from the resolved endpoint. */
async readSession(params: {
endpointId?: string;
threadId: string;
includeTurns?: boolean;
}): Promise<Record<string, unknown>> {
const endpointId = await this.resolveEndpointId(params);
const connection = await this.connectionFor(endpointId);
try {
const result = await this.readThread(
connection,
params.threadId,
params.includeTurns === true,
);
if (!isRecord(result)) {
throw new Error("Codex thread/read returned a non-object response");
}
return result;
} catch (error) {
this.forgetEndpoint(endpointId);
throw error;
}
}
/** Starts a new turn or steers an active turn depending on requested mode. */
async sendToSession(params: {
endpointId?: string;
threadId: string;
text: string;
mode?: CodexSupervisorTurnMode;
}): Promise<CodexSupervisorSendResult> {
const endpointId = await this.resolveEndpointId(params);
const connection = await this.connectionFor(endpointId);
try {
const mode = params.mode ?? "auto";
if (mode === "start") {
return await this.startTurn(connection, endpointId, params.threadId, params.text);
}
const read = await this.readThread(connection, params.threadId, false);
const thread = extractThread(read);
if (!thread) {
throw new Error(`Codex thread not found: ${params.threadId}`);
}
const status = getStatusType(thread);
if (mode === "steer" || status === "active") {
const detailed = await this.readThread(connection, params.threadId, true);
const detailedThread = extractThread(detailed);
// Active-turn ids may appear in full thread turns or the summary API;
// try both before failing so steering handles materialized and lazy turns.
const turnId =
(detailedThread ? findInProgressTurnId(detailedThread) : undefined) ??
findInProgressTurnId(thread) ??
(await this.readActiveTurnId(connection, params.threadId));
if (!turnId) {
throw new Error(
`Codex thread ${params.threadId} is active but no in-progress turn is readable`,
);
}
await connection.request("turn/steer", {
threadId: params.threadId,
expectedTurnId: turnId,
input: [{ type: "text", text: params.text, text_elements: [] }],
});
return { endpointId, threadId: params.threadId, mode: "steer", turnId, status };
}
return await this.startTurn(connection, endpointId, params.threadId, params.text);
} catch (error) {
this.forgetEndpoint(endpointId);
throw error;
}
}
/** Interrupts an active Codex turn, resolving the turn id when omitted. */
async interruptSession(params: {
endpointId?: string;
threadId: string;
turnId?: string;
}): Promise<{ endpointId: string; threadId: string; turnId: string }> {
const endpointId = await this.resolveEndpointId(params);
const connection = await this.connectionFor(endpointId);
try {
let turnId = params.turnId;
if (!turnId) {
const read = await this.readThread(connection, params.threadId, true);
const thread = extractThread(read);
turnId =
(thread ? findInProgressTurnId(thread) : undefined) ??
(await this.readActiveTurnId(connection, params.threadId));
}
if (!turnId) {
throw new Error(`Codex thread ${params.threadId} has no readable in-progress turn`);
}
await connection.request("turn/interrupt", { threadId: params.threadId, turnId });
return { endpointId, threadId: params.threadId, turnId };
} catch (error) {
this.forgetEndpoint(endpointId);
throw error;
}
}
private async listEndpointSessions(
endpoint: CodexSupervisorEndpoint,
params: { includeStored?: boolean; maxStoredSessions?: number },
): Promise<CodexSupervisorSession[]> {
if (params.includeStored === true) {
const loaded = await this.listLoadedThreadSessions(endpoint);
const sessions = [...loaded];
for (const stored of await this.listStoredThreadSessions(
endpoint,
params.maxStoredSessions,
)) {
// Loaded sessions are authoritative for attachment/status; append stored
// history only for threads that are not already live.
if (!sessions.some((session) => session.threadId === stored.threadId)) {
sessions.push(stored);
}
}
return sessions;
}
return await this.listLoadedThreadSessions(endpoint);
}
private async listLoadedThreadSessions(
endpoint: CodexSupervisorEndpoint,
): Promise<CodexSupervisorSession[]> {
const sessions: CodexSupervisorSession[] = [];
const connection = await this.connectionFor(endpoint.id);
let cursor: string | undefined;
do {
const listed = await connection.request("thread/loaded/list", {
limit: 100,
...(cursor ? { cursor } : {}),
});
for (const threadId of extractStringList(listed)) {
if (sessions.some((entry) => entry.threadId === threadId)) {
continue;
}
const read = await this.readOptionalLoadedThread(connection, threadId);
const thread = extractThread(read);
const session = thread ? toSession(endpoint.id, thread, true) : undefined;
if (session) {
sessions.push(session);
}
}
cursor =
isRecord(listed) && typeof listed.nextCursor === "string" ? listed.nextCursor : undefined;
} while (cursor);
return sessions;
}
private async listStoredThreadSessions(
endpoint: CodexSupervisorEndpoint,
maxStoredSessions = DEFAULT_MAX_STORED_SESSIONS,
): Promise<CodexSupervisorSession[]> {
const sessionLimit = Number.isFinite(maxStoredSessions)
? Math.min(1000, Math.max(1, Math.floor(maxStoredSessions)))
: DEFAULT_MAX_STORED_SESSIONS;
const sessions: CodexSupervisorSession[] = [];
const connection = await this.connectionFor(endpoint.id);
let cursor: string | undefined;
do {
const remaining = sessionLimit - sessions.length;
if (remaining <= 0) {
break;
}
const listed = await connection.request("thread/list", {
limit: Math.min(100, remaining),
sourceKinds: ALL_CODEX_THREAD_SOURCE_KINDS,
useStateDbOnly: true,
...(cursor ? { cursor } : {}),
});
for (const thread of extractThreadList(listed)) {
if (typeof thread.id !== "string") {
continue;
}
if (
sessions.some((entry) => entry.endpointId === endpoint.id && entry.threadId === thread.id)
) {
continue;
}
const session = toSession(endpoint.id, thread);
if (session) {
sessions.push(session);
if (sessions.length >= sessionLimit) {
break;
}
}
}
cursor =
isRecord(listed) && typeof listed.nextCursor === "string" ? listed.nextCursor : undefined;
} while (cursor);
return sessions;
}
private async readOptionalLoadedThread(
connection: CodexJsonRpcConnection,
threadId: string,
): Promise<unknown> {
try {
return await this.readLoadedThread(connection, threadId, false);
} catch (error) {
if (isLoadedThreadReadMiss(error)) {
return undefined;
}
throw error;
}
}
private async readLoadedThread(
connection: CodexJsonRpcConnection,
threadId: string,
includeTurns: boolean,
): Promise<unknown> {
try {
return await connection.request("thread/read", { threadId, includeTurns });
} catch (error) {
if (!includeTurns) {
throw error;
}
const message = error instanceof Error ? error.message : String(error);
if (!message.includes("not materialized yet")) {
throw error;
}
return await connection.request("thread/read", { threadId, includeTurns: false });
}
}
private async startTurn(
connection: CodexJsonRpcConnection,
endpointId: string,
threadId: string,
text: string,
): Promise<CodexSupervisorSendResult> {
const result = await connection.request("turn/start", {
threadId,
input: [{ type: "text", text, text_elements: [] }],
});
const turn = isRecord(result) && isRecord(result.turn) ? result.turn : undefined;
return {
endpointId,
threadId,
mode: "start",
...(typeof turn?.id === "string" ? { turnId: turn.id } : {}),
...(typeof turn?.status === "string" ? { status: turn.status } : {}),
};
}
private async readThread(
connection: CodexJsonRpcConnection,
threadId: string,
includeTurns: boolean,
): Promise<unknown> {
return await this.readLoadedThread(connection, threadId, includeTurns);
}
private async readActiveTurnId(
connection: CodexJsonRpcConnection,
threadId: string,
): Promise<string | undefined> {
try {
const response = await connection.request("thread/turns/list", {
threadId,
limit: 10,
sortDirection: "desc",
itemsView: "summary",
});
return extractThreadList(response).find(
(turn) => turn.status === "inProgress" && typeof turn.id === "string",
)?.id as string | undefined;
} catch {
return undefined;
}
}
private async resolveEndpointId(params: {
endpointId?: string;
threadId: string;
}): Promise<string> {
if (params.endpointId) {
return params.endpointId;
}
const sessions = await this.listSessions();
const matches = sessions.filter((session) => session.threadId === params.threadId);
if (matches.length === 1) {
return matches[0].endpointId;
}
if (matches.length > 1) {
throw new Error(`Codex thread id is ambiguous across endpoints: ${params.threadId}`);
}
const endpointIds = new Set(matches.map((match) => match.endpointId));
for (const endpoint of this.endpoints) {
if (endpointIds.has(endpoint.id)) {
continue;
}
try {
const connection = await this.connectionFor(endpoint.id);
const read = await this.readThread(connection, params.threadId, false);
const thread = extractThread(read);
if (thread?.id === params.threadId) {
endpointIds.add(endpoint.id);
}
} catch (error) {
if (isLoadedThreadReadMiss(error)) {
continue;
}
this.forgetEndpoint(endpoint.id);
continue;
}
}
if (endpointIds.size === 1) {
for (const endpointId of endpointIds) {
return endpointId;
}
}
if (endpointIds.size > 1) {
throw new Error(`Codex thread id is ambiguous across endpoints: ${params.threadId}`);
}
throw new Error(`Codex thread not found: ${params.threadId}`);
}
private async connectionFor(endpointId: string): Promise<CodexJsonRpcConnection> {
const endpoint = this.endpoints.find((entry) => entry.id === endpointId);
if (!endpoint) {
throw new Error(`Unknown Codex supervisor endpoint: ${endpointId}`);
}
const existing = this.connections.get(endpoint.id);
if (existing) {
return await existing;
}
const created = this.connector(endpoint);
this.connections.set(endpoint.id, created);
void created.catch(() => {
if (this.connections.get(endpoint.id) === created) {
this.connections.delete(endpoint.id);
}
});
return await created;
}
private forgetEndpoint(endpointId: string): void {
const existing = this.connections.get(endpointId);
if (!existing) {
return;
}
this.connections.delete(endpointId);
void existing.then((connection) => connection.close()).catch(() => undefined);
}
}

View File

@@ -0,0 +1,69 @@
/**
* Public Codex Supervisor endpoint, session, and JSON-RPC connection types.
*/
/** Configured transport target for a Codex app-server endpoint. */
export type CodexSupervisorEndpoint =
| {
id: string;
label?: string;
transport: "stdio-proxy";
command?: string;
args?: string[];
cwd?: string;
}
| {
id: string;
label?: string;
transport: "websocket";
url: string;
authTokenEnv?: string;
};
/** Send behavior requested by supervisor write tools. */
export type CodexSupervisorTurnMode = "auto" | "start" | "steer";
/** App-server thread status string, preserved for forward compatibility. */
export type CodexSupervisorThreadStatus = string;
/** Normalized session summary returned by supervisor list operations. */
export type CodexSupervisorSession = {
endpointId: string;
threadId: string;
sessionId?: string;
cwd?: string;
preview?: string;
name?: string | null;
source?: string;
status: CodexSupervisorThreadStatus;
updatedAt?: number;
humanAttached?: boolean;
};
/** Result returned after starting or steering a Codex turn. */
export type CodexSupervisorSendResult = {
endpointId: string;
threadId: string;
mode: "start" | "steer";
turnId?: string;
status?: string;
};
/** Minimal JSON-RPC connection contract used by the supervisor. */
export type CodexJsonRpcConnection = {
request(method: string, params?: Record<string, unknown>): Promise<unknown>;
notify(method: string, params?: Record<string, unknown>): void;
close(): Promise<void>;
};
/** Health result for one configured supervisor endpoint. */
export type CodexSupervisorEndpointHealth = {
endpointId: string;
ok: boolean;
detail?: string;
};
/** Session list plus endpoint errors for tool-friendly structured output. */
export type CodexSupervisorSessionListResult = {
sessions: CodexSupervisorSession[];
errors: CodexSupervisorEndpointHealth[];
};