diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 020e626cba8..3e33c59c5b0 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1079,6 +1079,7 @@ export namespace Config { .array(z.string()) .optional() .describe("Tools that should only be available to primary agents."), + subagent_tools: z.array(z.string()).optional().describe("Tools that should only be available to subagents."), continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"), mcp_timeout: z .number() diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 8554b44a727..5331829c08b 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -654,6 +654,8 @@ export namespace SessionPrompt { using _ = log.time("resolveTools") const tools: Record = {} + const ruleset = PermissionNext.merge(input.agent.permission, input.session.permission ?? []) + const context = (args: any, options: ToolCallOptions): Tool.Context => ({ sessionID: input.session.id, abort: options.abortSignal!, @@ -688,10 +690,15 @@ export namespace SessionPrompt { }, }) - for (const item of await ToolRegistry.tools( + const items = await ToolRegistry.tools( { modelID: input.model.api.id, providerID: input.model.providerID }, input.agent, - )) { + ) + const mcpTools = await MCP.tools() + const denied = PermissionNext.disabled([...items.map((x) => x.id), ...Object.keys(mcpTools)], ruleset) + + for (const item of items) { + if (denied.has(item.id)) continue const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters)) tools[item.id] = tool({ id: item.id as any, @@ -725,7 +732,8 @@ export namespace SessionPrompt { }) } - for (const [key, item] of Object.entries(await MCP.tools())) { + for (const [key, item] of Object.entries(mcpTools)) { + if (denied.has(key)) continue const execute = item.execute if (!execute) continue diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index faa5f72bcce..49290ab0a83 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -129,6 +129,10 @@ export namespace ToolRegistry { agent?: Agent.Info, ) { const tools = await all() + const config = await Config.get() + const primaryTools = new Set(config.experimental?.primary_tools ?? []) + const subagentTools = new Set(config.experimental?.subagent_tools ?? []) + const result = await Promise.all( tools .filter((t) => { @@ -143,6 +147,9 @@ export namespace ToolRegistry { if (t.id === "apply_patch") return usePatch if (t.id === "edit" || t.id === "write") return !usePatch + if (agent?.mode === "subagent" && primaryTools.has(t.id)) return false + if (agent?.mode === "primary" && subagentTools.has(t.id)) return false + return true }) .map(async (t) => { diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index c87add638aa..72dedaf9ed6 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -9,8 +9,8 @@ import { Agent } from "../agent/agent" import { SessionPrompt } from "../session/prompt" import { iife } from "@/util/iife" import { defer } from "@/util/defer" -import { Config } from "../config/config" import { PermissionNext } from "@/permission/next" +import { Config } from "../config/config" const parameters = z.object({ description: z.string().describe("A short (3-5 words) description of the task"), @@ -39,8 +39,6 @@ export const TaskTool = Tool.define("task", async (ctx) => { description, parameters, async execute(params: z.infer, ctx) { - const config = await Config.get() - // Skip permission check when user explicitly invoked via @ or command subtask if (!ctx.extra?.bypassAgentCheck) { await ctx.ask({ @@ -58,42 +56,51 @@ export const TaskTool = Tool.define("task", async (ctx) => { if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) const hasTaskPermission = agent.permission.some((rule) => rule.permission === "task") + const config = await Config.get() + const primaryTools = config.experimental?.primary_tools ?? [] + + const permissions = [ + { + permission: "todowrite", + pattern: "*", + action: "deny" as const, + }, + { + permission: "todoread", + pattern: "*", + action: "deny" as const, + }, + ...(hasTaskPermission + ? [] + : [ + { + permission: "task" as const, + pattern: "*" as const, + action: "deny" as const, + }, + ]), + ...primaryTools.map((t) => ({ + permission: t, + pattern: "*" as const, + action: "deny" as const, + })), + ] const session = await iife(async () => { if (params.session_id) { const found = await Session.get(params.session_id).catch(() => {}) - if (found) return found + if (found) { + await Session.update(found.id, (draft) => { + draft.permission = PermissionNext.merge(draft.permission ?? [], permissions) + }) + return found + } } return await Session.create({ parentID: ctx.sessionID, title: params.description + ` (@${agent.name} subagent)`, - permission: [ - { - permission: "todowrite", - pattern: "*", - action: "deny", - }, - { - permission: "todoread", - pattern: "*", - action: "deny", - }, - ...(hasTaskPermission - ? [] - : [ - { - permission: "task" as const, - pattern: "*" as const, - action: "deny" as const, - }, - ]), - ...(config.experimental?.primary_tools?.map((t) => ({ - pattern: "*", - action: "allow" as const, - permission: t, - })) ?? []), - ], + permission: permissions, }) }) const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }) @@ -152,12 +159,6 @@ export const TaskTool = Tool.define("task", async (ctx) => { providerID: model.providerID, }, agent: agent.name, - tools: { - todowrite: false, - todoread: false, - ...(hasTaskPermission ? {} : { task: false }), - ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), - }, parts: promptParts, }) unsub() diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9258bc0cde6..8bdb2440e9b 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1815,6 +1815,10 @@ export type Config = { * Tools that should only be available to primary agents. */ primary_tools?: Array + /** + * Tools that should only be available to subagents. + */ + subagent_tools?: Array /** * Continue the agent loop when a tool call is denied */