openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,024 lines (1,023 loc) • 36.9 kB
JavaScript
import { b as readStringParam, l as jsonResult } from "../../common-C4yy9V-D.js";
import { t as buildJsonPluginConfigSchema } from "../../config-schema-D24DJjjb.js";
import { t as definePluginEntry } from "../../plugin-entry-C7DUzV0e.js";
import "../../core-DSxVv-v1.js";
import * as path$1 from "node:path";
import * as os$1 from "node:os";
import { spawn } from "node:child_process";
import * as net$1 from "node:net";
import { randomUUID } from "node:crypto";
import WebSocket from "ws";
import { Type } from "typebox";
//#region extensions/codex-supervisor/src/config.ts
/**
* Config parsing for Codex Supervisor endpoints and safety gates.
*/
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.
*/
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 });
function normalizeEndpointId(value, index) {
const trimmed = value.trim();
if (trimmed) return trimmed.replace(/[^a-zA-Z0-9_.:-]/g, "-");
return `endpoint-${index + 1}`;
}
function isRecord$2(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function parseEndpointRecord(value, index) {
if (!isRecord$2(value)) return;
const transport = typeof value.transport === "string" ? value.transport : void 0;
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 : void 0;
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 === void 0) {
const args = Array.isArray(value.args) ? value.args.filter((entry) => typeof entry === "string") : void 0;
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 } : {}
};
}
}
function requireUniqueEndpointIds(endpoints) {
const seen = /* @__PURE__ */ new Set();
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, index) {
const trimmed = token.trim();
if (!trimmed) return;
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) : void 0;
if (url?.startsWith("ws://") || url?.startsWith("wss://") || url?.startsWith("unix://")) return {
id: normalizeEndpointId(id ?? "", index),
transport: "websocket",
url
};
}
/**
* Loads endpoint definitions from environment, defaulting to the local Codex
* app-server unix socket.
*/
function loadCodexSupervisorEndpoints(env = process.env) {
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);
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) => Boolean(entry)));
}
return requireUniqueEndpointIds(raw.split(",").map(endpointFromToken).filter((entry) => Boolean(entry)));
}
function normalizeConfiguredEndpoints(endpoints) {
if (!endpoints || endpoints.length === 0) return;
const normalized = endpoints.map((entry, index) => parseEndpointRecord(entry, index)).filter((entry) => Boolean(entry));
return normalized.length > 0 ? requireUniqueEndpointIds(normalized) : void 0;
}
/**
* Resolves raw plugin config and env endpoints into validated runtime config.
*/
function resolveCodexSupervisorPluginConfig(rawConfig, env = process.env) {
const config = rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) ? rawConfig : {};
return {
endpoints: normalizeConfiguredEndpoints(config.endpoints) ?? loadCodexSupervisorEndpoints(env),
allowRawTranscripts: config.allowRawTranscripts === true,
allowWriteControls: config.allowWriteControls === true
};
}
//#endregion
//#region extensions/codex-supervisor/src/mcp-tools.ts
/** Env gate for exposing transcript-derived fields through standalone MCP. */
const RAW_TRANSCRIPTS_ENV = "OPENCLAW_CODEX_SUPERVISOR_ALLOW_RAW_TRANSCRIPTS";
function redactString(value) {
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.
*/
function redactCodexSupervisorValue(value, key = "") {
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).map(([entryKey, entryValue]) => [entryKey, redactCodexSupervisorValue(entryValue, entryKey)]));
}
function redactEndpointUrl(value) {
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. */
function redactCodexSupervisorEndpoint(endpoint) {
return {
id: endpoint.id,
transport: endpoint.transport,
...endpoint.label ? { label: endpoint.label } : {},
...endpoint.transport === "websocket" ? { url: redactEndpointUrl(endpoint.url) } : {}
};
}
function rawTranscriptReadsAllowed() {
return process.env[RAW_TRANSCRIPTS_ENV] === "1";
}
function sanitizeSessionForMcp(session, includeTranscriptDerivedFields) {
const sanitized = redactCodexSupervisorValue(session);
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.
*/
function sanitizeCodexSupervisorSessionListResult(result, includeTranscriptDerivedFields = rawTranscriptReadsAllowed()) {
return {
sessions: result.sessions.map((session) => sanitizeSessionForMcp(session, includeTranscriptDerivedFields)),
errors: includeTranscriptDerivedFields ? redactCodexSupervisorValue(result.errors) : result.errors.map(({ endpointId, ok }) => ({
endpointId,
ok
}))
};
}
//#endregion
//#region extensions/codex-supervisor/src/plugin-tools.ts
/**
* OpenClaw agent-tool definitions for Codex Supervisor endpoint and session
* controls.
*/
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: 1e3
}))
}, { 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 });
function asRecord(params) {
return params && typeof params === "object" && !Array.isArray(params) ? params : {};
}
function readBooleanParam(params, key) {
return params[key] === true;
}
function readIntegerParam(params, key) {
const value = params[key];
if (value === void 0) return;
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${key} must be an integer`);
if (value < 1 || value > 1e3) throw new Error(`${key} must be between 1 and 1000`);
return value;
}
function readModeParam(params) {
const mode = readStringParam(params, "mode");
if (!mode) return;
if (mode === "auto" || mode === "start" || mode === "steer") return mode;
throw new Error("mode must be auto, start, or steer");
}
function requireRawTranscriptAccess(policy) {
if (!policy.allowRawTranscripts) throw new Error("Codex session reads are disabled for this codex-supervisor plugin config.");
}
function requireWriteAccess(policy) {
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.
*/
function createCodexSupervisorTools({ supervisor, policy }) {
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) => {
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) => {
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
});
}
}
];
}
//#endregion
//#region extensions/codex-supervisor/src/json-rpc-client.ts
/**
* JSON-RPC transports for Codex app-server connections over stdio proxies or
* websocket/unix-socket endpoints.
*/
function isRecord$1(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function formatJsonRpcError(message) {
const error = isRecord$1(message.error) ? message.error : {};
const detail = typeof error.message === "string" ? error.message : "Codex app-server request failed";
return new Error(detail);
}
function formatMalformedMessageError(error) {
const detail = error instanceof Error ? error.message : String(error);
return /* @__PURE__ */ new Error(`Malformed Codex app-server message: ${detail}`);
}
/**
* Produces denial responses for app-server approval requests the supervisor
* deliberately cannot grant.
*/
function resolveSafeApprovalResult(method) {
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" };
}
var BaseCodexJsonRpcConnection = class {
constructor() {
this.pending = /* @__PURE__ */ new Map();
}
async initialize() {
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, params) {
if (this.closedError) return Promise.reject(this.closedError);
const id = randomUUID();
const payload = {
id,
method,
params: params ?? {}
};
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending.delete(id);
reject(/* @__PURE__ */ new Error(`Codex app-server request timed out: ${method}`));
}, 6e4);
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, params) {
const payload = {
method,
params: params ?? null
};
this.sendRaw(JSON.stringify(payload));
}
handleMessage(message) {
if (!isRecord$1(message)) return;
const id = typeof message.id === "string" || typeof message.id === "number" ? message.id : void 0;
const method = typeof message.method === "string" ? message.method : void 0;
if (id !== void 0 && method) {
const result = resolveSafeApprovalResult(method);
this.sendRaw(JSON.stringify(result === void 0 ? {
id,
error: {
code: -32601,
message: `OpenClaw Codex supervisor cannot handle app-server request: ${method}`
}
} : {
id,
result
}));
return;
}
if (id !== void 0) {
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);
}
}
rejectAll(error) {
for (const [id, pending] of this.pending) {
clearTimeout(pending.timeout);
this.pending.delete(id);
pending.reject(error);
}
}
fail(error) {
this.closedError ??= error;
this.rejectAll(this.closedError);
}
};
var StdioCodexJsonRpcConnection = class extends BaseCodexJsonRpcConnection {
constructor(endpoint) {
super();
this.buffer = "";
this.stderrTail = [];
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) => this.handleStdout(chunk));
this.proc.stderr.on("data", (chunk) => {
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(/* @__PURE__ */ new Error(`Codex app-server stdio transport closed. stderr_tail=${this.stderrTail.join("\n").slice(0, 1200)}`)));
}
sendRaw(line) {
this.proc.stdin.write(`${line}\n`, (error) => {
if (error) this.fail(error);
});
}
async close() {
this.proc.stdin.end();
this.proc.kill("SIGTERM");
}
handleStdout(chunk) {
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));
} catch (error) {
this.fail(formatMalformedMessageError(error));
this.close();
return;
}
}
}
};
function defaultCodexControlSocketPath() {
const codexHome = process.env.CODEX_HOME?.trim() || path$1.join(os$1.homedir(), ".codex");
return path$1.join(codexHome, "app-server-control", "app-server-control.sock");
}
function resolveUnixWebSocketPath(url) {
return url.slice(7) || defaultCodexControlSocketPath();
}
function connectCodexSupervisorUnixSocket(url) {
return net$1.createConnection(resolveUnixWebSocketPath(url));
}
function websocketMessageToString(data) {
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");
}
var WebSocketCodexJsonRpcConnection = class extends BaseCodexJsonRpcConnection {
constructor(endpoint) {
super();
this.closing = false;
const headers = {};
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));
} catch (error) {
this.fail(formatMalformedMessageError(error));
this.close();
}
});
this.ws.once("error", (error) => this.fail(error));
this.ws.once("close", () => {
if (!this.closing) this.fail(/* @__PURE__ */ new Error("Codex app-server websocket closed"));
});
}
async ready() {
await this.openPromise;
}
sendRaw(line) {
this.ws.send(line, (error) => {
if (error) this.fail(error);
});
}
async close() {
this.closing = true;
this.fail(/* @__PURE__ */ new Error("Codex app-server websocket closed"));
if (this.ws.readyState === WebSocket.CLOSED) return;
await new Promise((resolve) => {
const timeout = setTimeout(() => {
this.ws.terminate();
resolve();
}, 1e3);
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.
*/
async function connectCodexAppServerEndpoint(endpoint) {
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(() => void 0);
throw error;
}
}
//#endregion
//#region extensions/codex-supervisor/src/supervisor.ts
/**
* Codex app-server supervisor that lists sessions, reads transcripts, and
* starts/steers/interrupts turns across configured endpoints.
*/
const ALL_CODEX_THREAD_SOURCE_KINDS = [
"cli",
"vscode",
"exec",
"appServer",
"unknown"
];
const DEFAULT_MAX_STORED_SESSIONS = 200;
function isRecord(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function asRecordArray(value) {
if (!Array.isArray(value)) return [];
return value.filter(isRecord);
}
function extractThread(value) {
if (!isRecord(value)) return;
if (isRecord(value.thread)) return value.thread;
}
function extractThreadList(value) {
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) {
if (!isRecord(value) || !Array.isArray(value.data)) return [];
return value.data.filter((entry) => typeof entry === "string");
}
function getStatusType(thread) {
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, thread, humanAttached) {
if (typeof thread.id !== "string") return;
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 !== void 0 ? { humanAttached } : {}
};
}
function findInProgressTurnId(thread) {
const turns = asRecordArray(thread.turns);
for (const turn of turns.toReversed()) if (turn.status === "inProgress" && typeof turn.id === "string") return turn.id;
}
function isLoadedThreadReadMiss(error) {
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. */
var CodexSupervisor = class {
constructor(endpoints, connector = connectCodexAppServerEndpoint) {
this.endpoints = endpoints;
this.connector = connector;
this.connections = /* @__PURE__ */ new Map();
}
/** Returns configured endpoint definitions without opening connections. */
listEndpoints() {
return this.endpoints;
}
/** Closes all open app-server connections owned by this supervisor. */
async close() {
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() {
return await Promise.all(this.endpoints.map(async (endpoint) => {
try {
await (await this.connectionFor(endpoint.id)).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 = {}) {
return (await this.listSessionSnapshot(params)).sessions;
}
/** Lists sessions plus endpoint errors for structured tool output. */
async listSessionSnapshot(params = {}) {
const sessions = [];
const errors = [];
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) {
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) {
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 thread = extractThread(await this.readThread(connection, params.threadId, false));
if (!thread) throw new Error(`Codex thread not found: ${params.threadId}`);
const status = getStatusType(thread);
if (mode === "steer" || status === "active") {
const detailedThread = extractThread(await this.readThread(connection, params.threadId, true));
const turnId = (detailedThread ? findInProgressTurnId(detailedThread) : void 0) ?? 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) {
const endpointId = await this.resolveEndpointId(params);
const connection = await this.connectionFor(endpointId);
try {
let turnId = params.turnId;
if (!turnId) {
const thread = extractThread(await this.readThread(connection, params.threadId, true));
turnId = (thread ? findInProgressTurnId(thread) : void 0) ?? 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;
}
}
async listEndpointSessions(endpoint, params) {
if (params.includeStored === true) {
const sessions = [...await this.listLoadedThreadSessions(endpoint)];
for (const stored of await this.listStoredThreadSessions(endpoint, params.maxStoredSessions)) if (!sessions.some((session) => session.threadId === stored.threadId)) sessions.push(stored);
return sessions;
}
return await this.listLoadedThreadSessions(endpoint);
}
async listLoadedThreadSessions(endpoint) {
const sessions = [];
const connection = await this.connectionFor(endpoint.id);
let cursor;
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 thread = extractThread(await this.readOptionalLoadedThread(connection, threadId));
const session = thread ? toSession(endpoint.id, thread, true) : void 0;
if (session) sessions.push(session);
}
cursor = isRecord(listed) && typeof listed.nextCursor === "string" ? listed.nextCursor : void 0;
} while (cursor);
return sessions;
}
async listStoredThreadSessions(endpoint, maxStoredSessions = DEFAULT_MAX_STORED_SESSIONS) {
const sessionLimit = Number.isFinite(maxStoredSessions) ? Math.min(1e3, Math.max(1, Math.floor(maxStoredSessions))) : DEFAULT_MAX_STORED_SESSIONS;
const sessions = [];
const connection = await this.connectionFor(endpoint.id);
let cursor;
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 : void 0;
} while (cursor);
return sessions;
}
async readOptionalLoadedThread(connection, threadId) {
try {
return await this.readLoadedThread(connection, threadId, false);
} catch (error) {
if (isLoadedThreadReadMiss(error)) return;
throw error;
}
}
async readLoadedThread(connection, threadId, includeTurns) {
try {
return await connection.request("thread/read", {
threadId,
includeTurns
});
} catch (error) {
if (!includeTurns) throw error;
if (!(error instanceof Error ? error.message : String(error)).includes("not materialized yet")) throw error;
return await connection.request("thread/read", {
threadId,
includeTurns: false
});
}
}
async startTurn(connection, endpointId, threadId, text) {
const result = await connection.request("turn/start", {
threadId,
input: [{
type: "text",
text,
text_elements: []
}]
});
const turn = isRecord(result) && isRecord(result.turn) ? result.turn : void 0;
return {
endpointId,
threadId,
mode: "start",
...typeof turn?.id === "string" ? { turnId: turn.id } : {},
...typeof turn?.status === "string" ? { status: turn.status } : {}
};
}
async readThread(connection, threadId, includeTurns) {
return await this.readLoadedThread(connection, threadId, includeTurns);
}
async readActiveTurnId(connection, threadId) {
try {
return extractThreadList(await connection.request("thread/turns/list", {
threadId,
limit: 10,
sortDirection: "desc",
itemsView: "summary"
})).find((turn) => turn.status === "inProgress" && typeof turn.id === "string")?.id;
} catch {
return;
}
}
async resolveEndpointId(params) {
if (params.endpointId) return params.endpointId;
const matches = (await this.listSessions()).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);
if (extractThread(await this.readThread(connection, params.threadId, false))?.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}`);
}
async connectionFor(endpointId) {
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);
created.catch(() => {
if (this.connections.get(endpoint.id) === created) this.connections.delete(endpoint.id);
});
return await created;
}
forgetEndpoint(endpointId) {
const existing = this.connections.get(endpointId);
if (!existing) return;
this.connections.delete(endpointId);
existing.then((connection) => connection.close()).catch(() => void 0);
}
};
//#endregion
//#region extensions/codex-supervisor/index.ts
/**
* Bundled plugin entry that exposes Codex app-server supervisor tools to
* OpenClaw agents.
*/
var codex_supervisor_default = definePluginEntry({
id: "codex-supervisor",
name: "Codex Supervisor",
description: "Supervise Codex app-server sessions from OpenClaw.",
configSchema: buildJsonPluginConfigSchema(CodexSupervisorPluginConfigSchema),
register(api) {
const config = resolveCodexSupervisorPluginConfig(api.pluginConfig);
const supervisor = new CodexSupervisor(config.endpoints);
api.lifecycle.registerRuntimeLifecycle({
id: "codex-supervisor",
description: "Close Codex supervisor app-server connections.",
cleanup: () => supervisor.close()
});
for (const tool of createCodexSupervisorTools({
supervisor,
policy: {
allowRawTranscripts: config.allowRawTranscripts,
allowWriteControls: config.allowWriteControls
}
})) api.registerTool(tool);
}
});
//#endregion
export { codex_supervisor_default as default };