Skip to content

fix(permission): disabled() hides tools with per-subagent allow rules #401

@randomm

Description

@randomm

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

  1. Configure an agent with permission: { "task": { "*": "deny", "ops": "allow", "developer": "allow" } }
  2. Start a session with that agent
  3. The task tool is missing from the available tools list
  4. 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:

  1. 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.
  2. 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

  • Agent with task: { "*": "deny", "ops": "allow" } can see and use the task tool
  • Agent with task: { "*": "deny" } (no allow rules) correctly hides the task tool
  • Agent with edit: { "*": "deny", "src/*": "allow" } can see the edit tool
  • disabled() and evaluate() remain consistent — no security regression
  • New test: config { "*": "deny", "specific": "allow" } does NOT disable the tool
  • New test: config { "*": "deny" } (deny-only) DOES disable the tool
  • All existing tests in permission/next.test.ts pass

Definition of Done

  • Root cause identified and fixed
  • Tests written (TDD preferred)
  • No regression in related functionality

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions