Ver Fonte

fix(core): apply safe defaults to all agents (#40316)

Aiden Cline há 5 dias atrás
pai
commit
a4ad17347f

+ 20 - 2
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<Data, Draft>({
       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<Info>)
+          const defaults = Info.default(id)
+          const current =
+            draft.agents.get(id) ??
+            ({
+              ...defaults,
+              permissions: [...defaults.permissions, ...permissions],
+            } as Types.DeepMutable<Info>)
           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] })

+ 4 - 1
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<Service, Interface>()("@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.`,
                   "</env>",
                 ].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] })

+ 17 - 52
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. `<data>/shell/<projectID>/<shellID>.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" })
       })
     })
   }),

+ 36 - 12
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<unknown, never>,
 )
@@ -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) =>

+ 14 - 9
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()

+ 2 - 2
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)
   })
 

+ 2 - 1
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.",
           "</env>",
           "",
           `Today's date: ${localDate(timestamp)}`,

+ 3 - 0
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,
     })),

+ 0 - 20
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 {

+ 3 - 2
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 }),

+ 10 - 5
packages/www/content/docs/(Configure)/permissions.mdx

@@ -81,8 +81,7 @@ current built-in actions use these resources:
 | `<server>_<tool>` | `*` 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