fix(codex): harden computer use setup states

This commit is contained in:
Peter Steinberger
2026-04-27 23:46:11 +01:00
parent f7983a07a4
commit f7815cdd8f
3 changed files with 176 additions and 20 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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<v2.PluginInstallResponse>(
@@ -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;