diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 0aeb864e8679..96bc470a49dd 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -544,15 +544,27 @@ export const RunCommand = cmd({ if (event.type === "permission.asked") { const permission = event.properties if (permission.sessionID !== sessionID) continue - UI.println( - UI.Style.TEXT_WARNING_BOLD + "!", - UI.Style.TEXT_NORMAL + - `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, - ) - await sdk.permission.reply({ - requestID: permission.id, - reply: "reject", - }) + if (Flag.OPENCODE_YOLO) { + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL + + `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-approving (yolo)`, + ) + await sdk.permission.reply({ + requestID: permission.id, + reply: "always", + }) + } else { + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL + + `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, + ) + await sdk.permission.reply({ + requestID: permission.id, + reply: "reject", + }) + } } } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 850bcc28bcd9..e24a2dcf45ea 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -997,6 +997,12 @@ export namespace Config { instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), layout: Layout.optional().describe("@deprecated Always uses stretch layout."), permission: Permission.optional(), + yolo: z + .boolean() + .optional() + .describe( + "When true, auto-approve all permission requests without prompting. Equivalent to the --yolo CLI flag. Use with caution.", + ), tools: z.record(z.string(), z.boolean()).optional(), enterprise: z .object({ @@ -1452,6 +1458,10 @@ export namespace Config { result.compaction = { ...result.compaction, prune: false } } + if (result.yolo && !Flag.OPENCODE_YOLO) { + process.env.OPENCODE_YOLO = "1" + } + return { config: result, directories, diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 1ac52dd17fa1..9be069a27110 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -44,6 +44,7 @@ export namespace Flag { export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"] export const OPENCODE_SERVER_USERNAME = process.env["OPENCODE_SERVER_USERNAME"] export const OPENCODE_ENABLE_QUESTION_TOOL = truthy("OPENCODE_ENABLE_QUESTION_TOOL") + export declare const OPENCODE_YOLO: boolean // Experimental export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL") @@ -153,3 +154,14 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", { enumerable: true, configurable: false, }) + +// Dynamic getter for OPENCODE_YOLO +// This must be evaluated at access time, not module load time, +// because the CLI can set this flag at runtime via --yolo +Object.defineProperty(Flag, "OPENCODE_YOLO", { + get() { + return truthy("OPENCODE_YOLO") + }, + enumerable: true, + configurable: false, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 1fa027abf904..85c64bb07cd9 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -82,11 +82,19 @@ const cli = yargs(args) describe: "run without external plugins", type: "boolean", }) + .option("yolo", { + describe: "auto-approve all permission requests without prompting", + type: "boolean", + }) .middleware(async (opts) => { if (opts.pure) { process.env.OPENCODE_PURE = "1" } + if (opts.yolo) { + process.env.OPENCODE_YOLO = "1" + } + await Log.init({ print: process.argv.includes("--print-logs"), dev: Installation.isLocal(), diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index b2cc0f9bbc07..862e0612c3af 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -2,6 +2,7 @@ import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { Config } from "@/config/config" import { InstanceState } from "@/effect/instance-state" +import { Flag } from "@/flag/flag" import { makeRuntime } from "@/effect/run-service" import { ProjectID } from "@/project/schema" import { Instance } from "@/project/instance" @@ -165,6 +166,13 @@ export namespace Permission { ) const ask = Effect.fn("Permission.ask")(function* (input: z.infer) { + if (Flag.OPENCODE_YOLO) { + log.info("yolo mode: auto-approving", { + permission: input.permission, + patterns: input.patterns, + }) + return + } const { approved, pending } = yield* InstanceState.get(state) const { ruleset, ...request } = input let needsAsk = false diff --git a/packages/opencode/test/permission/yolo.test.ts b/packages/opencode/test/permission/yolo.test.ts new file mode 100644 index 000000000000..f37fe4f04fc9 --- /dev/null +++ b/packages/opencode/test/permission/yolo.test.ts @@ -0,0 +1,141 @@ +import { afterEach, beforeEach, test, expect } from "bun:test" +import { Permission } from "../../src/permission" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" +import { SessionID } from "../../src/session/schema" + +let savedYolo: string | undefined + +beforeEach(() => { + savedYolo = process.env.OPENCODE_YOLO +}) + +afterEach(async () => { + if (savedYolo === undefined) { + delete process.env.OPENCODE_YOLO + } else { + process.env.OPENCODE_YOLO = savedYolo + } + await Instance.disposeAll() +}) + +test("yolo - auto-approves when OPENCODE_YOLO=1 and rules say ask", async () => { + process.env.OPENCODE_YOLO = "1" + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // With yolo, even "ask" rules should auto-approve (return undefined) + const result = await Permission.ask({ + sessionID: SessionID.make("session_yolo"), + permission: "bash", + patterns: ["rm -rf /"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + }) + expect(result).toBeUndefined() + }, + }) +}) + +test("yolo - auto-approves when OPENCODE_YOLO=1 and rules say deny", async () => { + process.env.OPENCODE_YOLO = "1" + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // With yolo, even "deny" rules should be bypassed + const result = await Permission.ask({ + sessionID: SessionID.make("session_yolo"), + permission: "bash", + patterns: ["rm -rf /"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "deny" }], + }) + expect(result).toBeUndefined() + }, + }) +}) + +test("yolo - does not auto-approve when OPENCODE_YOLO is unset", async () => { + delete process.env.OPENCODE_YOLO + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // Without yolo, "deny" rules should throw DeniedError + await expect( + Permission.ask({ + sessionID: SessionID.make("session_no_yolo"), + permission: "bash", + patterns: ["rm -rf /"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "deny" }], + }), + ).rejects.toBeInstanceOf(Permission.DeniedError) + }, + }) +}) + +test("yolo - no pending requests created when OPENCODE_YOLO=1", async () => { + process.env.OPENCODE_YOLO = "1" + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await Permission.ask({ + sessionID: SessionID.make("session_yolo_pending"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [], + }) + // No pending requests should have been created + const list = await Permission.list() + expect(list).toHaveLength(0) + }, + }) +}) + +test("yolo - OPENCODE_YOLO=true also works", async () => { + process.env.OPENCODE_YOLO = "true" + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await Permission.ask({ + sessionID: SessionID.make("session_yolo_true"), + permission: "edit", + patterns: ["/etc/passwd"], + metadata: {}, + always: [], + ruleset: [{ permission: "edit", pattern: "*", action: "ask" }], + }) + expect(result).toBeUndefined() + }, + }) +}) + +test("yolo - OPENCODE_YOLO=0 does not auto-approve", async () => { + process.env.OPENCODE_YOLO = "0" + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await expect( + Permission.ask({ + sessionID: SessionID.make("session_yolo_zero"), + permission: "bash", + patterns: ["rm -rf /"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "deny" }], + }), + ).rejects.toBeInstanceOf(Permission.DeniedError) + }, + }) +})