refactor(codex): clarify computer use setup state

This commit is contained in:
Peter Steinberger
2026-04-27 23:52:28 +01:00
parent 87345c0667
commit ea2d95e23e
5 changed files with 196 additions and 51 deletions

View File

@@ -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",
}),
);

View File

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

View File

@@ -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) {

View File

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