docs: document codex sandbox exec server

This commit is contained in:
Peter Steinberger
2026-06-04 08:39:30 -04:00
parent 29e9625b18
commit 226f5ac17f
2 changed files with 25 additions and 0 deletions

View File

@@ -1,3 +1,7 @@
/**
* Test helpers for standing up fake sandbox contexts and driving the Codex
* sandbox exec-server JSON-RPC/WebSocket protocol.
*/
import type { SandboxContext } from "openclaw/plugin-sdk/sandbox";
import { vi } from "vitest";
import WebSocket from "ws";
@@ -8,6 +12,7 @@ type RpcResponse = {
error?: { message: string };
};
/** Builds a minimal enabled sandbox context with overridable backend and fs bridge hooks. */
export function createSandboxContext(overrides: {
buildExecSpec?: NonNullable<SandboxContext["backend"]>["buildExecSpec"];
finalizeExec?: NonNullable<SandboxContext["backend"]>["finalizeExec"];
@@ -72,6 +77,7 @@ export function createSandboxContext(overrides: {
} as unknown as SandboxContext;
}
/** Creates a fake Codex app-server client with a configurable server version. */
export function createClient(options: { serverVersion?: string } = {}) {
return {
getServerVersion: vi.fn(() => options.serverVersion ?? "0.132.0"),
@@ -79,6 +85,7 @@ export function createClient(options: { serverVersion?: string } = {}) {
};
}
/** Reads the registered exec-server URL from a fake client's environment/add call. */
export function execServerUrlFromClient(
client: ReturnType<typeof createClient>,
callIndex = 0,
@@ -94,6 +101,7 @@ export function execServerUrlFromClient(
return execServerUrl;
}
/** Builds a Codex-style managed filesystem sandbox context for RPC params. */
export function codexFsSandboxContext(params: {
entries: Array<{ path: unknown; access: "read" | "write" | "none" | "deny" }>;
cwd?: string;
@@ -114,6 +122,7 @@ export function codexFsSandboxContext(params: {
};
}
/** Builds a Codex filesystem special-path selector for tests. */
export function specialPath(kind: string, subpath?: string): unknown {
return {
type: "special",
@@ -124,6 +133,7 @@ export function specialPath(kind: string, subpath?: string): unknown {
};
}
/** Builds a Codex filesystem glob-path selector for tests. */
export function globPath(pattern: string): unknown {
return {
type: "glob_pattern",
@@ -131,6 +141,7 @@ export function globPath(pattern: string): unknown {
};
}
/** Opens a WebSocket connection and resolves only after the socket is ready. */
export function openSocket(url: string): Promise<WebSocket> {
return new Promise((resolve, reject) => {
const socket = new WebSocket(url);
@@ -139,6 +150,7 @@ export function openSocket(url: string): Promise<WebSocket> {
});
}
/** Collects server-originated JSON-RPC notifications seen by a WebSocket. */
export function collectNotifications(
socket: WebSocket,
): Array<{ method: string; params?: unknown }> {
@@ -156,6 +168,7 @@ export function collectNotifications(
return notifications;
}
/** Polls process/read until the managed sandbox process reports closed. */
export async function readUntilClosed(
socket: WebSocket,
processId: string,
@@ -189,12 +202,14 @@ export async function readUntilClosed(
throw new Error(`process ${processId} did not close`);
}
/** Resolves with the WebSocket close code once the socket closes. */
export function waitForSocketClose(socket: WebSocket): Promise<{ code: number }> {
return new Promise((resolve) => {
socket.once("close", (code) => resolve({ code }));
});
}
/** Waits until the requested number of streaming HTTP body-delta notifications arrive. */
export async function waitForHttpBodyDeltas(
notifications: Array<{ method: string; params?: unknown }>,
count: number,
@@ -213,10 +228,12 @@ export async function waitForHttpBodyDeltas(
throw new Error(`expected ${count} http body deltas`);
}
/** Quotes a value for POSIX shell snippets embedded in sandbox test commands. */
export function shellQuote(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
/** Sends one JSON-RPC request and resolves/rejects from the matching response id. */
export function rpc(socket: WebSocket, method: string, params: unknown): Promise<unknown> {
const id = Math.floor(Math.random() * 1_000_000);
return new Promise((resolve, reject) => {

View File

@@ -1,3 +1,7 @@
/**
* Hosts the local OpenClaw sandbox exec-server that Codex app-server native
* execution can register as an external environment.
*/
import { createHash, randomUUID } from "node:crypto";
import { once } from "node:events";
import type { IncomingMessage } from "node:http";
@@ -37,6 +41,7 @@ import type {
} from "./sandbox-exec-server/types.js";
import { MIN_CODEX_SANDBOX_EXEC_SERVER_APP_SERVER_VERSION } from "./version.js";
/** Codex environment metadata registered for one sandbox exec-server lease. */
export type CodexSandboxExecEnvironment = {
environmentId: string;
cwd: string;
@@ -44,6 +49,7 @@ export type CodexSandboxExecEnvironment = {
const SANDBOX_EXEC_SERVERS = new Map<string, Promise<OpenClawExecServer>>();
/** Closes all cached sandbox exec-server instances for deterministic tests. */
export async function closeCodexSandboxExecServersForTests(): Promise<void> {
const servers = await Promise.allSettled(SANDBOX_EXEC_SERVERS.values());
SANDBOX_EXEC_SERVERS.clear();
@@ -57,6 +63,7 @@ export async function closeCodexSandboxExecServersForTests(): Promise<void> {
);
}
/** Starts or reuses a sandbox exec-server and registers it with Codex app-server. */
export async function ensureCodexSandboxExecServerEnvironment(params: {
client: CodexAppServerClient;
sandbox: SandboxContext | null;
@@ -99,6 +106,7 @@ export async function ensureCodexSandboxExecServerEnvironment(params: {
};
}
/** Releases the sandbox exec-server lease associated with a sandbox runtime. */
export async function releaseCodexSandboxExecServerEnvironment(
sandbox: SandboxContext | null | undefined,
): Promise<void> {