diff --git a/CHANGELOG.md b/CHANGELOG.md index 4efcbad0776..88922dae625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Codex: add Computer Use setup for Codex-mode agents, including `/codex computer-use status/install`, marketplace discovery, optional auto-install, and fail-closed MCP server checks before Codex-mode turns start. Fixes #72094. (#71842) Thanks @pash-openai. - Matrix/streaming: stream tool-progress updates into live Matrix preview edits by default when preview streaming is active, with `streaming.preview.toolProgress: false` to keep answer previews while hiding interim tool lines. Thanks @gumadeiras. - Plugins/models: wire manifest `modelCatalog.aliases` and `modelCatalog.suppressions` into model-catalog planning and built-in model suppression, with OpenAI stale Spark suppression now declared in the plugin manifest before runtime fallback. Thanks @shakkernerd. - Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (`openclaw-plugin-yuanbao`) in the official channel catalog, contract suites, and community plugin docs, with a new `docs/channels/yuanbao.md` quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay. diff --git a/extensions/codex/src/app-server/computer-use.test.ts b/extensions/codex/src/app-server/computer-use.test.ts index 39d9c4651da..13f9e872e12 100644 --- a/extensions/codex/src/app-server/computer-use.test.ts +++ b/extensions/codex/src/app-server/computer-use.test.ts @@ -52,6 +52,27 @@ describe("Codex Computer Use setup", () => { expect(request).not.toHaveBeenCalledWith("plugin/install", expect.anything()); }); + it("reports an installed but disabled Computer Use plugin separately", async () => { + const request = createComputerUseRequest({ installed: true, enabled: false }); + + await expect( + readCodexComputerUseStatus({ + pluginConfig: { computerUse: { enabled: true, marketplaceName: "desktop-tools" } }, + request, + }), + ).resolves.toEqual( + expect.objectContaining({ + ready: false, + installed: true, + pluginEnabled: false, + mcpServerAvailable: false, + message: + "Computer Use is installed, but the computer-use plugin is disabled. Run /codex computer-use install or enable computerUse.autoInstall to re-enable it.", + }), + ); + expect(request).not.toHaveBeenCalledWith("plugin/install", expect.anything()); + }); + it("does not register marketplace sources during status checks", async () => { const request = createComputerUseRequest({ installed: true }); @@ -129,6 +150,28 @@ describe("Codex Computer Use setup", () => { expect(request).toHaveBeenCalledWith("config/mcpServer/reload", undefined); }); + it("re-enables an installed but disabled Computer Use plugin during install", async () => { + const request = createComputerUseRequest({ installed: true, enabled: false }); + + await expect( + installCodexComputerUse({ + pluginConfig: { computerUse: { marketplaceName: "desktop-tools" } }, + request, + }), + ).resolves.toEqual( + expect.objectContaining({ + ready: true, + installed: true, + pluginEnabled: true, + message: "Computer Use is ready.", + }), + ); + expect(request).toHaveBeenCalledWith("plugin/install", { + marketplacePath: "/marketplaces/desktop-tools/.agents/plugins/marketplace.json", + pluginName: "computer-use", + }); + }); + it("fails closed when Computer Use is required but not installed", async () => { const request = createComputerUseRequest({ installed: false }); @@ -240,6 +283,27 @@ describe("Codex Computer Use setup", () => { expect(request).not.toHaveBeenCalledWith("plugin/read", expect.anything()); }); + it("fails closed instead of installing from a remote-only Codex marketplace", async () => { + const request = createRemoteOnlyComputerUseRequest(); + + await expect( + installCodexComputerUse({ + pluginConfig: { computerUse: { marketplaceName: "openai-curated" } }, + request, + }), + ).rejects.toMatchObject({ + status: expect.objectContaining({ + ready: false, + installed: false, + pluginEnabled: false, + marketplaceName: "openai-curated", + message: + "Computer Use is available in remote Codex marketplace openai-curated, but Codex app-server does not support remote plugin install yet. Configure computerUse.marketplaceSource or computerUse.marketplacePath for a local marketplace, then run /codex computer-use install.", + }), + }); + expect(request).not.toHaveBeenCalledWith("plugin/install", expect.anything()); + }); + it("waits for the default Codex marketplace during install", async () => { vi.useFakeTimers(); const request = createComputerUseRequest({ @@ -291,9 +355,11 @@ describe("Codex Computer Use setup", () => { function createComputerUseRequest(params: { installed: boolean; + enabled?: boolean; marketplaceAvailableAfterListCalls?: number; }): CodexComputerUseRequest { let installed = params.installed; + let enabled = params.enabled ?? installed; let pluginListCalls = 0; return vi.fn(async (method: string, requestParams?: unknown) => { if (method === "experimentalFeature/enablement/set") { @@ -317,7 +383,7 @@ function createComputerUseRequest(params: { name: "desktop-tools", path: "/marketplaces/desktop-tools/.agents/plugins/marketplace.json", interface: null, - plugins: [pluginSummary(installed)], + plugins: [pluginSummary(installed, "desktop-tools", enabled)], }, ] : [], @@ -335,7 +401,7 @@ function createComputerUseRequest(params: { plugin: { marketplaceName: "desktop-tools", marketplacePath: "/marketplaces/desktop-tools/.agents/plugins/marketplace.json", - summary: pluginSummary(installed), + summary: pluginSummary(installed, "desktop-tools", enabled), description: "Control desktop apps.", skills: [], apps: [], @@ -345,6 +411,7 @@ function createComputerUseRequest(params: { } if (method === "plugin/install") { installed = true; + enabled = true; return { authPolicy: "ON_INSTALL", appsNeedingAuth: [] }; } if (method === "config/mcpServer/reload") { @@ -352,22 +419,23 @@ function createComputerUseRequest(params: { } if (method === "mcpServerStatus/list") { return { - data: installed - ? [ - { - name: "computer-use", - tools: { - list_apps: { - name: "list_apps", - inputSchema: { type: "object" }, + data: + installed && enabled + ? [ + { + name: "computer-use", + tools: { + list_apps: { + name: "list_apps", + inputSchema: { type: "object" }, + }, }, + resources: [], + resourceTemplates: [], + authStatus: "unsupported", }, - resources: [], - resourceTemplates: [], - authStatus: "unsupported", - }, - ] - : [], + ] + : [], nextCursor: null, }; } @@ -375,6 +443,46 @@ function createComputerUseRequest(params: { }) as CodexComputerUseRequest; } +function createRemoteOnlyComputerUseRequest(): CodexComputerUseRequest { + return vi.fn(async (method: string, requestParams?: unknown) => { + if (method === "experimentalFeature/enablement/set") { + return { enablement: { plugins: true } }; + } + if (method === "plugin/list") { + return { + marketplaces: [ + { + name: "openai-curated", + path: null, + interface: null, + plugins: [pluginSummary(false, "openai-curated", false, "remote")], + }, + ], + marketplaceLoadErrors: [], + featuredPluginIds: [], + }; + } + if (method === "plugin/read") { + expect(requestParams).toEqual({ + remoteMarketplaceName: "openai-curated", + pluginName: "computer-use", + }); + return { + plugin: { + marketplaceName: "openai-curated", + marketplacePath: null, + summary: pluginSummary(false, "openai-curated", false, "remote"), + description: "Control desktop apps.", + skills: [], + apps: [], + mcpServers: ["computer-use"], + }, + }; + } + throw new Error(`unexpected request ${method}`); + }) as CodexComputerUseRequest; +} + function createAmbiguousComputerUseRequest(): CodexComputerUseRequest { return vi.fn(async (method: string) => { if (method === "plugin/list") { @@ -488,13 +596,21 @@ function marketplaceEntry(marketplaceName: string, installed: boolean) { }; } -function pluginSummary(installed: boolean, marketplaceName = "desktop-tools") { +function pluginSummary( + installed: boolean, + marketplaceName = "desktop-tools", + enabled = installed, + source: "local" | "remote" = "local", +) { return { id: `computer-use@${marketplaceName}`, name: "computer-use", - source: { type: "local", path: `/marketplaces/${marketplaceName}/plugins/computer-use` }, + source: + source === "local" + ? { type: "local", path: `/marketplaces/${marketplaceName}/plugins/computer-use` } + : { type: "remote" }, installed, - enabled: installed, + enabled, installPolicy: "AVAILABLE", authPolicy: "ON_INSTALL", interface: null, diff --git a/extensions/codex/src/app-server/computer-use.ts b/extensions/codex/src/app-server/computer-use.ts index b0138e88d18..786547d226c 100644 --- a/extensions/codex/src/app-server/computer-use.ts +++ b/extensions/codex/src/app-server/computer-use.ts @@ -180,7 +180,15 @@ async function inspectCodexComputerUse(params: { config: params.config, plugin, tools: [], - message: `Computer Use is available but not installed. Run /codex computer-use install or enable computerUse.autoInstall.`, + message: pluginSetupMessage(params.config, plugin, marketplace.marketplace), + }); + } + if (!marketplace.marketplace.path) { + return statusFromPlugin({ + config: params.config, + plugin, + tools: [], + message: remoteInstallUnsupportedMessage(plugin, marketplace.marketplace), }); } await request( @@ -197,6 +205,14 @@ async function inspectCodexComputerUse(params: { params.config.pluginName, ); } + if (!plugin.summary.installed || !plugin.summary.enabled) { + return statusFromPlugin({ + config: params.config, + plugin, + tools: [], + message: pluginSetupMessage(params.config, plugin, marketplace.marketplace), + }); + } let server = await readMcpServerStatus(request, params.config.mcpServerName); if (!server && params.installPlugin) { @@ -418,6 +434,29 @@ function pluginRequestParams(marketplace: MarketplaceRef, pluginName: string) { }; } +function pluginSetupMessage( + config: ResolvedCodexComputerUseConfig, + plugin: v2.PluginDetail, + marketplace: MarketplaceRef, +): string { + if (!marketplace.path) { + return remoteInstallUnsupportedMessage(plugin, marketplace); + } + 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 remoteInstallUnsupportedMessage( + plugin: v2.PluginDetail, + marketplace: MarketplaceRef, +): string { + const marketplaceName = marketplace.name ?? plugin.marketplaceName; + const state = plugin.summary.installed ? "installed but disabled" : "available"; + return `Computer Use is ${state} in remote Codex marketplace ${marketplaceName}, but Codex app-server does not support remote plugin install yet. Configure computerUse.marketplaceSource or computerUse.marketplacePath for a local marketplace, then run /codex computer-use install.`; +} + function statusFromPlugin(params: { config: ResolvedCodexComputerUseConfig; plugin: v2.PluginDetail;