mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 16:56:33 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f299594633 | |||
| d925e34200 |
@@ -26,7 +26,6 @@ export interface Selection {
|
||||
|
||||
type Data = {
|
||||
agents: Map<ID, Types.DeepMutable<Info>>
|
||||
permissions: Types.DeepMutable<Info["permissions"]>
|
||||
default?: ID
|
||||
}
|
||||
|
||||
@@ -34,7 +33,6 @@ export type Draft = {
|
||||
list: () => readonly Info[]
|
||||
get: (id: ID) => Info | undefined
|
||||
default: (id: ID | undefined) => void
|
||||
permissions: (permissions: Info["permissions"]) => void
|
||||
update: (id: ID, fn: (agent: Types.DeepMutable<Info>) => void) => void
|
||||
remove: (id: ID) => void
|
||||
}
|
||||
@@ -55,25 +53,15 @@ const layer = Layer.effect(
|
||||
const bus = yield* Bus.Service
|
||||
const state = State.create<Data, Draft>({
|
||||
name: "agent",
|
||||
initial: () => ({ agents: new Map(), permissions: [] }),
|
||||
initial: () => ({ agents: new Map() }),
|
||||
draft: (draft) => ({
|
||||
list: () => Array.fromIterable(draft.agents.values()) as Info[],
|
||||
get: (id) => draft.agents.get(id),
|
||||
default: (id) => {
|
||||
draft.default = id
|
||||
},
|
||||
permissions: (permissions) => {
|
||||
draft.permissions.push(...permissions)
|
||||
for (const agent of draft.agents.values()) agent.permissions.push(...permissions)
|
||||
},
|
||||
update: (id, fn) => {
|
||||
const defaults = Info.default(id)
|
||||
const current =
|
||||
draft.agents.get(id) ??
|
||||
({
|
||||
...defaults,
|
||||
permissions: [...defaults.permissions, ...draft.permissions],
|
||||
} as Types.DeepMutable<Info>)
|
||||
const current = draft.agents.get(id) ?? (Info.default(id) as Types.DeepMutable<Info>)
|
||||
if (!draft.agents.has(id)) draft.agents.set(id, current)
|
||||
fn(current)
|
||||
current.id = id
|
||||
|
||||
@@ -2,12 +2,15 @@ export * as AgentPlugin from "./agent"
|
||||
|
||||
import path from "path"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Agent } from "../agent"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Location } from "../location"
|
||||
import { Permission } from "../permission"
|
||||
import { Reference } from "../reference"
|
||||
import { Skill } from "../skill"
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -97,39 +100,38 @@ Rules:
|
||||
export const Plugin = define({
|
||||
id: "opencode.agent",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const global = yield* Global.Service
|
||||
const references = yield* Reference.Service
|
||||
const skills = yield* Skill.Service
|
||||
const externalDirectories: { current: Permission.Ruleset } = { current: [] }
|
||||
const refreshExternalDirectories = Effect.fn("AgentPlugin.refreshExternalDirectories")(function* () {
|
||||
const [referenceList, skillSources, skillList] = yield* Effect.all([
|
||||
references.list(),
|
||||
skills.sources(),
|
||||
skills.list(),
|
||||
])
|
||||
externalDirectories.current = Array.from(
|
||||
new Set([
|
||||
path.join(global.data, "shell", "*", "*"),
|
||||
path.join(global.data, "tool-output", "*"),
|
||||
path.join(global.tmp, "*"),
|
||||
path.join(global.config, "*"),
|
||||
...referenceList.map((reference) => path.join(reference.path, "*")),
|
||||
...skillSources.flatMap((source) => (source.type === "directory" ? [path.join(source.path, "*")] : [])),
|
||||
...skillList.map((skill) => path.join(path.dirname(skill.location), "*")),
|
||||
]),
|
||||
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" }),
|
||||
)
|
||||
})
|
||||
|
||||
yield* refreshExternalDirectories()
|
||||
),
|
||||
]
|
||||
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.permissions(externalDirectories.current)
|
||||
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({ action: "question", resource: "*", effect: "allow" })
|
||||
item.permissions.push(
|
||||
...Permission.merge(defaults, [
|
||||
{ action: "question", resource: "*", effect: "allow" },
|
||||
{ action: "plan_enter", resource: "*", effect: "allow" },
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
draft.update(Agent.ID.make("plan"), (item) => {
|
||||
@@ -137,8 +139,18 @@ export const Plugin = define({
|
||||
item.description = "Plan mode. Disallows all edit tools."
|
||||
item.mode = "primary"
|
||||
item.permissions.push(
|
||||
{ action: "question", resource: "*", effect: "allow" },
|
||||
{ action: "edit", resource: "*", effect: "deny" },
|
||||
...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",
|
||||
},
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -147,10 +159,7 @@ 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(
|
||||
{ action: "question", resource: "*", effect: "deny" },
|
||||
{ action: "subagent", resource: "*", effect: "deny" },
|
||||
)
|
||||
item.permissions.push(...Permission.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
|
||||
})
|
||||
|
||||
draft.update(Agent.ID.make("explore"), (item) => {
|
||||
@@ -161,6 +170,7 @@ export const Plugin = define({
|
||||
item.mode = "subagent"
|
||||
item.permissions.push(
|
||||
...Permission.merge(
|
||||
defaults,
|
||||
[
|
||||
{ action: "*", resource: "*", effect: "deny" },
|
||||
{ action: "grep", resource: "*", effect: "allow" },
|
||||
@@ -170,7 +180,7 @@ export const Plugin = define({
|
||||
{ action: "read", resource: "*", effect: "allow" },
|
||||
{ action: "subagent", resource: "*", effect: "deny" },
|
||||
],
|
||||
[{ action: "external_directory", resource: "*", effect: "ask" }, ...externalDirectories.current],
|
||||
readonlyExternalDirectory,
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -180,7 +190,7 @@ export const Plugin = define({
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_COMPACTION
|
||||
item.permissions.push({ action: "*", resource: "*", effect: "deny" })
|
||||
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
||||
})
|
||||
|
||||
draft.update(Agent.ID.make("title"), (item) => {
|
||||
@@ -188,7 +198,7 @@ export const Plugin = define({
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_TITLE
|
||||
item.permissions.push({ action: "*", resource: "*", effect: "deny" })
|
||||
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
||||
})
|
||||
|
||||
draft.update(Agent.ID.make("summary"), (item) => {
|
||||
@@ -196,13 +206,8 @@ export const Plugin = define({
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_SUMMARY
|
||||
item.permissions.push({ action: "*", resource: "*", effect: "deny" })
|
||||
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
||||
})
|
||||
})
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "reference.updated" || event.type === "skill.updated"),
|
||||
Stream.runForEach(() => refreshExternalDirectories().pipe(Effect.andThen(ctx.agent.reload()))),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -96,7 +96,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../p
|
||||
list: () => mutable(draft.list()),
|
||||
get: (id) => mutable(draft.get(Agent.ID.make(id))),
|
||||
default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)),
|
||||
permissions: draft.permissions,
|
||||
update: (id, update) => draft.update(Agent.ID.make(id), update),
|
||||
remove: (id) => draft.remove(Agent.ID.make(id)),
|
||||
})
|
||||
|
||||
@@ -159,9 +159,9 @@ const pre = [
|
||||
|
||||
const post = [
|
||||
ConfigReferencePlugin.Plugin,
|
||||
ConfigSkillPlugin.Plugin,
|
||||
ConfigAgentPlugin.Plugin,
|
||||
ConfigCommandPlugin.Plugin,
|
||||
ConfigSkillPlugin.Plugin,
|
||||
ConfigProviderPlugin.Plugin,
|
||||
ConfigWebSearchPlugin.Plugin,
|
||||
VariantPlugin.Plugin,
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { Content } from "@opencode-ai/schema/tool"
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Deferred, Effect, Schema, Scope } from "effect"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { LocationMutation } from "../../location-mutation"
|
||||
import { Permission } from "../../permission"
|
||||
import { PluginRuntime } from "../../plugin/runtime"
|
||||
@@ -36,7 +35,6 @@ const description = (shell?: string) =>
|
||||
...(shell ? [`Commands run on ${OS} using ${shell}.`] : []),
|
||||
"Quote file paths containing spaces or special characters.",
|
||||
"Prefer dedicated tools over shell commands when possible.",
|
||||
`Use \`${Global.Path.tmp}\` for temporary work outside the workspace. It already exists and is pre-approved for external-directory access.`,
|
||||
"When output is large, the full result is saved to a file and a truncated preview is returned.",
|
||||
"Rely on automatic truncation unless filtering the output is more useful.",
|
||||
"Commands accept an optional timeout, background commands have no timeout by default.",
|
||||
|
||||
@@ -8,36 +8,13 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Permission } from "@opencode-ai/core/permission"
|
||||
import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Skill } from "@opencode-ai/core/skill"
|
||||
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 referencePath = AbsolutePath.make("/references/docs")
|
||||
const skillPath = AbsolutePath.make("/skills/team")
|
||||
const references = Reference.Service.of({
|
||||
list: () =>
|
||||
Effect.succeed([
|
||||
Reference.Info.make({
|
||||
name: "docs",
|
||||
path: referencePath,
|
||||
source: Reference.LocalSource.make({ type: "local", path: referencePath }),
|
||||
}),
|
||||
]),
|
||||
transform: () => Effect.succeed({ dispose: Effect.void }),
|
||||
reload: () => Effect.void,
|
||||
})
|
||||
const skills = Skill.Service.of({
|
||||
sources: () => Effect.succeed([Skill.DirectorySource.make({ type: "directory", path: skillPath })]),
|
||||
list: () => Effect.succeed([]),
|
||||
transform: () => Effect.succeed({ dispose: Effect.void }),
|
||||
reload: () => Effect.void,
|
||||
})
|
||||
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, Location.node]), [
|
||||
@@ -150,24 +127,6 @@ describe("Agent", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies runtime permissions to existing and future agents", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* Agent.Service
|
||||
const existing = Agent.ID.make("existing")
|
||||
const future = Agent.ID.make("future")
|
||||
const permission = { action: "external_directory", resource: "/tmp/*", effect: "allow" } as const
|
||||
|
||||
yield* agent.transform((draft) => {
|
||||
draft.update(existing, () => {})
|
||||
draft.permissions([permission])
|
||||
draft.update(future, () => {})
|
||||
})
|
||||
|
||||
expect((yield* agent.get(existing))?.permissions).toContainEqual(permission)
|
||||
expect((yield* agent.get(future))?.permissions).toContainEqual(permission)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not ambiently opt built-in agents into bash", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* Agent.Service
|
||||
@@ -176,9 +135,6 @@ describe("Agent", () => {
|
||||
agent: agentHost(agent),
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provideService(Global.Service, Global.Service.of(Global.make())),
|
||||
Effect.provideService(Reference.Service, references),
|
||||
Effect.provideService(Skill.Service, skills),
|
||||
Effect.provideService(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
||||
@@ -196,12 +152,6 @@ describe("Agent", () => {
|
||||
"title",
|
||||
])
|
||||
expect((yield* agent.get(Agent.defaultID))?.system).toBeUndefined()
|
||||
const permissions = (yield* agent.get(Agent.defaultID))?.permissions ?? []
|
||||
expect(Permission.evaluate("external_directory", "/references/docs/*", permissions).effect).toBe("allow")
|
||||
expect(Permission.evaluate("external_directory", "/skills/team/*", permissions).effect).toBe("allow")
|
||||
expect(
|
||||
Permission.evaluate("external_directory", `${Global.Path.config}/*`, permissions).effect,
|
||||
).toBe("allow")
|
||||
for (const item of agents) {
|
||||
expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false)
|
||||
}
|
||||
@@ -216,9 +166,6 @@ describe("Agent", () => {
|
||||
agent: agentHost(agent),
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provideService(Global.Service, Global.Service.of(Global.make())),
|
||||
Effect.provideService(Reference.Service, references),
|
||||
Effect.provideService(Skill.Service, skills),
|
||||
Effect.provideService(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
||||
|
||||
@@ -23,9 +23,6 @@ const decode = Schema.decodeUnknownSync(Config.Info)
|
||||
const defaultPermissions = [
|
||||
{ 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 Permission.Ruleset
|
||||
|
||||
test("rejects named agent color tokens", () => {
|
||||
|
||||
@@ -38,10 +38,10 @@ describe("config plugin reloads", () => {
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
const test = yield* Config.Test
|
||||
|
||||
yield* ConfigReferencePlugin.Plugin.effect(host)
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host)
|
||||
yield* ConfigCommandPlugin.Plugin.effect(host)
|
||||
yield* ConfigSkillPlugin.Plugin.effect(host)
|
||||
yield* ConfigReferencePlugin.Plugin.effect(host)
|
||||
yield* ConfigProviderPlugin.Plugin.effect(host)
|
||||
|
||||
expect((yield* agents.get(Agent.ID.make("first")))?.description).toBe("First agent")
|
||||
|
||||
@@ -145,7 +145,6 @@ export function agentHost(agent: Agent.Interface): Plugin.Context["agent"] {
|
||||
return value && agentInfo(value)
|
||||
},
|
||||
default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)),
|
||||
permissions: draft.permissions,
|
||||
update: (id, update) =>
|
||||
draft.update(Agent.ID.make(id), (value) => {
|
||||
const current = agentInfo(value)
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface AgentDraft {
|
||||
list(): readonly Types.DeepMutable<Agent.Info>[]
|
||||
get(id: string): Types.DeepMutable<Agent.Info> | undefined
|
||||
default(id: string | undefined): void
|
||||
permissions(permissions: Agent.Info["permissions"]): void
|
||||
update(id: string, update: (agent: Types.DeepMutable<Agent.Info>) => void): void
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface AgentDraft {
|
||||
list(): readonly DeepMutable<Agent.Info>[]
|
||||
get(id: string): DeepMutable<Agent.Info> | undefined
|
||||
default(id: string | undefined): void
|
||||
permissions(permissions: Agent.Info["permissions"]): void
|
||||
update(id: string, update: (agent: DeepMutable<Agent.Info>) => void): void
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
@@ -46,9 +46,6 @@ 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,
|
||||
})),
|
||||
|
||||
@@ -125,6 +125,7 @@ type ToolName =
|
||||
| "webfetch"
|
||||
| "websearch"
|
||||
| "skill"
|
||||
| "plan_exit"
|
||||
|
||||
type ToolRule = {
|
||||
view: ToolView
|
||||
@@ -515,6 +516,15 @@ 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 })}`
|
||||
@@ -1067,6 +1077,16 @@ 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 {
|
||||
|
||||
@@ -81,7 +81,8 @@ 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 |
|
||||
|
||||
`doom_loop` and `lsp` are not current V2 Core permission actions.
|
||||
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.
|
||||
|
||||
## External directories
|
||||
|
||||
@@ -131,9 +132,7 @@ matching, so authorize only trusted directory boundaries.
|
||||
|
||||
## 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:
|
||||
The evaluator's fallback is `ask`, but shipped agents include ordered defaults:
|
||||
|
||||
| Agent | Effective default policy |
|
||||
| --- | --- |
|
||||
@@ -154,11 +153,8 @@ The base read rules are ordered as follows:
|
||||
]
|
||||
```
|
||||
|
||||
OpenCode also permits its managed tool-output, shell-output, temporary, global
|
||||
configuration, configured reference, and discovered skill directories. These
|
||||
exceptions apply only to the external-directory boundary for every agent; the
|
||||
underlying action still uses its own permission rules. Later global and
|
||||
agent-specific rules can override them.
|
||||
OpenCode also permits its managed tool-output and temporary directories where
|
||||
needed. These exceptions do not grant general external-directory access.
|
||||
|
||||
## Agent overrides
|
||||
|
||||
|
||||
Reference in New Issue
Block a user