Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/plugins/codex-computer-use.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ report disabled even after a one-off install command.
`install` enables Codex app-server plugin support, optionally adds a configured
marketplace source, installs or re-enables the configured plugin through Codex
app-server, reloads MCP servers, and verifies that the MCP server exposes tools.
Because installation changes trusted host resources, only an owner or an
`operator.admin` Gateway client can run `install`. Other authorized senders can
continue to use the read-only `status` command, including with overrides.

## Marketplace choices

Expand Down
5 changes: 5 additions & 0 deletions extensions/codex/src/command-authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry";

export function canMutateCodexHost(ctx: PluginCommandContext): boolean {
return ctx.senderIsOwner === true || ctx.gatewayClientScopes?.includes("operator.admin") === true;
}
7 changes: 6 additions & 1 deletion extensions/codex/src/command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
writeCodexAppServerBinding,
} from "./app-server/session-binding.js";
import { readCodexAccountAuthOverview } from "./command-account.js";
import { canMutateCodexHost } from "./command-authorization.js";
import {
buildHelp,
formatAccount,
Expand Down Expand Up @@ -497,7 +498,7 @@ export async function handleCodexSubcommand(
return buildCodexComputerUseMenuReply();
}
return {
text: await handleComputerUseCommand(deps, options.pluginConfig, rest),
text: await handleComputerUseCommand(deps, ctx, options.pluginConfig, rest),
};
}
if (normalized === "mcp") {
Expand Down Expand Up @@ -631,6 +632,7 @@ function returnsBeforeNativeCodexResume(args: readonly string[]): boolean {

async function handleComputerUseCommand(
deps: CodexCommandDeps,
ctx: PluginCommandContext,
pluginConfig: unknown,
args: string[],
): Promise<string> {
Expand All @@ -641,6 +643,9 @@ async function handleComputerUseCommand(
"Checks or installs the configured Codex Computer Use plugin through app-server.",
].join("\n");
}
if (parsed.action === "install" && !canMutateCodexHost(ctx)) {
return "Only an owner or operator.admin gateway client can configure Codex Computer Use.";
}
const params: CodexComputerUseSetupParams = {
pluginConfig,
forceEnable: parsed.action === "install" || parsed.hasOverrides,
Expand Down
10 changes: 2 additions & 8 deletions extensions/codex/src/command-plugins-management.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Codex plugin module implements command plugins management behavior.
import type { PluginCommandContext, PluginCommandResult } from "openclaw/plugin-sdk/plugin-entry";
import { canMutateCodexHost } from "./command-authorization.js";
import { formatCodexDisplayText } from "./command-formatters.js";
import {
buildCodexCommandPickerPresentation,
Expand Down Expand Up @@ -79,7 +80,7 @@ export async function handleCodexPluginsSubcommand(
if (!target || args.length > 1) {
return { text: `Usage: /codex plugins ${normalized} <name>` };
}
if (!canMutateCodexPlugins(ctx)) {
if (!canMutateCodexHost(ctx)) {
return {
text: `Only an owner or operator.admin gateway client can run /codex plugins ${normalized}.`,
};
Expand Down Expand Up @@ -197,13 +198,6 @@ function buildPluginNamePickerReply(
};
}

function canMutateCodexPlugins(ctx: PluginCommandContext): boolean {
if (ctx.senderIsOwner === true) {
return true;
}
return ctx.gatewayClientScopes?.includes("operator.admin") === true;
}

export function buildPluginsHelp(): string {
return [
"Codex sub-plugin management (writes only to ~/.openclaw/openclaw.json, never to ~/.codex/config.toml):",
Expand Down
62 changes: 62 additions & 0 deletions extensions/codex/src/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2100,6 +2100,68 @@ describe("codex command", () => {
});
});

it("rejects Computer Use installation from non-owner non-admin callers", async () => {
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());
const ctx = createContext(
"computer-use install --source attacker/marketplace --plugin untrusted",
undefined,
{ senderIsOwner: false, gatewayClientScopes: ["operator.write"] },
);

const result = await handleCodexCommand(ctx, {
deps: createDeps({ installCodexComputerUse }),
});

expectResultTextContains(result, "Only an owner or operator.admin");
expect(installCodexComputerUse).not.toHaveBeenCalled();
});

it("keeps Computer Use status overrides read-only for non-owner callers", async () => {
const readCodexComputerUseStatus = vi.fn(async () => computerUseReadyStatus());
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());
const ctx = createContext(
"computer-use status --source existing/source --marketplace-path /existing/marketplace --marketplace existing --plugin existing-plugin --mcp-server existing-server",
undefined,
{
senderIsOwner: false,
gatewayClientScopes: ["operator.write"],
},
);

const result = await handleCodexCommand(ctx, {
deps: createDeps({ readCodexComputerUseStatus, installCodexComputerUse }),
});

expectResultTextContains(result, "Computer Use: ready");
expect(readCodexComputerUseStatus).toHaveBeenCalledWith({
pluginConfig: undefined,
forceEnable: true,
overrides: {
marketplaceSource: "existing/source",
marketplacePath: "/existing/marketplace",
marketplaceName: "existing",
pluginName: "existing-plugin",
mcpServerName: "existing-server",
},
});
expect(installCodexComputerUse).not.toHaveBeenCalled();
});

it("allows operator.admin gateway callers to install Codex Computer Use", async () => {
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());
const ctx = createContext("computer-use install", undefined, {
senderIsOwner: false,
gatewayClientScopes: ["operator.admin"],
});

const result = await handleCodexCommand(ctx, {
deps: createDeps({ installCodexComputerUse }),
});

expectResultTextContains(result, "Computer Use: ready");
expect(installCodexComputerUse).toHaveBeenCalledOnce();
});

it("shows help when Computer Use option values are missing", async () => {
const installCodexComputerUse = vi.fn(async () => computerUseReadyStatus());

Expand Down
Loading