Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 11 additions & 3 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,8 @@ export namespace SessionPrompt {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}

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!,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand Down
73 changes: 37 additions & 36 deletions packages/opencode/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -39,8 +39,6 @@ export const TaskTool = Tool.define("task", async (ctx) => {
description,
parameters,
async execute(params: z.infer<typeof parameters>, ctx) {
const config = await Config.get()

// Skip permission check when user explicitly invoked via @ or command subtask
if (!ctx.extra?.bypassAgentCheck) {
await ctx.ask({
Expand All @@ -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 })
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,10 @@ export type Config = {
* Tools that should only be available to primary agents.
*/
primary_tools?: Array<string>
/**
* Tools that should only be available to subagents.
*/
subagent_tools?: Array<string>
/**
* Continue the agent loop when a tool call is denied
*/
Expand Down