From a4ad17347f84b29569d1e511176ff0a7aa4650a4 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:56:04 -0500 Subject: [PATCH] fix(core): apply safe defaults to all agents (#40316) --- packages/core/src/agent.ts | 22 +++++- packages/core/src/instructions/builtins.ts | 5 +- packages/core/src/plugin/agent.ts | 69 +++++-------------- packages/core/test/agent.test.ts | 48 +++++++++---- packages/core/test/config/agent.test.ts | 23 ++++--- packages/core/test/global.test.ts | 4 +- .../core/test/instructions/builtins.test.ts | 3 +- packages/schema/src/agent.ts | 3 + packages/tui/src/mini/tool.ts | 20 ------ packages/util/src/global.ts | 5 +- .../content/docs/(Configure)/permissions.mdx | 15 ++-- 11 files changed, 111 insertions(+), 106 deletions(-) diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 13cdca22291..da6c782bf69 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -1,11 +1,16 @@ export * as Agent from "./agent" +import path from "path" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Array, Context, Effect, Layer, Types } from "effect" import { Agent } from "@opencode-ai/schema/agent" +import { Global } from "@opencode-ai/util/global" import { Bus } from "./bus" import { State } from "./state" +const SHELL_OUTPUT_GLOB = (data: string) => path.join(data, "shell", "*", "*") +const TOOL_OUTPUT_GLOB = (data: string) => path.join(data, "tool-output", "*") + export const ID = Agent.ID export type ID = typeof ID.Type export const Name = Agent.Name @@ -51,6 +56,13 @@ const layer = Layer.effect( Service, Effect.gen(function* () { const bus = yield* Bus.Service + const global = yield* Global.Service + const permissions: Info["permissions"] = [ + { action: "external_directory", resource: SHELL_OUTPUT_GLOB(global.data), effect: "allow" }, + { action: "external_directory", resource: TOOL_OUTPUT_GLOB(global.data), effect: "allow" }, + { action: "external_directory", resource: path.join(global.tmp, "*"), effect: "allow" }, + { action: "external_directory", resource: path.join(global.config, "*"), effect: "allow" }, + ] const state = State.create({ name: "agent", initial: () => ({ agents: new Map() }), @@ -61,7 +73,13 @@ const layer = Layer.effect( draft.default = id }, update: (id, fn) => { - const current = draft.agents.get(id) ?? (Info.default(id) as Types.DeepMutable) + const defaults = Info.default(id) + const current = + draft.agents.get(id) ?? + ({ + ...defaults, + permissions: [...defaults.permissions, ...permissions], + } as Types.DeepMutable) if (!draft.agents.has(id)) draft.agents.set(id, current) fn(current) current.id = id @@ -114,4 +132,4 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Global.node] }) diff --git a/packages/core/src/instructions/builtins.ts b/packages/core/src/instructions/builtins.ts index fdb8f87fcdf..a44154936b9 100644 --- a/packages/core/src/instructions/builtins.ts +++ b/packages/core/src/instructions/builtins.ts @@ -2,6 +2,7 @@ export * as InstructionBuiltIns from "./builtins" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, DateTime, Effect, Layer, Schema } from "effect" +import { Global } from "@opencode-ai/util/global" import { Location } from "../location" import { SessionSchema } from "../session/schema" import { Instructions } from "./index" @@ -15,6 +16,7 @@ export class Service extends Context.Service()("@opencode/In const layer = Layer.effect( Service, Effect.gen(function* () { + const global = yield* Global.Service const location = yield* Location.Service return Service.of({ load: (sessionID) => @@ -31,6 +33,7 @@ const layer = Layer.effect( ` Workspace root folder: ${location.project.directory}`, ` Is directory a git repo: ${location.vcs?.type === "git" ? "yes" : "no"}`, ` Platform: ${process.platform}`, + ` Use ${global.tmp} for temporary work outside the workspace; it already exists and is pre-approved for external directory access.`, "", ].join("\n"), ), @@ -58,4 +61,4 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Global.node, Location.node] }) diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index c94e735faa2..afbc9b8e04e 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -1,17 +1,10 @@ export * as AgentPlugin from "./agent" -import path from "path" import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" import { Agent } from "../agent" -import { Global } from "@opencode-ai/util/global" -import { Location } from "../location" import { Permission } from "../permission" -// Combined output files written by the Shell service, e.g. `/shell//.out`. -// Whitelisted so agents can read a command's full captured output without an external-directory prompt. -const SHELL_OUTPUT_GLOB = path.join(Global.Path.data, "shell", "*", "*") - const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases. Your strengths: @@ -100,38 +93,12 @@ Rules: export const Plugin = define({ id: "opencode.agent", effect: Effect.fn(function* (ctx) { - const location = yield* Location.Service - const worktree = location.directory - const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")] - const readonlyExternalDirectory: Permission.Ruleset = [ - { action: "external_directory", resource: "*", effect: "ask" }, - ...whitelistedDirs.map( - (resource): Permission.Rule => ({ action: "external_directory", resource, effect: "allow" }), - ), - ] - const defaults: Permission.Ruleset = [ - { action: "*", resource: "*", effect: "allow" }, - ...readonlyExternalDirectory, - { action: "question", resource: "*", effect: "deny" }, - { action: "plan_enter", resource: "*", effect: "deny" }, - { action: "plan_exit", resource: "*", effect: "deny" }, - { action: "read", resource: "*", effect: "allow" }, - { action: "read", resource: "*.env", effect: "ask" }, - { action: "read", resource: "*.env.*", effect: "ask" }, - { action: "read", resource: "*.env.example", effect: "allow" }, - ] - yield* ctx.agent.transform((draft) => { draft.update(Agent.defaultID, (item) => { item.name = Agent.Name.make("Build") item.description = "The default agent. Executes tools based on configured permissions." item.mode = "primary" - item.permissions.push( - ...Permission.merge(defaults, [ - { action: "question", resource: "*", effect: "allow" }, - { action: "plan_enter", resource: "*", effect: "allow" }, - ]), - ) + item.permissions.push({ action: "question", resource: "*", effect: "allow" }) }) draft.update(Agent.ID.make("plan"), (item) => { @@ -139,18 +106,8 @@ export const Plugin = define({ item.description = "Plan mode. Disallows all edit tools." item.mode = "primary" item.permissions.push( - ...Permission.merge(defaults, [ - { action: "question", resource: "*", effect: "allow" }, - { action: "plan_exit", resource: "*", effect: "allow" }, - { action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" }, - { action: "edit", resource: "*", effect: "deny" }, - { action: "edit", resource: path.join(".opencode", "plans", "*.md"), effect: "allow" }, - { - action: "edit", - resource: path.relative(worktree, path.join(Global.Path.data, "plans", "*.md")), - effect: "allow", - }, - ]), + { action: "question", resource: "*", effect: "allow" }, + { action: "edit", resource: "*", effect: "deny" }, ) }) @@ -159,10 +116,16 @@ export const Plugin = define({ item.description = "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." item.mode = "subagent" - item.permissions.push(...Permission.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }])) + item.permissions.push( + { action: "question", resource: "*", effect: "deny" }, + { action: "subagent", resource: "*", effect: "deny" }, + ) }) draft.update(Agent.ID.make("explore"), (item) => { + const externalDirectories = item.permissions.filter( + (rule) => rule.action === "external_directory" && rule.effect === "allow", + ) item.name = Agent.Name.make("Explore") item.description = 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.' @@ -170,7 +133,6 @@ export const Plugin = define({ item.mode = "subagent" item.permissions.push( ...Permission.merge( - defaults, [ { action: "*", resource: "*", effect: "deny" }, { action: "grep", resource: "*", effect: "allow" }, @@ -178,9 +140,12 @@ export const Plugin = define({ { action: "webfetch", resource: "*", effect: "allow" }, { action: "websearch", resource: "*", effect: "allow" }, { action: "read", resource: "*", effect: "allow" }, + { action: "read", resource: "*.env", effect: "ask" }, + { action: "read", resource: "*.env.*", effect: "ask" }, + { action: "read", resource: "*.env.example", effect: "allow" }, { action: "subagent", resource: "*", effect: "deny" }, ], - readonlyExternalDirectory, + [{ action: "external_directory", resource: "*", effect: "ask" }, ...externalDirectories], ), ) }) @@ -190,7 +155,7 @@ export const Plugin = define({ item.mode = "primary" item.hidden = true item.system = PROMPT_COMPACTION - item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push({ action: "*", resource: "*", effect: "deny" }) }) draft.update(Agent.ID.make("title"), (item) => { @@ -198,7 +163,7 @@ export const Plugin = define({ item.mode = "primary" item.hidden = true item.system = PROMPT_TITLE - item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push({ action: "*", resource: "*", effect: "deny" }) }) draft.update(Agent.ID.make("summary"), (item) => { @@ -206,7 +171,7 @@ export const Plugin = define({ item.mode = "primary" item.hidden = true item.system = PROMPT_SUMMARY - item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push({ action: "*", resource: "*", effect: "deny" }) }) }) }), diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts index 645db471d79..a00554ca4fd 100644 --- a/packages/core/test/agent.test.ts +++ b/packages/core/test/agent.test.ts @@ -1,3 +1,4 @@ +import path from "path" import { describe, expect } from "bun:test" import { Effect, Exit, Fiber, Layer, Scope, Stream } from "effect" import { TestClock } from "effect/testing" @@ -9,15 +10,19 @@ import { Location } from "@opencode-ai/core/location" import { Permission } from "@opencode-ai/core/permission" import { AgentPlugin } from "@opencode-ai/core/plugin/agent" import { AbsolutePath } from "@opencode-ai/core/schema" +import { Global } from "@opencode-ai/util/global" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" import { agentHost, host } from "./plugin/host" const testLocation = location({ directory: AbsolutePath.make("/project") }) const locationLayer = Layer.succeed(Location.Service, Location.Service.of(testLocation)) +const global = Global.make({ data: "/data", config: "/config", tmp: "/tmp/opencode" }) +const globalLayer = Layer.succeed(Global.Service, Global.Service.of(global)) const it = testEffect( AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, Location.node]), [ + [Global.node, globalLayer], [Location.node, locationLayer], ]) as unknown as Layer.Layer, ) @@ -120,25 +125,35 @@ describe("Agent", () => { const id = Agent.ID.make("custom") yield* agent.transform((editor) => editor.update(id, () => {})) - expect(yield* agent.get(id)).toEqual(Agent.Info.default(id)) + const info = yield* agent.get(id) + expect(info?.permissions.slice(0, Agent.Info.default(id).permissions.length)).toEqual( + Agent.Info.default(id).permissions, + ) + expect(Permission.evaluate("external_directory", path.join(global.data, "shell", "*", "*"), info?.permissions ?? []).effect).toBe( + "allow", + ) + expect(Permission.evaluate("external_directory", path.join(global.data, "tool-output", "*"), info?.permissions ?? []).effect).toBe( + "allow", + ) + expect(Permission.evaluate("external_directory", path.join(global.config, "*"), info?.permissions ?? []).effect).toBe( + "allow", + ) + expect(Permission.evaluate("external_directory", path.join(global.tmp, "*"), info?.permissions ?? []).effect).toBe( + "allow", + ) yield* agent.transform((editor) => editor.remove(id)) expect(yield* agent.get(id)).toBeUndefined() }), ) - it.effect("does not ambiently opt built-in agents into bash", () => + it.effect("applies managed external directories without opting built-in agents into bash", () => Effect.gen(function* () { const agent = yield* Agent.Service yield* AgentPlugin.Plugin.effect( host({ agent: agentHost(agent), }), - ).pipe( - Effect.provideService( - Location.Service, - Location.Service.of(location({ directory: AbsolutePath.make("/project") })), - ), ) const agents = yield* agent.list() @@ -152,6 +167,20 @@ describe("Agent", () => { "title", ]) expect((yield* agent.get(Agent.defaultID))?.system).toBeUndefined() + const permissions = (yield* agent.get(Agent.defaultID))?.permissions ?? [] + expect( + Permission.evaluate("external_directory", path.join(global.data, "shell", "*", "*"), permissions).effect, + ).toBe("allow") + expect( + Permission.evaluate("external_directory", path.join(global.data, "tool-output", "*"), permissions).effect, + ).toBe("allow") + expect(Permission.evaluate("external_directory", path.join(global.config, "*"), permissions).effect).toBe("allow") + expect(Permission.evaluate("external_directory", path.join(global.tmp, "*"), permissions).effect).toBe("allow") + const explore = yield* agent.get(Agent.ID.make("explore")) + expect(Permission.evaluate("read", ".env", explore?.permissions ?? []).effect).toBe("ask") + expect(Permission.evaluate("read", ".env.local", explore?.permissions ?? []).effect).toBe("ask") + expect(Permission.evaluate("read", ".env.example", explore?.permissions ?? []).effect).toBe("allow") + expect(Permission.evaluate("read", "src/index.ts", explore?.permissions ?? []).effect).toBe("allow") for (const item of agents) { expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false) } @@ -165,11 +194,6 @@ describe("Agent", () => { host({ agent: agentHost(agent), }), - ).pipe( - Effect.provideService( - Location.Service, - Location.Service.of(location({ directory: AbsolutePath.make("/project") })), - ), ) yield* Effect.forEach(["general", "explore"], (id) => diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index aba3c11fb16..2cb8f0e9ccd 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -20,10 +20,13 @@ import { agentHost, host } from "../plugin/host" const it = testEffect(AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, FSUtil.node, Global.node]))) const decode = Schema.decodeUnknownSync(Config.Info) -const defaultPermissions = [ - { action: "*", resource: "*", effect: "allow" }, - { action: "external_directory", resource: "*", effect: "ask" }, -] satisfies Permission.Ruleset +const defaultPermissions = (global: Global.Interface): Permission.Ruleset => [ + ...Agent.Info.default(Agent.ID.make("test")).permissions, + { action: "external_directory", resource: path.join(global.data, "shell", "*", "*"), effect: "allow" }, + { action: "external_directory", resource: path.join(global.data, "tool-output", "*"), effect: "allow" }, + { action: "external_directory", resource: path.join(global.tmp, "*"), effect: "allow" }, + { action: "external_directory", resource: path.join(global.config, "*"), effect: "allow" }, +] test("rejects named agent color tokens", () => { expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow() @@ -58,6 +61,7 @@ describe("ConfigAgentPlugin.Plugin", () => { it.effect("applies all global permissions before agent-specific permissions", () => Effect.gen(function* () { const agents = yield* Agent.Service + const global = yield* Global.Service const build = Agent.ID.make("build") yield* agents.transform((editor) => editor.update(build, (agent) => { @@ -110,7 +114,7 @@ describe("ConfigAgentPlugin.Plugin", () => { const buildAgent = yield* agents.get(build) if (!buildAgent) throw new Error("expected configured build agent") expect(buildAgent.permissions).toEqual([ - ...defaultPermissions, + ...defaultPermissions(global), { action: "bash", resource: "*", effect: "allow" }, { action: "bash", resource: "*", effect: "ask" }, { action: "read", resource: "*", effect: "allow" }, @@ -128,7 +132,7 @@ describe("ConfigAgentPlugin.Plugin", () => { model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" }, }) expect(reviewer.permissions).toEqual([ - ...defaultPermissions, + ...defaultPermissions(global), { action: "bash", resource: "*", effect: "ask" }, { action: "read", resource: "*", effect: "allow" }, { action: "edit", resource: "*", effect: "deny" }, @@ -136,7 +140,7 @@ describe("ConfigAgentPlugin.Plugin", () => { ]) expect(Permission.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny") expect((yield* agents.get(Agent.ID.make("late")))?.permissions).toEqual([ - ...defaultPermissions, + ...defaultPermissions(global), { action: "bash", resource: "*", effect: "ask" }, { action: "read", resource: "*", effect: "allow" }, { action: "edit", resource: "*", effect: "allow" }, @@ -270,6 +274,7 @@ Use native v2 fields.`, await fs.writeFile(path.join(tmp.path, "modes", "plan.md"), "Make a plan.") }) const agents = yield* Agent.Service + const global = yield* Global.Service const entries = [ new Config.Document({ type: "document", @@ -287,13 +292,13 @@ Use native v2 fields.`, system: "Review carefully.", description: "Markdown description", request: { body: { temperature: 0.5 } }, - permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }], + permissions: [...defaultPermissions(global), { action: "edit", resource: "*", effect: "deny" }], }) expect(yield* agents.get(Agent.ID.make("team/helper"))).toMatchObject({ system: "Help the team." }) expect(yield* agents.get(Agent.ID.make("native"))).toMatchObject({ system: "Use native v2 fields.", request: { headers: { "x-agent": "native" }, body: { effort: "high" } }, - permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }], + permissions: [...defaultPermissions(global), { action: "edit", resource: "*", effect: "deny" }], }) expect(yield* agents.get(Agent.ID.make("disabled"))).toBeUndefined() expect(yield* agents.get(Agent.ID.make("empty"))).toBeUndefined() diff --git a/packages/core/test/global.test.ts b/packages/core/test/global.test.ts index ca061094194..f4213785fa5 100644 --- a/packages/core/test/global.test.ts +++ b/packages/core/test/global.test.ts @@ -5,8 +5,8 @@ import path from "path" import { Global } from "@opencode-ai/util/global" describe("global paths", () => { - test("tmp path is under the system temp directory", () => { - expect(Global.Path.tmp).toBe(path.join(os.tmpdir(), "opencode")) + test("tmp path is the canonical system temp directory", async () => { + expect(Global.Path.tmp).toBe(await fs.realpath(path.join(os.tmpdir(), "opencode"))) expect(Global.make().tmp).toBe(Global.Path.tmp) }) diff --git a/packages/core/test/instructions/builtins.test.ts b/packages/core/test/instructions/builtins.test.ts index 4f8ffc20ef0..3f1c572722d 100644 --- a/packages/core/test/instructions/builtins.test.ts +++ b/packages/core/test/instructions/builtins.test.ts @@ -29,7 +29,7 @@ const locationLayer = Layer.succeed( const it = testEffect( AppNodeBuilder.build(InstructionBuiltIns.node, [ [Location.node, locationLayer], - [Global.node, Global.layerWith({ config: "/global" })], + [Global.node, Global.layerWith({ config: "/global", tmp: "/temporary" })], ]), ) @@ -49,6 +49,7 @@ describe("InstructionBuiltIns", () => { ` Workspace root folder: ${projectDirectory}`, " Is directory a git repo: yes", ` Platform: ${process.platform}`, + " Use /temporary for temporary work outside the workspace; it already exists and is pre-approved for external directory access.", "", "", `Today's date: ${localDate(timestamp)}`, diff --git a/packages/schema/src/agent.ts b/packages/schema/src/agent.ts index 3bde5a1facb..cdc95bb77e3 100644 --- a/packages/schema/src/agent.ts +++ b/packages/schema/src/agent.ts @@ -46,6 +46,9 @@ export const Info = Schema.Struct({ permissions: [ { action: "*", resource: "*", effect: "allow" }, { action: "external_directory", resource: "*", effect: "ask" }, + { action: "read", resource: "*.env", effect: "ask" }, + { action: "read", resource: "*.env.*", effect: "ask" }, + { action: "read", resource: "*.env.example", effect: "allow" }, ], }) satisfies Info, })), diff --git a/packages/tui/src/mini/tool.ts b/packages/tui/src/mini/tool.ts index f4fc0a3975d..176fb900ab6 100644 --- a/packages/tui/src/mini/tool.ts +++ b/packages/tui/src/mini/tool.ts @@ -125,7 +125,6 @@ type ToolName = | "webfetch" | "websearch" | "skill" - | "plan_exit" type ToolRule = { view: ToolView @@ -516,15 +515,6 @@ function runLsp(p: ToolProps): ToolInline { } } -function runPlanExit(p: ToolProps): ToolInline { - return { - icon: "→", - title: "Switching to build agent", - mode: "block", - body: p.frame.status === "completed" ? p.frame.output : undefined, - } -} - function patchTitle(file: PatchFile, directory?: string): string { if (file.status === "added") { return `# Created ${toolPath(file.file, { directory })}` @@ -1077,16 +1067,6 @@ const TOOL_RULES = { start: scrollSkillStart, }, }, - plan_exit: { - view: { - output: true, - final: false, - }, - run: runPlanExit, - scroll: { - start: () => "", - }, - }, } as const satisfies ToolRegistry function key(name: string): name is ToolName { diff --git a/packages/util/src/global.ts b/packages/util/src/global.ts index 3edbde5ce27..a350db44007 100644 --- a/packages/util/src/global.ts +++ b/packages/util/src/global.ts @@ -13,6 +13,8 @@ const config = path.join(xdgConfig!, app) const state = path.join(xdgState!, app) const tmp = path.join(os.tmpdir(), app) +await fs.mkdir(tmp, { recursive: true }) + const paths = { get home() { return process.env.OPENCODE_TEST_HOME ?? os.homedir() @@ -24,7 +26,7 @@ const paths = { cache, config, state, - tmp, + tmp: await fs.realpath(tmp), } export const Path = paths @@ -35,7 +37,6 @@ await Promise.all([ fs.mkdir(Path.data, { recursive: true }), fs.mkdir(Path.config, { recursive: true }), fs.mkdir(Path.state, { recursive: true }), - fs.mkdir(Path.tmp, { recursive: true }), fs.mkdir(Path.log, { recursive: true }), fs.mkdir(Path.bin, { recursive: true }), fs.mkdir(Path.repos, { recursive: true }), diff --git a/packages/www/content/docs/(Configure)/permissions.mdx b/packages/www/content/docs/(Configure)/permissions.mdx index c9e464be230..0a53548c928 100644 --- a/packages/www/content/docs/(Configure)/permissions.mdx +++ b/packages/www/content/docs/(Configure)/permissions.mdx @@ -81,8 +81,7 @@ current built-in actions use these resources: | `_` | `*` for an MCP tool; unsupported characters in both names become `_` | | `execute` | `*`; controls availability of the Code Mode dispatcher, while each nested tool still enforces its own permission | -Built-in agent policy also reserves `plan_enter` and `plan_exit` for plan-mode -transitions. `doom_loop` and `lsp` are not current V2 Core permission actions. +`doom_loop` and `lsp` are not current V2 Core permission actions. ## External directories @@ -132,7 +131,9 @@ matching, so authorize only trusted directory boundaries. ## Defaults -The evaluator's fallback is `ask`, but shipped agents include ordered defaults: +Every agent, including custom agents, starts with ordered defaults that allow +tools, ask for external directories, ask for `.env` reads, and allow +`.env.example` reads. Shipped agents then add their own policies: | Agent | Effective default policy | | --- | --- | @@ -153,8 +154,12 @@ The base read rules are ordered as follows: ] ``` -OpenCode also permits its managed tool-output and temporary directories where -needed. These exceptions do not grant general external-directory access. +OpenCode also permits its managed tool-output, shell-output, temporary, and +global configuration directories. These exceptions apply only to the +external-directory boundary for every agent; the underlying action still uses +its own permission rules. The environment instructions identify the temporary +directory available for work outside the workspace. Later global and +agent-specific rules can override these defaults. ## Agent overrides