Skip to main content

Module permissions

Module permissions 

Source
Expand description

P5-1 (COMPOSABLE-HARNESS-DESIGN.md §2 modules 10-11, §2.1 D-3, §2.2 C5, §5.3 risk 1): the permissions engine — command canonicalization + rule algebra + approval policy/cache + oc/cx import translators.

Risk 1 (§5.3): “a wrong translation is a SILENT privilege escalation.” This module is built to that standard: ONE engine, ONE evaluation order (deny→ask→allow first-match, rules); a tree-sitter-based canonicalizer that fails CLOSED on anything it can’t parse cleanly (canon); an import translator that WARNS (never silently) when a translated rule’s fixed point differs from its source (translate); and a golden-vector + adversarial-bypass test suite (crates/harness/tests/permissions_engine.rs) pinning the documented cc§4/oc§4 semantics as a regression bar.

Integration. The engine activates via capabilities.permissions.enabled (crate::Config::permissions_enabled, populated by crate::configfile::materialize_config) and is consumed at crate::agent::Agent’s tool-dispatch gate (Agent::prepare_tool_call). DEFAULT: disabled — the gate falls through to the pre-P5-1 crate::Config::needs_approval path byte-for-byte, so every existing test and today’s default posture (approval=never, sandbox=none) is unchanged.

protected_paths coverage, honestly stated (F4, Fable-5 adversarial review; corrected by the F6/F7 delta review, the round-3 delta review, then the round-4 delta review below). capabilities.permissions.protected_paths is a RULE-LAYER floor, not an OS-level one: it is enforced for file-tool calls (read_file/ write_file/edit_file), for a bash command’s direct shell redirect targets and a best-effort set of known argv-writers (tee, dd of=, cp/mv/install/ln — including their -t DIR/ --target-directory=DIR form (F6) and its getopt-bundled short-flag equivalent -ft DIR/-Dt DIR/… (round-3) — sed -i, truncate, sort -o FILE, split’s PREFIX (round-3 hardening) — see canon::known_writer_targets’s doc comment for that heuristic’s named gaps), and for apply_patch’s target path(s). A write this layer RECOGNIZES but can’t statically resolve to a concrete destination — an opaque wrapper, a $VAR/`cmd` dynamic target, an unquoted glob metacharacter (*/?/[) that bash would pathname-expand before the write (F7), or a known argv-writer flag shape [canon:: known_writer_targets] can’t confidently resolve to a destination token (F6, including its round-3 bundled-short-flag extension, and — round-4 — the SAME bundled-short-flag extension now also covering sed -i and sort -o) — is forced to at least Ask, NEVER silently Allow. This is now true without exception for every write shape this rule layer claims to cover (the F6/F7 delta review found and closed two forms where it wasn’t; the round-3 delta review found and closed a third: F6’s own fix didn’t yet recognize a getopt-BUNDLED short-flag -t, e.g. cp -ft .git a; the round-4 delta review found and closed a fourth: round-3’s bundled-short-flag fix was only made -t-specific, leaving the identical blind spot open on sed -i/sort -osed -ni s/../PWNED/ .env and sort -uo .env a both still fell through to a silent Allow). Round-4 closes this as a CLASS, not a third patched instance: every flag-driven known-writer detection (-t for cp/mv/install/ln, -i for sed, -o for sort) now routes through one shared, generalized bundled-short-flag scan (canon::known_writer_targets’s doc comment names it) — a future flag-driven writer inherits the fix by construction instead of needing its own bundled-flag audit.

What “claims to cover” does NOT mean, stated plainly (round-3 hardening; then STOP enumerating). sort -o/split (round-3) are new ADDITIONS to the enumerated-writer set, not a fix to a row already claimed covered — before that change, a write via sort -o/split simply wasn’t recognized AT ALL, i.e. a false-Allow gap of exactly the same shape every OTHER un-enumerated writer still is today: any interpreter’s own file-write builtins, a compiler’s -o, a database client’s export command, or any other bash construct this rule layer doesn’t specifically parse. This module does not, and does not claim to, enumerate every file-writing command that could ever appear in a bash tool call — doing so is an unbounded, always-incomplete list. What IS true, and load-bearing, is the narrower claim above: for every write SHAPE this rule layer does recognize (the enumerated writers, shell redirects, apply_patch), fail-closed holds without exception — an unresolvable target never silently resolves to Allow. An un-enumerated writer is a false NEGATIVE at this rule layer (nothing flagged, default policy decides), honestly named here rather than silently claimed covered — complete OS-level write confinement of arbitrary bash, which closes that gap entirely regardless of which command wrote the file, is capabilities.permissions.sandbox’s job (P5 module 10, a later unit), not this rule-layer heuristic’s. See crate::Config::permissions_protected_paths’s doc comment for the same note where the config field is defined.

Re-exports§

pub use approval::decision_to_approved;
pub use approval::resolve_ask;
pub use approval::ApprovalCache;
pub use approval::ApprovalOutcome;
pub use approval::ApprovalRequest;
pub use approval::PermissionsApprovalHandler;
pub use canon::canonicalize;
pub use canon::CanonResult;
pub use canon::CanonSubcommand;
pub use rules::evaluate_command;
pub use rules::evaluate_path;
pub use rules::evaluate_path_safe;
pub use rules::evaluate_path_subject_safe;
pub use rules::protected_path_deny_rules;
pub use rules::Decision;
pub use rules::PathKind;
pub use rules::RuleSet;
pub use translate::opencode_default_policy;
pub use translate::translate_last_match_to_first_match;
pub use translate::SourceRule;
pub use translate::Translated;

Modules§

approval
P5-1 (COMPOSABLE-HARNESS-DESIGN.md §2 module 10, §2.10): approval policy plumbing — the POLICY + session-scoped CACHE + a non-interactive decision path. The INTERACTIVE ask-UI itself is the tui module (P5 row 4, not this unit) — PermissionsApprovalHandler is the seam a CLI/TUI/SDK embedder implements to plug an interactive (or scripted/headless) prompt into crate::agent::Agent’s tool-dispatch gate, mirroring the existing crate::reduce::summarize::SpanSummarizer/crate::session_title::SessionTitler “installing one alone changes nothing, the Config gate is what turns it on” pattern (Agent::set_span_summarizer/Agent::set_session_titler).
canon
P5-1 (COMPOSABLE-HARNESS-DESIGN.md §5.3 risk 1, D-3): the command canonicalizer — the security-critical parser rule-matching depends on.
rules
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.
translate
P5-1 (COMPOSABLE-HARNESS-DESIGN.md §2.2 conflict C5, §5.3 risk 1, §4.4 oc-parity): oc/cx import translators. When importing/emulating a last-match-wins rule set (opencode’s native algebra — oc§4:239-241), this module translates it into the engine’s first-match deny→ask→allow form and EMITS A WARNING on every pattern whose translated fixed point differs from the source’s — the risk-1 mitigation verbatim (“import-time translators emit warnings on any rule whose translated fixed point differs”).