docs: document cloudflare and codex supervisor plugins

This commit is contained in:
Peter Steinberger
2026-06-04 08:14:22 -04:00
parent 802cdc7783
commit 8b477d2887
15 changed files with 173 additions and 0 deletions

View File

@@ -1,3 +1,7 @@
/**
* Public Cloudflare AI Gateway provider helpers shared by onboarding, catalog,
* and tests.
*/
export {
buildCloudflareAiGatewayModelDefinition,
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID,

View File

@@ -1,3 +1,7 @@
/**
* Builds runtime model catalog entries from stored Cloudflare AI Gateway auth
* profiles.
*/
import {
coerceSecretRef,
resolveNonEnvSecretRefApiKeyMarker,
@@ -46,6 +50,10 @@ function resolveCloudflareAiGatewayMetadata(cred: CloudflareAiGatewayCredential)
};
}
/**
* Returns a provider catalog entry when credentials and Gateway metadata are
* complete enough to construct an Anthropic-compatible base URL.
*/
export function buildCloudflareAiGatewayCatalogProvider(params: {
credential: CloudflareAiGatewayCredential;
envApiKey?: string;

View File

@@ -1,3 +1,7 @@
/**
* Bundled provider plugin entry for Cloudflare AI Gateway setup, catalog
* discovery, failover classification, and stream wrapping.
*/
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import {
applyAuthProfileConfig,
@@ -96,6 +100,8 @@ export default definePluginEntry({
let capturedSecretInput: Parameters<typeof buildApiKeyCredential>[1] = "";
let capturedCredential = false;
let capturedMode: "plaintext" | "ref" | undefined;
// Capture through the shared provider auth helper so plaintext,
// env refs, and secret refs keep the same validation path.
await ensureApiKeyFromOptionEnvOrPrompt({
token: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayApiKey),
tokenProvider: "cloudflare-ai-gateway",
@@ -178,6 +184,8 @@ export default definePluginEntry({
return null;
}
if (resolved.source !== "profile") {
// Persist newly supplied credentials with Gateway metadata; a
// profile-sourced key already owns its existing auth-store record.
const credential = ctx.toApiKeyCredential({
provider: PROVIDER_ID,
resolved,

View File

@@ -1,7 +1,14 @@
/**
* Model ids, default model metadata, and URL construction for the Cloudflare AI
* Gateway provider.
*/
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
/** Provider id used in model refs and auth profiles. */
export const CLOUDFLARE_AI_GATEWAY_PROVIDER_ID = "cloudflare-ai-gateway";
/** Default Cloudflare AI Gateway model id exposed by the bundled provider. */
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID = "claude-sonnet-4-6";
/** Fully-qualified default model ref used by onboarding. */
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF = `${CLOUDFLARE_AI_GATEWAY_PROVIDER_ID}/${CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID}`;
const CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW = 200_000;
@@ -13,6 +20,10 @@ const CLOUDFLARE_AI_GATEWAY_DEFAULT_COST = {
cacheWrite: 3.75,
};
/**
* Builds a provider model definition, allowing tests/catalog code to override
* the model id while preserving Cloudflare defaults.
*/
export function buildCloudflareAiGatewayModelDefinition(params?: {
id?: string;
name?: string;
@@ -31,6 +42,10 @@ export function buildCloudflareAiGatewayModelDefinition(params?: {
};
}
/**
* Constructs the Anthropic Messages base URL for a Cloudflare account/gateway
* pair, returning an empty string for incomplete metadata.
*/
export function resolveCloudflareAiGatewayBaseUrl(params: {
accountId: string;
gatewayId: string;

View File

@@ -1,3 +1,7 @@
/**
* Config patch helpers used by Cloudflare AI Gateway interactive and
* non-interactive onboarding flows.
*/
import {
applyAgentDefaultModelPrimary,
applyProviderConfigWithDefaultModel,
@@ -9,6 +13,9 @@ import {
resolveCloudflareAiGatewayBaseUrl,
} from "./models.js";
/**
* Builds the minimal config patch for provider setup and default model aliasing.
*/
export function buildCloudflareAiGatewayConfigPatch(params: {
accountId: string;
gatewayId: string;
@@ -36,6 +43,9 @@ export function buildCloudflareAiGatewayConfigPatch(params: {
};
}
/**
* Applies provider model config while preserving existing agent model aliases.
*/
export function applyCloudflareAiGatewayProviderConfig(
cfg: OpenClawConfig,
params?: { accountId?: string; gatewayId?: string },
@@ -80,6 +90,9 @@ export function applyCloudflareAiGatewayProviderConfig(
});
}
/**
* Applies Cloudflare AI Gateway config and makes its default model primary.
*/
export function applyCloudflareAiGatewayConfig(
cfg: OpenClawConfig,
params?: { accountId?: string; gatewayId?: string },

View File

@@ -1,3 +1,7 @@
/**
* Stream wrapper for Cloudflare AI Gateway's Anthropic Messages compatibility
* quirks.
*/
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import { createAnthropicThinkingPrefillPayloadWrapper } from "openclaw/plugin-sdk/provider-stream-shared";
@@ -9,6 +13,10 @@ function shouldPatchAnthropicMessagesPayload(model: ProviderWrapStreamFnContext[
return model?.api === undefined || model.api === "anthropic-messages";
}
/**
* Creates a wrapper that removes trailing assistant prefill messages before
* extended-thinking Anthropic requests are sent through Cloudflare.
*/
export function createCloudflareAiGatewayAnthropicThinkingPrefillWrapper(
baseStreamFn: StreamFn | undefined,
): StreamFn {
@@ -19,6 +27,9 @@ export function createCloudflareAiGatewayAnthropicThinkingPrefillWrapper(
});
}
/**
* Applies the Anthropic payload wrapper only for Anthropic-compatible models.
*/
export function wrapCloudflareAiGatewayProviderStream(
ctx: ProviderWrapStreamFnContext,
): StreamFn | undefined {
@@ -28,5 +39,6 @@ export function wrapCloudflareAiGatewayProviderStream(
return createCloudflareAiGatewayAnthropicThinkingPrefillWrapper(ctx.streamFn);
}
/** Test-only access to wrapper decisions and logger injection points. */
export const testing = { log, shouldPatchAnthropicMessagesPayload };
export { testing as __testing };

View File

@@ -1,3 +1,7 @@
/**
* Bundled plugin entry that exposes Codex app-server supervisor tools to
* OpenClaw agents.
*/
import { buildJsonPluginConfigSchema, definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import {
CodexSupervisorPluginConfigSchema,

View File

@@ -1,3 +1,7 @@
/**
* Public Codex Supervisor API barrel for plugin tools, MCP serving, config, and
* session types.
*/
export {
CodexSupervisorPluginConfigSchema,
loadCodexSupervisorEndpoints,

View File

@@ -1,3 +1,6 @@
/**
* Config parsing for Codex Supervisor endpoints and safety gates.
*/
import { Type, type Static } from "typebox";
import type { CodexSupervisorEndpoint } from "./types.js";
@@ -26,6 +29,9 @@ const WebSocketEndpointSchema = Type.Object(
{ additionalProperties: false },
);
/**
* Plugin config schema accepted by the bundled plugin manifest.
*/
export const CodexSupervisorPluginConfigSchema = Type.Object(
{
endpoints: Type.Optional(
@@ -37,8 +43,10 @@ export const CodexSupervisorPluginConfigSchema = Type.Object(
{ 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;
@@ -140,6 +148,10 @@ function endpointFromToken(token: string, index: number): CodexSupervisorEndpoin
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[] {
@@ -185,6 +197,9 @@ function normalizeConfiguredEndpoints(
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,

View File

@@ -1,3 +1,7 @@
/**
* 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";
@@ -28,6 +32,10 @@ function formatMalformedMessageError(error: unknown): 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 {
@@ -121,6 +129,8 @@ abstract class BaseCodexJsonRpcConnection implements CodexJsonRpcConnection {
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
@@ -339,6 +349,10 @@ class WebSocketCodexJsonRpcConnection extends BaseCodexJsonRpcConnection {
}
}
/**
* Opens, initializes, and returns a JSON-RPC connection for one supervisor
* endpoint.
*/
export async function connectCodexAppServerEndpoint(
endpoint: CodexSupervisorEndpoint,
): Promise<CodexJsonRpcConnection> {

View File

@@ -1,3 +1,7 @@
/**
* 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";
@@ -18,11 +22,15 @@ function routeLogsToStderr(): void {
}
}
/** 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;
@@ -41,6 +49,10 @@ export function createCodexSupervisorMcpServer(opts: CodexSupervisorMcpServeOpti
};
}
/**
* Serves Codex Supervisor tools over MCP stdio until transport or process
* shutdown.
*/
export async function serveCodexSupervisorMcp(
opts: CodexSupervisorMcpServeOptions = {},
): Promise<void> {
@@ -63,6 +75,8 @@ export async function serveCodexSupervisorMcp(
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);
};

View File

@@ -1,3 +1,7 @@
/**
* 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";
@@ -7,9 +11,12 @@ import type {
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;
@@ -36,6 +43,10 @@ function redactString(value: string): string {
.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)) {
@@ -74,6 +85,7 @@ function redactEndpointUrl(value: string): string {
}
}
/** Returns endpoint metadata safe for tool results. */
export function redactCodexSupervisorEndpoint(
endpoint: CodexSupervisorEndpoint,
): Record<string, unknown> {
@@ -115,6 +127,10 @@ function sanitizeSessionForMcp(
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(),
@@ -129,6 +145,10 @@ export function sanitizeCodexSupervisorSessionListResult(
};
}
/**
* Registers MCP tools for endpoint probing, session listing, reads, sends, and
* interrupts.
*/
export function registerCodexSupervisorMcpTools(
server: McpServer,
supervisor: CodexSupervisor,

View File

@@ -1,3 +1,7 @@
/**
* 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 {
@@ -48,11 +52,13 @@ const SessionInterruptParamsSchema = Type.Object(
{ 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;
@@ -105,6 +111,10 @@ function requireWriteAccess(policy: CodexSupervisorToolPolicy): void {
}
}
/**
* Creates the OpenClaw tools that expose Codex endpoint health and session
* controls.
*/
export function createCodexSupervisorTools({
supervisor,
policy,
@@ -151,6 +161,8 @@ export function createCodexSupervisorTools({
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 });
@@ -172,6 +184,8 @@ export function createCodexSupervisorTools({
"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({

View File

@@ -1,3 +1,7 @@
/**
* Codex app-server supervisor that lists sessions, reads transcripts, and
* starts/steers/interrupts turns across configured endpoints.
*/
import { connectCodexAppServerEndpoint } from "./json-rpc-client.js";
import type {
CodexJsonRpcConnection,
@@ -109,6 +113,7 @@ function isLoadedThreadReadMiss(error: unknown): boolean {
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>>();
@@ -117,10 +122,12 @@ export class CodexSupervisor {
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();
@@ -133,6 +140,7 @@ export class CodexSupervisor {
);
}
/** Checks whether each endpoint can service a lightweight thread list call. */
async probeEndpoints(): Promise<CodexSupervisorEndpointHealth[]> {
return await Promise.all(
this.endpoints.map(async (endpoint) => {
@@ -152,12 +160,14 @@ export class CodexSupervisor {
);
}
/** 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> {
@@ -178,6 +188,7 @@ export class CodexSupervisor {
return { sessions, errors };
}
/** Reads a single Codex session transcript from the resolved endpoint. */
async readSession(params: {
endpointId?: string;
threadId: string;
@@ -201,6 +212,7 @@ export class CodexSupervisor {
}
}
/** Starts a new turn or steers an active turn depending on requested mode. */
async sendToSession(params: {
endpointId?: string;
threadId: string;
@@ -224,6 +236,8 @@ export class CodexSupervisor {
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) ??
@@ -247,6 +261,7 @@ export class CodexSupervisor {
}
}
/** Interrupts an active Codex turn, resolving the turn id when omitted. */
async interruptSession(params: {
endpointId?: string;
threadId: string;
@@ -285,6 +300,8 @@ export class CodexSupervisor {
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);
}

View File

@@ -1,3 +1,7 @@
/**
* Public Codex Supervisor endpoint, session, and JSON-RPC connection types.
*/
/** Configured transport target for a Codex app-server endpoint. */
export type CodexSupervisorEndpoint =
| {
id: string;
@@ -15,10 +19,13 @@ export type CodexSupervisorEndpoint =
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;
@@ -32,6 +39,7 @@ export type CodexSupervisorSession = {
humanAttached?: boolean;
};
/** Result returned after starting or steering a Codex turn. */
export type CodexSupervisorSendResult = {
endpointId: string;
threadId: string;
@@ -40,18 +48,21 @@ export type CodexSupervisorSendResult = {
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[];