Skip to content
Closed
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
30 changes: 21 additions & 9 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
})
8 changes: 8 additions & 0 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
8 changes: 8 additions & 0 deletions packages/opencode/src/permission/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -165,6 +166,13 @@ export namespace Permission {
)

const ask = Effect.fn("Permission.ask")(function* (input: z.infer<typeof AskInput>) {
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
Expand Down
141 changes: 141 additions & 0 deletions packages/opencode/test/permission/yolo.test.ts
Original file line number Diff line number Diff line change
@@ -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)
},
})
})
Loading