fix: auto-register bundled computer use marketplace

This commit is contained in:
Peter Steinberger
2026-04-28 00:36:12 +01:00
parent 802f13ac15
commit 697d85aefe
4 changed files with 269 additions and 17 deletions

View File

@@ -1,3 +1,6 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
ensureCodexComputerUse,
@@ -7,8 +10,13 @@ import {
} from "./computer-use.js";
describe("Codex Computer Use setup", () => {
const cleanupPaths: string[] = [];
afterEach(() => {
vi.useRealTimers();
for (const cleanupPath of cleanupPaths.splice(0)) {
fs.rmSync(cleanupPath, { recursive: true, force: true });
}
});
it("stays disabled until configured", async () => {
@@ -253,6 +261,69 @@ describe("Codex Computer Use setup", () => {
});
});
it("auto-registers the bundled Codex app marketplace during auto-install", async () => {
const bundledMarketplacePath = fs.mkdtempSync(
path.join(os.tmpdir(), "openclaw-codex-bundled-marketplace-"),
);
cleanupPaths.push(bundledMarketplacePath);
const request = createBundledMarketplaceComputerUseRequest(bundledMarketplacePath);
await expect(
ensureCodexComputerUse({
pluginConfig: {
computerUse: {
enabled: true,
autoInstall: true,
},
},
request,
defaultBundledMarketplacePath: bundledMarketplacePath,
}),
).resolves.toEqual(
expect.objectContaining({
ready: true,
reason: "ready",
marketplaceName: "openai-bundled",
message: "Computer Use is ready.",
}),
);
expect(request).toHaveBeenCalledWith("marketplace/add", {
source: bundledMarketplacePath,
});
expect(request).toHaveBeenCalledWith("plugin/install", {
marketplacePath: `${bundledMarketplacePath}/.agents/plugins/marketplace.json`,
pluginName: "computer-use",
});
});
it("allows auto-install from a configured local marketplace path", async () => {
const request = createComputerUseRequest({ installed: false });
await expect(
ensureCodexComputerUse({
pluginConfig: {
computerUse: {
enabled: true,
autoInstall: true,
marketplacePath: "/marketplaces/desktop-tools/.agents/plugins/marketplace.json",
},
},
request,
}),
).resolves.toEqual(
expect.objectContaining({
ready: true,
reason: "ready",
message: "Computer Use is ready.",
}),
);
expect(request).not.toHaveBeenCalledWith("marketplace/add", expect.anything());
expect(request).toHaveBeenCalledWith("plugin/install", {
marketplacePath: "/marketplaces/desktop-tools/.agents/plugins/marketplace.json",
pluginName: "computer-use",
});
});
it("requires an explicit install command for configured marketplace sources", async () => {
const request = createComputerUseRequest({ installed: false });
@@ -607,6 +678,87 @@ function createMultiMarketplaceComputerUseRequest(): CodexComputerUseRequest {
}) as CodexComputerUseRequest;
}
function createBundledMarketplaceComputerUseRequest(
bundledMarketplacePath: string,
): CodexComputerUseRequest {
let registered = false;
let installed = false;
return vi.fn(async (method: string, requestParams?: unknown) => {
if (method === "experimentalFeature/enablement/set") {
return { enablement: { plugins: true } };
}
if (method === "marketplace/add") {
expect(requestParams).toEqual({
source: bundledMarketplacePath,
});
registered = true;
return {
marketplaceName: "openai-bundled",
installedRoot: bundledMarketplacePath,
alreadyAdded: false,
};
}
if (method === "plugin/list") {
return {
marketplaces: registered
? [
{
name: "openai-bundled",
path: `${bundledMarketplacePath}/.agents/plugins/marketplace.json`,
interface: null,
plugins: [pluginSummary(installed, "openai-bundled")],
},
]
: [],
marketplaceLoadErrors: [],
featuredPluginIds: [],
};
}
if (method === "plugin/read") {
return {
plugin: {
marketplaceName: "openai-bundled",
marketplacePath: `${bundledMarketplacePath}/.agents/plugins/marketplace.json`,
summary: pluginSummary(installed, "openai-bundled"),
description: "Control desktop apps.",
skills: [],
apps: [],
mcpServers: ["computer-use"],
},
};
}
if (method === "plugin/install") {
installed = true;
return { authPolicy: "ON_INSTALL", appsNeedingAuth: [] };
}
if (method === "config/mcpServer/reload") {
return undefined;
}
if (method === "mcpServerStatus/list") {
return {
data: installed
? [
{
name: "computer-use",
tools: {
list_apps: {
name: "list_apps",
inputSchema: { type: "object" },
},
},
resources: [],
resourceTemplates: [],
authStatus: "unsupported",
},
]
: [],
nextCursor: null,
};
}
throw new Error(`unexpected request ${method}`);
}) as CodexComputerUseRequest;
}
function marketplaceEntry(marketplaceName: string, installed: boolean) {
return {
name: marketplaceName,

View File

@@ -1,3 +1,4 @@
import { existsSync } from "node:fs";
import { describeControlFailure } from "./capabilities.js";
import type { CodexAppServerClient } from "./client.js";
import {
@@ -59,6 +60,7 @@ export type CodexComputerUseSetupParams = {
timeoutMs?: number;
signal?: AbortSignal;
forceEnable?: boolean;
defaultBundledMarketplacePath?: string;
};
type MarketplaceRef =
@@ -90,6 +92,8 @@ type PluginInspection =
const CURATED_MARKETPLACE_POLL_INTERVAL_MS = 2_000;
const COMPUTER_USE_MARKETPLACE_NAME_PRIORITY = ["openai-bundled", "openai-curated", "local"];
const DEFAULT_CODEX_BUNDLED_MARKETPLACE_PATH =
"/Applications/Codex.app/Contents/Resources/plugins/openai-bundled";
export async function readCodexComputerUseStatus(
params: CodexComputerUseSetupParams = {},
@@ -176,6 +180,7 @@ async function inspectCodexComputerUse(params: {
signal?: AbortSignal;
config: ResolvedCodexComputerUseConfig;
installPlugin: boolean;
defaultBundledMarketplacePath?: string;
}): Promise<CodexComputerUseStatus> {
const request = createComputerUseRequest(params);
if (params.installPlugin) {
@@ -192,6 +197,7 @@ async function inspectCodexComputerUse(params: {
config: params.config,
allowAdd: params.installPlugin,
signal: params.signal,
defaultBundledMarketplacePath: params.defaultBundledMarketplacePath,
});
if (!marketplace.marketplace) {
return unavailableStatus(
@@ -320,6 +326,7 @@ async function resolveMarketplaceRef(params: {
config: ResolvedCodexComputerUseConfig;
allowAdd: boolean;
signal?: AbortSignal;
defaultBundledMarketplacePath?: string;
}): Promise<MarketplaceResolution> {
let preferredMarketplaceName = params.config.marketplaceName;
if (params.config.marketplaceSource && params.allowAdd) {
@@ -336,16 +343,19 @@ async function resolveMarketplaceRef(params: {
return { marketplace };
}
let candidates: MarketplaceRef[] = [];
let candidates = await listComputerUseMarketplaceCandidates(params.request, params.config);
if (candidates.length === 0 && shouldAddBundledComputerUseMarketplace(params)) {
const bundledMarketplacePath =
params.defaultBundledMarketplacePath ?? DEFAULT_CODEX_BUNDLED_MARKETPLACE_PATH;
const added = await params.request<v2.MarketplaceAddResponse>("marketplace/add", {
source: bundledMarketplacePath,
} satisfies v2.MarketplaceAddParams);
preferredMarketplaceName ??= added.marketplaceName;
candidates = await listComputerUseMarketplaceCandidates(params.request, params.config);
}
const waitUntil = marketplaceDiscoveryWaitUntil(params);
while (candidates.length === 0) {
const listed = await params.request<v2.PluginListResponse>("plugin/list", {
cwds: [],
} satisfies v2.PluginListParams);
candidates = findComputerUseMarketplaces(listed, params.config.pluginName);
if (candidates.length > 0) {
break;
}
if (Date.now() >= waitUntil) {
break;
}
@@ -353,6 +363,7 @@ async function resolveMarketplaceRef(params: {
Math.min(CURATED_MARKETPLACE_POLL_INTERVAL_MS, waitUntil - Date.now()),
params.signal,
);
candidates = await listComputerUseMarketplaceCandidates(params.request, params.config);
}
if (preferredMarketplaceName) {
@@ -383,16 +394,42 @@ async function resolveMarketplaceRef(params: {
return marketplace ? { marketplace } : {};
}
async function listComputerUseMarketplaceCandidates(
request: CodexComputerUseRequest,
config: ResolvedCodexComputerUseConfig,
): Promise<MarketplaceRef[]> {
const listed = await request<v2.PluginListResponse>("plugin/list", {
cwds: [],
} satisfies v2.PluginListParams);
return findComputerUseMarketplaces(listed, config.pluginName);
}
function blockUnsafeAutoInstallStatus(
config: ResolvedCodexComputerUseConfig,
): CodexComputerUseStatus | undefined {
if (!config.marketplaceSource && !config.marketplacePath) {
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 or path.",
"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;
}): boolean {
const bundledMarketplacePath =
params.defaultBundledMarketplacePath ?? DEFAULT_CODEX_BUNDLED_MARKETPLACE_PATH;
return (
params.allowAdd &&
!params.config.marketplaceSource &&
!params.config.marketplacePath &&
!params.config.marketplaceName &&
existsSync(bundledMarketplacePath)
);
}