mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(codex): candidate checks reject a mismatched app-server (#124137)
* fix(codex): validate managed app-server candidates * docs(codex): document managed binary preflight * test(codex): review managed preflight execution * docs(codex): document managed app-server check * fix(pr): support macOS system Bash operation locks * fix(codex): scope managed preflight to Codex agents
This commit is contained in:
@@ -149,6 +149,21 @@ desktop-first rule applies when an isolated agent home's effective Codex config
|
||||
enables native Computer Use. If no desktop app bundle is installed, OpenClaw
|
||||
falls back to the pinned package binary.
|
||||
|
||||
Before cutting over a staged OpenClaw package, run the opt-in managed-binary
|
||||
check against the candidate installation:
|
||||
|
||||
```bash
|
||||
openclaw doctor --lint --only codex/managed-app-server --json
|
||||
```
|
||||
|
||||
The check is read-only. For every configured Codex agent it applies the same
|
||||
final command selection as a live harness turn, then verifies that a selected
|
||||
package-owned native binary exists and reports the plugin's exact pinned
|
||||
version. A selected Codex Desktop binary, an explicit custom command, and a
|
||||
remote app-server are outside this package check. The command exits nonzero on
|
||||
an error-level finding, so a deployer can reject the candidate before cutover
|
||||
without changing Codex state or app-server settings.
|
||||
|
||||
Executable handoff and native-config fencing coordinate clients inside one
|
||||
running Gateway process. Restart the Gateway after another process changes the
|
||||
native Codex plugin config.
|
||||
|
||||
@@ -462,6 +462,18 @@ Then check Codex app-server state:
|
||||
/codex binding
|
||||
```
|
||||
|
||||
After installing or updating OpenClaw, explicitly verify the managed package
|
||||
binary before cutover:
|
||||
|
||||
```bash
|
||||
openclaw doctor --lint --only codex/managed-app-server --json
|
||||
```
|
||||
|
||||
For an effective Codex route using the managed stdio app-server, this
|
||||
default-disabled check resolves the platform-native executable and requires the
|
||||
exact Codex version pinned by OpenClaw. It does not execute custom, remote, or
|
||||
macOS desktop-owned app-servers.
|
||||
|
||||
`/status` reports the resolved OpenClaw Fast policy (`on`, `off`, or `auto`)
|
||||
and the selected runtime. It does not report the upstream service tier actually
|
||||
honored or returned for a completed request. `/codex binding` reports the
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { HealthCheck } from "openclaw/plugin-sdk/health";
|
||||
import {
|
||||
CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
registerCodexManagedAppServerDoctorChecks as registerChecks,
|
||||
} from "./src/doctor.js";
|
||||
|
||||
const CODEX_PLUGIN_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export { CODEX_MANAGED_APP_SERVER_CHECK_ID };
|
||||
|
||||
export function registerCodexManagedAppServerDoctorChecks(host: {
|
||||
registerHealthCheck(check: HealthCheck): void;
|
||||
}): void {
|
||||
registerChecks({ ...host, pluginRoot: CODEX_PLUGIN_ROOT });
|
||||
}
|
||||
@@ -76,10 +76,7 @@ export function resolveManagedCodexNativeCommand(
|
||||
options: ResolveManagedCodexNativeCommandOptions = {},
|
||||
): string | undefined {
|
||||
const platform = options.platform ?? process.platform;
|
||||
if (
|
||||
platform === "darwin" &&
|
||||
MACOS_DESKTOP_CODEX_APP_SERVER_COMMANDS.some((candidate) => candidate === command)
|
||||
) {
|
||||
if (isManagedCodexDesktopCommand(command, platform)) {
|
||||
return command;
|
||||
}
|
||||
const target = resolveCodexNativeTarget(platform, options.arch ?? process.arch);
|
||||
@@ -111,6 +108,17 @@ export function resolveManagedCodexNativeCommand(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Returns whether a resolved managed command is owned by the macOS desktop app. */
|
||||
export function isManagedCodexDesktopCommand(
|
||||
command: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): boolean {
|
||||
return (
|
||||
platform === "darwin" &&
|
||||
MACOS_DESKTOP_CODEX_APP_SERVER_COMMANDS.some((candidate) => candidate === command)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveManagedCodexPackageRootForCommand(
|
||||
command: string,
|
||||
platform: NodeJS.Platform,
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { HealthCheck, OpenClawConfig } from "openclaw/plugin-sdk/health";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CODEX_APP_SERVER_VERSION } from "./app-server/version.js";
|
||||
import {
|
||||
CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
registerCodexManagedAppServerDoctorChecks,
|
||||
} from "./doctor.js";
|
||||
|
||||
function config(appServer: Record<string, unknown> = {}): OpenClawConfig {
|
||||
return {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.6-sol" },
|
||||
models: {
|
||||
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
codex: {
|
||||
enabled: true,
|
||||
config: {
|
||||
appServer: {
|
||||
args: [
|
||||
"app-server",
|
||||
"--listen",
|
||||
"stdio://",
|
||||
"-c",
|
||||
"model_context_window=1000000",
|
||||
"-c",
|
||||
"model_auto_compact_token_limit=700000",
|
||||
"-c",
|
||||
"model_auto_compact_token_limit_scope=total",
|
||||
],
|
||||
...appServer,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function context(cfg: OpenClawConfig) {
|
||||
return {
|
||||
mode: "lint" as const,
|
||||
runtime: {} as never,
|
||||
cfg,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
};
|
||||
}
|
||||
|
||||
function managedDeps(version = CODEX_APP_SERVER_VERSION) {
|
||||
const resolveNativeCommand = vi.fn(
|
||||
(_command: string): string | undefined => "/candidate/plugin/codex-native",
|
||||
);
|
||||
return {
|
||||
resolveStartOptions: vi.fn(async (start) => ({
|
||||
...start,
|
||||
command:
|
||||
start.managedCommandOrder === "desktop-first"
|
||||
? "/Applications/ChatGPT.app/Contents/Resources/codex"
|
||||
: "/candidate/plugin/codex",
|
||||
commandSource: "resolved-managed" as const,
|
||||
})),
|
||||
isDesktopCommand: vi.fn((command: string) => command.startsWith("/Applications/")),
|
||||
resolveNativeCommand,
|
||||
runVersionCommand: vi.fn(async () => ({ stdout: `codex-cli ${version}\n`, stderr: "" })),
|
||||
};
|
||||
}
|
||||
|
||||
function createCheck(deps: ReturnType<typeof managedDeps>) {
|
||||
let check: HealthCheck | undefined;
|
||||
registerCodexManagedAppServerDoctorChecks(
|
||||
{
|
||||
pluginRoot: "/candidate/plugin",
|
||||
registerHealthCheck(value) {
|
||||
check = value;
|
||||
},
|
||||
},
|
||||
deps,
|
||||
);
|
||||
if (!check) {
|
||||
throw new Error("Codex managed health check was not registered");
|
||||
}
|
||||
return check;
|
||||
}
|
||||
|
||||
describe("managed Codex doctor check", () => {
|
||||
it("accepts the exact pinned native binary without changing explicit long-context config", async () => {
|
||||
const cfg = config();
|
||||
const before = structuredClone(cfg);
|
||||
const deps = managedDeps();
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(cfg))).resolves.toEqual([]);
|
||||
expect(deps.runVersionCommand).toHaveBeenCalledWith("/candidate/plugin/codex-native");
|
||||
expect(cfg).toEqual(before);
|
||||
});
|
||||
|
||||
it("reports the exact expected and detected versions", async () => {
|
||||
const deps = managedDeps("0.146.0");
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(config()))).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
severity: "error",
|
||||
path: "/candidate/plugin/codex-native",
|
||||
message: `Managed Codex app-server version mismatch: expected ${CODEX_APP_SERVER_VERSION}, detected 0.146.0.`,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports a missing managed launcher before execution", async () => {
|
||||
const deps = managedDeps();
|
||||
deps.resolveStartOptions.mockRejectedValueOnce(new Error("managed launcher missing"));
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(config()))).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
message: "Managed Codex app-server could not be resolved: managed launcher missing",
|
||||
}),
|
||||
]);
|
||||
expect(deps.runVersionCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports a launcher whose platform-native artifact is absent", async () => {
|
||||
const deps = managedDeps();
|
||||
deps.resolveNativeCommand.mockReturnValueOnce(undefined);
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(config()))).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
path: "/candidate/plugin/codex",
|
||||
message: "Managed Codex app-server resolved a launcher without a native artifact.",
|
||||
}),
|
||||
]);
|
||||
expect(deps.runVersionCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports a bounded version command failure", async () => {
|
||||
const deps = managedDeps();
|
||||
deps.runVersionCommand.mockRejectedValueOnce(new Error("timed out after 5000 ms"));
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(config()))).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
path: "/candidate/plugin/codex-native",
|
||||
message: "Managed Codex app-server version check failed: timed out after 5000 ms",
|
||||
requirement: `Codex ${CODEX_APP_SERVER_VERSION} must report its version within 5000 ms`,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["custom command", { command: "/operator/codex" }],
|
||||
["websocket transport", { transport: "websocket", url: "ws://127.0.0.1:4500" }],
|
||||
["unix transport", { transport: "unix", url: "unix:///tmp/codex.sock", homeScope: "user" }],
|
||||
])("does not probe a %s", async (_label, appServer) => {
|
||||
const deps = managedDeps();
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(config(appServer)))).resolves.toEqual([]);
|
||||
expect(deps.resolveStartOptions).not.toHaveBeenCalled();
|
||||
expect(deps.runVersionCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not enforce the package pin on a selected desktop-owned command", async () => {
|
||||
const deps = managedDeps("0.146.0");
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(config({ homeScope: "user" })))).resolves.toEqual([]);
|
||||
expect(deps.resolveStartOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ managedCommandOrder: "desktop-first" }),
|
||||
{ pluginRoot: "/candidate/plugin" },
|
||||
);
|
||||
expect(deps.resolveNativeCommand).not.toHaveBeenCalled();
|
||||
expect(deps.runVersionCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses persisted per-agent Computer Use state before selecting the managed command", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-doctor-agent-"));
|
||||
try {
|
||||
await fs.mkdir(path.join(agentDir, "codex-home"));
|
||||
await fs.writeFile(
|
||||
path.join(agentDir, "codex-home", "config.toml"),
|
||||
'[plugins."computer-use@openai-bundled"]\nenabled = true\n',
|
||||
);
|
||||
const cfg = config();
|
||||
cfg.agents = {
|
||||
...cfg.agents,
|
||||
list: [{ id: "main", agentDir }],
|
||||
};
|
||||
const deps = managedDeps("0.146.0");
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(cfg))).resolves.toEqual([]);
|
||||
expect(deps.resolveStartOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ managedCommandOrder: "desktop-first" }),
|
||||
{ pluginRoot: "/candidate/plugin" },
|
||||
);
|
||||
expect(deps.resolveNativeCommand).not.toHaveBeenCalled();
|
||||
expect(deps.runVersionCommand).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("still validates the package when any configured agent can select it", async () => {
|
||||
const desktopAgentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "openclaw-codex-doctor-desktop-agent-"),
|
||||
);
|
||||
const packageAgentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "openclaw-codex-doctor-package-agent-"),
|
||||
);
|
||||
try {
|
||||
await fs.mkdir(path.join(desktopAgentDir, "codex-home"));
|
||||
await fs.writeFile(
|
||||
path.join(desktopAgentDir, "codex-home", "config.toml"),
|
||||
'[plugins."computer-use@openai-bundled"]\nenabled = true\n',
|
||||
);
|
||||
const cfg = config();
|
||||
cfg.agents = {
|
||||
...cfg.agents,
|
||||
list: [
|
||||
{ id: "desktop", agentDir: desktopAgentDir },
|
||||
{ id: "package", agentDir: packageAgentDir },
|
||||
],
|
||||
};
|
||||
const deps = managedDeps("0.146.0");
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(cfg))).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
message: `Managed Codex app-server version mismatch: expected ${CODEX_APP_SERVER_VERSION}, detected 0.146.0.`,
|
||||
}),
|
||||
]);
|
||||
expect(deps.resolveStartOptions).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ managedCommandOrder: "desktop-first" }),
|
||||
{ pluginRoot: "/candidate/plugin" },
|
||||
);
|
||||
expect(deps.resolveStartOptions).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.not.objectContaining({ managedCommandOrder: expect.anything() }),
|
||||
{ pluginRoot: "/candidate/plugin" },
|
||||
);
|
||||
expect(deps.runVersionCommand).toHaveBeenCalledWith("/candidate/plugin/codex-native");
|
||||
} finally {
|
||||
await Promise.all(
|
||||
[desktopAgentDir, packageAgentDir].map((agentDir) =>
|
||||
fs.rm(agentDir, { recursive: true, force: true }),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores managed commands for agents whose effective runtime is not Codex", async () => {
|
||||
const desktopAgentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "openclaw-codex-doctor-desktop-agent-"),
|
||||
);
|
||||
try {
|
||||
await fs.mkdir(path.join(desktopAgentDir, "codex-home"));
|
||||
await fs.writeFile(
|
||||
path.join(desktopAgentDir, "codex-home", "config.toml"),
|
||||
'[plugins."computer-use@openai-bundled"]\nenabled = true\n',
|
||||
);
|
||||
const cfg = config();
|
||||
cfg.agents = {
|
||||
...cfg.agents,
|
||||
list: [
|
||||
{ id: "desktop", agentDir: desktopAgentDir },
|
||||
{
|
||||
id: "openclaw",
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": { agentRuntime: { id: "openclaw" } },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const deps = managedDeps("0.146.0");
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(cfg))).resolves.toEqual([]);
|
||||
expect(deps.resolveStartOptions).toHaveBeenCalledTimes(1);
|
||||
expect(deps.runVersionCommand).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await fs.rm(desktopAgentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("still validates a package fallback selected after desktop-first resolution", async () => {
|
||||
const deps = managedDeps("0.146.0");
|
||||
deps.resolveStartOptions.mockImplementationOnce(async (start) => ({
|
||||
...start,
|
||||
command: "/candidate/plugin/codex",
|
||||
commandSource: "resolved-managed" as const,
|
||||
}));
|
||||
const check = createCheck(deps);
|
||||
|
||||
await expect(check.detect(context(config({ homeScope: "user" })))).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
message: `Managed Codex app-server version mismatch: expected ${CODEX_APP_SERVER_VERSION}, detected 0.146.0.`,
|
||||
}),
|
||||
]);
|
||||
expect(deps.runVersionCommand).toHaveBeenCalledWith("/candidate/plugin/codex-native");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,216 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { listAgentIds, resolveAgentDir } from "openclaw/plugin-sdk/agent-scope-runtime";
|
||||
import { resolveEffectiveAgentRuntime } from "openclaw/plugin-sdk/command-auth-native";
|
||||
import { getHealthCheck, type HealthCheck, type HealthFinding } from "openclaw/plugin-sdk/health";
|
||||
import {
|
||||
resolveCodexAppServerRuntimeOptions,
|
||||
resolveCodexAppServerStartOptionsForAgent,
|
||||
} from "./app-server/config.js";
|
||||
import {
|
||||
isManagedCodexDesktopCommand,
|
||||
resolveManagedCodexAppServerStartOptions,
|
||||
resolveManagedCodexNativeCommand,
|
||||
} from "./app-server/managed-binary.js";
|
||||
import { CODEX_APP_SERVER_VERSION } from "./app-server/version.js";
|
||||
|
||||
export const CODEX_MANAGED_APP_SERVER_CHECK_ID = "codex/managed-app-server";
|
||||
const CODEX_VERSION_TIMEOUT_MS = 5_000;
|
||||
const CODEX_VERSION_MAX_BUFFER_BYTES = 64 * 1024;
|
||||
|
||||
type VersionCommandResult = {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
type CodexManagedDoctorDependencies = {
|
||||
resolveAgentStartOptions?: typeof resolveCodexAppServerStartOptionsForAgent;
|
||||
resolveStartOptions?: typeof resolveManagedCodexAppServerStartOptions;
|
||||
isDesktopCommand?: typeof isManagedCodexDesktopCommand;
|
||||
resolveNativeCommand?: typeof resolveManagedCodexNativeCommand;
|
||||
runVersionCommand?: (command: string) => Promise<VersionCommandResult>;
|
||||
};
|
||||
|
||||
type CodexManagedDoctorRegistrationHost = {
|
||||
readonly registerHealthCheck: (check: HealthCheck) => void;
|
||||
readonly pluginRoot: string;
|
||||
};
|
||||
|
||||
function managedCodexFinding(params: {
|
||||
message: string;
|
||||
path?: string;
|
||||
requirement?: string;
|
||||
fixHint?: string;
|
||||
}): HealthFinding {
|
||||
return {
|
||||
checkId: CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
severity: "error",
|
||||
source: "codex",
|
||||
message: params.message,
|
||||
...(params.path ? { path: params.path } : {}),
|
||||
...(params.requirement ? { requirement: params.requirement } : {}),
|
||||
...(params.fixHint ? { fixHint: params.fixHint } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function readErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function parseCodexVersion(output: string): string | undefined {
|
||||
return /(?:^|\s)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?:\s|$)/u.exec(
|
||||
output,
|
||||
)?.[1];
|
||||
}
|
||||
|
||||
function runVersionCommand(command: string): Promise<VersionCommandResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
command,
|
||||
["--version"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
maxBuffer: CODEX_VERSION_MAX_BUFFER_BYTES,
|
||||
timeout: CODEX_VERSION_TIMEOUT_MS,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(readErrorMessage(error), { cause: error }));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function createCodexManagedAppServerHealthCheck(params: {
|
||||
pluginRoot: string;
|
||||
deps?: CodexManagedDoctorDependencies;
|
||||
}): HealthCheck & { readonly defaultEnabled: false } {
|
||||
const resolveStartOptions =
|
||||
params.deps?.resolveStartOptions ?? resolveManagedCodexAppServerStartOptions;
|
||||
const resolveAgentStartOptions =
|
||||
params.deps?.resolveAgentStartOptions ?? resolveCodexAppServerStartOptionsForAgent;
|
||||
const isDesktopCommand = params.deps?.isDesktopCommand ?? isManagedCodexDesktopCommand;
|
||||
const resolveNativeCommand =
|
||||
params.deps?.resolveNativeCommand ?? resolveManagedCodexNativeCommand;
|
||||
const executeVersion = params.deps?.runVersionCommand ?? runVersionCommand;
|
||||
|
||||
return {
|
||||
id: CODEX_MANAGED_APP_SERVER_CHECK_ID,
|
||||
kind: "plugin",
|
||||
description: "Verify the selected managed Codex app-server binary and pinned version.",
|
||||
source: "codex",
|
||||
defaultEnabled: false,
|
||||
async detect(ctx) {
|
||||
const pluginConfig = ctx.cfg.plugins?.entries?.codex?.config;
|
||||
const start = resolveCodexAppServerRuntimeOptions({
|
||||
pluginConfig,
|
||||
env: ctx.env ?? process.env,
|
||||
}).start;
|
||||
if (start.transport !== "stdio" || start.commandSource !== "managed") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const env = ctx.env ?? process.env;
|
||||
let resolved;
|
||||
for (const agentId of listAgentIds(ctx.cfg)) {
|
||||
const model = resolveDefaultModelForAgent({ cfg: ctx.cfg, agentId });
|
||||
if (
|
||||
resolveEffectiveAgentRuntime({
|
||||
cfg: ctx.cfg,
|
||||
provider: model.provider,
|
||||
modelId: model.model,
|
||||
agentId,
|
||||
}) !== "codex"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const agentStart = resolveAgentStartOptions({
|
||||
startOptions: start,
|
||||
agentDir: resolveAgentDir(ctx.cfg, agentId, env),
|
||||
env,
|
||||
});
|
||||
try {
|
||||
resolved = await resolveStartOptions(agentStart, { pluginRoot: params.pluginRoot });
|
||||
} catch (error) {
|
||||
return [
|
||||
managedCodexFinding({
|
||||
message: `Managed Codex app-server could not be resolved: ${readErrorMessage(error)}`,
|
||||
path: params.pluginRoot,
|
||||
requirement: `an executable Codex ${CODEX_APP_SERVER_VERSION} managed artifact`,
|
||||
fixHint:
|
||||
"Reinstall the staged OpenClaw package with its @openai/codex platform dependency, then rerun the candidate check.",
|
||||
}),
|
||||
];
|
||||
}
|
||||
if (!isDesktopCommand(resolved.command)) {
|
||||
break;
|
||||
}
|
||||
resolved = undefined;
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nativeCommand = resolveNativeCommand(resolved.command);
|
||||
if (!nativeCommand) {
|
||||
return [
|
||||
managedCodexFinding({
|
||||
message: "Managed Codex app-server resolved a launcher without a native artifact.",
|
||||
path: resolved.command,
|
||||
requirement: `the platform-native Codex ${CODEX_APP_SERVER_VERSION} executable`,
|
||||
fixHint:
|
||||
"Reinstall the staged OpenClaw package with the matching @openai/codex platform package, then rerun the candidate check.",
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
let output: VersionCommandResult;
|
||||
try {
|
||||
output = await executeVersion(nativeCommand);
|
||||
} catch (error) {
|
||||
return [
|
||||
managedCodexFinding({
|
||||
message: `Managed Codex app-server version check failed: ${readErrorMessage(error)}`,
|
||||
path: nativeCommand,
|
||||
requirement: `Codex ${CODEX_APP_SERVER_VERSION} must report its version within ${CODEX_VERSION_TIMEOUT_MS} ms`,
|
||||
fixHint:
|
||||
"Repair or reinstall the staged OpenClaw package, then rerun the candidate check before cutover.",
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
const detectedVersion = parseCodexVersion(`${output.stdout}\n${output.stderr}`);
|
||||
if (detectedVersion !== CODEX_APP_SERVER_VERSION) {
|
||||
return [
|
||||
managedCodexFinding({
|
||||
message: detectedVersion
|
||||
? `Managed Codex app-server version mismatch: expected ${CODEX_APP_SERVER_VERSION}, detected ${detectedVersion}.`
|
||||
: `Managed Codex app-server did not report a parseable version; expected ${CODEX_APP_SERVER_VERSION}.`,
|
||||
path: nativeCommand,
|
||||
requirement: `the exact OpenClaw-pinned Codex version ${CODEX_APP_SERVER_VERSION}`,
|
||||
fixHint:
|
||||
"Reinstall the staged OpenClaw package so its managed @openai/codex dependency matches the pinned version, then rerun the candidate check.",
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function registerCodexManagedAppServerDoctorChecks(
|
||||
host: CodexManagedDoctorRegistrationHost,
|
||||
deps?: CodexManagedDoctorDependencies,
|
||||
): void {
|
||||
if (getHealthCheck(CODEX_MANAGED_APP_SERVER_CHECK_ID)) {
|
||||
return;
|
||||
}
|
||||
host.registerHealthCheck(
|
||||
createCodexManagedAppServerHealthCheck({ pluginRoot: host.pluginRoot, deps }),
|
||||
);
|
||||
}
|
||||
@@ -141,7 +141,11 @@ finish_pr_operation_completion() {
|
||||
local operation_status="$1"
|
||||
trap - EXIT
|
||||
trap '' PIPE
|
||||
if [ "$BASHPID" = "$PR_OPERATION_COMPLETION_LEADER_PID" ]; then
|
||||
# macOS system Bash 3.2 has no BASHPID. $$ identifies the top-level shell,
|
||||
# while BASH_SUBSHELL fences forked subshells that retain the same $$.
|
||||
if [ "${BASH_SUBSHELL:-0}" -eq 0 ] &&
|
||||
[ "$$" = "$PR_OPERATION_COMPLETION_LEADER_PID" ]
|
||||
then
|
||||
notify_pr_operation_phase operation-complete 2>/dev/null || :
|
||||
fi
|
||||
exit "$operation_status"
|
||||
@@ -149,7 +153,7 @@ finish_pr_operation_completion() {
|
||||
|
||||
install_pr_operation_completion_trap() {
|
||||
[ -z "$PR_OPERATION_COMPLETION_LEADER_PID" ] || return 0
|
||||
PR_OPERATION_COMPLETION_LEADER_PID="$BASHPID"
|
||||
PR_OPERATION_COMPLETION_LEADER_PID="$$"
|
||||
trap 'finish_pr_operation_completion "$?"' EXIT
|
||||
}
|
||||
|
||||
@@ -158,11 +162,11 @@ install_pr_operation_completion_trap() {
|
||||
if [ "${OPENCLAW_PR_DEDICATED_PROCESS_GROUP:-}" = "1" ]; then
|
||||
if [ "${OPENCLAW_PR_LOCK_NOTIFY_FD:-}" = "3" ] &&
|
||||
[ "${OPENCLAW_PR_LOCK_SUPERVISOR_PID:-}" = "$PPID" ] &&
|
||||
[ "$BASHPID" = "$$" ]
|
||||
[ "${BASH_SUBSHELL:-0}" -eq 0 ]
|
||||
then
|
||||
pr_operation_entry_pgid=$(ps -o pgid= -p "$BASHPID" 2>/dev/null || true)
|
||||
pr_operation_entry_pgid=$(ps -o pgid= -p "$$" 2>/dev/null || true)
|
||||
pr_operation_entry_pgid="${pr_operation_entry_pgid//[[:space:]]/}"
|
||||
if [ "$pr_operation_entry_pgid" = "$BASHPID" ]; then
|
||||
if [ "$pr_operation_entry_pgid" = "$$" ]; then
|
||||
install_pr_operation_completion_trap
|
||||
fi
|
||||
unset pr_operation_entry_pgid
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
const STATE_DEFERRED_CHECK_ID = "memory-core/managed-local-embedding-setup";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
registerCodexManagedAppServerDoctorChecks: vi.fn(),
|
||||
inspectEmbeddingProviderSetup: vi.fn(),
|
||||
loadPluginManifestRegistryForPluginRegistry: vi.fn(() => ({
|
||||
plugins: [],
|
||||
@@ -28,7 +29,12 @@ const mocks = vi.hoisted(() => ({
|
||||
}
|
||||
: dirName === "cua-computer"
|
||||
? { registerCuaDriverDoctorChecks: mocks.registerCuaDriverDoctorChecks }
|
||||
: { registerPolicyDoctorChecks: mocks.registerPolicyDoctorChecks },
|
||||
: dirName === "codex"
|
||||
? {
|
||||
registerCodexManagedAppServerDoctorChecks:
|
||||
mocks.registerCodexManagedAppServerDoctorChecks,
|
||||
}
|
||||
: { registerPolicyDoctorChecks: mocks.registerPolicyDoctorChecks },
|
||||
),
|
||||
resolveProviderPolicySurface: vi.fn((): ProviderPolicySurface | null => ({
|
||||
inspectEmbeddingProviderSetup: mocks.inspectEmbeddingProviderSetup,
|
||||
@@ -282,6 +288,63 @@ describe("registerBundledHealthChecks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("loads managed Codex health when an effective model route selects Codex", () => {
|
||||
registerBundledHealthChecks({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.6-sol" },
|
||||
models: {
|
||||
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cwd: workspaceDir,
|
||||
});
|
||||
|
||||
expect(mocks.loadBundledPluginPublicArtifactModuleSync).toHaveBeenCalledWith({
|
||||
dirName: "codex",
|
||||
artifactBasename: "api.js",
|
||||
});
|
||||
expect(mocks.registerCodexManagedAppServerDoctorChecks).toHaveBeenCalledWith({
|
||||
registerHealthCheck: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not load managed Codex health for OpenClaw routes or disabled Codex", () => {
|
||||
for (const cfg of [
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.6-sol" },
|
||||
models: {
|
||||
"openai/gpt-5.6-sol": { agentRuntime: { id: "openclaw" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.6-sol" },
|
||||
models: {
|
||||
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: { entries: { codex: { enabled: false } } },
|
||||
},
|
||||
]) {
|
||||
vi.clearAllMocks();
|
||||
registerBundledHealthChecks({ cfg, cwd: workspaceDir });
|
||||
expect(mocks.loadBundledPluginPublicArtifactModuleSync).not.toHaveBeenCalledWith({
|
||||
dirName: "codex",
|
||||
artifactBasename: "api.js",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("does not use policy.jsonc existence as extension activation", () => {
|
||||
writeFileSync(join(workspaceDir, "policy.jsonc"), "{}\n", "utf-8");
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Bundled health checks define built-in doctor checks for runtime readiness.
|
||||
import { asOptionalObjectRecord as readRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizePluginId, normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import { passesManifestOwnerBasePolicy } from "../plugins/manifest-owner-policy.js";
|
||||
@@ -16,6 +17,9 @@ type EmbeddingProviderSetupInspectionResult =
|
||||
|
||||
// Bridges bundled plugin doctor checks into the core health registry.
|
||||
type BundledHealthApi = {
|
||||
registerCodexManagedAppServerDoctorChecks?: (host: {
|
||||
registerHealthCheck: typeof registerHealthCheck;
|
||||
}) => void;
|
||||
pluginStateIsolatedDoctorCheckIds?: readonly string[];
|
||||
registerCuaDriverDoctorChecks?: (host: {
|
||||
registerHealthCheck: typeof registerHealthCheck;
|
||||
@@ -105,6 +109,12 @@ export function registerBundledHealthChecks(params: {
|
||||
},
|
||||
memoryCoreActive: isMemoryCoreActive(params.cfg),
|
||||
});
|
||||
if (shouldRegisterCodexManagedHealth(params.cfg)) {
|
||||
loadBundledPluginPublicArtifactModuleSync<BundledHealthApi>({
|
||||
dirName: "codex",
|
||||
artifactBasename: "api.js",
|
||||
}).registerCodexManagedAppServerDoctorChecks?.({ registerHealthCheck });
|
||||
}
|
||||
if (shouldRegisterPolicyHealth(params)) {
|
||||
loadBundledPluginPublicArtifactModuleSync<BundledHealthApi>({
|
||||
dirName: "policy",
|
||||
@@ -119,6 +129,19 @@ export function registerBundledHealthChecks(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRegisterCodexManagedHealth(cfg: OpenClawConfig): boolean {
|
||||
if (!collectConfiguredAgentHarnessRuntimes(cfg).includes("codex")) {
|
||||
return false;
|
||||
}
|
||||
if (cfg.plugins?.entries?.codex?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
return passesManifestOwnerBasePolicy({
|
||||
plugin: { id: "codex" },
|
||||
normalizedConfig: normalizePluginsConfig(cfg.plugins),
|
||||
});
|
||||
}
|
||||
|
||||
function isMemoryCoreActive(cfg: OpenClawConfig): boolean {
|
||||
const plugins = normalizePluginsConfig(cfg.plugins);
|
||||
const selectedMemoryPluginId =
|
||||
|
||||
@@ -30,6 +30,7 @@ const REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDING_COUNTS = new Map<string, nu
|
||||
["@openclaw/codex:dangerous-exec:src/app-server/sandbox-exec-server/http.ts", 1],
|
||||
["@openclaw/codex:dangerous-exec:src/app-server/sandbox-exec-server/processes.ts", 1],
|
||||
["@openclaw/codex:dangerous-exec:src/app-server/transport-stdio.ts", 1],
|
||||
["@openclaw/codex:dangerous-exec:src/doctor.ts", 1],
|
||||
["@openclaw/codex:dangerous-exec:src/node-cli-sessions.ts", 1],
|
||||
["@openclaw/discord:dangerous-exec:src/voice/audio.ts", 1],
|
||||
["@openclaw/imessage:dangerous-exec:src/client.ts", 1],
|
||||
@@ -46,7 +47,8 @@ const REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDING_COUNTS = new Map<string, nu
|
||||
const OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDING_COUNTS = new Map<string, number>([
|
||||
["@openclaw/acpx:dangerous-exec:dist/mcp-proxy.mjs", 1],
|
||||
["@openclaw/acpx:dangerous-exec:dist/service-<hash>.js", 1],
|
||||
["@openclaw/codex:dangerous-exec:dist/run-attempt-<hash>.js", 2],
|
||||
["@openclaw/codex:dangerous-exec:dist/api.js", 1],
|
||||
["@openclaw/codex:dangerous-exec:dist/dynamic-tools-<hash>.js", 2],
|
||||
["@openclaw/codex:dangerous-exec:dist/session-catalog-<hash>.js", 1],
|
||||
["@openclaw/codex:dangerous-exec:dist/transport-stdio-<hash>.js", 1],
|
||||
["@openclaw/llama-cpp-provider:dangerous-exec:dist/index.js", 1],
|
||||
@@ -96,6 +98,7 @@ function isScannerWalkedPackedPath(packedPath: string): boolean {
|
||||
|
||||
function normalizePackedFindingPath(packedPath: string): string {
|
||||
for (const prefix of [
|
||||
"dynamic-tools",
|
||||
"outbound-payload.test-harness",
|
||||
"run-attempt",
|
||||
"runtime-entry",
|
||||
@@ -330,14 +333,20 @@ describe("publishable plugin npm package install security scan", () => {
|
||||
});
|
||||
|
||||
it("requires exact occurrence counts for reviewed Codex dist chunks", () => {
|
||||
const runAttemptKey = "@openclaw/codex:dangerous-exec:dist/run-attempt-<hash>.js";
|
||||
const dynamicToolsKey = "@openclaw/codex:dangerous-exec:dist/dynamic-tools-<hash>.js";
|
||||
|
||||
expect(
|
||||
expectedOptionalReviewedFindingsForPackedPath(
|
||||
"@openclaw/codex",
|
||||
"dist/dynamic-tools-current.js",
|
||||
),
|
||||
).toEqual([dynamicToolsKey, dynamicToolsKey]);
|
||||
expect(
|
||||
expectedOptionalReviewedFindingsForPackedPath(
|
||||
"@openclaw/codex",
|
||||
"dist/run-attempt-current.js",
|
||||
),
|
||||
).toEqual([runAttemptKey, runAttemptKey]);
|
||||
).toEqual([]);
|
||||
expect(
|
||||
expectedOptionalReviewedFindingsForPackedPath(
|
||||
"@openclaw/codex",
|
||||
|
||||
Reference in New Issue
Block a user