mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
* fix(codex): settle sandbox processes and readiness probes * fix(codex): revoke sandbox termination signals at exit * fix(codex): preserve sandbox command process identity * fix(ci): recognize cancellation-aware sandbox upload checks * fix(ci): register sandbox upload scanner fixtures * fix(gateway): settle cancelled chat waiters Publish cancellation completion from the common dispatch finalizer for both resolved and rejected runtimes. Preserve the abort marker captured before rejection cleanup, session binding, and completed-delivery barrier. Release-note context: agent.wait already watching a chat.send now finishes after chat.abort, including CLI runtimes that resolve with partial output. The real Gateway regression fails on the original code and passes after this owner repair; 59 focused tests, selected changed checks, final targeted checks, and independent P2 review passed. * test(pdf): cancel worker extraction deterministically Abort synchronously after submitting the real extraction so a worker result cannot race the former zero-delay timer. Preserve exact cancellation reason, pre-aborted rejection, and successful subsequent extraction assertions.
1225 lines
39 KiB
TypeScript
1225 lines
39 KiB
TypeScript
/**
|
|
* Computer Use plugin/MCP readiness checks and optional install flow for Codex
|
|
* app-server sessions.
|
|
*/
|
|
import { existsSync } from "node:fs";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { reconcileCodexComputerUseStartArtifacts } from "./auth-bridge.js";
|
|
import { resolveCodexAppServerHomeDir } from "./auth-start-options.js";
|
|
import { describeControlFailure } from "./capabilities.js";
|
|
import {
|
|
isCodexAppServerConnectionClosedError,
|
|
isCodexAppServerIndeterminateRequestCancellationError,
|
|
isCodexAppServerIndeterminateTransportError,
|
|
type CodexAppServerClient,
|
|
} from "./client.js";
|
|
import { resolveCodexManagedBundledMarketplacePath } from "./computer-use-marketplace.js";
|
|
import {
|
|
runCodexComputerUseLiveTest,
|
|
skippedLiveTestStatus,
|
|
type CodexComputerUseLiveTestStatus,
|
|
type CodexComputerUseRepairStatus,
|
|
type CodexComputerUseRequest,
|
|
} from "./computer-use-readiness.js";
|
|
import { assertNotSymlink } from "./computer-use-service-path.js";
|
|
import {
|
|
resolveCodexAppServerRuntimeOptions,
|
|
resolveCodexComputerUseConfig,
|
|
type CodexComputerUseConfig,
|
|
type ResolvedCodexComputerUseConfig,
|
|
} from "./config.js";
|
|
import {
|
|
resolveFirstExistingMacOSDesktopCodexBundledMarketplacePath,
|
|
resolveMacOSDesktopCodexBundledMarketplaceCandidates,
|
|
} from "./desktop-app-paths.js";
|
|
import { isManagedCodexDesktopCommand } from "./managed-binary.js";
|
|
import { acquireCodexNativeConfigFence } from "./native-config-fence.js";
|
|
import type {
|
|
CodexAppServerRequestResult,
|
|
CodexConfigReadResponse,
|
|
CodexListMcpServerStatusResponse,
|
|
CodexMcpServerStatus,
|
|
CodexPluginDetail,
|
|
CodexPluginListResponse,
|
|
CodexPluginReadResponse,
|
|
CodexRequestObject,
|
|
JsonValue,
|
|
} from "./protocol.js";
|
|
import { requestCodexAppServerClientJson } from "./request.js";
|
|
import {
|
|
assertCodexAppServerClientStartSelectionCurrent,
|
|
getLeasedSharedCodexAppServerClient,
|
|
readCodexAppServerClientDesktopGeneration,
|
|
readCodexAppServerClientProcessIdentity,
|
|
releaseLeasedSharedCodexAppServerClient,
|
|
resolveCodexNativeConfigFenceKey,
|
|
waitForCodexAppServerClientDesktopGenerationDrain,
|
|
withLeasedCodexAppServerClientStartSelectionRetry,
|
|
type CodexAppServerClientLease,
|
|
} from "./shared-client.js";
|
|
|
|
type CodexComputerUseStatusReason =
|
|
| "disabled"
|
|
| "marketplace_missing"
|
|
| "plugin_not_installed"
|
|
| "plugin_disabled"
|
|
| "mcp_missing"
|
|
| "live_test_failed"
|
|
| "ready"
|
|
| "check_failed"
|
|
| "auto_install_blocked";
|
|
|
|
type CodexComputerUseInstallationStatus =
|
|
| "disabled"
|
|
| "marketplace_missing"
|
|
| "not_installed"
|
|
| "installed_disabled"
|
|
| "installed";
|
|
|
|
type CodexComputerUseExposureStatus = "skipped" | "missing" | "available";
|
|
|
|
type CodexComputerUseStatusSection = {
|
|
status: string;
|
|
ok: boolean;
|
|
message: string;
|
|
};
|
|
|
|
/** Readiness status for Codex Computer Use plugin and MCP server wiring. */
|
|
export type CodexComputerUseStatus = {
|
|
enabled: boolean;
|
|
ready: boolean;
|
|
reason: CodexComputerUseStatusReason;
|
|
installed: boolean;
|
|
pluginEnabled: boolean;
|
|
mcpServerAvailable: boolean;
|
|
pluginName: string;
|
|
mcpServerName: string;
|
|
marketplaceName?: string;
|
|
marketplacePath?: string;
|
|
tools: string[];
|
|
installation: CodexComputerUseStatusSection & {
|
|
status: CodexComputerUseInstallationStatus;
|
|
};
|
|
exposure: CodexComputerUseStatusSection & {
|
|
status: CodexComputerUseExposureStatus;
|
|
};
|
|
liveTest: CodexComputerUseLiveTestStatus;
|
|
repair?: CodexComputerUseRepairStatus;
|
|
warnings: string[];
|
|
message: string;
|
|
};
|
|
|
|
class CodexComputerUseSetupError extends Error {
|
|
readonly status: CodexComputerUseStatus;
|
|
|
|
constructor(status: CodexComputerUseStatus) {
|
|
super(status.message);
|
|
this.name = "CodexComputerUseSetupError";
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
/** Inputs for checking, ensuring, or installing Codex Computer Use support. */
|
|
export type CodexComputerUseSetupParams = {
|
|
pluginConfig?: unknown;
|
|
config?: Parameters<typeof requestCodexAppServerClientJson>[0]["config"];
|
|
agentDir?: string;
|
|
overrides?: Partial<CodexComputerUseConfig>;
|
|
/** Caller-owned injection seam for tests; production mutation safety requires `client`. */
|
|
request?: CodexComputerUseRequest;
|
|
client?: CodexAppServerClient;
|
|
timeoutMs?: number;
|
|
signal?: AbortSignal;
|
|
forceEnable?: boolean;
|
|
defaultBundledMarketplacePath?: string;
|
|
defaultBundledMarketplacePathCandidates?: readonly string[];
|
|
releaseNativeConfigFence?: () => void;
|
|
};
|
|
|
|
type CodexComputerUseInspectionParams = {
|
|
pluginConfig?: unknown;
|
|
config?: CodexComputerUseSetupParams["config"];
|
|
agentDir?: string;
|
|
request?: CodexComputerUseRequest;
|
|
client?: CodexAppServerClient;
|
|
timeoutMs?: number;
|
|
signal?: AbortSignal;
|
|
computerUseConfig: ResolvedCodexComputerUseConfig;
|
|
runLiveTest: boolean;
|
|
installPlugin: boolean;
|
|
defaultBundledMarketplacePath?: string;
|
|
defaultBundledMarketplacePathCandidates?: readonly string[];
|
|
releaseNativeConfigFence?: () => void;
|
|
explicitManagedInstall?: ExplicitManagedComputerUseInstallContext;
|
|
};
|
|
|
|
type ExplicitManagedComputerUseInstallContext = {
|
|
client: CodexAppServerClient;
|
|
agentDir: string;
|
|
codexHome: string;
|
|
command: string;
|
|
desktopGeneration: NonNullable<ReturnType<typeof readCodexAppServerClientDesktopGeneration>>;
|
|
};
|
|
|
|
type MarketplaceRef =
|
|
| {
|
|
kind: "local";
|
|
name?: string;
|
|
path: string;
|
|
}
|
|
| {
|
|
kind: "remote";
|
|
name: string;
|
|
remoteMarketplaceName: string;
|
|
remotePluginId: string;
|
|
};
|
|
|
|
type MarketplaceResolution = {
|
|
marketplace?: MarketplaceRef;
|
|
message?: string;
|
|
};
|
|
|
|
type PluginInspection =
|
|
| {
|
|
ok: true;
|
|
plugin: CodexPluginDetail;
|
|
}
|
|
| {
|
|
ok: false;
|
|
status: CodexComputerUseStatus;
|
|
};
|
|
|
|
const CURATED_MARKETPLACE_POLL_INTERVAL_MS = 2_000;
|
|
const BUNDLED_MARKETPLACE_NAME = "openai-bundled";
|
|
const COMPUTER_USE_MARKETPLACE_NAME_PRIORITY = [
|
|
BUNDLED_MARKETPLACE_NAME,
|
|
"openai-curated",
|
|
"openai-api-curated",
|
|
"openai-curated-remote",
|
|
"local",
|
|
];
|
|
/** Reads Computer Use readiness without installing or mutating app-server state. */
|
|
export async function readCodexComputerUseStatus(
|
|
params: CodexComputerUseSetupParams = {},
|
|
): Promise<CodexComputerUseStatus> {
|
|
const config = resolveComputerUseConfig(params);
|
|
if (!config.enabled) {
|
|
return disabledStatus(config);
|
|
}
|
|
try {
|
|
return await inspectCodexComputerUse({
|
|
...params,
|
|
computerUseConfig: config,
|
|
runLiveTest: true,
|
|
installPlugin: false,
|
|
});
|
|
} catch (error) {
|
|
return unavailableStatus(
|
|
config,
|
|
"check_failed",
|
|
`Computer Use check failed: ${describeControlFailure(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures installation and MCP exposure before a turn, optionally installing when
|
|
* config allows safe auto-install. Only strict startup waits for a live probe.
|
|
*/
|
|
export async function ensureCodexComputerUse(
|
|
params: CodexComputerUseSetupParams = {},
|
|
): Promise<CodexComputerUseStatus> {
|
|
const config = resolveComputerUseConfig(params);
|
|
if (!config.enabled) {
|
|
return disabledStatus(config);
|
|
}
|
|
const status = await inspectCodexComputerUse({
|
|
...params,
|
|
computerUseConfig: config,
|
|
runLiveTest: config.strictReadiness,
|
|
installPlugin: false,
|
|
});
|
|
if (status.ready) {
|
|
return status;
|
|
}
|
|
if (config.autoInstall) {
|
|
const blockedAutoInstallStatus = blockUnsafeAutoInstallStatus(config);
|
|
if (blockedAutoInstallStatus) {
|
|
throw new CodexComputerUseSetupError(blockedAutoInstallStatus);
|
|
}
|
|
const installedStatus = await inspectCodexComputerUse({
|
|
...params,
|
|
computerUseConfig: config,
|
|
runLiveTest: config.strictReadiness,
|
|
installPlugin: true,
|
|
});
|
|
if (!installedStatus.ready) {
|
|
throw new CodexComputerUseSetupError(installedStatus);
|
|
}
|
|
return installedStatus;
|
|
}
|
|
if (!status.ready) {
|
|
throw new CodexComputerUseSetupError(status);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
/** Forces Computer Use plugin installation and returns the ready status. */
|
|
export async function installCodexComputerUse(
|
|
params: CodexComputerUseSetupParams = {},
|
|
): Promise<CodexComputerUseStatus> {
|
|
const config = resolveComputerUseConfig({
|
|
...params,
|
|
forceEnable: true,
|
|
overrides: { ...params.overrides, enabled: true, autoInstall: true },
|
|
});
|
|
const status = await inspectCodexComputerUse({
|
|
...params,
|
|
computerUseConfig: config,
|
|
runLiveTest: true,
|
|
installPlugin: true,
|
|
});
|
|
if (!status.ready) {
|
|
throw new CodexComputerUseSetupError(status);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
async function inspectCodexComputerUse(
|
|
params: CodexComputerUseInspectionParams,
|
|
): Promise<CodexComputerUseStatus> {
|
|
const resolvedRuntime = resolveCodexAppServerRuntimeOptions({
|
|
pluginConfig: params.pluginConfig,
|
|
managedCommandOrder: "desktop-first",
|
|
});
|
|
const operationTimeoutMs = params.timeoutMs ?? resolvedRuntime.requestTimeoutMs;
|
|
const deadline = operationTimeoutMs > 0 ? Date.now() + operationTimeoutMs : undefined;
|
|
const remainingTimeoutMs = () =>
|
|
deadline === undefined ? operationTimeoutMs : Math.max(1, deadline - Date.now());
|
|
const clientOptions = {
|
|
startOptions: resolvedRuntime.start,
|
|
pluginConfig: params.pluginConfig,
|
|
config: params.config,
|
|
agentDir: params.agentDir,
|
|
abandonSignal: params.signal,
|
|
};
|
|
const lease: CodexAppServerClientLease = {};
|
|
try {
|
|
let client = params.client;
|
|
if (!client && !params.request) {
|
|
client = await getLeasedSharedCodexAppServerClient({
|
|
...clientOptions,
|
|
timeoutMs: remainingTimeoutMs(),
|
|
});
|
|
lease.client = client;
|
|
}
|
|
if (!params.installPlugin) {
|
|
if (!lease.client) {
|
|
return await inspectCodexComputerUseWithoutFence(params);
|
|
}
|
|
return await withLeasedCodexAppServerClientStartSelectionRetry({
|
|
lease,
|
|
options: { ...clientOptions, timeoutMs: remainingTimeoutMs() },
|
|
signal: params.signal,
|
|
run: async (readClient, requestOptions) => {
|
|
const { assertCurrent } = requestOptions();
|
|
return await inspectCodexComputerUseWithoutFence({
|
|
...params,
|
|
client: readClient,
|
|
request: async <T>(
|
|
method: string,
|
|
requestParams?: unknown,
|
|
options?: { timeoutMs?: number; signal?: AbortSignal },
|
|
) => {
|
|
// Cleanup keeps its own deadline after the operation expires or is aborted.
|
|
const scopedOptions =
|
|
method === "thread/unsubscribe"
|
|
? {
|
|
timeoutMs: options?.timeoutMs ?? operationTimeoutMs,
|
|
signal: options?.signal,
|
|
assertCurrent,
|
|
}
|
|
: requestOptions();
|
|
return await requestCodexAppServerClientJson<T>({
|
|
client: readClient,
|
|
method,
|
|
requestParams,
|
|
config: params.config,
|
|
timeoutMs: Math.min(
|
|
options?.timeoutMs ?? operationTimeoutMs,
|
|
scopedOptions.timeoutMs,
|
|
),
|
|
signal: scopedOptions.signal,
|
|
assertCurrent: scopedOptions.assertCurrent,
|
|
});
|
|
},
|
|
});
|
|
},
|
|
});
|
|
}
|
|
const explicitManagedInstall =
|
|
client && !resolveCodexComputerUseConfig({ pluginConfig: params.pluginConfig }).autoInstall
|
|
? await resolveExplicitManagedComputerUseInstallContext({ ...params, client })
|
|
: undefined;
|
|
if (explicitManagedInstall) {
|
|
await waitForCodexAppServerClientDesktopGenerationDrain({
|
|
client: explicitManagedInstall.client,
|
|
timeoutMs: remainingTimeoutMs(),
|
|
...(params.signal ? { signal: params.signal } : {}),
|
|
});
|
|
assertCodexAppServerClientStartSelectionCurrent({ client: explicitManagedInstall.client });
|
|
}
|
|
const inspectionParams: CodexComputerUseInspectionParams = {
|
|
...params,
|
|
...(client ? { client } : {}),
|
|
timeoutMs: remainingTimeoutMs(),
|
|
...(explicitManagedInstall ? { explicitManagedInstall } : {}),
|
|
};
|
|
const fenceKey = resolveCodexNativeConfigFenceKey({
|
|
client,
|
|
startOptions: resolvedRuntime.start,
|
|
agentDir: params.agentDir,
|
|
config: params.config,
|
|
});
|
|
if (!fenceKey) {
|
|
return await inspectCodexComputerUseWithoutFence(inspectionParams);
|
|
}
|
|
const release = await acquireCodexNativeConfigFence(fenceKey, {
|
|
signal: params.signal,
|
|
timeoutMs: remainingTimeoutMs(),
|
|
timeoutMessage: "Codex Computer Use install timed out waiting for native config",
|
|
abortMessage: "Codex Computer Use install aborted waiting for native config",
|
|
});
|
|
let releaseFenceOnReturn = true;
|
|
try {
|
|
try {
|
|
return await inspectCodexComputerUseWithoutFence({
|
|
...inspectionParams,
|
|
releaseNativeConfigFence: release,
|
|
});
|
|
} catch (error) {
|
|
if (
|
|
client &&
|
|
(isCodexAppServerIndeterminateRequestCancellationError(error) ||
|
|
isCodexAppServerIndeterminateTransportError(error) ||
|
|
isCodexAppServerConnectionClosedError(error))
|
|
) {
|
|
// Codex may still commit a config mutation after local cancellation.
|
|
// Transfer fence ownership to physical process exit before surfacing it.
|
|
releaseFenceOnReturn = false;
|
|
await client.closeAndRunAfterExit(release, "Computer Use config mutation");
|
|
}
|
|
throw error;
|
|
}
|
|
} finally {
|
|
if (releaseFenceOnReturn) {
|
|
release();
|
|
}
|
|
}
|
|
} finally {
|
|
if (lease.client) {
|
|
releaseLeasedSharedCodexAppServerClient(lease.client);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function inspectCodexComputerUseWithoutFence(
|
|
params: CodexComputerUseInspectionParams,
|
|
): Promise<CodexComputerUseStatus> {
|
|
const request = createComputerUseRequest(params);
|
|
if (params.installPlugin) {
|
|
if (!resolveCodexComputerUseConfig({ pluginConfig: params.pluginConfig }).autoInstall) {
|
|
await prepareExplicitManagedComputerUseInstall(params);
|
|
}
|
|
await request<JsonValue>("experimentalFeature/enablement/set", {
|
|
enablement: { plugins: true },
|
|
} satisfies CodexRequestObject);
|
|
}
|
|
|
|
const managedMarketplacePath = await resolveClientManagedBundledMarketplacePath(
|
|
params.client,
|
|
params.agentDir,
|
|
);
|
|
const managedCodexHome = managedMarketplacePath
|
|
? params.client?.getRuntimeIdentity()?.codexHome
|
|
: undefined;
|
|
if (params.installPlugin && managedCodexHome) {
|
|
await assertNotSymlink(path.join(managedCodexHome, "config.toml"), "Codex config");
|
|
}
|
|
const marketplace = await resolveMarketplaceRef({
|
|
request,
|
|
config: params.computerUseConfig,
|
|
allowAdd: params.installPlugin,
|
|
signal: params.signal,
|
|
defaultBundledMarketplacePath: params.defaultBundledMarketplacePath ?? managedMarketplacePath,
|
|
defaultBundledMarketplacePathCandidates: params.defaultBundledMarketplacePathCandidates,
|
|
managedCodexHome,
|
|
});
|
|
if (!marketplace.marketplace) {
|
|
return unavailableStatus(
|
|
params.computerUseConfig,
|
|
"marketplace_missing",
|
|
marketplace.message ??
|
|
`No Codex marketplace containing ${params.computerUseConfig.pluginName} is registered. Configure computerUse.marketplaceSource or computerUse.marketplacePath, then run /codex computer-use install.`,
|
|
);
|
|
}
|
|
|
|
const pluginInspection = await ensureComputerUsePlugin({
|
|
request,
|
|
config: params.computerUseConfig,
|
|
marketplace: marketplace.marketplace,
|
|
installPlugin: params.installPlugin,
|
|
});
|
|
if (!pluginInspection.ok) {
|
|
return pluginInspection.status;
|
|
}
|
|
|
|
return await readComputerUseTools({
|
|
request,
|
|
client: params.client,
|
|
signal: params.signal,
|
|
config: params.computerUseConfig,
|
|
plugin: pluginInspection.plugin,
|
|
runLiveTest: params.runLiveTest,
|
|
installPlugin: params.installPlugin,
|
|
releaseNativeConfigFence: params.releaseNativeConfigFence,
|
|
});
|
|
}
|
|
|
|
async function prepareExplicitManagedComputerUseInstall(
|
|
params: CodexComputerUseInspectionParams,
|
|
): Promise<void> {
|
|
const context = params.explicitManagedInstall;
|
|
if (!context) {
|
|
return;
|
|
}
|
|
await reconcileCodexComputerUseStartArtifacts({
|
|
startOptions: {
|
|
transport: "stdio",
|
|
command: context.command,
|
|
commandSource: "resolved-managed",
|
|
args: ["app-server"],
|
|
headers: {},
|
|
env: { CODEX_HOME: context.codexHome },
|
|
},
|
|
agentDir: context.agentDir,
|
|
pluginConfig: { computerUse: { ...params.computerUseConfig, autoInstall: true } },
|
|
ownsIsolatedCodexHome: true,
|
|
desktopGeneration: context.desktopGeneration,
|
|
forceCacheRefresh: true,
|
|
assertCurrent: () =>
|
|
assertCodexAppServerClientStartSelectionCurrent({ client: context.client }),
|
|
});
|
|
}
|
|
|
|
async function resolveExplicitManagedComputerUseInstallContext(
|
|
params: CodexComputerUseInspectionParams & { client: CodexAppServerClient },
|
|
): Promise<ExplicitManagedComputerUseInstallContext | undefined> {
|
|
if (!params.agentDir) {
|
|
return undefined;
|
|
}
|
|
const codexHome = params.client.getRuntimeIdentity()?.codexHome;
|
|
const processIdentity = readCodexAppServerClientProcessIdentity(params.client);
|
|
const command =
|
|
processIdentity?.nativeCommand ??
|
|
(processIdentity && isManagedCodexDesktopCommand(processIdentity.command, "darwin")
|
|
? processIdentity.command
|
|
: undefined);
|
|
if (!codexHome || !command) {
|
|
return undefined;
|
|
}
|
|
const desktopGeneration = readCodexAppServerClientDesktopGeneration(params.client);
|
|
if (!desktopGeneration) {
|
|
throw new Error(
|
|
"Codex Computer Use install requires a desktop-generation-bound client; reconnect and retry.",
|
|
);
|
|
}
|
|
const expectedHome = resolveCodexAppServerHomeDir(params.agentDir);
|
|
const [actualRealHome, expectedRealHome] = await Promise.all([
|
|
fs.realpath(codexHome).catch(() => undefined),
|
|
fs.realpath(expectedHome).catch(() => undefined),
|
|
]);
|
|
if (!actualRealHome || actualRealHome !== expectedRealHome) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
client: params.client,
|
|
agentDir: params.agentDir,
|
|
codexHome,
|
|
command,
|
|
desktopGeneration,
|
|
};
|
|
}
|
|
|
|
async function resolveClientManagedBundledMarketplacePath(
|
|
client: CodexAppServerClient | undefined,
|
|
agentDir: string | undefined,
|
|
): Promise<string | undefined> {
|
|
const codexHome = client?.getRuntimeIdentity()?.codexHome;
|
|
if (!codexHome || !agentDir) {
|
|
return undefined;
|
|
}
|
|
const [actualRealHome, expectedRealHome] = await Promise.all([
|
|
fs.realpath(codexHome).catch(() => undefined),
|
|
fs.realpath(resolveCodexAppServerHomeDir(agentDir)).catch(() => undefined),
|
|
]);
|
|
if (!actualRealHome || actualRealHome !== expectedRealHome) {
|
|
return undefined;
|
|
}
|
|
const managedPath = resolveCodexManagedBundledMarketplacePath(codexHome);
|
|
return existsSync(managedPath) ? managedPath : undefined;
|
|
}
|
|
|
|
async function ensureComputerUsePlugin(params: {
|
|
request: CodexComputerUseRequest;
|
|
config: ResolvedCodexComputerUseConfig;
|
|
marketplace: MarketplaceRef;
|
|
installPlugin: boolean;
|
|
}): Promise<PluginInspection> {
|
|
let plugin = await readComputerUsePlugin(
|
|
params.request,
|
|
params.marketplace,
|
|
params.config.pluginName,
|
|
);
|
|
if (!plugin.summary.installed || !plugin.summary.enabled) {
|
|
if (!params.installPlugin) {
|
|
return {
|
|
ok: false,
|
|
status: statusFromPlugin({
|
|
config: params.config,
|
|
plugin,
|
|
tools: [],
|
|
reason: pluginSetupReason(plugin),
|
|
message: pluginSetupMessage(params.config, plugin),
|
|
}),
|
|
};
|
|
}
|
|
await params.request<JsonValue>(
|
|
"plugin/install",
|
|
pluginRequestParams(params.marketplace, params.config.pluginName),
|
|
);
|
|
await reloadMcpServers(params.request);
|
|
plugin = await readComputerUsePlugin(
|
|
params.request,
|
|
params.marketplace,
|
|
params.config.pluginName,
|
|
);
|
|
}
|
|
if (!plugin.summary.installed || !plugin.summary.enabled) {
|
|
return {
|
|
ok: false,
|
|
status: statusFromPlugin({
|
|
config: params.config,
|
|
plugin,
|
|
tools: [],
|
|
reason: pluginSetupReason(plugin),
|
|
message: pluginSetupMessage(params.config, plugin),
|
|
}),
|
|
};
|
|
}
|
|
return { ok: true, plugin };
|
|
}
|
|
|
|
async function readComputerUseTools(params: {
|
|
request: CodexComputerUseRequest;
|
|
client?: CodexAppServerClient;
|
|
signal?: AbortSignal;
|
|
config: ResolvedCodexComputerUseConfig;
|
|
plugin: CodexPluginDetail;
|
|
runLiveTest: boolean;
|
|
installPlugin: boolean;
|
|
releaseNativeConfigFence?: () => void;
|
|
}): Promise<CodexComputerUseStatus> {
|
|
let server = await readMcpServerStatus(params.request, params.config.mcpServerName);
|
|
let tools = Object.keys(server?.tools ?? {}).toSorted();
|
|
if ((!server || tools.length === 0) && params.installPlugin) {
|
|
await reloadMcpServers(params.request);
|
|
server = await readMcpServerStatus(params.request, params.config.mcpServerName);
|
|
tools = Object.keys(server?.tools ?? {}).toSorted();
|
|
}
|
|
if (!server) {
|
|
return statusFromPlugin({
|
|
config: params.config,
|
|
plugin: params.plugin,
|
|
tools: [],
|
|
reason: "mcp_missing",
|
|
message: `Computer Use is installed, but the ${params.config.mcpServerName} MCP server is not available.`,
|
|
});
|
|
}
|
|
if (tools.length === 0) {
|
|
return statusFromPlugin({
|
|
config: params.config,
|
|
plugin: params.plugin,
|
|
tools,
|
|
reason: "mcp_missing",
|
|
message: `Computer Use is installed, but the ${params.config.mcpServerName} MCP server exposes no tools.`,
|
|
});
|
|
}
|
|
|
|
const status = statusFromPlugin({
|
|
config: params.config,
|
|
plugin: params.plugin,
|
|
tools,
|
|
reason: "ready",
|
|
message: "Computer Use is ready.",
|
|
});
|
|
// Non-strict turns need installation and exposure, not a desktop round trip.
|
|
// Explicit diagnostics and the client-owned health monitor still probe live use.
|
|
if (!params.runLiveTest) {
|
|
return status;
|
|
}
|
|
// The readiness thread reacquires this fence before loading native config.
|
|
params.releaseNativeConfigFence?.();
|
|
const { liveTest, repair } = await runCodexComputerUseLiveTest({
|
|
request: params.request,
|
|
client: params.client,
|
|
signal: params.signal,
|
|
config: params.config,
|
|
tools,
|
|
});
|
|
const compatibilityStartupAllowed = !liveTest.ok && !params.config.strictReadiness;
|
|
return {
|
|
...status,
|
|
ready: liveTest.ok,
|
|
reason: liveTest.ok ? "ready" : "live_test_failed",
|
|
liveTest,
|
|
...(repair ? { repair } : {}),
|
|
warnings: [
|
|
...status.warnings,
|
|
...(repair?.warnings ?? []),
|
|
...(compatibilityStartupAllowed
|
|
? [
|
|
"Computer Use live test failed, but compatibility startup remains enabled; set computerUse.strictReadiness to true to fail closed.",
|
|
]
|
|
: []),
|
|
],
|
|
message: liveTest.ok
|
|
? "Computer Use is ready."
|
|
: compatibilityStartupAllowed
|
|
? `${liveTest.message} Startup is allowed because computerUse.strictReadiness is false.`
|
|
: liveTest.message,
|
|
};
|
|
}
|
|
|
|
async function resolveMarketplaceRef(params: {
|
|
request: CodexComputerUseRequest;
|
|
config: ResolvedCodexComputerUseConfig;
|
|
allowAdd: boolean;
|
|
signal?: AbortSignal;
|
|
defaultBundledMarketplacePath?: string;
|
|
defaultBundledMarketplacePathCandidates?: readonly string[];
|
|
managedCodexHome?: string;
|
|
}): Promise<MarketplaceResolution> {
|
|
let preferredMarketplaceName = params.config.marketplaceName;
|
|
if (params.config.marketplaceSource && params.allowAdd) {
|
|
const added = await params.request<{ marketplaceName?: string }>("marketplace/add", {
|
|
source: params.config.marketplaceSource,
|
|
} satisfies CodexRequestObject);
|
|
preferredMarketplaceName ??= added.marketplaceName;
|
|
}
|
|
|
|
if (params.config.marketplacePath) {
|
|
const marketplace: MarketplaceRef = preferredMarketplaceName
|
|
? { kind: "local", name: preferredMarketplaceName, path: params.config.marketplacePath }
|
|
: { kind: "local", path: params.config.marketplacePath };
|
|
return { marketplace };
|
|
}
|
|
|
|
let candidates = await listComputerUseMarketplaceCandidates(params.request, params.config);
|
|
const bundledMarketplacePath = resolveBundledComputerUseMarketplacePath(params);
|
|
if (
|
|
candidates.length === 0 &&
|
|
bundledMarketplacePath &&
|
|
shouldAddBundledComputerUseMarketplace(params)
|
|
) {
|
|
if (params.managedCodexHome) {
|
|
await migrateLegacyBundledMarketplaceSource({
|
|
request: params.request,
|
|
bundledMarketplacePath,
|
|
legacySources: params.defaultBundledMarketplacePathCandidates,
|
|
userConfigPath: path.join(params.managedCodexHome, "config.toml"),
|
|
});
|
|
}
|
|
const added = await params.request<{ marketplaceName?: string }>("marketplace/add", {
|
|
source: bundledMarketplacePath,
|
|
} satisfies CodexRequestObject);
|
|
preferredMarketplaceName ??= added.marketplaceName;
|
|
candidates = await listComputerUseMarketplaceCandidates(params.request, params.config);
|
|
}
|
|
|
|
const waitUntil = marketplaceDiscoveryWaitUntil(params);
|
|
if (
|
|
candidates.length === 0 &&
|
|
waitUntil > Date.now() &&
|
|
(await codexNativePluginsDisabled(params.request))
|
|
) {
|
|
return {
|
|
message:
|
|
"Codex native plugin support is disabled (features.plugins = false). Enable it in the Codex config, then run /codex computer-use install.",
|
|
};
|
|
}
|
|
while (candidates.length === 0) {
|
|
if (Date.now() >= waitUntil) {
|
|
break;
|
|
}
|
|
await delay(
|
|
Math.min(CURATED_MARKETPLACE_POLL_INTERVAL_MS, waitUntil - Date.now()),
|
|
params.signal,
|
|
);
|
|
candidates = await listComputerUseMarketplaceCandidates(params.request, params.config);
|
|
}
|
|
|
|
if (preferredMarketplaceName) {
|
|
const preferred = candidates.find((candidate) => candidate.name === preferredMarketplaceName);
|
|
if (preferred) {
|
|
return { marketplace: preferred };
|
|
}
|
|
return {
|
|
message: `Configured Codex marketplace ${preferredMarketplaceName} was not found or does not contain ${params.config.pluginName}. Run /codex computer-use install with a source or path to install from a new marketplace.`,
|
|
};
|
|
}
|
|
if (candidates.length > 1) {
|
|
const preferred = chooseKnownComputerUseMarketplace(candidates);
|
|
if (preferred) {
|
|
return { marketplace: preferred };
|
|
}
|
|
return {
|
|
message: `Multiple Codex marketplaces contain ${params.config.pluginName}. Configure computerUse.marketplaceName or computerUse.marketplacePath to choose one.`,
|
|
};
|
|
}
|
|
if (params.config.marketplaceSource && !params.allowAdd && candidates.length === 0) {
|
|
return {
|
|
message:
|
|
"Computer Use marketplace source is configured but has not been registered. Run /codex computer-use install to register it.",
|
|
};
|
|
}
|
|
const marketplace = candidates[0];
|
|
return marketplace ? { marketplace } : {};
|
|
}
|
|
|
|
async function migrateLegacyBundledMarketplaceSource(params: {
|
|
request: CodexComputerUseRequest;
|
|
bundledMarketplacePath: string;
|
|
legacySources?: readonly string[];
|
|
userConfigPath: string;
|
|
}): Promise<void> {
|
|
const response = await params.request<
|
|
CodexConfigReadResponse & {
|
|
config: { marketplaces?: Record<string, { source_type?: string; source?: string }> };
|
|
}
|
|
>("config/read", { includeLayers: false });
|
|
const bundled = response.config.marketplaces?.[BUNDLED_MARKETPLACE_NAME];
|
|
const sourceOrigin = response.origins[`marketplaces.${BUNDLED_MARKETPLACE_NAME}.source`];
|
|
if (
|
|
bundled?.source_type !== "local" ||
|
|
!bundled.source ||
|
|
sourceOrigin?.name.type !== "user" ||
|
|
sourceOrigin.name.profile !== null ||
|
|
path.resolve(sourceOrigin.name.file) !== path.resolve(params.userConfigPath)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Codex hides a reserved marketplace whose old direct source violates its
|
|
// managed-root policy. Remove only sources OpenClaw previously provisioned.
|
|
const configuredSource = path.resolve(bundled.source);
|
|
if (configuredSource === path.resolve(params.bundledMarketplacePath)) {
|
|
return;
|
|
}
|
|
const legacySources =
|
|
params.legacySources ?? resolveMacOSDesktopCodexBundledMarketplaceCandidates();
|
|
if (!legacySources.some((source) => path.resolve(source) === configuredSource)) {
|
|
return;
|
|
}
|
|
await params.request("marketplace/remove", { marketplaceName: BUNDLED_MARKETPLACE_NAME });
|
|
}
|
|
|
|
async function listComputerUseMarketplaceCandidates(
|
|
request: CodexComputerUseRequest,
|
|
config: ResolvedCodexComputerUseConfig,
|
|
): Promise<MarketplaceRef[]> {
|
|
const listed = await request<CodexPluginListResponse>("plugin/list", {
|
|
cwds: [],
|
|
} satisfies CodexRequestObject);
|
|
return findComputerUseMarketplaces(listed, config.pluginName);
|
|
}
|
|
|
|
async function codexNativePluginsDisabled(request: CodexComputerUseRequest): Promise<boolean> {
|
|
const response = await request<CodexAppServerRequestResult<"experimentalFeature/list">>(
|
|
"experimentalFeature/list",
|
|
{},
|
|
);
|
|
// Codex returns the full catalog when limit is omitted; absent plugins remains unknown so polling continues.
|
|
return response.data.find(({ name }) => name === "plugins")?.enabled === false;
|
|
}
|
|
|
|
function blockUnsafeAutoInstallStatus(
|
|
config: ResolvedCodexComputerUseConfig,
|
|
): CodexComputerUseStatus | undefined {
|
|
if (!config.marketplaceSource) {
|
|
return undefined;
|
|
}
|
|
return unavailableStatus(
|
|
config,
|
|
"auto_install_blocked",
|
|
"Computer Use auto-install only uses marketplaces Codex app-server has already discovered. Run /codex computer-use install to install from a configured marketplace source.",
|
|
);
|
|
}
|
|
|
|
function shouldAddBundledComputerUseMarketplace(params: {
|
|
config: ResolvedCodexComputerUseConfig;
|
|
allowAdd: boolean;
|
|
defaultBundledMarketplacePath?: string;
|
|
defaultBundledMarketplacePathCandidates?: readonly string[];
|
|
}): boolean {
|
|
return (
|
|
params.allowAdd &&
|
|
!params.config.marketplaceSource &&
|
|
!params.config.marketplacePath &&
|
|
!params.config.marketplaceName &&
|
|
Boolean(resolveBundledComputerUseMarketplacePath(params))
|
|
);
|
|
}
|
|
|
|
function resolveBundledComputerUseMarketplacePath(params: {
|
|
defaultBundledMarketplacePath?: string;
|
|
defaultBundledMarketplacePathCandidates?: readonly string[];
|
|
}): string | undefined {
|
|
if (params.defaultBundledMarketplacePath) {
|
|
return existsSync(params.defaultBundledMarketplacePath)
|
|
? params.defaultBundledMarketplacePath
|
|
: undefined;
|
|
}
|
|
if (!params.defaultBundledMarketplacePathCandidates) {
|
|
return undefined;
|
|
}
|
|
return resolveFirstExistingMacOSDesktopCodexBundledMarketplacePath({
|
|
candidates: params.defaultBundledMarketplacePathCandidates,
|
|
});
|
|
}
|
|
|
|
function findComputerUseMarketplaces(
|
|
listed: CodexPluginListResponse,
|
|
pluginName: string,
|
|
): MarketplaceRef[] {
|
|
return listed.marketplaces.flatMap((marketplace): MarketplaceRef[] => {
|
|
const plugin = marketplace.plugins.find(
|
|
(candidate) =>
|
|
candidate.name === pluginName ||
|
|
candidate.id === pluginName ||
|
|
candidate.id === `${pluginName}@${marketplace.name}`,
|
|
);
|
|
if (!plugin) {
|
|
return [];
|
|
}
|
|
if (marketplace.path) {
|
|
return [{ kind: "local", name: marketplace.name, path: marketplace.path }];
|
|
}
|
|
const remotePluginId = plugin.remotePluginId?.trim();
|
|
if (!remotePluginId) {
|
|
// Remote plugin/read and plugin/install reject the human-readable slug.
|
|
return [];
|
|
}
|
|
return [
|
|
{
|
|
kind: "remote",
|
|
name: marketplace.name,
|
|
remoteMarketplaceName: marketplace.name,
|
|
remotePluginId,
|
|
},
|
|
];
|
|
});
|
|
}
|
|
|
|
function chooseKnownComputerUseMarketplace(
|
|
candidates: MarketplaceRef[],
|
|
): MarketplaceRef | undefined {
|
|
for (const marketplaceName of COMPUTER_USE_MARKETPLACE_NAME_PRIORITY) {
|
|
const candidate = candidates.find((marketplace) => marketplace.name === marketplaceName);
|
|
if (candidate) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function marketplaceDiscoveryWaitUntil(params: {
|
|
config: ResolvedCodexComputerUseConfig;
|
|
allowAdd: boolean;
|
|
}): number {
|
|
if (
|
|
params.allowAdd &&
|
|
!params.config.marketplaceSource &&
|
|
!params.config.marketplacePath &&
|
|
!params.config.marketplaceName
|
|
) {
|
|
return Date.now() + params.config.marketplaceDiscoveryTimeoutMs;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
async function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
if (signal?.aborted) {
|
|
throw abortError(signal);
|
|
}
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onAbort = () => {
|
|
clearTimeout(timer);
|
|
signal?.removeEventListener("abort", onAbort);
|
|
reject(abortError(signal));
|
|
};
|
|
const timer: ReturnType<typeof setTimeout> = setTimeout(() => {
|
|
signal?.removeEventListener("abort", onAbort);
|
|
resolve();
|
|
}, ms);
|
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
});
|
|
}
|
|
|
|
function abortError(signal?: AbortSignal): Error {
|
|
const reason = signal?.reason;
|
|
return reason instanceof Error ? reason : new Error("Computer Use setup was aborted.");
|
|
}
|
|
|
|
async function readComputerUsePlugin(
|
|
request: CodexComputerUseRequest,
|
|
marketplace: MarketplaceRef,
|
|
pluginName: string,
|
|
): Promise<CodexPluginDetail> {
|
|
const response = await request<CodexPluginReadResponse>(
|
|
"plugin/read",
|
|
pluginRequestParams(marketplace, pluginName),
|
|
);
|
|
return response.plugin;
|
|
}
|
|
|
|
async function readMcpServerStatus(
|
|
request: CodexComputerUseRequest,
|
|
serverName: string,
|
|
): Promise<CodexMcpServerStatus | undefined> {
|
|
let cursor: string | null | undefined;
|
|
do {
|
|
const response = await request<CodexListMcpServerStatusResponse>("mcpServerStatus/list", {
|
|
cursor,
|
|
limit: 100,
|
|
detail: "toolsAndAuthOnly",
|
|
} satisfies CodexRequestObject);
|
|
const found = response.data.find((server) => server.name === serverName);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
cursor = response.nextCursor;
|
|
} while (cursor);
|
|
return undefined;
|
|
}
|
|
|
|
async function reloadMcpServers(request: CodexComputerUseRequest): Promise<void> {
|
|
await request("config/mcpServer/reload", undefined);
|
|
}
|
|
|
|
function pluginRequestParams(marketplace: MarketplaceRef, pluginName: string) {
|
|
return marketplace.kind === "local"
|
|
? { marketplacePath: marketplace.path, pluginName }
|
|
: {
|
|
remoteMarketplaceName: marketplace.remoteMarketplaceName,
|
|
pluginName: marketplace.remotePluginId,
|
|
};
|
|
}
|
|
|
|
function pluginSetupReason(plugin: CodexPluginDetail): CodexComputerUseStatusReason {
|
|
return plugin.summary.installed ? "plugin_disabled" : "plugin_not_installed";
|
|
}
|
|
|
|
function pluginSetupMessage(
|
|
config: ResolvedCodexComputerUseConfig,
|
|
plugin: CodexPluginDetail,
|
|
): string {
|
|
if (!plugin.summary.installed) {
|
|
return "Computer Use is available but not installed. Run /codex computer-use install or enable computerUse.autoInstall.";
|
|
}
|
|
return `Computer Use is installed, but the ${config.pluginName} plugin is disabled. Run /codex computer-use install or enable computerUse.autoInstall to re-enable it.`;
|
|
}
|
|
|
|
function statusFromPlugin(params: {
|
|
config: ResolvedCodexComputerUseConfig;
|
|
plugin: CodexPluginDetail;
|
|
tools: string[];
|
|
reason: CodexComputerUseStatusReason;
|
|
message: string;
|
|
}): CodexComputerUseStatus {
|
|
return {
|
|
enabled: true,
|
|
ready:
|
|
params.plugin.summary.installed && params.plugin.summary.enabled && params.tools.length > 0,
|
|
reason: params.reason,
|
|
installed: params.plugin.summary.installed,
|
|
pluginEnabled: params.plugin.summary.enabled,
|
|
mcpServerAvailable: params.tools.length > 0,
|
|
pluginName: params.config.pluginName,
|
|
mcpServerName: params.config.mcpServerName,
|
|
marketplaceName: params.plugin.marketplaceName,
|
|
...(params.plugin.marketplacePath ? { marketplacePath: params.plugin.marketplacePath } : {}),
|
|
tools: params.tools,
|
|
installation: installationStatusFromPlugin(params.plugin, params.message),
|
|
exposure: exposureStatusFromTools(params.config, params.tools),
|
|
liveTest: skippedLiveTestStatus(params.config, "Computer Use live test was not run."),
|
|
warnings: pluginWarnings(params.plugin),
|
|
message: params.message,
|
|
};
|
|
}
|
|
|
|
function disabledStatus(config: ResolvedCodexComputerUseConfig): CodexComputerUseStatus {
|
|
return {
|
|
enabled: false,
|
|
ready: false,
|
|
reason: "disabled",
|
|
installed: false,
|
|
pluginEnabled: false,
|
|
mcpServerAvailable: false,
|
|
pluginName: config.pluginName,
|
|
mcpServerName: config.mcpServerName,
|
|
tools: [],
|
|
installation: {
|
|
status: "disabled",
|
|
ok: false,
|
|
message: "Computer Use is disabled.",
|
|
},
|
|
exposure: {
|
|
status: "skipped",
|
|
ok: false,
|
|
message: "MCP exposure was not checked because Computer Use is disabled.",
|
|
},
|
|
liveTest: skippedLiveTestStatus(
|
|
config,
|
|
"Computer Use live test was not run because Computer Use is disabled.",
|
|
),
|
|
warnings: [],
|
|
message: "Computer Use is disabled.",
|
|
};
|
|
}
|
|
|
|
function unavailableStatus(
|
|
config: ResolvedCodexComputerUseConfig,
|
|
reason: CodexComputerUseStatusReason,
|
|
message: string,
|
|
): CodexComputerUseStatus {
|
|
return {
|
|
enabled: true,
|
|
ready: false,
|
|
reason,
|
|
installed: false,
|
|
pluginEnabled: false,
|
|
mcpServerAvailable: false,
|
|
pluginName: config.pluginName,
|
|
mcpServerName: config.mcpServerName,
|
|
...(config.marketplaceName ? { marketplaceName: config.marketplaceName } : {}),
|
|
...(config.marketplacePath ? { marketplacePath: config.marketplacePath } : {}),
|
|
tools: [],
|
|
installation: {
|
|
status: reason === "marketplace_missing" ? "marketplace_missing" : "not_installed",
|
|
ok: false,
|
|
message,
|
|
},
|
|
exposure: {
|
|
status: "skipped",
|
|
ok: false,
|
|
message: "MCP exposure was not checked because Computer Use installation is not ready.",
|
|
},
|
|
liveTest: skippedLiveTestStatus(
|
|
config,
|
|
"Computer Use live test was not run because installation is not ready.",
|
|
),
|
|
warnings: [],
|
|
message,
|
|
};
|
|
}
|
|
|
|
function installationStatusFromPlugin(
|
|
plugin: CodexPluginDetail,
|
|
message: string,
|
|
): CodexComputerUseStatus["installation"] {
|
|
if (!plugin.summary.installed) {
|
|
return {
|
|
status: "not_installed",
|
|
ok: false,
|
|
message,
|
|
};
|
|
}
|
|
if (!plugin.summary.enabled) {
|
|
return {
|
|
status: "installed_disabled",
|
|
ok: false,
|
|
message,
|
|
};
|
|
}
|
|
return {
|
|
status: "installed",
|
|
ok: true,
|
|
message: "Computer Use plugin is installed and enabled.",
|
|
};
|
|
}
|
|
|
|
function exposureStatusFromTools(
|
|
config: ResolvedCodexComputerUseConfig,
|
|
tools: string[],
|
|
): CodexComputerUseStatus["exposure"] {
|
|
if (tools.length === 0) {
|
|
return {
|
|
status: "missing",
|
|
ok: false,
|
|
message: `Computer Use MCP server ${config.mcpServerName} is not exposed.`,
|
|
};
|
|
}
|
|
return {
|
|
status: "available",
|
|
ok: true,
|
|
message: `Computer Use MCP server ${config.mcpServerName} exposes ${tools.length} tools.`,
|
|
};
|
|
}
|
|
|
|
function pluginWarnings(plugin: CodexPluginDetail): string[] {
|
|
const warnings: string[] = [];
|
|
const source = plugin.summary.source;
|
|
if (source && typeof source === "object" && "type" in source && source.type === "remote") {
|
|
warnings.push(
|
|
"Computer Use plugin is resolved from a remote marketplace; live local bundles are preferred.",
|
|
);
|
|
}
|
|
return warnings;
|
|
}
|
|
|
|
function createComputerUseRequest(params: {
|
|
request?: CodexComputerUseRequest;
|
|
client?: CodexAppServerClient;
|
|
timeoutMs?: number;
|
|
signal?: AbortSignal;
|
|
}): CodexComputerUseRequest {
|
|
if (params.request) {
|
|
return params.request;
|
|
}
|
|
const client = params.client;
|
|
if (!client) {
|
|
throw new Error("Computer Use setup requires an acquired app-server client");
|
|
}
|
|
return async <T = JsonValue | undefined>(
|
|
method: string,
|
|
requestParams?: unknown,
|
|
options?: { timeoutMs?: number; signal?: AbortSignal },
|
|
) =>
|
|
await client.request<T>(method, requestParams, {
|
|
timeoutMs: options?.timeoutMs ?? params.timeoutMs,
|
|
signal: options?.signal ?? params.signal,
|
|
});
|
|
}
|
|
|
|
function resolveComputerUseConfig(
|
|
params: Pick<CodexComputerUseSetupParams, "pluginConfig" | "overrides" | "forceEnable">,
|
|
): ResolvedCodexComputerUseConfig {
|
|
const overrides = params.forceEnable ? { ...params.overrides, enabled: true } : params.overrides;
|
|
return resolveCodexComputerUseConfig({
|
|
pluginConfig: params.pluginConfig,
|
|
overrides,
|
|
});
|
|
}
|
|
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|