Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,8 @@ export namespace Config {
// Convert legacy maxSteps to steps
const steps = agent.steps ?? agent.maxSteps

return { ...agent, options, permission, steps } as typeof agent & {
const { tools, maxSteps, ...rest } = agent
return { ...rest, options, permission, steps } as typeof agent & {
options?: Record<string, unknown>
permission?: Permission
steps?: number
Expand Down
17 changes: 16 additions & 1 deletion packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,29 @@ export namespace Session {
directory: string
permission?: PermissionNext.Ruleset
}) {
// Inherit permissions from parent session if parentID is provided
let mergedPermissions = input.permission
if (input.parentID) {
try {
const parentSession = await Storage.read<Info>(["session", Instance.project.id, input.parentID])
if (parentSession?.permission) {
const parentPermissions = parentSession.permission
const childPermissions = input.permission ?? []
mergedPermissions = PermissionNext.merge(parentPermissions, childPermissions)
}
} catch (error) {
log.warn("Failed to load parent session for permission inheritance", { parentID: input.parentID, error })
}
}

const result: Info = {
id: Identifier.descending("session", input.id),
version: Installation.VERSION,
projectID: Instance.project.id,
directory: input.directory,
parentID: input.parentID,
title: input.title ?? createDefaultTitle(!!input.parentID),
permission: input.permission,
permission: mergedPermissions,
time: {
created: Date.now(),
updated: Date.now(),
Expand Down
7 changes: 4 additions & 3 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,11 @@ export namespace SessionPrompt {
})
}
if (permissions.length > 0) {
session.permission = permissions
await Session.update(session.id, (draft) => {
draft.permission = permissions
const updatedSession = await Session.update(session.id, (draft) => {
draft.permission = PermissionNext.merge(draft.permission ?? [], permissions)
})
// Update session object to reflect the changes
session.permission = updatedSession.permission
}

if (input.noReply === true) {
Expand Down
42 changes: 42 additions & 0 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { Flag } from "@/flag/flag"
import { Log } from "@/util/log"
import { LspTool } from "./lsp"
import { Truncate } from "./truncation"
import { PermissionNext } from "@/permission/next"
import { Wildcard } from "@/util/wildcard"

export namespace ToolRegistry {
const log = Log.create({ service: "tool.registry" })
Expand Down Expand Up @@ -119,13 +121,53 @@ export namespace ToolRegistry {

export async function tools(providerID: string, agent?: Agent.Info) {
const tools = await all()
const config = await Config.get()

// Build permission ruleset: global config + agent permissions (agent takes precedence)
let permissionRuleset: PermissionNext.Ruleset = []
if (config.permission) {
// Global config permissions come first
permissionRuleset = PermissionNext.merge(permissionRuleset, PermissionNext.fromConfig(config.permission))
}
if (agent?.permission) {
// Agent permissions come last (they win on last-match-wins evaluation)
permissionRuleset = PermissionNext.merge(permissionRuleset, agent.permission)
}

// Filter out disabled tools based on permissions
// Only filter tools if there are no allow rules for that permission type
const result = await Promise.all(
tools
.filter((t) => {
// Check if tool has an explicit deny rule with no allow rules
const permission = t.id

// Find all rules for this permission
const permissionRules = permissionRuleset.filter((r) => Wildcard.match(permission, r.permission))

// If no rules apply, allow the tool
if (permissionRules.length === 0) {
return true
}

// If there are allow rules, allow the tool (command-level filtering happens later)
const hasAllowRule = permissionRules.some((r) => r.action === "allow")
if (hasAllowRule) {
return true
}

// If there are only deny rules and one matches "*" pattern, filter out the tool
const hasWildcardDeny = permissionRules.some((r) => r.action === "deny" && r.pattern === "*")

if (hasWildcardDeny) {
return false
}

// Enable websearch/codesearch for zen users OR via enable flag
if (t.id === "codesearch" || t.id === "websearch") {
return providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA
}

return true
})
.map(async (t) => {
Expand Down
88 changes: 49 additions & 39 deletions packages/opencode/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
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"
Expand Down Expand Up @@ -54,41 +54,50 @@ export const TaskTool = Tool.define("task", async (ctx) => {
})
}

// Find the agent by subagent_type
const agent = await Agent.get(params.subagent_type)
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
const session = await iife(async () => {
if (params.session_id) {
const found = await Session.get(params.session_id).catch(() => {})
if (found) 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",
},
{
permission: "task",
pattern: "*",
action: "deny",
},
...(config.experimental?.primary_tools?.map((t) => ({
pattern: "*",
action: "allow" as const,
permission: t,
})) ?? []),
],
})
// Create session for the subagent task with proper security restrictions
// IMPORTANT: Task restrictions must come AFTER agent permissions to override "*": "allow"
const taskPermissions = PermissionNext.merge(
agent.permission ?? [],
[
{
permission: "task",
pattern: "*",
action: "deny",
},
{
permission: "todowrite",
pattern: "*",
action: "deny",
},
{
permission: "todoread",
pattern: "*",
action: "deny",
},
],
// Git-agent restrictions must be LAST to override agent defaults
[
{
permission: "git",
pattern: "*",
action: "deny",
},
...(config.experimental?.primary_tools?.map((t) => ({
pattern: "*",
action: "allow" as const,
permission: t,
})) ?? []),
],
)

const session = await Session.create({
parentID: ctx.sessionID,
permission: taskPermissions,
})

const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")

Expand Down Expand Up @@ -143,12 +152,6 @@ export const TaskTool = Tool.define("task", async (ctx) => {
providerID: model.providerID,
},
agent: agent.name,
tools: {
todowrite: false,
todoread: false,
task: false,
...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])),
},
parts: promptParts,
})
unsub()
Expand Down Expand Up @@ -179,3 +182,10 @@ export const TaskTool = Tool.define("task", async (ctx) => {
},
}
})

export function filterSubagents(agents: Agent.Info[], ruleset: PermissionNext.Ruleset): Agent.Info[] {
return agents.filter((agent) => {
const permission = PermissionNext.evaluate("task", agent.name, ruleset)
return permission.action !== "deny"
})
}
Loading
Loading