mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
refactor(codex): clarify computer use setup state
This commit is contained in:
@@ -590,7 +590,10 @@ failing closed for unknown ambiguous matches.
|
||||
Use `marketplaceSource` for a non-default Codex marketplace source that
|
||||
app-server can add, or `marketplacePath` for a local marketplace file that
|
||||
already exists on the machine. If the marketplace is already registered with
|
||||
Codex app-server, use `marketplaceName` instead. The defaults are
|
||||
Codex app-server, use `marketplaceName` instead. `marketplaceName` can also
|
||||
select a remote-only Codex catalog entry for status checks, but Codex app-server
|
||||
does not yet support remote `plugin/install`; installs and re-enables still need
|
||||
`marketplaceSource` or `marketplacePath`. The defaults are
|
||||
`pluginName: "computer-use"` and `mcpServerName: "computer-use"`.
|
||||
For safety, turn-start auto-install only uses marketplaces app-server has
|
||||
already discovered. Use `/codex computer-use install` for explicit installs from
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
CodexComputerUseSetupError,
|
||||
ensureCodexComputerUse,
|
||||
installCodexComputerUse,
|
||||
readCodexComputerUseStatus,
|
||||
@@ -19,6 +18,7 @@ describe("Codex Computer Use setup", () => {
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
ready: false,
|
||||
reason: "disabled",
|
||||
message: "Computer Use is disabled.",
|
||||
}),
|
||||
);
|
||||
@@ -36,6 +36,7 @@ describe("Codex Computer Use setup", () => {
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
installed: true,
|
||||
pluginEnabled: true,
|
||||
mcpServerAvailable: true,
|
||||
@@ -63,6 +64,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: false,
|
||||
reason: "plugin_disabled",
|
||||
installed: true,
|
||||
pluginEnabled: false,
|
||||
mcpServerAvailable: false,
|
||||
@@ -89,6 +91,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
message: "Computer Use is ready.",
|
||||
}),
|
||||
);
|
||||
@@ -110,6 +113,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: false,
|
||||
reason: "marketplace_missing",
|
||||
message:
|
||||
"Multiple Codex marketplaces contain computer-use. Configure computerUse.marketplaceName or computerUse.marketplacePath to choose one.",
|
||||
}),
|
||||
@@ -132,6 +136,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
installed: true,
|
||||
pluginEnabled: true,
|
||||
tools: ["list_apps"],
|
||||
@@ -161,6 +166,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
installed: true,
|
||||
pluginEnabled: true,
|
||||
message: "Computer Use is ready.",
|
||||
@@ -180,7 +186,11 @@ describe("Codex Computer Use setup", () => {
|
||||
pluginConfig: { computerUse: { enabled: true, marketplaceName: "desktop-tools" } },
|
||||
request,
|
||||
}),
|
||||
).rejects.toThrow(CodexComputerUseSetupError);
|
||||
).rejects.toMatchObject({
|
||||
status: expect.objectContaining({
|
||||
reason: "plugin_not_installed",
|
||||
}),
|
||||
});
|
||||
expect(request).not.toHaveBeenCalledWith("plugin/install", expect.anything());
|
||||
});
|
||||
|
||||
@@ -201,6 +211,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
message: "Computer Use is ready.",
|
||||
}),
|
||||
);
|
||||
@@ -228,6 +239,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
message: "Computer Use is ready.",
|
||||
}),
|
||||
);
|
||||
@@ -255,7 +267,11 @@ describe("Codex Computer Use setup", () => {
|
||||
},
|
||||
request,
|
||||
}),
|
||||
).rejects.toThrow(CodexComputerUseSetupError);
|
||||
).rejects.toMatchObject({
|
||||
status: expect.objectContaining({
|
||||
reason: "auto_install_blocked",
|
||||
}),
|
||||
});
|
||||
expect(request).not.toHaveBeenCalledWith("marketplace/add", expect.anything());
|
||||
expect(request).not.toHaveBeenCalledWith("plugin/install", expect.anything());
|
||||
});
|
||||
@@ -276,6 +292,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: false,
|
||||
reason: "marketplace_missing",
|
||||
message:
|
||||
"Configured Codex marketplace missing-marketplace was not found or does not contain computer-use. Run /codex computer-use install with a source or path to install from a new marketplace.",
|
||||
}),
|
||||
@@ -294,6 +311,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).rejects.toMatchObject({
|
||||
status: expect.objectContaining({
|
||||
ready: false,
|
||||
reason: "remote_install_unsupported",
|
||||
installed: false,
|
||||
pluginEnabled: false,
|
||||
marketplaceName: "openai-curated",
|
||||
@@ -320,6 +338,7 @@ describe("Codex Computer Use setup", () => {
|
||||
await expect(installed).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
message: "Computer Use is ready.",
|
||||
}),
|
||||
);
|
||||
@@ -343,6 +362,7 @@ describe("Codex Computer Use setup", () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
marketplaceName: "openai-curated",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -15,9 +15,21 @@ export type CodexComputerUseRequest = <T = JsonValue | undefined>(
|
||||
params?: unknown,
|
||||
) => Promise<T>;
|
||||
|
||||
export type CodexComputerUseStatusReason =
|
||||
| "disabled"
|
||||
| "marketplace_missing"
|
||||
| "plugin_not_installed"
|
||||
| "plugin_disabled"
|
||||
| "remote_install_unsupported"
|
||||
| "mcp_missing"
|
||||
| "ready"
|
||||
| "check_failed"
|
||||
| "auto_install_blocked";
|
||||
|
||||
export type CodexComputerUseStatus = {
|
||||
enabled: boolean;
|
||||
ready: boolean;
|
||||
reason: CodexComputerUseStatusReason;
|
||||
installed: boolean;
|
||||
pluginEnabled: boolean;
|
||||
mcpServerAvailable: boolean;
|
||||
@@ -49,17 +61,33 @@ export type CodexComputerUseSetupParams = {
|
||||
forceEnable?: boolean;
|
||||
};
|
||||
|
||||
type MarketplaceRef = {
|
||||
name?: string;
|
||||
path?: string;
|
||||
remoteMarketplaceName?: string;
|
||||
};
|
||||
type MarketplaceRef =
|
||||
| {
|
||||
kind: "local";
|
||||
name?: string;
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
kind: "remote";
|
||||
name: string;
|
||||
remoteMarketplaceName: string;
|
||||
};
|
||||
|
||||
type MarketplaceResolution = {
|
||||
marketplace?: MarketplaceRef;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type PluginInspection =
|
||||
| {
|
||||
ok: true;
|
||||
plugin: v2.PluginDetail;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
status: CodexComputerUseStatus;
|
||||
};
|
||||
|
||||
const CURATED_MARKETPLACE_POLL_INTERVAL_MS = 2_000;
|
||||
const COMPUTER_USE_MARKETPLACE_NAME_PRIORITY = ["openai-bundled", "openai-curated", "local"];
|
||||
|
||||
@@ -77,7 +105,11 @@ export async function readCodexComputerUseStatus(
|
||||
installPlugin: false,
|
||||
});
|
||||
} catch (error) {
|
||||
return unavailableStatus(config, `Computer Use check failed: ${describeControlFailure(error)}`);
|
||||
return unavailableStatus(
|
||||
config,
|
||||
"check_failed",
|
||||
`Computer Use check failed: ${describeControlFailure(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,74 +196,121 @@ async function inspectCodexComputerUse(params: {
|
||||
if (!marketplace.marketplace) {
|
||||
return unavailableStatus(
|
||||
params.config,
|
||||
"marketplace_missing",
|
||||
marketplace.message ??
|
||||
`No Codex marketplace containing ${params.config.pluginName} is registered. Configure computerUse.marketplaceSource or computerUse.marketplacePath, then run /codex computer-use install.`,
|
||||
);
|
||||
}
|
||||
|
||||
let plugin = await readComputerUsePlugin(
|
||||
const pluginInspection = await ensureComputerUsePlugin({
|
||||
request,
|
||||
marketplace.marketplace,
|
||||
config: params.config,
|
||||
marketplace: marketplace.marketplace,
|
||||
installPlugin: params.installPlugin,
|
||||
});
|
||||
if (!pluginInspection.ok) {
|
||||
return pluginInspection.status;
|
||||
}
|
||||
|
||||
return await readComputerUseTools({
|
||||
request,
|
||||
config: params.config,
|
||||
plugin: pluginInspection.plugin,
|
||||
installPlugin: params.installPlugin,
|
||||
});
|
||||
}
|
||||
|
||||
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 statusFromPlugin({
|
||||
config: params.config,
|
||||
plugin,
|
||||
tools: [],
|
||||
message: pluginSetupMessage(params.config, plugin, marketplace.marketplace),
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
status: statusFromPlugin({
|
||||
config: params.config,
|
||||
plugin,
|
||||
tools: [],
|
||||
reason: pluginSetupReason(plugin, params.marketplace),
|
||||
message: pluginSetupMessage(params.config, plugin, params.marketplace),
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (!marketplace.marketplace.path) {
|
||||
return statusFromPlugin({
|
||||
config: params.config,
|
||||
plugin,
|
||||
tools: [],
|
||||
message: remoteInstallUnsupportedMessage(plugin, marketplace.marketplace),
|
||||
});
|
||||
if (params.marketplace.kind === "remote") {
|
||||
return {
|
||||
ok: false,
|
||||
status: statusFromPlugin({
|
||||
config: params.config,
|
||||
plugin,
|
||||
tools: [],
|
||||
reason: "remote_install_unsupported",
|
||||
message: remoteInstallUnsupportedMessage(plugin, params.marketplace),
|
||||
}),
|
||||
};
|
||||
}
|
||||
await request<v2.PluginInstallResponse>(
|
||||
await params.request<v2.PluginInstallResponse>(
|
||||
"plugin/install",
|
||||
pluginRequestParams(
|
||||
marketplace.marketplace,
|
||||
params.marketplace,
|
||||
params.config.pluginName,
|
||||
) satisfies v2.PluginInstallParams,
|
||||
);
|
||||
await reloadMcpServers(request);
|
||||
await reloadMcpServers(params.request);
|
||||
plugin = await readComputerUsePlugin(
|
||||
request,
|
||||
marketplace.marketplace,
|
||||
params.request,
|
||||
params.marketplace,
|
||||
params.config.pluginName,
|
||||
);
|
||||
}
|
||||
if (!plugin.summary.installed || !plugin.summary.enabled) {
|
||||
return statusFromPlugin({
|
||||
config: params.config,
|
||||
plugin,
|
||||
tools: [],
|
||||
message: pluginSetupMessage(params.config, plugin, marketplace.marketplace),
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
status: statusFromPlugin({
|
||||
config: params.config,
|
||||
plugin,
|
||||
tools: [],
|
||||
reason: pluginSetupReason(plugin, params.marketplace),
|
||||
message: pluginSetupMessage(params.config, plugin, params.marketplace),
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { ok: true, plugin };
|
||||
}
|
||||
|
||||
let server = await readMcpServerStatus(request, params.config.mcpServerName);
|
||||
async function readComputerUseTools(params: {
|
||||
request: CodexComputerUseRequest;
|
||||
config: ResolvedCodexComputerUseConfig;
|
||||
plugin: v2.PluginDetail;
|
||||
installPlugin: boolean;
|
||||
}): Promise<CodexComputerUseStatus> {
|
||||
let server = await readMcpServerStatus(params.request, params.config.mcpServerName);
|
||||
if (!server && params.installPlugin) {
|
||||
await reloadMcpServers(request);
|
||||
server = await readMcpServerStatus(request, params.config.mcpServerName);
|
||||
await reloadMcpServers(params.request);
|
||||
server = await readMcpServerStatus(params.request, params.config.mcpServerName);
|
||||
}
|
||||
if (!server) {
|
||||
return statusFromPlugin({
|
||||
config: params.config,
|
||||
plugin,
|
||||
plugin: params.plugin,
|
||||
tools: [],
|
||||
reason: "mcp_missing",
|
||||
message: `Computer Use is installed, but the ${params.config.mcpServerName} MCP server is not available.`,
|
||||
});
|
||||
}
|
||||
|
||||
return statusFromPlugin({
|
||||
config: params.config,
|
||||
plugin,
|
||||
plugin: params.plugin,
|
||||
tools: Object.keys(server.tools).toSorted(),
|
||||
reason: "ready",
|
||||
message: "Computer Use is ready.",
|
||||
});
|
||||
}
|
||||
@@ -252,8 +331,8 @@ async function resolveMarketplaceRef(params: {
|
||||
|
||||
if (params.config.marketplacePath) {
|
||||
const marketplace: MarketplaceRef = preferredMarketplaceName
|
||||
? { name: preferredMarketplaceName, path: params.config.marketplacePath }
|
||||
: { path: params.config.marketplacePath };
|
||||
? { kind: "local", name: preferredMarketplaceName, path: params.config.marketplacePath }
|
||||
: { kind: "local", path: params.config.marketplacePath };
|
||||
return { marketplace };
|
||||
}
|
||||
|
||||
@@ -312,6 +391,7 @@ function blockUnsafeAutoInstallStatus(
|
||||
}
|
||||
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.",
|
||||
);
|
||||
}
|
||||
@@ -331,9 +411,9 @@ function findComputerUseMarketplaces(
|
||||
)
|
||||
.map((marketplace) => {
|
||||
if (marketplace.path) {
|
||||
return { name: marketplace.name, path: marketplace.path };
|
||||
return { kind: "local", name: marketplace.name, path: marketplace.path };
|
||||
}
|
||||
return { name: marketplace.name, remoteMarketplaceName: marketplace.name };
|
||||
return { kind: "remote", name: marketplace.name, remoteMarketplaceName: marketplace.name };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -426,20 +506,30 @@ async function reloadMcpServers(request: CodexComputerUseRequest): Promise<void>
|
||||
|
||||
function pluginRequestParams(marketplace: MarketplaceRef, pluginName: string) {
|
||||
return {
|
||||
...(marketplace.path ? { marketplacePath: marketplace.path } : {}),
|
||||
...(!marketplace.path && marketplace.remoteMarketplaceName
|
||||
...(marketplace.kind === "local" ? { marketplacePath: marketplace.path } : {}),
|
||||
...(marketplace.kind === "remote"
|
||||
? { remoteMarketplaceName: marketplace.remoteMarketplaceName }
|
||||
: {}),
|
||||
pluginName,
|
||||
};
|
||||
}
|
||||
|
||||
function pluginSetupReason(
|
||||
plugin: v2.PluginDetail,
|
||||
marketplace: MarketplaceRef,
|
||||
): CodexComputerUseStatusReason {
|
||||
if (marketplace.kind === "remote") {
|
||||
return "remote_install_unsupported";
|
||||
}
|
||||
return plugin.summary.installed ? "plugin_disabled" : "plugin_not_installed";
|
||||
}
|
||||
|
||||
function pluginSetupMessage(
|
||||
config: ResolvedCodexComputerUseConfig,
|
||||
plugin: v2.PluginDetail,
|
||||
marketplace: MarketplaceRef,
|
||||
): string {
|
||||
if (!marketplace.path) {
|
||||
if (marketplace.kind === "remote") {
|
||||
return remoteInstallUnsupportedMessage(plugin, marketplace);
|
||||
}
|
||||
if (!plugin.summary.installed) {
|
||||
@@ -461,12 +551,14 @@ function statusFromPlugin(params: {
|
||||
config: ResolvedCodexComputerUseConfig;
|
||||
plugin: v2.PluginDetail;
|
||||
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,
|
||||
@@ -483,6 +575,7 @@ function disabledStatus(config: ResolvedCodexComputerUseConfig): CodexComputerUs
|
||||
return {
|
||||
enabled: false,
|
||||
ready: false,
|
||||
reason: "disabled",
|
||||
installed: false,
|
||||
pluginEnabled: false,
|
||||
mcpServerAvailable: false,
|
||||
@@ -495,11 +588,13 @@ function disabledStatus(config: ResolvedCodexComputerUseConfig): CodexComputerUs
|
||||
|
||||
function unavailableStatus(
|
||||
config: ResolvedCodexComputerUseConfig,
|
||||
reason: CodexComputerUseStatusReason,
|
||||
message: string,
|
||||
): CodexComputerUseStatus {
|
||||
return {
|
||||
enabled: true,
|
||||
ready: false,
|
||||
reason,
|
||||
installed: false,
|
||||
pluginEnabled: false,
|
||||
mcpServerAvailable: false,
|
||||
|
||||
@@ -94,9 +94,7 @@ export function formatComputerUseStatus(status: CodexComputerUseStatus): string
|
||||
const lines = [
|
||||
`Computer Use: ${status.ready ? "ready" : status.enabled ? "not ready" : "disabled"}`,
|
||||
];
|
||||
lines.push(
|
||||
`Plugin: ${status.pluginName}${status.installed ? " (installed)" : " (not installed)"}`,
|
||||
);
|
||||
lines.push(`Plugin: ${status.pluginName} (${computerUsePluginState(status)})`);
|
||||
lines.push(
|
||||
`MCP server: ${status.mcpServerName}${
|
||||
status.mcpServerAvailable ? ` (${status.tools.length} tools)` : " (unavailable)"
|
||||
@@ -112,6 +110,13 @@ export function formatComputerUseStatus(status: CodexComputerUseStatus): string
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function computerUsePluginState(status: CodexComputerUseStatus): string {
|
||||
if (!status.installed) {
|
||||
return "not installed";
|
||||
}
|
||||
return status.pluginEnabled ? "installed" : "installed, disabled";
|
||||
}
|
||||
|
||||
export function formatList(response: JsonValue | undefined, label: string): string {
|
||||
const entries = extractArray(response);
|
||||
if (entries.length === 0) {
|
||||
|
||||
@@ -265,6 +265,27 @@ describe("codex command", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("formats disabled installed Codex Computer Use plugins", async () => {
|
||||
const readCodexComputerUseStatus = vi.fn(async () => ({
|
||||
...computerUseReadyStatus(),
|
||||
ready: false,
|
||||
reason: "plugin_disabled" as const,
|
||||
pluginEnabled: false,
|
||||
mcpServerAvailable: false,
|
||||
tools: [],
|
||||
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.",
|
||||
}));
|
||||
|
||||
await expect(
|
||||
handleCodexCommand(createContext("computer-use status"), {
|
||||
deps: createDeps({ readCodexComputerUseStatus }),
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
text: expect.stringContaining("Plugin: computer-use (installed, disabled)"),
|
||||
});
|
||||
});
|
||||
|
||||
it("installs Codex Computer Use from command overrides", async () => {
|
||||
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());
|
||||
|
||||
@@ -667,6 +688,7 @@ function computerUseReadyStatus(): CodexComputerUseStatus {
|
||||
return {
|
||||
enabled: true,
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
installed: true,
|
||||
pluginEnabled: true,
|
||||
mcpServerAvailable: true,
|
||||
|
||||
Reference in New Issue
Block a user