T24 — Wildcard last-match-wins permission evaluation
rules.findLast(rule => match(perm, rule.perm) && match(pattern, rule.pattern)) — rule sau override rule trước. Cho phép: allow bash:* rồi deny bash:rm -rf *.Tổng quan Permission
Tại sao quan trọng. Permission system cần vừa "allow broadly" vừa "deny specifically". Nếu first-match-wins: user phải đặt deny trước allow — counter-intuitive. Last-match-wins cho phép viết theo logic tự nhiên:
allow: bash:* (mặc định cho phép bash) → deny: bash:rm -rf * (nhưng block destructive). Rule gần cuối file = rule có priority cao nhất — giống nginx, iptables, CSS specificity.
Default "ask" là safety net: Khi không có rule nào match, system default sang
"ask" — không phải "deny" cứng nhắc cũng không phải "allow" tiện lợi. User luôn được inform và có quyền quyết định.
Phân tích code chi tiết Anatomy
evaluate() — 15 dòng, full permission logic
permission/evaluate.ts
TS
{`
import { Wildcard } from "./wildcard"
export function evaluate(
rules: PermissionRule[],
permission: string, // "bash" | "edit" | "write" | "doom_loop" | ...
pattern: string, // "rm -rf *" | "/tmp/**" | command string
): "allow" | "deny" | "ask" {
// findLast = iterate từ cuối → rule sau win (last-match-wins)
const match = rules.findLast(
(rule) =>
Wildcard.match(permission, rule.permission) &&
Wildcard.match(pattern, rule.pattern)
)
// Default "ask" khi không có rule nào match
return match?.action ?? "ask"
}
`}Wildcard matching — shell-style glob
Wildcard.match — * là wildcard
TS
{`
// Wildcard.match("bash:rm -rf /tmp", "bash:rm *") → true
// Wildcard.match("edit:/src/**", "edit:*") → true
// Wildcard.match("doom_loop", "bash:*") → false
function wildcard(value: string, pattern: string): boolean {
// Convert shell glob * → regex .*
const re = new RegExp(
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*") + "$"
)
return re.test(value)
}
`}
rules = [
{ permission: "bash", pattern: "*", action: "allow" }, // rule 1
{ permission: "bash", pattern: "rm -rf *", action: "deny" }, // rule 2
{ permission: "edit", pattern: "/tmp/**", action: "allow" }, // rule 3
]
Request: permission="bash", pattern="rm -rf /home"
│
▼
findLast iteration (từ cuối):
rule 3: bash != edit → skip
rule 2: bash==bash, "rm -rf /home" matches "rm -rf *" → MATCH → return "deny"
(rule 1 không được xét vì findLast trả về ngay khi tìm thấy)
Result: "deny"
Request: permission="bash", pattern="ls /home"
│
▼
findLast:
rule 2: "ls /home" không match "rm -rf *" → skip
rule 1: bash==bash, "ls /home" matches "*" → MATCH → return "allow"
Result: "allow"
Tương tác với kỹ thuật khác Interaction
- T18 (Bash tree-sitter): tree-sitter extract paths từ bash command → paths trở thành
patternargument trongevaluate(). Accuracy của T18 ảnh hưởng trực tiếp đến permission decision. - T25 (Session-scoped state):
evaluate()chỉ check static rules từ config. T25 bổ sung check dynamic "alwaysAllow" đã được user approve trong session. Hai layer nối tiếp nhau. - T26 (Arity normalization): Trước khi đưa vào evaluate, command được normalize:
"npm run build --watch"→"npm run build"để match đúng pattern user đã set.
Failure modes Failure
1. Order-sensitive — dễ sắp xếp sai
Last-match-wins phụ thuộc thứ tự rules. Nếu user drag-drop rule lên trên trong UI mà không hiểu semantics → behavior thay đổi không mong muốn. Cần UI cảnh báo khi order change affect outcome.
2. Wildcard không đủ biểu đạt
* simple không support negation (!rm*), character class ([a-z]), hay regex phức tạp. Pattern như "cho phép mọi git command trừ push --force" không biểu đạt được với wildcard đơn giản.
3. Không có "deny wins" mode
Nếu muốn "deny luôn thắng bất kể thứ tự", cần thêm mode riêng. Hiện tại: deny chỉ thắng nếu đứng sau allow. Security-critical deployment cần option này.
So sánh với các harness khác Compare
| Harness | Permission model | Default action | Wildcard |
|---|---|---|---|
| opencode | Wildcard last-match-wins rules | "ask" (safe default) | ✅ shell glob style |
| Claude Code | allowedTools + deniedTools lists | "ask" (safe default) | ✅ glob + regex |
| OpenHarness | Sensitive path hardcoded + config allow | "deny" for hardcoded | ⚠️ basic |
| Aider | Auto-approve flag hoặc manual confirm | User-configured | ❌ |
| Cline | Always ask (strict) hoặc never ask (open) | "always ask" by default | ❌ |
Implementation recipe Recipe
TS
{`
interface Rule {
permission: string // "bash" | "edit" | "*"
pattern: string // "rm *" | "/tmp/**" | "*"
action: "allow" | "deny" | "ask"
}
function matchWildcard(value: string, pattern: string): boolean {
const re = new RegExp(
"^" + pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex chars
.replace(/\*/g, ".*") // * → .*
+ "$",
"i"
)
return re.test(value)
}
function evaluate(rules: Rule[], permission: string, pattern: string): "allow" | "deny" | "ask" {
const match = [...rules]
.reverse() // last-match-wins: reverse array then find first
.find(r =>
matchWildcard(permission, r.permission) &&
matchWildcard(pattern, r.pattern)
)
return match?.action ?? "ask" // safe default
}
// Example rules (YAML config → parse to Rule[]):
// rules:
// - { permission: "bash", pattern: "*", action: allow }
// - { permission: "bash", pattern: "rm *", action: deny }
// - { permission: "edit", pattern: "/src/**", action: allow }
`}