Skip to content

Commit 0ed00ca

Browse files
committed
fix(codex): restrict computer-use installation
1 parent 15de9d8 commit 0ed00ca

5 files changed

Lines changed: 78 additions & 9 deletions

File tree

docs/plugins/codex-computer-use.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ report disabled even after a one-off install command.
147147
`install` enables Codex app-server plugin support, optionally adds a configured
148148
marketplace source, installs or re-enables the configured plugin through Codex
149149
app-server, reloads MCP servers, and verifies that the MCP server exposes tools.
150+
Because installation changes trusted host resources, only an owner or an
151+
`operator.admin` Gateway client can run `install`. Other authorized senders can
152+
continue to use the read-only `status` command, including with overrides.
150153

151154
## Marketplace choices
152155

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry";
2+
3+
export function canMutateCodexHost(ctx: PluginCommandContext): boolean {
4+
return ctx.senderIsOwner === true || ctx.gatewayClientScopes?.includes("operator.admin") === true;
5+
}

extensions/codex/src/command-handlers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
writeCodexAppServerBinding,
2525
} from "./app-server/session-binding.js";
2626
import { readCodexAccountAuthOverview } from "./command-account.js";
27+
import { canMutateCodexHost } from "./command-authorization.js";
2728
import {
2829
buildHelp,
2930
formatAccount,
@@ -497,7 +498,7 @@ export async function handleCodexSubcommand(
497498
return buildCodexComputerUseMenuReply();
498499
}
499500
return {
500-
text: await handleComputerUseCommand(deps, options.pluginConfig, rest),
501+
text: await handleComputerUseCommand(deps, ctx, options.pluginConfig, rest),
501502
};
502503
}
503504
if (normalized === "mcp") {
@@ -631,6 +632,7 @@ function returnsBeforeNativeCodexResume(args: readonly string[]): boolean {
631632

632633
async function handleComputerUseCommand(
633634
deps: CodexCommandDeps,
635+
ctx: PluginCommandContext,
634636
pluginConfig: unknown,
635637
args: string[],
636638
): Promise<string> {
@@ -641,6 +643,9 @@ async function handleComputerUseCommand(
641643
"Checks or installs the configured Codex Computer Use plugin through app-server.",
642644
].join("\n");
643645
}
646+
if (parsed.action === "install" && !canMutateCodexHost(ctx)) {
647+
return "Only an owner or operator.admin gateway client can configure Codex Computer Use.";
648+
}
644649
const params: CodexComputerUseSetupParams = {
645650
pluginConfig,
646651
forceEnable: parsed.action === "install" || parsed.hasOverrides,

extensions/codex/src/command-plugins-management.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Codex plugin module implements command plugins management behavior.
22
import type { PluginCommandContext, PluginCommandResult } from "openclaw/plugin-sdk/plugin-entry";
3+
import { canMutateCodexHost } from "./command-authorization.js";
34
import { formatCodexDisplayText } from "./command-formatters.js";
45
import {
56
buildCodexCommandPickerPresentation,
@@ -79,7 +80,7 @@ export async function handleCodexPluginsSubcommand(
7980
if (!target || args.length > 1) {
8081
return { text: `Usage: /codex plugins ${normalized} <name>` };
8182
}
82-
if (!canMutateCodexPlugins(ctx)) {
83+
if (!canMutateCodexHost(ctx)) {
8384
return {
8485
text: `Only an owner or operator.admin gateway client can run /codex plugins ${normalized}.`,
8586
};
@@ -197,13 +198,6 @@ function buildPluginNamePickerReply(
197198
};
198199
}
199200

200-
function canMutateCodexPlugins(ctx: PluginCommandContext): boolean {
201-
if (ctx.senderIsOwner === true) {
202-
return true;
203-
}
204-
return ctx.gatewayClientScopes?.includes("operator.admin") === true;
205-
}
206-
207201
export function buildPluginsHelp(): string {
208202
return [
209203
"Codex sub-plugin management (writes only to ~/.openclaw/openclaw.json, never to ~/.codex/config.toml):",

extensions/codex/src/commands.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2100,6 +2100,68 @@ describe("codex command", () => {
21002100
});
21012101
});
21022102

2103+
it("rejects Computer Use installation from non-owner non-admin callers", async () => {
2104+
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());
2105+
const ctx = createContext(
2106+
"computer-use install --source attacker/marketplace --plugin untrusted",
2107+
undefined,
2108+
{ senderIsOwner: false, gatewayClientScopes: ["operator.write"] },
2109+
);
2110+
2111+
const result = await handleCodexCommand(ctx, {
2112+
deps: createDeps({ installCodexComputerUse }),
2113+
});
2114+
2115+
expectResultTextContains(result, "Only an owner or operator.admin");
2116+
expect(installCodexComputerUse).not.toHaveBeenCalled();
2117+
});
2118+
2119+
it("keeps Computer Use status overrides read-only for non-owner callers", async () => {
2120+
const readCodexComputerUseStatus = vi.fn(async () => computerUseReadyStatus());
2121+
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());
2122+
const ctx = createContext(
2123+
"computer-use status --source existing/source --marketplace-path /existing/marketplace --marketplace existing --plugin existing-plugin --mcp-server existing-server",
2124+
undefined,
2125+
{
2126+
senderIsOwner: false,
2127+
gatewayClientScopes: ["operator.write"],
2128+
},
2129+
);
2130+
2131+
const result = await handleCodexCommand(ctx, {
2132+
deps: createDeps({ readCodexComputerUseStatus, installCodexComputerUse }),
2133+
});
2134+
2135+
expectResultTextContains(result, "Computer Use: ready");
2136+
expect(readCodexComputerUseStatus).toHaveBeenCalledWith({
2137+
pluginConfig: undefined,
2138+
forceEnable: true,
2139+
overrides: {
2140+
marketplaceSource: "existing/source",
2141+
marketplacePath: "/existing/marketplace",
2142+
marketplaceName: "existing",
2143+
pluginName: "existing-plugin",
2144+
mcpServerName: "existing-server",
2145+
},
2146+
});
2147+
expect(installCodexComputerUse).not.toHaveBeenCalled();
2148+
});
2149+
2150+
it("allows operator.admin gateway callers to install Codex Computer Use", async () => {
2151+
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());
2152+
const ctx = createContext("computer-use install", undefined, {
2153+
senderIsOwner: false,
2154+
gatewayClientScopes: ["operator.admin"],
2155+
});
2156+
2157+
const result = await handleCodexCommand(ctx, {
2158+
deps: createDeps({ installCodexComputerUse }),
2159+
});
2160+
2161+
expectResultTextContains(result, "Computer Use: ready");
2162+
expect(installCodexComputerUse).toHaveBeenCalledOnce();
2163+
});
2164+
21032165
it("shows help when Computer Use option values are missing", async () => {
21042166
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());
21052167

0 commit comments

Comments
 (0)