Expand description
P5-1 (COMPOSABLE-HARNESS-DESIGN.md §2 module 11, §2.2 conflict C5, §5.3
risk 1): the rule engine. ONE engine, ONE evaluation order — deny →
ask → allow, FIRST-MATCH within that fixed tier priority (the C5
decision: “the engine evaluates deny→ask→allow first-match (CC); OC-style
[last-match] sets are translated at preset-import time” —
crate::permissions::translate is that translator). This module never
implements last-match semantics itself.
Fail-closed is the law. evaluate_command can return Allow ONLY
when every extracted sub-command (a) parsed cleanly, (b) is not
crate::permissions::canon::CanonSubcommand::opaque, and (c) either
matches an explicit allow rule or the caller’s default was itself
Allow. Any doubt anywhere in that chain resolves to at least Ask —
see the doc comments on each branch below for exactly where.
Structs§
- RuleSet
- A first-match deny→ask→allow rule set (§2 module 11, C5). Each list holds pattern strings in one of two forms:
Enums§
- Decision
- The three-way outcome the engine can reach for a tool call. Ordered by
strictness for
Decision::stricter(Denystrictest,Allowloosest) — NOT by numeric severity in the tier-priority sense (which is a fixed deny→ask→allow scan order, not a totally-ordered scale); the ordering here exists purely to fold multiple sub-command decisions down to “the single worst one wins”, the compound-safety invariant D-3/risk-1 demands. - Path
Kind - Which access
evaluate_pathis checking — matches module 11’sread(...)/write(...)pseudo-tool rule pattern names.
Functions§
- evaluate_
command - Evaluate a (possibly compound) shell command against
rules, folding every extracted sub-command’s decision down to the single strictest one (§5.3 risk 1’s compound-safety invariant).toolis the calling tool’s name ("bash","shell", …) — sub-command patterns match astool(cmdglob)againsttool, e.g. a rule written"bash(rm -rf*)"applies to every sub-command of abashcall, not to ashellcall. - evaluate_
path - Evaluate a single resolved path against
rules, as either a"read"or"write"pseudo-tool (module 11’s “path rules”: read/write globs — seeRuleSet’s doc comment). Falls back todefaultwhen nothing matches. - evaluate_
path_ safe - SECURITY (CRITICAL fix, guarantor audit, traced to this file’s former
evaluate_pathdoc comment claiming “no canonicalization … is involved here … no unparseable case to fail closed on” — the flawed assumption that let a traversal payload bypassprotected_paths): the safe entry point for evaluating a RAW, model-suppliedpathtool argument (relative or absolute, exactly as it arrives inargs["path"]) as a"read"/"write"pseudo-tool subject. Resolvesraw_pathagainstrootthroughcrate::safe_path::resolve_for_matching— the SAME dual lexical+symlink-resolved checkcrate::checkpoint’s P5-9 fix uses — and foldsevaluate_pathagainst the RAW subject, the lexically-normalized project-relative form, AND the symlink-resolved project-relative form down to the single strictestDecision(ties broken toward stricter, viaDecision::stricter), so a rule can never be satisfied by matching only one of these three views. - evaluate_
path_ subject_ safe - Like
evaluate_path_safe, but for an arbitrarytoolname subject instead of theread/writepseudo-tool — e.g. a rule authored against the REAL tool name with a path subject (design §4.4’s"read_file(*.env)"syntax, or anapply_patch-targeted rule).evaluate_path_safeis a thin wrapper over this for the pseudo-tool case; callers that need BOTH (the permissions gate always does — seecrate::agent’spermissions_gate_denial_impl) call this function a second time withtoolset to the real tool name and fold the two results together. - protected_
path_ deny_ rules - Module 13 (
permissions.protected_paths): build theread(...)+write(...)deny rules a protected-paths glob list expands to — a protected path is unconditionally denied for BOTH read and write, unlike an ordinary rule (this is the “never auto-approved” floor cc§4 documents for.git/**/.env*/etc, not an ordinary ask/allow-able rule). Callers fold the result into aRuleSet’sdenylist (seecrate::configfile::materialize_config), which — becausedenyis always checked first, unconditionally, with no override — makes a protected path exactly as hard a floor asConfig::tool_deny_patternsalready is (config.rs).