From 6eccffa59cd068c81d0cf4d8f8160c23b05f7811 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Tue, 17 Mar 2026 12:56:30 +0100 Subject: [PATCH 1/6] feat(opencode): Inherit top level permissions when using legacy tools setting of agents --- packages/opencode/src/config/config.ts | 62 ++++-- packages/opencode/src/installation/index.ts | 4 +- packages/opencode/test/agent/agent.test.ts | 131 ++++++++++++ packages/opencode/test/config/config.test.ts | 212 +++++++++++++++++++ packages/web/src/content/docs/agents.mdx | 8 +- 5 files changed, 399 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 47afdfd7d0f1..c0d845d3f95c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -75,6 +75,42 @@ export namespace Config { return merged } + function permissionRule(tool: string, rootPermissions: Permission | undefined) { + if (rootPermissions?.[tool] !== undefined) return rootPermissions[tool] + if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") return rootPermissions?.edit // Edit tool represents all file modification tools + return undefined + } + + function inheritPermission(rule: PermissionRule | undefined): PermissionRule { + if (!rule) return "allow" + if (typeof rule === "string") return rule + if ("*" in rule) return rule + return { + "*": "allow", + ...rule, + } + } + + function resolveAgentToolPermissions(agent: Agent, rootPermissions: Permission | undefined) { + if (!agent.tools) return agent + + const permission: Permission = { ...(agent.permission ?? {}) } + for (const [tool, enabled] of Object.entries(agent.tools)) { + const key = tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit" ? "edit" : tool + if (permission[key] !== undefined) continue + if (!enabled) { + permission[key] = "deny" // Normally with the legacy tool option when its false its equvalent tot he deny permission + continue + } + permission[key] = inheritPermission(permissionRule(tool, rootPermissions)) + } + + return { + ...agent, + permission, + } + } + export const state = Instance.state(async () => { const auth = await Auth.all() @@ -241,6 +277,12 @@ export namespace Config { result.permission = mergeDeep(perms, result.permission ?? {}) } + const agents = result.agent ?? {} + for (const [name, agent] of Object.entries(agents)) { + agents[name] = resolveAgentToolPermissions(agent, result.permission) + } + result.agent = agents + if (!result.username) result.username = os.userInfo().username // Handle migration from autoshare to share field @@ -771,23 +813,15 @@ export namespace Config { if (!knownKeys.has(key)) options[key] = value } - // Convert legacy tools config to permissions - const permission: Permission = {} - for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { - const action = enabled ? "allow" : "deny" - // write, edit, patch, multiedit all map to edit permission - if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { - permission.edit = action - } else { - permission[tool] = action - } - } - Object.assign(permission, agent.permission) - // Convert legacy maxSteps to steps const steps = agent.steps ?? agent.maxSteps - return { ...agent, options, permission, steps } as typeof agent & { + return { + ...agent, + options, + permission: agent.permission ?? {}, + steps, + } as typeof agent & { options?: Record permission?: Permission steps?: number diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 92a3bfc79613..270f845dac2e 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -87,7 +87,7 @@ export namespace Installation { } export function isLocal() { - return CHANNEL === "local" + return CHANNEL === "local" || (VERSION.startsWith("0.0.0-") && !VERSION.startsWith("0.0.0-beta")) // Prevents all other versions exepct beta from being installed } export async function method() { @@ -162,7 +162,7 @@ export namespace Installation { } export async function upgrade(method: Method, target: string) { - let result: Awaited> | undefined + let result: Awaited | Process.Result> | undefined switch (method) { case "curl": result = await upgradeCurl(target) diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 497b6019d3ec..a15fd2201588 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -462,6 +462,137 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a }) }) +test("legacy agent tools keep top-level tool restrictions", async () => { + await using tmp = await tmpdir({ + config: { + permission: { + bash: { + "git *": "deny", + }, + }, + agent: { + build: { + tools: { + bash: true, + }, + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const build = await Agent.get("build") + expect(PermissionNext.evaluate("bash", "ls", build!.permission).action).toBe("allow") + expect(PermissionNext.evaluate("bash", "git status", build!.permission).action).toBe("deny") + }, + }) +}) + +test("explicit agent permission overrides inherited legacy tool restrictions", async () => { + await using tmp = await tmpdir({ + config: { + permission: { + bash: { + "git *": "deny", + }, + }, + agent: { + build: { + permission: { + bash: "allow", + }, + tools: { + bash: true, + }, + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const build = await Agent.get("build") + expect(PermissionNext.evaluate("bash", "git status", build!.permission).action).toBe("allow") + }, + }) +}) + +test("legacy agent tools preserve inherited ask action at runtime", async () => { + await using tmp = await tmpdir({ + config: { + permission: { + question: "ask", + }, + agent: { + review: { + tools: { + question: true, + }, + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const review = await Agent.get("review") + expect(evalPerm(review, "question")).toBe("ask") + }, + }) +}) + +test("legacy agent tools preserve inherited ask object rules at runtime", async () => { + await using tmp = await tmpdir({ + config: { + permission: { + external_directory: { + "*": "ask", + "/safe/*": "allow", + }, + }, + agent: { + review: { + tools: { + external_directory: true, + }, + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const review = await Agent.get("review") + expect(PermissionNext.evaluate("external_directory", "/other/path", review!.permission).action).toBe("ask") + expect(PermissionNext.evaluate("external_directory", "/safe/project", review!.permission).action).toBe("allow") + }, + }) +}) + +test("legacy agent tools materialize allow when no root permission exists", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + review: { + tools: { + external_directory: true, + doom_loop: true, + }, + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const review = await Agent.get("review") + expect(PermissionNext.evaluate("external_directory", "/other/path", review!.permission).action).toBe("allow") + expect(evalPerm(review, "doom_loop")).toBe("allow") + }, + }) +}) + test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => { const { Truncate } = await import("../../src/tool/truncation") await using tmp = await tmpdir({ diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index baf209d86079..ba7cc59c7c1d 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1386,6 +1386,218 @@ test("merges legacy tools with existing permission config", async () => { }) }) +test("legacy agent tools inherit top-level tool rules", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + permission: { + bash: { + "git *": "deny", + }, + }, + agent: { + test: { + tools: { + bash: true, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test"]?.permission).toEqual({ + bash: { + "*": "allow", + "git *": "deny", + }, + }) + }, + }) +}) + +test("explicit agent permission overrides inherited legacy tool rules", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + permission: { + bash: { + "git *": "deny", + }, + }, + agent: { + test: { + permission: { + bash: "allow", + }, + tools: { + bash: true, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test"]?.permission).toEqual({ + bash: "allow", + }) + }, + }) +}) + +test("legacy agent tools inherit matching custom tool rules", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + permission: { + deploy: { + production: "deny", + }, + }, + agent: { + test: { + tools: { + deploy: true, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test"]?.permission).toEqual({ + deploy: { + "*": "allow", + production: "deny", + }, + }) + }, + }) +}) + +test("legacy agent tools preserve inherited ask permissions", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + permission: { + question: "ask", + }, + agent: { + test: { + tools: { + question: true, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test"]?.permission).toEqual({ + question: "ask", + }) + }, + }) +}) + +test("legacy agent tools preserve inherited ask object permissions", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + permission: { + external_directory: { + "*": "ask", + "/safe/*": "allow", + }, + }, + agent: { + test: { + tools: { + external_directory: true, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test"]?.permission).toEqual({ + external_directory: { + "*": "ask", + "/safe/*": "allow", + }, + }) + }, + }) +}) + +test("legacy agent tools materialize allow without root permission", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + agent: { + test: { + tools: { + external_directory: true, + doom_loop: true, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test"]?.permission).toEqual({ + doom_loop: "allow", + external_directory: "allow", + }) + }, + }) +}) + test("permission config preserves key order", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 876464212487..826b8df0348f 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -370,6 +370,10 @@ The model ID in your OpenCode config uses the format `provider/model-id`. For ex Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`. +This is a legacy shorthand for simple on/off access. In an agent's `tools` config, `true` behaves like `allow` and `false` behaves like `deny` for that tool. + +For more fine-grained control, use the agent's [`permission`](#permissions) field instead. That supports `ask` and tool-specific rules such as bash command patterns. If a matching top-level `permission` rule exists, it is inherited by legacy agent `tools` entries. + ```json title="opencode.json" {3-6,9-12} { "$schema": "https://opencode.ai/config.json", @@ -389,10 +393,10 @@ Control which tools are available in this agent with the `tools` config. You can ``` :::note -The agent-specific config overrides the global config. +Legacy agent `tools` entries inherit matching top-level `permission` rules. Use agent-level `permission` to override inherited behavior with more specific rules. ::: -You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server: +You can also use wildcards in legacy `tools` entries to control multiple tools at once. For example, to disable all tools from an MCP server: ```json title="opencode.json" { From 6be28719dd6b8ca4ffe520680ba5ebd543afae62 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Tue, 17 Mar 2026 13:03:55 +0100 Subject: [PATCH 2/6] chore: removed installation file changes --- packages/opencode/src/installation/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 270f845dac2e..92a3bfc79613 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -87,7 +87,7 @@ export namespace Installation { } export function isLocal() { - return CHANNEL === "local" || (VERSION.startsWith("0.0.0-") && !VERSION.startsWith("0.0.0-beta")) // Prevents all other versions exepct beta from being installed + return CHANNEL === "local" } export async function method() { @@ -162,7 +162,7 @@ export namespace Installation { } export async function upgrade(method: Method, target: string) { - let result: Awaited | Process.Result> | undefined + let result: Awaited> | undefined switch (method) { case "curl": result = await upgradeCurl(target) From 6204413009efa868a289284f4dde5b83a72e545f Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Tue, 17 Mar 2026 13:13:47 +0100 Subject: [PATCH 3/6] chore: add comment explaining use of typeof string --- packages/opencode/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index c0d845d3f95c..73e92a726953 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -83,7 +83,7 @@ export namespace Config { function inheritPermission(rule: PermissionRule | undefined): PermissionRule { if (!rule) return "allow" - if (typeof rule === "string") return rule + if (typeof rule === "string") return rule // "ask" | "allow" | "deny" if ("*" in rule) return rule return { "*": "allow", From 4cc37f3e6d5ad18e0f0c84b12ae09ea2e2172765 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Tue, 17 Mar 2026 15:29:07 +0100 Subject: [PATCH 4/6] reverted update to config inheritance --- packages/opencode/src/config/config.ts | 62 ++---- packages/opencode/test/agent/agent.test.ts | 131 ------------ packages/opencode/test/config/config.test.ts | 212 ------------------- 3 files changed, 14 insertions(+), 391 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 73e92a726953..47afdfd7d0f1 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -75,42 +75,6 @@ export namespace Config { return merged } - function permissionRule(tool: string, rootPermissions: Permission | undefined) { - if (rootPermissions?.[tool] !== undefined) return rootPermissions[tool] - if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") return rootPermissions?.edit // Edit tool represents all file modification tools - return undefined - } - - function inheritPermission(rule: PermissionRule | undefined): PermissionRule { - if (!rule) return "allow" - if (typeof rule === "string") return rule // "ask" | "allow" | "deny" - if ("*" in rule) return rule - return { - "*": "allow", - ...rule, - } - } - - function resolveAgentToolPermissions(agent: Agent, rootPermissions: Permission | undefined) { - if (!agent.tools) return agent - - const permission: Permission = { ...(agent.permission ?? {}) } - for (const [tool, enabled] of Object.entries(agent.tools)) { - const key = tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit" ? "edit" : tool - if (permission[key] !== undefined) continue - if (!enabled) { - permission[key] = "deny" // Normally with the legacy tool option when its false its equvalent tot he deny permission - continue - } - permission[key] = inheritPermission(permissionRule(tool, rootPermissions)) - } - - return { - ...agent, - permission, - } - } - export const state = Instance.state(async () => { const auth = await Auth.all() @@ -277,12 +241,6 @@ export namespace Config { result.permission = mergeDeep(perms, result.permission ?? {}) } - const agents = result.agent ?? {} - for (const [name, agent] of Object.entries(agents)) { - agents[name] = resolveAgentToolPermissions(agent, result.permission) - } - result.agent = agents - if (!result.username) result.username = os.userInfo().username // Handle migration from autoshare to share field @@ -813,15 +771,23 @@ export namespace Config { if (!knownKeys.has(key)) options[key] = value } + // Convert legacy tools config to permissions + const permission: Permission = {} + for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { + const action = enabled ? "allow" : "deny" + // write, edit, patch, multiedit all map to edit permission + if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { + permission.edit = action + } else { + permission[tool] = action + } + } + Object.assign(permission, agent.permission) + // Convert legacy maxSteps to steps const steps = agent.steps ?? agent.maxSteps - return { - ...agent, - options, - permission: agent.permission ?? {}, - steps, - } as typeof agent & { + return { ...agent, options, permission, steps } as typeof agent & { options?: Record permission?: Permission steps?: number diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index a15fd2201588..497b6019d3ec 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -462,137 +462,6 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a }) }) -test("legacy agent tools keep top-level tool restrictions", async () => { - await using tmp = await tmpdir({ - config: { - permission: { - bash: { - "git *": "deny", - }, - }, - agent: { - build: { - tools: { - bash: true, - }, - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const build = await Agent.get("build") - expect(PermissionNext.evaluate("bash", "ls", build!.permission).action).toBe("allow") - expect(PermissionNext.evaluate("bash", "git status", build!.permission).action).toBe("deny") - }, - }) -}) - -test("explicit agent permission overrides inherited legacy tool restrictions", async () => { - await using tmp = await tmpdir({ - config: { - permission: { - bash: { - "git *": "deny", - }, - }, - agent: { - build: { - permission: { - bash: "allow", - }, - tools: { - bash: true, - }, - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const build = await Agent.get("build") - expect(PermissionNext.evaluate("bash", "git status", build!.permission).action).toBe("allow") - }, - }) -}) - -test("legacy agent tools preserve inherited ask action at runtime", async () => { - await using tmp = await tmpdir({ - config: { - permission: { - question: "ask", - }, - agent: { - review: { - tools: { - question: true, - }, - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const review = await Agent.get("review") - expect(evalPerm(review, "question")).toBe("ask") - }, - }) -}) - -test("legacy agent tools preserve inherited ask object rules at runtime", async () => { - await using tmp = await tmpdir({ - config: { - permission: { - external_directory: { - "*": "ask", - "/safe/*": "allow", - }, - }, - agent: { - review: { - tools: { - external_directory: true, - }, - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const review = await Agent.get("review") - expect(PermissionNext.evaluate("external_directory", "/other/path", review!.permission).action).toBe("ask") - expect(PermissionNext.evaluate("external_directory", "/safe/project", review!.permission).action).toBe("allow") - }, - }) -}) - -test("legacy agent tools materialize allow when no root permission exists", async () => { - await using tmp = await tmpdir({ - config: { - agent: { - review: { - tools: { - external_directory: true, - doom_loop: true, - }, - }, - }, - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const review = await Agent.get("review") - expect(PermissionNext.evaluate("external_directory", "/other/path", review!.permission).action).toBe("allow") - expect(evalPerm(review, "doom_loop")).toBe("allow") - }, - }) -}) - test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => { const { Truncate } = await import("../../src/tool/truncation") await using tmp = await tmpdir({ diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index ba7cc59c7c1d..baf209d86079 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1386,218 +1386,6 @@ test("merges legacy tools with existing permission config", async () => { }) }) -test("legacy agent tools inherit top-level tool rules", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Filesystem.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - permission: { - bash: { - "git *": "deny", - }, - }, - agent: { - test: { - tools: { - bash: true, - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test"]?.permission).toEqual({ - bash: { - "*": "allow", - "git *": "deny", - }, - }) - }, - }) -}) - -test("explicit agent permission overrides inherited legacy tool rules", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Filesystem.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - permission: { - bash: { - "git *": "deny", - }, - }, - agent: { - test: { - permission: { - bash: "allow", - }, - tools: { - bash: true, - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test"]?.permission).toEqual({ - bash: "allow", - }) - }, - }) -}) - -test("legacy agent tools inherit matching custom tool rules", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Filesystem.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - permission: { - deploy: { - production: "deny", - }, - }, - agent: { - test: { - tools: { - deploy: true, - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test"]?.permission).toEqual({ - deploy: { - "*": "allow", - production: "deny", - }, - }) - }, - }) -}) - -test("legacy agent tools preserve inherited ask permissions", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Filesystem.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - permission: { - question: "ask", - }, - agent: { - test: { - tools: { - question: true, - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test"]?.permission).toEqual({ - question: "ask", - }) - }, - }) -}) - -test("legacy agent tools preserve inherited ask object permissions", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Filesystem.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - permission: { - external_directory: { - "*": "ask", - "/safe/*": "allow", - }, - }, - agent: { - test: { - tools: { - external_directory: true, - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test"]?.permission).toEqual({ - external_directory: { - "*": "ask", - "/safe/*": "allow", - }, - }) - }, - }) -}) - -test("legacy agent tools materialize allow without root permission", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Filesystem.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - agent: { - test: { - tools: { - external_directory: true, - doom_loop: true, - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test"]?.permission).toEqual({ - doom_loop: "allow", - external_directory: "allow", - }) - }, - }) -}) - test("permission config preserves key order", async () => { await using tmp = await tmpdir({ init: async (dir) => { From 1b00b6692f7089f7b5e381829b31fc66e553c312 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Tue, 17 Mar 2026 15:42:42 +0100 Subject: [PATCH 5/6] docs(opencode): deprecate `tools` config in favor of `permission` field for better control --- packages/web/src/content/docs/agents.mdx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 826b8df0348f..b5603294e397 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -368,11 +368,9 @@ The model ID in your OpenCode config uses the format `provider/model-id`. For ex ### Tools -Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`. +`tools` is **deprecated**. Prefer the agent's [`permission`](#permissions) field for new configs, updates and more fine-grained control. -This is a legacy shorthand for simple on/off access. In an agent's `tools` config, `true` behaves like `allow` and `false` behaves like `deny` for that tool. - -For more fine-grained control, use the agent's [`permission`](#permissions) field instead. That supports `ask` and tool-specific rules such as bash command patterns. If a matching top-level `permission` rule exists, it is inherited by legacy agent `tools` entries. +Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`. In an agent's `tools` config, `true` is equivalent to `{"*": "allow"}` and `false` is equivalent to `{"*": "deny"}`. ```json title="opencode.json" {3-6,9-12} { @@ -393,7 +391,7 @@ For more fine-grained control, use the agent's [`permission`](#permissions) fiel ``` :::note -Legacy agent `tools` entries inherit matching top-level `permission` rules. Use agent-level `permission` to override inherited behavior with more specific rules. +The agent-specific config overrides the global config. ::: You can also use wildcards in legacy `tools` entries to control multiple tools at once. For example, to disable all tools from an MCP server: From 666882cab01a1372bfcc7517ff9b2c297f04118d Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Tue, 17 Mar 2026 16:05:10 +0100 Subject: [PATCH 6/6] docs(agents): update `tools` section to indicate deprecation and clarify usage --- packages/web/src/content/docs/agents.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index b5603294e397..5522f77aae61 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -366,11 +366,11 @@ The model ID in your OpenCode config uses the format `provider/model-id`. For ex --- -### Tools +### Tools (deprecated) `tools` is **deprecated**. Prefer the agent's [`permission`](#permissions) field for new configs, updates and more fine-grained control. -Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`. In an agent's `tools` config, `true` is equivalent to `{"*": "allow"}` and `false` is equivalent to `{"*": "deny"}`. +Allows you to control which tools are available in this agent. You can enable or disable specific tools by setting them to `true` or `false`. In an agent's `tools` config, `true` is equivalent to `{"*": "allow"}` permission and `false` is equivalent to `{"*": "deny"}` permission. ```json title="opencode.json" {3-6,9-12} {