mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:50:43 +00:00
fix: auto-register bundled computer use marketplace
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user