From d457c06fe82033fa9d0083464fd6423bc1f25822 Mon Sep 17 00:00:00 2001 From: "M. Adel Alhashemi" Date: Fri, 25 Sep 2026 07:39:10 +0300 Subject: [PATCH 1/2] fix: follow the Codex approval policy and drop the approvals option The approvals option offered three ways to answer Computer Use's "Allow Computer Use to use ?" prompts, but OpenCode's plugin API cannot show those prompts to the user, so every mode was a workaround: accept-session approved everything and decline broke the plugin. App access now always follows the Codex approval_policy and the approvals stored in ChatGPT/Codex. A prompt Codex forwards is declined with a note that names the app and tells the user how to allow it. /computer-use-doctor reports the effective policy. A leftover approvals setting is ignored with a warning. --- CONTRIBUTING.md | 18 ++++++------- README.md | 35 ++++++++++++++++--------- SECURITY.md | 5 ++-- src/bridge.ts | 52 ++++++++++++++++++++++--------------- src/doctor.ts | 13 +++++++++- src/index.ts | 1 + src/options.ts | 13 +++++++--- test/bridge.test.ts | 33 ++++++++--------------- test/fixtures/fake-codex.ts | 6 +++++ test/options.test.ts | 21 ++++++++++++--- 10 files changed, 122 insertions(+), 75 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08d66d0..8fbd309 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,15 +33,15 @@ project's `plugins`. After editing, run `opencode api post /api/location/reload` ### How the code fits together -| File | Role | -| ------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `src/index.ts` | Plugin entry: options, the `computer_use` tools, the doctor command, turn and session events | -| `src/bridge.ts` | One `codex app-server` per plugin instance, one ephemeral Codex thread per OpenCode session, approvals, turn metadata | -| `src/app-server.ts` | JSON-RPC client for `codex app-server --listen stdio://` | -| `src/content.ts` | Converts Codex results to OpenCode content: screenshots, OCR, output cap | -| `src/ocr.ts` | On-device OCR with the macOS Vision framework | -| `src/surfaces.ts` | Turns native apps or browser tabs off inside the runtime | -| `src/doctor.ts` | Setup checks shared by `/computer-use-doctor` and `scripts/smoke.ts` | +| File | Role | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `src/index.ts` | Plugin entry: options, the `computer_use` tools, the doctor command, turn and session events | +| `src/bridge.ts` | One `codex app-server` per plugin instance, one ephemeral Codex thread per OpenCode session, app-access prompts, turn metadata | +| `src/app-server.ts` | JSON-RPC client for `codex app-server --listen stdio://` | +| `src/content.ts` | Converts Codex results to OpenCode content: screenshots, OCR, output cap | +| `src/ocr.ts` | On-device OCR with the macOS Vision framework | +| `src/surfaces.ts` | Turns native apps or browser tabs off inside the runtime | +| `src/doctor.ts` | Setup checks shared by `/computer-use-doctor` and `scripts/smoke.ts` | The protocol the bridge speaks is defined in the open-source [Codex app-server protocol](https://github.com/openai/codex/tree/main/codex-rs/app-server-protocol). diff --git a/README.md b/README.md index f64df62..915839f 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ From a checkout, `bun run doctor` runs the same checks in a terminal (`bun scrip - ✅ **codex executable**: /Applications/ChatGPT.app/Contents/Resources/codex (codex-cli 0.155.0-alpha.16) - ✅ **Computer Use app**: ~/.codex/computer-use/Codex Computer Use.app (26.916.1001103) - ✅ **Computer Use runtime**: Codex app-server running with `cua_repl` +- ✅ **App access**: Codex approves app access itself (approval_policy = "never") - ✅ **Native apps**: Engine reachable, 17 apps listed - ✅ **Screenshots**: Finder screenshot captured (image/jpeg, 132 KB) - ✅ **OCR**: 61 text lines recognized in 894 ms @@ -141,7 +142,6 @@ Pass options with the object form of the plugin entry. Every option is optional; { "package": "opencode-codex-computer-use", "options": { - "approvals": "codex", "surfaces": ["apps", "browser"], "screenshots": "image", "maxOutputKB": 128, @@ -154,16 +154,27 @@ Pass options with the object form of the plugin entry. Every option is optional; } ``` -| Option | Default | Meaning | -| --------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `approvals` | `"codex"` | How "Allow Computer Use to use _App_?" prompts are answered. `"codex"` follows the `approval_policy` in your `~/.codex/config.toml` (with `"never"`, Codex answers them itself; any prompt Codex still forwards is declined). `"accept-session"` accepts each prompt for the current session only. `"decline"` declines them all. Apps blocked by the engine or your organization stay blocked in every mode. | -| `surfaces` | `["apps", "browser"]` | What the agent may operate: native apps, browser tabs, or both. Turning one off removes it from the tool description and makes its `cua` functions throw. This scopes the model; it is not a security boundary. | -| `screenshots` | `"image"` | How screenshots reach the model. `"image"`: as images. `"ocr"`: as text recognized on-device (macOS Vision), one line per text block with its `[x,y]` position, which is also a valid click coordinate; for models without vision, or to keep pixels off your model provider. `"both"`: image plus text. `"off"`: replaced by a placeholder, so the model relies on the accessibility tree. | -| `maxOutputKB` | `128` | Maximum text one call returns. Longer output is cut, with a note telling the model to narrow its query. `0` disables the limit. Images are not counted. | -| `idleShutdownMinutes` | `0` (never) | Stop the background Codex process after this many minutes without Computer Use calls. The default keeps it running until OpenCode stops, so work can wait indefinitely for you (for example, a login in a tab the agent handed over). | -| `callTimeoutSeconds` | `300` | Maximum time for one `computer_use` call. | -| `codexPath` | auto | Path to `codex`. Otherwise `$OPENCODE_CODEX_COMPUTER_USE_CODEX_PATH`, then (on macOS) the ChatGPT app, then the Codex app, then `PATH`. | -| `debug` | `false` | `true` (or a file path) writes every message exchanged with Codex to `~/.local/share/opencode/log/codex-computer-use.jsonl`, with screenshots replaced by their size. The log contains accessibility trees and page text, so it can hold sensitive data. | +| Option | Default | Meaning | +| --------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `surfaces` | `["apps", "browser"]` | What the agent may operate: native apps, browser tabs, or both. Turning one off removes it from the tool description and makes its `cua` functions throw. This scopes the model; it is not a security boundary. | +| `screenshots` | `"image"` | How screenshots reach the model. `"image"`: as images. `"ocr"`: as text recognized on-device (macOS Vision), one line per text block with its `[x,y]` position, which is also a valid click coordinate; for models without vision, or to keep pixels off your model provider. `"both"`: image plus text. `"off"`: replaced by a placeholder, so the model relies on the accessibility tree. | +| `maxOutputKB` | `128` | Maximum text one call returns. Longer output is cut, with a note telling the model to narrow its query. `0` disables the limit. Images are not counted. | +| `idleShutdownMinutes` | `0` (never) | Stop the background Codex process after this many minutes without Computer Use calls. The default keeps it running until OpenCode stops, so work can wait indefinitely for you (for example, a login in a tab the agent handed over). | +| `callTimeoutSeconds` | `300` | Maximum time for one `computer_use` call. | +| `codexPath` | auto | Path to `codex`. Otherwise `$OPENCODE_CODEX_COMPUTER_USE_CODEX_PATH`, then (on macOS) the ChatGPT app, then the Codex app, then `PATH`. | +| `debug` | `false` | `true` (or a file path) writes every message exchanged with Codex to `~/.local/share/opencode/log/codex-computer-use.jsonl`, with screenshots replaced by their size. The log contains accessibility trees and page text, so it can hold sensitive data. | + +### App access + +Computer Use asks before it touches an app ("Allow Computer Use to use Calculator?"). Your Codex settings answer that +question, as they do inside Codex: + +- With `approval_policy = "never"` in `~/.codex/config.toml`, Codex approves app access itself. +- Apps you approved permanently in ChatGPT or Codex are always allowed. +- Any other app is declined, and the agent tells you which app needs permission. OpenCode's plugin API cannot show + these prompts as OpenCode permission questions yet; once it can, they will appear in OpenCode instead. + +Apps blocked by the engine or your organization stay blocked. `/computer-use-doctor` shows which policy applies. ### Permissions @@ -213,7 +224,7 @@ Start with `/computer-use-doctor`; it names the missing piece. | `Could not find the codex executable` | Install the ChatGPT desktop app in `/Applications`, or set `codexPath`. | | `Codex has no cua_repl MCP server` | Turn on Computer Use in the ChatGPT/Codex app, then run `/computer-use-doctor` again. | | Permission errors from the engine | Grant Accessibility and Screen Recording to _Codex Computer Use_, then retry. | -| `Declined "Allow Computer Use to use …"` | Approve the app once in ChatGPT/Codex, or set `approvals` to `"accept-session"`. | +| The agent says an app needs permission | Approve the app permanently in ChatGPT/Codex, or set `approval_policy = "never"`; see [App access](#app-access). | | No browsers listed / Chrome tab calls fail | Install and connect the ChatGPT for Chrome extension from the ChatGPT/Codex app, keep Chrome running, then run `/computer-use-doctor`. | | Anything else | Plugin messages are prefixed `[codex-computer-use]` in `~/.local/share/opencode/log/opencode.log`; set `"debug": true` to log the full exchange with Codex. | diff --git a/SECURITY.md b/SECURITY.md index 4da0c03..7438c70 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,8 +13,9 @@ published with credit to you, unless you prefer otherwise. This plugin lets an AI agent operate the apps and browser tabs on your computer, so these areas matter most: -- **App approvals.** App-access prompts from Computer Use follow your Codex `approval_policy` by default and are never - accepted permanently. A way to make the plugin accept access the configuration does not allow is a vulnerability. +- **App approvals.** The plugin never approves app access itself: Codex's `approval_policy` and the approvals stored + in ChatGPT/Codex decide, and any prompt Codex forwards to the plugin is declined. A way to make the plugin approve + app access is a vulnerability. - **Surfaces.** The `surfaces` option narrows what a cooperative model uses; it is documented as not being a security boundary. A bypass of OpenCode's `computer_use` permission rules would be in scope. - **The Codex process.** The plugin starts `codex app-server` from the ChatGPT or Codex app bundle, the `codexPath` diff --git a/src/bridge.ts b/src/bridge.ts index 78b54bb..f3e9b38 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -4,18 +4,8 @@ import { BUNDLED_CODEX_PATHS, CODEX_PATH_ENV, resolveCodexPath } from "./codex-p /** Codex's MCP server that hosts the Computer Use JavaScript runtime (`cua`). */ export const CUA_SERVER = "cua_repl" -/** - * How app-access prompts from Computer Use ("Allow Computer Use to use X?") are answered. - * - `codex`: follow the user's Codex `approval_policy`; prompts Codex forwards anyway are declined. - * - `accept-session`: accept each prompt for the current thread only (never "always"). - * - `decline`: decline every prompt; only apps the engine already allows can be used. - */ -export type ApprovalMode = "codex" | "accept-session" | "decline" -export const APPROVAL_MODES: readonly ApprovalMode[] = ["codex", "accept-session", "decline"] - export interface BridgeOptions { codexPath?: string - approvals: ApprovalMode idleShutdownMs: number callTimeoutMs: number cwd: string @@ -206,6 +196,17 @@ export class ComputerUseBridge { }) } + /** The effective Codex `approval_policy` (a granular policy is reported as "granular"), if Codex reports it. */ + async approvalPolicy(): Promise { + return this.busy(async () => { + const client = await this.ensureClient() + const response = await client.request("config/read", { cwd: this.options.cwd }, { timeoutMs: 15_000 }) + const policy = response?.config?.approval_policy + if (typeof policy === "string") return policy + return policy && typeof policy === "object" ? "granular" : undefined + }) + } + get codexPath(): string | undefined { return resolveCodexPath(this.options.codexPath) } @@ -295,8 +296,8 @@ export class ComputerUseBridge { const existing = this.sessions.get(sessionID) if (existing && existing.generation === this.generation) return existing.threadID + // No approvalPolicy: app access follows the user's Codex approval_policy. const params: Record = { ephemeral: true, cwd: this.options.cwd } - if (this.options.approvals !== "codex") params.approvalPolicy = "on-request" const threadID = client.request("thread/start", params, { timeoutMs: 60_000 }).then((response: any) => { const id = response?.thread?.id if (typeof id !== "string") throw new Error("thread/start returned no thread id") @@ -318,17 +319,10 @@ export class ComputerUseBridge { if (method !== "mcpServer/elicitation/request") { throw new Error(`${method} is not supported by opencode-codex-computer-use`) } + // OpenCode's plugin API cannot raise a permission question yet, so a prompt Codex forwards + // (because approval_policy is not "never") is declined, and the model is told how the user can allow the app. const message = typeof params?.message === "string" ? params.message : "Computer Use requested approval" - if (this.options.approvals === "accept-session") { - this.options.log(`accepted for this session: ${message}`) - return { action: "accept", content: {}, _meta: { persist: "session" } } - } - const reason = - this.options.approvals === "decline" - ? 'the plugin option `approvals` is "decline"' - : "Codex asked for a decision instead of applying its own approval_policy. Approve the app once in the " + - 'ChatGPT/Codex app, or set the plugin option `approvals` to "accept-session"' - this.addApprovalNote(params?.threadId, `Declined "${message}" because ${reason}.`) + this.addApprovalNote(params?.threadId, declinedNote(message, appName(params))) this.options.log(`declined: ${message}`) return { action: "decline", content: null } } @@ -377,6 +371,22 @@ export class ComputerUseBridge { } } +function appName(params: any): string | undefined { + const display = params?._meta?.tool_params_display + const entry = Array.isArray(display) ? display.find((item: { name?: string }) => item?.name === "app") : undefined + return typeof entry?.value === "string" ? entry.value : undefined +} + +export function declinedNote(message: string, app?: string): string { + const target = app ? `"${app}"` : "this app" + return ( + `Computer Use asked "${message}" and it could not be approved from OpenCode. Tell the user that Computer Use ` + + `needs permission to use ${target}. They can approve it permanently in the ChatGPT or Codex app (use the app ` + + 'once through Computer Use there and choose to always allow it), or set approval_policy = "never" in ' + + "~/.codex/config.toml so Codex approves apps itself." + ) +} + /** * Per-call metadata Codex attaches to MCP tool calls during a turn. The browser surface of `cua_repl` requires * `session_id` and `turn_id`; `call_id` links approval prompts to the originating tool call. diff --git a/src/doctor.ts b/src/doctor.ts index 7a36b8e..f46b3cf 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -60,6 +60,7 @@ export async function runDoctor( return report(checks, options) } add("Computer Use runtime", "ok", `Codex app-server running with \`${CUA_SERVER}\``) + add("App access", ...approvalCheck(await bridge.approvalPolicy().catch(() => undefined))) const probe = (code: string) => bridge.run(sessionKey, code) @@ -128,13 +129,23 @@ function report(checks: Check[], options: Options): DoctorReport { "", ...checks.map((check) => `- ${ICONS[check.status]} **${check.name}**: ${check.detail}`), "", - `Options: approvals=${options.approvals}, surfaces=${options.surfaces.join("+")}, screenshots=${options.screenshots}, ` + + `Options: surfaces=${options.surfaces.join("+")}, screenshots=${options.screenshots}, ` + `maxOutputKB=${Math.round(options.maxOutputBytes / 1024) || "unlimited"}, ` + `idleShutdownMinutes=${options.idleShutdownMs / 60_000 || "never"}, debug=${options.debugLog ?? "off"}`, ] return { ok, checks, text: lines.join("\n") } } +export function approvalCheck(policy: string | undefined): [Status, string] { + if (policy === "never") return ["ok", 'Codex approves app access itself (approval_policy = "never")'] + const setting = policy ? `approval_policy = "${policy}"` : "approval_policy unknown" + return [ + "warn", + `${setting}: apps you have not approved permanently in ChatGPT/Codex will be declined, because OpenCode cannot ` + + 'show Computer Use\'s approval prompts yet. Approve apps once in ChatGPT/Codex, or set approval_policy = "never".', + ] +} + async function platformCheck(): Promise<[Status, string]> { if (process.platform === "darwin") { const version = (await run("/usr/bin/sw_vers", ["-productVersion"]).catch(() => "")).trim() diff --git a/src/index.ts b/src/index.ts index a6976bc..3a22adf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,6 +61,7 @@ export default Plugin.define({ id: "codex-computer-use", async setup(ctx) { const options = parseOptions(ctx.options) + for (const warning of options.warnings) log(warning) const bridge = new ComputerUseBridge({ ...options, cwd: ctx.location.directory, diff --git a/src/options.ts b/src/options.ts index 0f5839b..6267926 100644 --- a/src/options.ts +++ b/src/options.ts @@ -3,8 +3,6 @@ import { join } from "node:path" import type { PluginOptions } from "@opencode/plugin" -import { APPROVAL_MODES, type ApprovalMode } from "./bridge" - export const SCREENSHOT_MODES = ["image", "ocr", "both", "off"] as const export type ScreenshotMode = (typeof SCREENSHOT_MODES)[number] @@ -13,7 +11,6 @@ export type Surface = (typeof SURFACES)[number] export interface Options { codexPath?: string - approvals: ApprovalMode idleShutdownMs: number callTimeoutMs: number /** Maximum text returned by one call, in bytes. 0 disables the limit. */ @@ -22,6 +19,8 @@ export interface Options { surfaces: readonly Surface[] /** JSONL file receiving the app-server traffic, or undefined when debugging is off. */ debugLog?: string + /** Messages about options that were accepted but have no effect. */ + warnings: readonly string[] } export const DEFAULT_DEBUG_LOG = join( @@ -32,15 +31,21 @@ export const DEFAULT_DEBUG_LOG = join( ) export function parseOptions(raw: PluginOptions): Options { + const warnings: string[] = [] + if (raw.approvals !== undefined) { + warnings.push( + 'The "approvals" option was removed in 0.1.1 and is ignored: app access follows your Codex approval_policy.', + ) + } return { codexPath: typeof raw.codexPath === "string" && raw.codexPath.trim() ? raw.codexPath.trim() : undefined, - approvals: oneOf("approvals", raw.approvals, APPROVAL_MODES, "codex"), idleShutdownMs: nonNegative(raw.idleShutdownMinutes, 0) * 60_000, callTimeoutMs: positive(raw.callTimeoutSeconds, 300) * 1_000, maxOutputBytes: Math.round(nonNegative(raw.maxOutputKB, 128) * 1024), screenshots: oneOf("screenshots", raw.screenshots, SCREENSHOT_MODES, "image"), surfaces: surfaces(raw.surfaces), debugLog: debugLog(raw.debug), + warnings, } } diff --git a/test/bridge.test.ts b/test/bridge.test.ts index 2aeeee3..bcf1079 100644 --- a/test/bridge.test.ts +++ b/test/bridge.test.ts @@ -3,7 +3,7 @@ import { mkdtempSync, readFileSync, existsSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" -import { ComputerUseBridge, SetupError, type ApprovalMode } from "../src/bridge" +import { ComputerUseBridge, SetupError } from "../src/bridge" const FAKE_CODEX = join(import.meta.dir, "fixtures", "fake-codex") const bridges: ComputerUseBridge[] = [] @@ -11,11 +11,11 @@ const bridges: ComputerUseBridge[] = [] afterEach(async () => { await Promise.all(bridges.splice(0).map((bridge) => bridge.dispose())) delete process.env.FAKE_CODEX_NO_CUA + delete process.env.FAKE_CODEX_POLICY }) function setup( input: { - approvals?: ApprovalMode idleShutdownMs?: number codePrefix?: string onTraffic?: (direction: "send" | "receive", message: unknown) => void @@ -26,7 +26,6 @@ function setup( const logs: string[] = [] const bridge = new ComputerUseBridge({ codexPath: FAKE_CODEX, - approvals: input.approvals ?? "codex", idleShutdownMs: input.idleShutdownMs ?? 0, callTimeoutMs: 10_000, cwd: "/tmp/project", @@ -110,30 +109,20 @@ describe("ComputerUseBridge", () => { expect(text(result)).toContain("ReferenceError") }) - test("codex mode keeps the Codex approval policy and declines forwarded prompts with a note", async () => { - const { bridge, calls } = setup({ approvals: "codex" }) + test("follows the Codex approval policy and declines forwarded prompts with an actionable note", async () => { + const { bridge, calls } = setup() const result = await bridge.run("ses_a", "ELICIT") expect(calls("thread/start")[0].params.approvalPolicy).toBeUndefined() expect(text(result)).toBe("elicitation:decline") - expect(result.notes[0]).toContain('Declined "Allow Computer Use to use "Calculator"?"') - }) - - test("accept-session mode asks Codex to forward prompts and accepts them for the session only", async () => { - const { bridge, calls, received } = setup({ approvals: "accept-session" }) - const result = await bridge.run("ses_a", "ELICIT") - expect(calls("thread/start")[0].params.approvalPolicy).toBe("on-request") - expect(text(result)).toBe("elicitation:accept") - const answer = received().find((message) => message.id === 1000) - expect(answer.result).toEqual({ action: "accept", content: {}, _meta: { persist: "session" } }) - expect(result.notes).toEqual([]) + expect(result.notes[0]).toContain('needs permission to use "Calculator"') + expect(result.notes[0]).toContain('approval_policy = "never"') }) - test("decline mode declines prompts", async () => { - const { bridge, calls } = setup({ approvals: "decline" }) - const result = await bridge.run("ses_a", "ELICIT") - expect(calls("thread/start")[0].params.approvalPolicy).toBe("on-request") - expect(text(result)).toBe("elicitation:decline") - expect(result.notes[0]).toContain('`approvals` is "decline"') + test("reads the Codex approval policy", async () => { + process.env.FAKE_CODEX_POLICY = "never" + expect(await setup().bridge.approvalPolicy()).toBe("never") + process.env.FAKE_CODEX_POLICY = "granular" + expect(await setup().bridge.approvalPolicy()).toBe("granular") }) test("sends turn_ended once per turn that used Computer Use", async () => { diff --git a/test/fixtures/fake-codex.ts b/test/fixtures/fake-codex.ts index 775c1e9..1f12431 100644 --- a/test/fixtures/fake-codex.ts +++ b/test/fixtures/fake-codex.ts @@ -30,6 +30,11 @@ createInterface({ input: process.stdin }).on("line", async (line) => { return case "thread/start": return send({ id, result: { thread: { id: `thread-${++threads}`, ephemeral: params.ephemeral, path: null } } }) + case "config/read": { + const policy = process.env.FAKE_CODEX_POLICY + const value = policy === "granular" ? { granular: {} } : policy + return send({ id, result: { config: { approval_policy: value ?? null }, origins: {} } }) + } case "thread/unsubscribe": return send({ id, result: { status: "unsubscribed" } }) case "mcpServerStatus/list": @@ -81,6 +86,7 @@ async function toolCall(id: number, params: any) { serverName: "cua_repl", mode: "form", message: 'Allow Computer Use to use "Calculator"?', + _meta: { tool_params_display: [{ name: "app", display_name: "App", value: "Calculator" }] }, }, }) const result = await answer diff --git a/test/options.test.ts b/test/options.test.ts index 6d45ff1..823c99e 100644 --- a/test/options.test.ts +++ b/test/options.test.ts @@ -3,6 +3,7 @@ import { homedir } from "node:os" import { join } from "node:path" import { stripImages } from "../src/debug" +import { approvalCheck } from "../src/doctor" import { describeTool } from "../src/index" import { DEFAULT_DEBUG_LOG, parseOptions } from "../src/options" import { surfaceGuard } from "../src/surfaces" @@ -11,13 +12,13 @@ describe("parseOptions", () => { test("defaults", () => { expect(parseOptions({})).toEqual({ codexPath: undefined, - approvals: "codex", idleShutdownMs: 0, callTimeoutMs: 300_000, maxOutputBytes: 128 * 1024, screenshots: "image", surfaces: ["apps", "browser"], debugLog: undefined, + warnings: [], }) }) @@ -25,7 +26,6 @@ describe("parseOptions", () => { expect( parseOptions({ codexPath: " /x/codex ", - approvals: "accept-session", idleShutdownMinutes: 30, callTimeoutSeconds: 60, maxOutputKB: 0, @@ -35,26 +35,39 @@ describe("parseOptions", () => { }), ).toEqual({ codexPath: "/x/codex", - approvals: "accept-session", idleShutdownMs: 1_800_000, callTimeoutMs: 60_000, maxOutputBytes: 0, screenshots: "ocr", surfaces: ["browser"], debugLog: join(homedir(), "cu.jsonl"), + warnings: [], }) expect(parseOptions({ debug: true }).debugLog).toBe(DEFAULT_DEBUG_LOG) expect(parseOptions({ surfaces: "apps" }).surfaces).toEqual(["apps"]) }) test("rejects invalid values", () => { - expect(() => parseOptions({ approvals: "always" })).toThrow("invalid option approvals") expect(() => parseOptions({ screenshots: "video" })).toThrow("invalid option screenshots") expect(() => parseOptions({ surfaces: [] })).toThrow("invalid option surfaces") expect(() => parseOptions({ surfaces: ["apps", "files"] })).toThrow("invalid option surfaces") }) }) +test("the removed approvals option is ignored with a warning", () => { + const options = parseOptions({ approvals: "accept-session" }) + expect(options.warnings[0]).toContain('"approvals" option was removed') + expect("approvals" in options).toBe(false) +}) + +describe("approvalCheck", () => { + test("only never is fully automatic", () => { + expect(approvalCheck("never")[0]).toBe("ok") + expect(approvalCheck("on-request")).toEqual(["warn", expect.stringContaining('approval_policy = "on-request"')]) + expect(approvalCheck(undefined)[1]).toContain("approval_policy unknown") + }) +}) + describe("surfaceGuard", () => { test("adds nothing when every surface is enabled", () => { expect(surfaceGuard(["apps", "browser"])).toBeUndefined() From 95a80078d0fb9caa59b60a74d167b3b2349a2beb Mon Sep 17 00:00:00 2001 From: "M. Adel Alhashemi" Date: Fri, 25 Sep 2026 07:40:52 +0300 Subject: [PATCH 2/2] docs: explain deny and ask rules for computer_use in OpenCode 2.0.16 deny hides the tools; ask does not prompt yet for external plugin tools (anomalyco/opencode#50652). Link the plugin permission API PR that will let app prompts appear in OpenCode. --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 915839f..acc342c 100644 --- a/README.md +++ b/README.md @@ -172,21 +172,26 @@ question, as they do inside Codex: - With `approval_policy = "never"` in `~/.codex/config.toml`, Codex approves app access itself. - Apps you approved permanently in ChatGPT or Codex are always allowed. - Any other app is declined, and the agent tells you which app needs permission. OpenCode's plugin API cannot show - these prompts as OpenCode permission questions yet; once it can, they will appear in OpenCode instead. + these prompts as OpenCode permission questions yet; once it can + ([anomalyco/opencode#46530](https://github.com/anomalyco/opencode/pull/46530)), they will appear in OpenCode instead. Apps blocked by the engine or your organization stay blocked. `/computer-use-doctor` shows which policy applies. ### Permissions -Both tools use the OpenCode permission action `computer_use`. OpenCode allows it by default; to be asked before every -call, or to turn it off, add a rule: +Both tools use the OpenCode permission action `computer_use`, which OpenCode allows by default. To turn Computer Use +off for a project or an agent, deny it; the tools then disappear from the model's tool list: ```jsonc { - "permissions": [{ "action": "computer_use", "resource": "*", "effect": "ask" }], + "permissions": [{ "action": "computer_use", "resource": "*", "effect": "deny" }], } ``` +An `ask` rule is meant to prompt before every call, but OpenCode 2.0.16 does not yet apply `ask` to tools from external +plugins ([anomalyco/opencode#50652](https://github.com/anomalyco/opencode/issues/50652), fix in +[#50657](https://github.com/anomalyco/opencode/pull/50657)); until that fix ships, `ask` behaves like `allow`. + ## How it works ```