mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:00:44 +00:00
Manage the Codex app-server binary in OpenClaw (#71808)
* Manage Codex app-server binary * Use plugin deps for Codex app-server binary * Stabilize media model registry test * Exclude checkpoint transcripts from memory ingestion
This commit is contained in:
@@ -57,10 +57,7 @@
|
||||
"enum": ["stdio", "websocket"],
|
||||
"default": "stdio"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"default": "codex"
|
||||
},
|
||||
"command": { "type": "string" },
|
||||
"args": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -132,7 +129,7 @@
|
||||
},
|
||||
"appServer.command": {
|
||||
"label": "Command",
|
||||
"help": "Executable used for stdio transport.",
|
||||
"help": "Executable used for stdio transport. Leave unset to use OpenClaw's managed Codex binary.",
|
||||
"advanced": true
|
||||
},
|
||||
"appServer.args": {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-coding-agent": "0.70.2",
|
||||
"@openai/codex": "0.125.0",
|
||||
"ajv": "^8.18.0",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.3.6"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createInterface, type Interface as ReadlineInterface } from "node:readline";
|
||||
import { embeddedAgentLog, OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { resolveCodexAppServerRuntimeOptions, type CodexAppServerStartOptions } from "./config.js";
|
||||
import { MIN_CODEX_APP_SERVER_VERSION } from "./version.js";
|
||||
import {
|
||||
type CodexAppServerRequestMethod,
|
||||
type CodexAppServerRequestParams,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
type CodexAppServerTransport,
|
||||
} from "./transport.js";
|
||||
|
||||
export const MIN_CODEX_APP_SERVER_VERSION = "0.125.0";
|
||||
export { MIN_CODEX_APP_SERVER_VERSION } from "./version.js";
|
||||
const CODEX_APP_SERVER_PARSE_LOG_MAX = 500;
|
||||
|
||||
type PendingRequest = {
|
||||
@@ -99,6 +100,9 @@ export class CodexAppServerClient {
|
||||
...options,
|
||||
headers: options?.headers ?? defaults.headers,
|
||||
};
|
||||
if (startOptions.transport === "stdio" && startOptions.commandSource === "managed") {
|
||||
throw new Error("Managed Codex app-server start options must be resolved before spawn.");
|
||||
}
|
||||
if (startOptions.transport === "websocket") {
|
||||
return new CodexAppServerClient(createWebSocketTransport(startOptions));
|
||||
}
|
||||
@@ -407,12 +411,12 @@ function assertSupportedCodexAppServerVersion(response: CodexInitializeResponse)
|
||||
const detectedVersion = readCodexVersionFromUserAgent(response.userAgent);
|
||||
if (!detectedVersion) {
|
||||
throw new Error(
|
||||
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but OpenClaw could not determine the running Codex version. Upgrade Codex CLI and retry.`,
|
||||
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but OpenClaw could not determine the running Codex version. Update the configured Codex app-server binary, or remove custom command overrides to use the managed binary.`,
|
||||
);
|
||||
}
|
||||
if (compareVersions(detectedVersion, MIN_CODEX_APP_SERVER_VERSION) < 0) {
|
||||
throw new Error(
|
||||
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but detected ${detectedVersion}. Upgrade Codex CLI and retry.`,
|
||||
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but detected ${detectedVersion}. Update the configured Codex app-server binary, or remove custom command overrides to use the managed binary.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,36 @@ describe("Codex app-server config", () => {
|
||||
approvalPolicy: "never",
|
||||
sandbox: "danger-full-access",
|
||||
approvalsReviewer: "user",
|
||||
start: expect.objectContaining({
|
||||
command: "codex",
|
||||
commandSource: "managed",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("treats configured and environment commands as explicit overrides", () => {
|
||||
expect(
|
||||
resolveCodexAppServerRuntimeOptions({
|
||||
pluginConfig: { appServer: { command: "/opt/codex/bin/codex" } },
|
||||
env: { OPENCLAW_CODEX_APP_SERVER_BIN: "/usr/local/bin/codex" },
|
||||
}).start,
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
command: "/opt/codex/bin/codex",
|
||||
commandSource: "config",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
resolveCodexAppServerRuntimeOptions({
|
||||
pluginConfig: {},
|
||||
env: { OPENCLAW_CODEX_APP_SERVER_BIN: "/usr/local/bin/codex" },
|
||||
}).start,
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
command: "/usr/local/bin/codex",
|
||||
commandSource: "env",
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -244,6 +274,7 @@ describe("Codex app-server config", () => {
|
||||
};
|
||||
const appServerProperties = manifest.configSchema.properties.appServer.properties;
|
||||
|
||||
expect(appServerProperties.command?.default).toBeUndefined();
|
||||
expect(appServerProperties.approvalPolicy?.default).toBeUndefined();
|
||||
expect(appServerProperties.sandbox?.default).toBeUndefined();
|
||||
expect(appServerProperties.approvalsReviewer?.default).toBeUndefined();
|
||||
|
||||
@@ -7,10 +7,12 @@ export type CodexAppServerPolicyMode = "yolo" | "guardian";
|
||||
export type CodexAppServerApprovalPolicy = "never" | "on-request" | "on-failure" | "untrusted";
|
||||
export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
||||
export type CodexAppServerApprovalsReviewer = "user" | "auto_review" | "guardian_subagent";
|
||||
export type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "env";
|
||||
|
||||
export type CodexAppServerStartOptions = {
|
||||
transport: CodexAppServerTransportMode;
|
||||
command: string;
|
||||
commandSource?: CodexAppServerCommandSource;
|
||||
args: string[];
|
||||
url?: string;
|
||||
authToken?: string;
|
||||
@@ -125,8 +127,14 @@ export function resolveCodexAppServerRuntimeOptions(
|
||||
const env = params.env ?? process.env;
|
||||
const config = readCodexPluginConfig(params.pluginConfig).appServer ?? {};
|
||||
const transport = resolveTransport(config.transport);
|
||||
const command =
|
||||
readNonEmptyString(config.command) ?? env.OPENCLAW_CODEX_APP_SERVER_BIN ?? "codex";
|
||||
const configCommand = readNonEmptyString(config.command);
|
||||
const envCommand = readNonEmptyString(env.OPENCLAW_CODEX_APP_SERVER_BIN);
|
||||
const command = configCommand ?? envCommand ?? "codex";
|
||||
const commandSource: CodexAppServerCommandSource = configCommand
|
||||
? "config"
|
||||
: envCommand
|
||||
? "env"
|
||||
: "managed";
|
||||
const args = resolveArgs(config.args, env.OPENCLAW_CODEX_APP_SERVER_ARGS);
|
||||
const headers = normalizeHeaders(config.headers);
|
||||
const authToken = readNonEmptyString(config.authToken);
|
||||
@@ -146,6 +154,7 @@ export function resolveCodexAppServerRuntimeOptions(
|
||||
start: {
|
||||
transport,
|
||||
command,
|
||||
commandSource,
|
||||
args: args.length > 0 ? args : ["app-server", "--listen", "stdio://"],
|
||||
...(url ? { url } : {}),
|
||||
...(authToken ? { authToken } : {}),
|
||||
@@ -174,6 +183,7 @@ export function codexAppServerStartOptionsKey(
|
||||
return JSON.stringify({
|
||||
transport: options.transport,
|
||||
command: options.command,
|
||||
commandSource: options.commandSource ?? null,
|
||||
args: options.args,
|
||||
url: options.url ?? null,
|
||||
authToken: hashSecretForKey(options.authToken),
|
||||
|
||||
95
extensions/codex/src/app-server/managed-binary.test.ts
Normal file
95
extensions/codex/src/app-server/managed-binary.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
import {
|
||||
resolveManagedCodexAppServerPaths,
|
||||
resolveManagedCodexAppServerStartOptions,
|
||||
} from "./managed-binary.js";
|
||||
|
||||
function startOptions(
|
||||
commandSource: CodexAppServerStartOptions["commandSource"],
|
||||
): CodexAppServerStartOptions {
|
||||
return {
|
||||
transport: "stdio",
|
||||
command: "codex",
|
||||
commandSource,
|
||||
args: ["app-server", "--listen", "stdio://"],
|
||||
headers: {},
|
||||
};
|
||||
}
|
||||
|
||||
function managedCommandPath(root: string, platform: NodeJS.Platform): string {
|
||||
return path.join(root, "node_modules", ".bin", platform === "win32" ? "codex.cmd" : "codex");
|
||||
}
|
||||
|
||||
describe("managed Codex app-server binary", () => {
|
||||
it("leaves explicit command overrides unchanged", async () => {
|
||||
const explicitOptions = startOptions("config");
|
||||
const pathExists = vi.fn(async () => false);
|
||||
|
||||
await expect(
|
||||
resolveManagedCodexAppServerStartOptions(explicitOptions, {
|
||||
platform: "darwin",
|
||||
pathExists,
|
||||
}),
|
||||
).resolves.toBe(explicitOptions);
|
||||
expect(pathExists).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves the plugin-local bundled Codex binary", async () => {
|
||||
const pluginRoot = path.join("/tmp", "openclaw", "extensions", "codex");
|
||||
const paths = resolveManagedCodexAppServerPaths({ platform: "darwin", pluginRoot });
|
||||
const pathExists = vi.fn(async (filePath: string) => filePath === paths.commandPath);
|
||||
|
||||
await expect(
|
||||
resolveManagedCodexAppServerStartOptions(startOptions("managed"), {
|
||||
platform: "darwin",
|
||||
pluginRoot,
|
||||
pathExists,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
...startOptions("managed"),
|
||||
command: paths.commandPath,
|
||||
commandSource: "resolved-managed",
|
||||
});
|
||||
expect(paths.commandPath).toBe(managedCommandPath(pluginRoot, "darwin"));
|
||||
});
|
||||
|
||||
it("resolves Windows Codex command shims", () => {
|
||||
const pluginRoot = path.win32.join("C:\\", "OpenClaw", "dist", "extensions", "codex");
|
||||
const paths = resolveManagedCodexAppServerPaths({ platform: "win32", pluginRoot });
|
||||
|
||||
expect(paths.commandPath.endsWith(path.win32.join("node_modules", ".bin", "codex.cmd"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("finds Codex in the external runtime-deps install root used by packaged plugins", async () => {
|
||||
const installRoot = path.join("/tmp", "openclaw-runtime-deps", "codex");
|
||||
const pluginRoot = path.join(installRoot, "dist", "extensions", "codex");
|
||||
const installedCommand = managedCommandPath(installRoot, "linux");
|
||||
const pathExists = vi.fn(async (filePath: string) => filePath === installedCommand);
|
||||
|
||||
await expect(
|
||||
resolveManagedCodexAppServerStartOptions(startOptions("managed"), {
|
||||
platform: "linux",
|
||||
pluginRoot,
|
||||
pathExists,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
...startOptions("managed"),
|
||||
command: installedCommand,
|
||||
commandSource: "resolved-managed",
|
||||
});
|
||||
});
|
||||
|
||||
it("fails clearly when bundled runtime deps did not stage Codex", async () => {
|
||||
await expect(
|
||||
resolveManagedCodexAppServerStartOptions(startOptions("managed"), {
|
||||
platform: "darwin",
|
||||
pluginRoot: path.join("/tmp", "openclaw", "extensions", "codex"),
|
||||
pathExists: vi.fn(async () => false),
|
||||
}),
|
||||
).rejects.toThrow("Managed Codex app-server binary was not found");
|
||||
});
|
||||
});
|
||||
121
extensions/codex/src/app-server/managed-binary.ts
Normal file
121
extensions/codex/src/app-server/managed-binary.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import { access } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
import { MANAGED_CODEX_APP_SERVER_PACKAGE } from "./version.js";
|
||||
|
||||
const CODEX_PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
type ManagedCodexAppServerPaths = {
|
||||
commandPath: string;
|
||||
candidateCommandPaths: string[];
|
||||
};
|
||||
|
||||
export type ResolveManagedCodexAppServerOptions = {
|
||||
platform?: NodeJS.Platform;
|
||||
pluginRoot?: string;
|
||||
pathExists?: (filePath: string, platform: NodeJS.Platform) => Promise<boolean>;
|
||||
};
|
||||
|
||||
export async function resolveManagedCodexAppServerStartOptions(
|
||||
startOptions: CodexAppServerStartOptions,
|
||||
options: ResolveManagedCodexAppServerOptions = {},
|
||||
): Promise<CodexAppServerStartOptions> {
|
||||
if (startOptions.transport !== "stdio" || startOptions.commandSource !== "managed") {
|
||||
return startOptions;
|
||||
}
|
||||
|
||||
const platform = options.platform ?? process.platform;
|
||||
const paths = resolveManagedCodexAppServerPaths({
|
||||
platform,
|
||||
pluginRoot: options.pluginRoot,
|
||||
});
|
||||
const pathExists = options.pathExists ?? commandPathExists;
|
||||
const commandPath = await findManagedCodexAppServerCommandPath({
|
||||
candidateCommandPaths: paths.candidateCommandPaths,
|
||||
pathExists,
|
||||
platform,
|
||||
});
|
||||
|
||||
return {
|
||||
...startOptions,
|
||||
command: commandPath,
|
||||
commandSource: "resolved-managed",
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveManagedCodexAppServerPaths(params: {
|
||||
platform?: NodeJS.Platform;
|
||||
pluginRoot?: string;
|
||||
}): ManagedCodexAppServerPaths {
|
||||
const platform = params.platform ?? process.platform;
|
||||
const candidateCommandPaths = resolveManagedCodexAppServerCommandCandidates(
|
||||
params.pluginRoot ?? CODEX_PLUGIN_ROOT,
|
||||
platform,
|
||||
);
|
||||
return {
|
||||
commandPath: candidateCommandPaths[0] ?? "",
|
||||
candidateCommandPaths,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveManagedCodexAppServerCommandCandidates(
|
||||
pluginRoot: string,
|
||||
platform: NodeJS.Platform,
|
||||
): string[] {
|
||||
const pathApi = pathForPlatform(platform);
|
||||
const commandName = platform === "win32" ? "codex.cmd" : "codex";
|
||||
const roots = [
|
||||
pluginRoot,
|
||||
pathApi.dirname(pluginRoot),
|
||||
pathApi.dirname(pathApi.dirname(pluginRoot)),
|
||||
isDistExtensionRoot(pluginRoot, platform)
|
||||
? pathApi.dirname(pathApi.dirname(pathApi.dirname(pluginRoot)))
|
||||
: null,
|
||||
].filter((root): root is string => Boolean(root));
|
||||
return [...new Set(roots.map((root) => pathApi.join(root, "node_modules", ".bin", commandName)))];
|
||||
}
|
||||
|
||||
function isDistExtensionRoot(pluginRoot: string, platform: NodeJS.Platform): boolean {
|
||||
const pathApi = pathForPlatform(platform);
|
||||
const extensionsDir = pathApi.dirname(pluginRoot);
|
||||
const distDir = pathApi.dirname(extensionsDir);
|
||||
return (
|
||||
pathApi.basename(extensionsDir) === "extensions" &&
|
||||
(pathApi.basename(distDir) === "dist" || pathApi.basename(distDir) === "dist-runtime")
|
||||
);
|
||||
}
|
||||
|
||||
function pathForPlatform(platform: NodeJS.Platform): typeof path {
|
||||
return platform === "win32" ? path.win32 : path.posix;
|
||||
}
|
||||
|
||||
async function findManagedCodexAppServerCommandPath(params: {
|
||||
candidateCommandPaths: readonly string[];
|
||||
pathExists: (filePath: string, platform: NodeJS.Platform) => Promise<boolean>;
|
||||
platform: NodeJS.Platform;
|
||||
}): Promise<string> {
|
||||
for (const commandPath of params.candidateCommandPaths) {
|
||||
if (await params.pathExists(commandPath, params.platform)) {
|
||||
return commandPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
`Managed Codex app-server binary was not found for ${MANAGED_CODEX_APP_SERVER_PACKAGE}.`,
|
||||
"Run OpenClaw with bundled plugin runtime dependencies enabled, or run pnpm install in a source checkout.",
|
||||
"Set plugins.entries.codex.config.appServer.command or OPENCLAW_CODEX_APP_SERVER_BIN to use a custom Codex binary.",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
async function commandPathExists(filePath: string, platform: NodeJS.Platform): Promise<boolean> {
|
||||
try {
|
||||
await access(filePath, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,13 @@ const mocks = vi.hoisted(() => {
|
||||
applyAuthProfile: vi.fn(async () => undefined),
|
||||
startOptions: vi.fn(async ({ startOptions }) => startOptions),
|
||||
};
|
||||
const managedBinary = {
|
||||
startOptions: vi.fn(async (startOptions) => startOptions),
|
||||
};
|
||||
const providerAuth = {
|
||||
agentDir: vi.fn(() => "/tmp/openclaw-agent"),
|
||||
};
|
||||
return { authBridge, providerAuth };
|
||||
return { authBridge, managedBinary, providerAuth };
|
||||
});
|
||||
|
||||
vi.mock("./auth-bridge.js", () => ({
|
||||
@@ -18,6 +21,10 @@ vi.mock("./auth-bridge.js", () => ({
|
||||
bridgeCodexAppServerStartOptions: mocks.authBridge.startOptions,
|
||||
}));
|
||||
|
||||
vi.mock("./managed-binary.js", () => ({
|
||||
resolveManagedCodexAppServerStartOptions: mocks.managedBinary.startOptions,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||
resolveOpenClawAgentDir: mocks.providerAuth.agentDir,
|
||||
}));
|
||||
@@ -38,6 +45,8 @@ describe("listCodexAppServerModels", () => {
|
||||
vi.restoreAllMocks();
|
||||
mocks.authBridge.applyAuthProfile.mockClear();
|
||||
mocks.authBridge.startOptions.mockClear();
|
||||
mocks.managedBinary.startOptions.mockClear();
|
||||
mocks.managedBinary.startOptions.mockImplementation(async (startOptions) => startOptions);
|
||||
mocks.providerAuth.agentDir.mockClear();
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createClientHarness } from "./test-support.js";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
bridgeCodexAppServerStartOptions: vi.fn(async ({ startOptions }) => startOptions),
|
||||
applyCodexAppServerAuthProfile: vi.fn(async () => undefined),
|
||||
resolveManagedCodexAppServerStartOptions: vi.fn(async (startOptions) => startOptions),
|
||||
embeddedAgentLog: { debug: vi.fn(), warn: vi.fn() },
|
||||
resolveOpenClawAgentDir: vi.fn(() => "/tmp/openclaw-agent"),
|
||||
}));
|
||||
|
||||
@@ -14,6 +16,15 @@ vi.mock("./auth-bridge.js", () => ({
|
||||
bridgeCodexAppServerStartOptions: mocks.bridgeCodexAppServerStartOptions,
|
||||
}));
|
||||
|
||||
vi.mock("./managed-binary.js", () => ({
|
||||
resolveManagedCodexAppServerStartOptions: mocks.resolveManagedCodexAppServerStartOptions,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/agent-harness-runtime", () => ({
|
||||
embeddedAgentLog: mocks.embeddedAgentLog,
|
||||
OPENCLAW_VERSION: "test",
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||
resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir,
|
||||
}));
|
||||
@@ -54,6 +65,12 @@ describe("shared Codex app-server client", () => {
|
||||
vi.restoreAllMocks();
|
||||
mocks.bridgeCodexAppServerStartOptions.mockClear();
|
||||
mocks.applyCodexAppServerAuthProfile.mockClear();
|
||||
mocks.resolveManagedCodexAppServerStartOptions.mockClear();
|
||||
mocks.resolveManagedCodexAppServerStartOptions.mockImplementation(
|
||||
async (startOptions) => startOptions,
|
||||
);
|
||||
mocks.embeddedAgentLog.debug.mockClear();
|
||||
mocks.embeddedAgentLog.warn.mockClear();
|
||||
mocks.resolveOpenClawAgentDir.mockClear();
|
||||
});
|
||||
|
||||
@@ -128,6 +145,42 @@ describe("shared Codex app-server client", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves the managed binary before bridging and spawning the shared client", async () => {
|
||||
const harness = createClientHarness();
|
||||
const startSpy = vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||
mocks.resolveManagedCodexAppServerStartOptions.mockImplementationOnce(async (startOptions) => ({
|
||||
...startOptions,
|
||||
command: "/cache/openclaw/codex",
|
||||
commandSource: "resolved-managed",
|
||||
}));
|
||||
|
||||
const listPromise = listCodexAppServerModels({ timeoutMs: 1000 });
|
||||
await sendInitializeResult(harness, "openclaw/0.125.0 (macOS; test)");
|
||||
await sendEmptyModelList(harness);
|
||||
|
||||
await expect(listPromise).resolves.toEqual({ models: [] });
|
||||
expect(mocks.resolveManagedCodexAppServerStartOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
command: "codex",
|
||||
commandSource: "managed",
|
||||
}),
|
||||
);
|
||||
expect(mocks.bridgeCodexAppServerStartOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
startOptions: expect.objectContaining({
|
||||
command: "/cache/openclaw/codex",
|
||||
commandSource: "resolved-managed",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(startSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
command: "/cache/openclaw/codex",
|
||||
commandSource: "resolved-managed",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("restarts the shared client when the bridged auth token changes", async () => {
|
||||
const first = createClientHarness();
|
||||
const second = createClientHarness();
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resolveCodexAppServerRuntimeOptions,
|
||||
type CodexAppServerStartOptions,
|
||||
} from "./config.js";
|
||||
import { resolveManagedCodexAppServerStartOptions } from "./managed-binary.js";
|
||||
import { withTimeout } from "./timeout.js";
|
||||
|
||||
type SharedCodexAppServerClientState = {
|
||||
@@ -30,9 +31,13 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
authProfileId?: string;
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const state = getSharedCodexAppServerClientState();
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const requestedStartOptions =
|
||||
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
|
||||
const startOptions = await bridgeCodexAppServerStartOptions({
|
||||
startOptions: options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start,
|
||||
agentDir: resolveOpenClawAgentDir(),
|
||||
startOptions: managedStartOptions,
|
||||
agentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
});
|
||||
const key = codexAppServerStartOptionsKey(startOptions, {
|
||||
@@ -52,7 +57,7 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
await client.initialize();
|
||||
await applyCodexAppServerAuthProfile({
|
||||
client,
|
||||
agentDir: resolveOpenClawAgentDir(),
|
||||
agentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
});
|
||||
return client;
|
||||
@@ -82,9 +87,13 @@ export async function createIsolatedCodexAppServerClient(options?: {
|
||||
timeoutMs?: number;
|
||||
authProfileId?: string;
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const requestedStartOptions =
|
||||
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
|
||||
const startOptions = await bridgeCodexAppServerStartOptions({
|
||||
startOptions: options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start,
|
||||
agentDir: resolveOpenClawAgentDir(),
|
||||
startOptions: managedStartOptions,
|
||||
agentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
});
|
||||
const client = CodexAppServerClient.start(startOptions);
|
||||
@@ -93,7 +102,7 @@ export async function createIsolatedCodexAppServerClient(options?: {
|
||||
await withTimeout(initialize, options?.timeoutMs ?? 0, "codex app-server initialize timed out");
|
||||
await applyCodexAppServerAuthProfile({
|
||||
client,
|
||||
agentDir: resolveOpenClawAgentDir(),
|
||||
agentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
});
|
||||
return client;
|
||||
|
||||
@@ -44,6 +44,22 @@ describe("resolveCodexAppServerSpawnInvocation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("requires managed Codex commands to be resolved before spawn", () => {
|
||||
expect(() =>
|
||||
resolveCodexAppServerSpawnInvocation(
|
||||
{
|
||||
...startOptions("codex"),
|
||||
commandSource: "managed",
|
||||
},
|
||||
{
|
||||
platform: "darwin",
|
||||
env: {},
|
||||
execPath: "/usr/local/bin/node",
|
||||
},
|
||||
),
|
||||
).toThrow("must be resolved before spawn");
|
||||
});
|
||||
|
||||
it("resolves Windows npm .cmd Codex shims through Node instead of raw spawn", async () => {
|
||||
const binDir = await createTempDir();
|
||||
const entryPath = path.join(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
|
||||
|
||||
@@ -22,6 +22,9 @@ export function resolveCodexAppServerSpawnInvocation(
|
||||
options: CodexAppServerStartOptions,
|
||||
runtime: CodexAppServerSpawnRuntime = DEFAULT_SPAWN_RUNTIME,
|
||||
): { command: string; args: string[]; shell?: boolean; windowsHide?: boolean } {
|
||||
if (options.commandSource === "managed") {
|
||||
throw new Error("Managed Codex app-server start options must be resolved before spawn.");
|
||||
}
|
||||
const program = resolveWindowsSpawnProgram({
|
||||
command: options.command,
|
||||
platform: runtime.platform,
|
||||
|
||||
3
extensions/codex/src/app-server/version.ts
Normal file
3
extensions/codex/src/app-server/version.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const MIN_CODEX_APP_SERVER_VERSION = "0.125.0";
|
||||
export const MANAGED_CODEX_APP_SERVER_PACKAGE = "@openai/codex";
|
||||
export const MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION = MIN_CODEX_APP_SERVER_VERSION;
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION } from "./app-server/version.js";
|
||||
|
||||
type CodexPackageManifest = {
|
||||
dependencies?: Record<string, string>;
|
||||
@@ -17,6 +18,9 @@ describe("codex package manifest", () => {
|
||||
) as CodexPackageManifest;
|
||||
|
||||
expect(packageJson.dependencies?.["@mariozechner/pi-coding-agent"]).toBeDefined();
|
||||
expect(packageJson.dependencies?.["@openai/codex"]).toBe(
|
||||
MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION,
|
||||
);
|
||||
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user