What happened
Agents using per-subagent permission configs like:
"task": { "*": "deny", "ops": "allow", "developer": "allow", "explore": "allow" }
cannot see the task tool at all. The tool is incorrectly filtered out by disabled(), making it impossible to dispatch subagents.
Expected behaviour
A tool with { "*": "deny", "specific": "allow" } should be visible because at least one subagent is allowed. The "*": "deny" is a default that specific allow rules override. The tool should be hidden only when ALL patterns lead to deny.
Steps to reproduce
- Configure an agent with
permission: { "task": { "*": "deny", "ops": "allow", "developer": "allow" } }
- Start a session with that agent
- The
task tool is missing from the available tools list
- Agent cannot dispatch to any subagent
Root cause
PR #389 rewrote disabled() to use evaluate(permission, "*", ruleset) for consistency with evaluate(). However, "*" as the query pattern only matches wildcard rule patterns — it does NOT match specific patterns like "ops", "developer", etc.
When evaluate("task", "*", ruleset) iterates through rules:
{task, *, deny} → Wildcard.match("*", "*") → matches → deny
{task, "ops", allow} → Wildcard.match("*", "ops") → pattern "ops" becomes regex ^ops$ → testing string "*" → does NOT match
The specific allow rules are invisible. Only the wildcard deny matches, so the tool is incorrectly hidden.
The old code used findLast() matching by permission only (ignoring pattern), which accidentally found specific allow rules since they came after the wildcard in the array.
The new code uses evaluate() with pattern "*", which only finds rules whose pattern matches the wildcard "*", missing all specific subagent allow rules.
Fix
disabled() must not use evaluate(permission, "*", ruleset). Instead, it should check whether a tool is denied for ALL patterns. A tool should be visible if ANY subagent pattern allows it.
Two approaches:
- Minimal fix: If
evaluate(permission, "*", ruleset).action === "deny", check if any other rule for this permission has action === "allow". If an allow rule exists, do not disable the tool.
- Correct fix: A tool is disabled only if there is no rule with
action === "allow" for this permission in the merged ruleset.
Environment
Acceptance Criteria
Definition of Done
What happened
Agents using per-subagent permission configs like:
cannot see the
tasktool at all. The tool is incorrectly filtered out bydisabled(), making it impossible to dispatch subagents.Expected behaviour
A tool with
{ "*": "deny", "specific": "allow" }should be visible because at least one subagent is allowed. The"*": "deny"is a default that specific allow rules override. The tool should be hidden only when ALL patterns lead to deny.Steps to reproduce
permission: { "task": { "*": "deny", "ops": "allow", "developer": "allow" } }tasktool is missing from the available tools listRoot cause
PR #389 rewrote
disabled()to useevaluate(permission, "*", ruleset)for consistency withevaluate(). However,"*"as the query pattern only matches wildcard rule patterns — it does NOT match specific patterns like"ops","developer", etc.When
evaluate("task", "*", ruleset)iterates through rules:{task, *, deny}→Wildcard.match("*", "*")→ matches → deny{task, "ops", allow}→Wildcard.match("*", "ops")→ pattern"ops"becomes regex^ops$→ testing string"*"→ does NOT matchThe specific allow rules are invisible. Only the wildcard deny matches, so the tool is incorrectly hidden.
The old code used
findLast()matching by permission only (ignoring pattern), which accidentally found specific allow rules since they came after the wildcard in the array.The new code uses
evaluate()with pattern"*", which only finds rules whose pattern matches the wildcard"*", missing all specific subagent allow rules.Fix
disabled()must not useevaluate(permission, "*", ruleset). Instead, it should check whether a tool is denied for ALL patterns. A tool should be visible if ANY subagent pattern allows it.Two approaches:
evaluate(permission, "*", ruleset).action === "deny", check if any other rule for this permission hasaction === "allow". If an allow rule exists, do not disable the tool.action === "allow"for this permission in the merged ruleset.Environment
packages/opencode/src/permission/next.ts, lines 260-272Acceptance Criteria
task: { "*": "deny", "ops": "allow" }can see and use thetasktooltask: { "*": "deny" }(no allow rules) correctly hides thetasktooledit: { "*": "deny", "src/*": "allow" }can see theedittooldisabled()andevaluate()remain consistent — no security regression{ "*": "deny", "specific": "allow" }does NOT disable the tool{ "*": "deny" }(deny-only) DOES disable the toolpermission/next.test.tspassDefinition of Done