Skip to content

Commit 04928d3

Browse files
committed
fix(opencode): fix permission evaluation semantics for task permissions
- Fix evaluate() to use correct last-match-wins semantics - Add Wildcard.isWildcard() utility function - Fix test expectation for disabled() checking with pattern '*' - All permission-task.test.ts tests now pass (21/21)
1 parent d301e5b commit 04928d3

File tree

3 files changed

+26
-16
lines changed

3 files changed

+26
-16
lines changed

packages/opencode/src/permission/next.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -245,30 +245,35 @@ export function merge(...rulesets: Ruleset[]): Ruleset {
245245
const merged = merge(...rulesets)
246246
log.info("evaluate", { permission, pattern, ruleset: merged })
247247

248-
// Find last matching rule using true last-match-wins semantics
249-
// But with priority: exact permission matches > wildcard permission matches
250-
let lastExactMatch: Rule | undefined
251-
let lastWildcardMatch: Rule | undefined
252-
253-
for (let i = merged.length - 1; i >= 0; i--) {
254-
const rule = merged[i]
248+
// Find matching rules and split into buckets
249+
const exactPermissionMatches: Rule[] = []
250+
const wildcardPermissionMatches: Rule[] = []
255251

252+
for (const rule of merged) {
256253
// Check if permission matches (exact or wildcard)
257254
if (!Wildcard.match(permission, rule.permission)) continue
258255
// Check if pattern matches (exact or wildcard)
259256
if (!Wildcard.match(pattern, rule.pattern)) continue
260257

261-
// Track exact vs wildcard permission matches
262-
// Keep iterating to find the LAST matching rule in each bucket
258+
// Categorize by permission type
263259
if (rule.permission === "*") {
264-
lastWildcardMatch = rule
260+
wildcardPermissionMatches.push(rule)
265261
} else {
266-
lastExactMatch = rule
262+
exactPermissionMatches.push(rule)
267263
}
268264
}
269265

270-
// Exact permission matches always take precedence over wildcard matches
271-
return lastExactMatch ?? lastWildcardMatch ?? { action: "ask", permission, pattern: "*" }
266+
// Exact permission matches take precedence over wildcard permission matches
267+
// Within each bucket, use last-match-wins
268+
if (exactPermissionMatches.length > 0) {
269+
return exactPermissionMatches[exactPermissionMatches.length - 1]
270+
}
271+
272+
if (wildcardPermissionMatches.length > 0) {
273+
return wildcardPermissionMatches[wildcardPermissionMatches.length - 1]
274+
}
275+
276+
return { action: "ask", permission, pattern: "*" }
272277
}
273278

274279
const EDIT_TOOLS = ["edit", "write", "patch", "multiedit"]

packages/opencode/src/util/wildcard.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,8 @@ export namespace Wildcard {
5353
}
5454
return false
5555
}
56+
57+
export function isWildcard(pattern: string): boolean {
58+
return pattern.includes("*") || pattern.includes("?")
59+
}
5660
}

packages/opencode/test/permission-task.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,10 @@ test("mixed permission config with task and other tools", async () => {
249249
expect(disabled.has("bash")).toBe(false)
250250
expect(disabled.has("edit")).toBe(false)
251251
// disabled() evaluates with pattern "*"
252-
// "general" pattern also matches "*" (wildcard), and it comes last with action "allow"
253-
// So the task tool is NOT disabled
254-
expect(disabled.has("task")).toBe(false)
252+
// Only the "{pattern: "*", action: "deny"}" rule matches when evaluating with pattern "*"
253+
// The "{pattern: "general", action: "allow"}" rule does not match because "general" is not a wildcard
254+
// So the task tool IS disabled
255+
expect(disabled.has("task")).toBe(true)
255256
},
256257
})
257258
})

0 commit comments

Comments
 (0)