diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index db057a4c41f..40ffafaf389 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -1,4 +1,4 @@ -import { Component, Show, createEffect, createMemo, createResource, type JSX } from "solid-js" +import { Component, Show, createEffect, createMemo, createResource, createSignal, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" @@ -10,6 +10,7 @@ import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useSettings, monoFontFamily } from "@/context/settings" +import { useGlobalSDK } from "@/context/global-sdk" import { playSound, SOUND_OPTIONS } from "@/utils/sound" import { Link } from "./link" @@ -37,6 +38,53 @@ export const SettingsGeneral: Component = () => { const language = useLanguage() const platform = usePlatform() const settings = useSettings() + const globalSDK = useGlobalSDK() + + // YOLO state - wird später vom Server geladen + const [yoloEnabled, setYoloEnabled] = createSignal(false) + const [yoloPersisted, setYoloPersisted] = createSignal(false) + + // Lade YOLO status beim Öffnen - mit kleinem Delay für Stabilität + const loadYoloStatus = () => { + const doFetch = platform.fetch ?? fetch + doFetch(`${globalSDK.url}/config/yolo`) + .then((response) => { + if (response.ok) return response.json() + return null + }) + .then((data) => { + if (data) { + setYoloEnabled(data.enabled === true) + setYoloPersisted(data.persisted === true) + } + }) + .catch(() => { + // Silently ignore errors + }) + } + + // Initialer Load mit kleinem Delay + setTimeout(loadYoloStatus, 100) + + const setYolo = (enabled: boolean, persist: boolean) => { + const doFetch = platform.fetch ?? fetch + doFetch(`${globalSDK.url}/config/yolo`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled, persist }), + }) + .then((response) => { + if (response.ok) return response.json() + return null + }) + .then((data) => { + if (data) { + setYoloEnabled(data.enabled === true) + setYoloPersisted(data.persisted === true) + } + }) + .catch((e) => console.error("Failed to set YOLO:", e)) + } const [store, setStore] = createStore({ checking: false, @@ -450,6 +498,128 @@ export const SettingsGeneral: Component = () => { ) }} + + {/* YOLO Mode Section */} +
+
+

YOLO Mode

+ + + ACTIVE + + +
+ +

+ Skip ALL permission prompts. OpenCode will execute without asking for confirmation. +

+ + {/* Warning */} +
+

+ Warning: This is dangerous. Only enable if you fully trust OpenCode's + actions. Explicit deny rules in your config will still be respected. +

+
+ + {/* This Session Only Card */} +
+
+
+
+ This Session Only + + ACTIVE + +
+ Resets when you restart OpenCode +
+ setYolo(true, false)} + class="px-3 py-1.5 rounded text-12-medium bg-fill-danger-base text-white hover:bg-fill-danger-strong transition-colors" + > + Enable + + } + > + + +
+
+ + {/* Always Enabled Card */} +
+
+
+
+ Always Enabled + + ACTIVE + Saved + +
+ Persists across restarts (saved in config.json) +
+ setYolo(true, true)} + class="px-3 py-1.5 rounded text-12-medium bg-fill-danger-base text-white hover:bg-fill-danger-strong transition-colors" + > + Save to Config + + } + > + + +
+
+ + {/* CLI Usage */} +
+ CLI Usage +
+
+ opencode --yolo + one session +
+
+ OPENCODE_YOLO=true + env var +
+
+
+
) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index a231a530072..291223290c1 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1163,6 +1163,10 @@ export namespace Config { prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"), }) .optional(), + yolo: z + .boolean() + .optional() + .describe("Enable YOLO mode - auto-approve all permission prompts (except explicit deny rules)"), experimental: z .object({ disable_paste_summary: z.boolean().optional(), diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index b11058b3405..cd6028a34ea 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -4,6 +4,7 @@ function truthy(key: string) { } export namespace Flag { + export const OPENCODE_YOLO = truthy("OPENCODE_YOLO") export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE") export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"] export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"] diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 2481f104ed1..bbcbad73e79 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -7,6 +7,7 @@ import { Storage } from "@/storage/storage" import { fn } from "@/util/fn" import { Log } from "@/util/log" import { Wildcard } from "@/util/wildcard" +import { Yolo } from "@/yolo" import os from "os" import z from "zod" @@ -137,6 +138,11 @@ export namespace PermissionNext { if (rule.action === "deny") throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission))) if (rule.action === "ask") { + // YOLO mode auto-approves all "ask" permissions (but respects explicit "deny") + if (Yolo.isEnabled()) { + log.warn("YOLO mode auto-approved", { permission: request.permission, pattern }) + continue + } const id = input.id ?? Identifier.ascending("permission") return new Promise((resolve, reject) => { const info: Request = { diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index efdcaba9909..ea1861f82f8 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -13,9 +13,11 @@ import { Log } from "@/util/log" import { ShareNext } from "@/share/share-next" import { Snapshot } from "../snapshot" import { Truncate } from "../tool/truncation" +import { Yolo } from "../yolo" export async function InstanceBootstrap() { Log.Default.info("bootstrapping", { directory: Instance.directory }) + await Yolo.init() await Plugin.init() Share.init() ShareNext.init() diff --git a/packages/opencode/src/server/routes/config.ts b/packages/opencode/src/server/routes/config.ts index 85d28f6aa6b..a0e55a09e5e 100644 --- a/packages/opencode/src/server/routes/config.ts +++ b/packages/opencode/src/server/routes/config.ts @@ -1,8 +1,11 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" +import path from "path" import { Config } from "../../config/config" import { Provider } from "../../provider/provider" +import { Yolo } from "../../yolo" +import { Global } from "../../global" import { mapValues } from "remeda" import { errors } from "../error" import { Log } from "../../util/log" @@ -10,6 +13,22 @@ import { lazy } from "../../util/lazy" const log = Log.create({ service: "server" }) +// Helper to read/write global config for YOLO persistence (uses config.json, not opencode.jsonc) +async function readGlobalConfig(): Promise> { + const filepath = path.join(Global.Path.config, "config.json") + try { + const text = await Bun.file(filepath).text() + return JSON.parse(text) + } catch { + return {} + } +} + +async function writeGlobalConfig(config: Record): Promise { + const filepath = path.join(Global.Path.config, "config.json") + await Bun.write(filepath, JSON.stringify(config, null, 2)) +} + export const ConfigRoutes = lazy(() => new Hono() .get( @@ -88,5 +107,82 @@ export const ConfigRoutes = lazy(() => default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), }) }, + ) + .get( + "/yolo", + describeRoute({ + summary: "Get YOLO mode status", + description: + "Check if YOLO mode is enabled. When enabled, all permission prompts are auto-approved (except explicit deny rules).", + operationId: "config.yolo.get", + responses: { + 200: { + description: "YOLO mode status", + content: { + "application/json": { + schema: resolver(z.object({ enabled: z.boolean(), persisted: z.boolean() })), + }, + }, + }, + }, + }), + async (c) => { + const globalConfig = await readGlobalConfig() + return c.json({ + enabled: Yolo.isEnabled(), + persisted: globalConfig.yolo === true, + }) + }, + ) + .post( + "/yolo", + describeRoute({ + summary: "Set YOLO mode", + description: + "Enable or disable YOLO mode. When enabled, all permission prompts are auto-approved (except explicit deny rules). Use with caution. Set persist=true to save to config file.", + operationId: "config.yolo.set", + responses: { + 200: { + description: "YOLO mode updated", + content: { + "application/json": { + schema: resolver(z.object({ enabled: z.boolean(), persisted: z.boolean() })), + }, + }, + }, + }, + }), + validator("json", z.object({ enabled: z.boolean(), persist: z.boolean().optional() })), + async (c) => { + const { enabled, persist } = c.req.valid("json") + Yolo.set(enabled) + + try { + const globalConfig = await readGlobalConfig() + const wasPersisted = globalConfig.yolo === true + + if (persist) { + // Explicitly save to or remove from config + if (enabled) { + globalConfig.yolo = true + } else { + delete globalConfig.yolo + } + await writeGlobalConfig(globalConfig) + log.info("YOLO mode config updated", { enabled, path: Global.Path.config }) + } else if (wasPersisted && enabled) { + // Downgrade from persistent to session-only: remove from config but keep enabled + delete globalConfig.yolo + await writeGlobalConfig(globalConfig) + log.info("YOLO mode downgraded to session-only", { path: Global.Path.config }) + } + } catch (e) { + log.error("Failed to update YOLO config", { error: e }) + } + + // Return the actual persisted state from config + const finalConfig = await readGlobalConfig() + return c.json({ enabled: Yolo.isEnabled(), persisted: finalConfig.yolo === true }) + }, ), ) diff --git a/packages/opencode/src/yolo/index.ts b/packages/opencode/src/yolo/index.ts new file mode 100644 index 00000000000..b36e809265d --- /dev/null +++ b/packages/opencode/src/yolo/index.ts @@ -0,0 +1,54 @@ +import { Bus } from "@/bus" +import { BusEvent } from "@/bus/bus-event" +import { Config } from "@/config/config" +import { Flag } from "@/flag/flag" +import { Log } from "@/util/log" +import z from "zod" + +export namespace Yolo { + const log = Log.create({ service: "yolo" }) + + let enabled = Flag.OPENCODE_YOLO + + export const Event = { + Changed: BusEvent.define( + "yolo.changed", + z.object({ + enabled: z.boolean(), + }), + ), + } + + export async function init() { + const config = await Config.global() + if (config.yolo === true) { + enabled = true + log.warn("YOLO mode enabled via config") + } + if (Flag.OPENCODE_YOLO) { + enabled = true + log.warn("YOLO mode enabled via OPENCODE_YOLO env var") + } + if (enabled) { + log.warn("YOLO mode is ACTIVE - all permission prompts will be auto-approved") + } + } + + export function isEnabled(): boolean { + return enabled + } + + export function set(value: boolean) { + const previous = enabled + enabled = value + if (previous !== value) { + log.warn(`YOLO mode ${value ? "ENABLED" : "DISABLED"}`) + Bus.publish(Event.Changed, { enabled: value }) + } + } + + export function toggle(): boolean { + set(!enabled) + return enabled + } +} diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b757b753507..9f989b73ab8 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -19,6 +19,8 @@ import type { ConfigProvidersResponses, ConfigUpdateErrors, ConfigUpdateResponses, + ConfigYoloGetResponses, + ConfigYoloSetResponses, EventSubscribeResponses, EventTuiCommandExecute, EventTuiPromptAppend, @@ -643,6 +645,64 @@ export class Pty extends HeyApiClient { } } +export class Yolo extends HeyApiClient { + /** + * Get YOLO mode status + * + * Check if YOLO mode is enabled. When enabled, all permission prompts are auto-approved (except explicit deny rules). + */ + public get( + parameters?: { + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) + return (options?.client ?? this.client).get({ + url: "/config/yolo", + ...options, + ...params, + }) + } + + /** + * Set YOLO mode + * + * Enable or disable YOLO mode. When enabled, all permission prompts are auto-approved (except explicit deny rules). Use with caution. Set persist=true to save to config file. + */ + public set( + parameters?: { + directory?: string + enabled?: boolean + persist?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "body", key: "enabled" }, + { in: "body", key: "persist" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/config/yolo", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Config2 extends HeyApiClient { /** * Get configuration @@ -716,6 +776,11 @@ export class Config2 extends HeyApiClient { ...params, }) } + + private _yolo?: Yolo + get yolo(): Yolo { + return (this._yolo ??= new Yolo({ client: this.client })) + } } export class Tool extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9543e5b5796..af5b1e01fa3 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -509,6 +509,13 @@ export type EventMessagePartRemoved = { } } +export type EventYoloChanged = { + type: "yolo.changed" + properties: { + enabled: boolean + } +} + export type PermissionRequest = { id: string sessionID: string @@ -919,6 +926,7 @@ export type Event = | EventMessageRemoved | EventMessagePartUpdated | EventMessagePartRemoved + | EventYoloChanged | EventPermissionAsked | EventPermissionReplied | EventSessionStatus @@ -1823,6 +1831,10 @@ export type Config = { */ prune?: boolean } + /** + * Enable YOLO mode - auto-approve all permission prompts (except explicit deny rules) + */ + yolo?: boolean experimental?: { disable_paste_summary?: boolean /** @@ -2673,6 +2685,51 @@ export type ConfigProvidersResponses = { export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] +export type ConfigYoloGetData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/config/yolo" +} + +export type ConfigYoloGetResponses = { + /** + * YOLO mode status + */ + 200: { + enabled: boolean + persisted: boolean + } +} + +export type ConfigYoloGetResponse = ConfigYoloGetResponses[keyof ConfigYoloGetResponses] + +export type ConfigYoloSetData = { + body?: { + enabled: boolean + persist?: boolean + } + path?: never + query?: { + directory?: string + } + url: "/config/yolo" +} + +export type ConfigYoloSetResponses = { + /** + * YOLO mode updated + */ + 200: { + enabled: boolean + persisted: boolean + } +} + +export type ConfigYoloSetResponse = ConfigYoloSetResponses[keyof ConfigYoloSetResponses] + export type ToolIdsData = { body?: never path?: never