Skip to main content

supercode_harness/permissions/
mod.rs

1//! P5-1 (COMPOSABLE-HARNESS-DESIGN.md §2 modules 10-11, §2.1 D-3, §2.2 C5,
2//! §5.3 risk 1): the permissions engine — command canonicalization + rule
3//! algebra + approval policy/cache + oc/cx import translators.
4//!
5//! **Risk 1 (§5.3): "a wrong translation is a SILENT privilege escalation."**
6//! This module is built to that standard: ONE engine, ONE evaluation order
7//! (deny→ask→allow first-match, [`rules`]); a tree-sitter-based canonicalizer
8//! that fails CLOSED on anything it can't parse cleanly ([`canon`]); an
9//! import translator that WARNS (never silently) when a translated rule's
10//! fixed point differs from its source ([`translate`]); and a golden-vector +
11//! adversarial-bypass test suite (`crates/harness/tests/permissions_engine.rs`)
12//! pinning the documented cc§4/oc§4 semantics as a regression bar.
13//!
14//! **Integration.** The engine activates via
15//! `capabilities.permissions.enabled` ([`crate::Config::permissions_enabled`],
16//! populated by `crate::configfile::materialize_config`) and is consumed at
17//! `crate::agent::Agent`'s tool-dispatch gate
18//! (`Agent::prepare_tool_call`). DEFAULT: disabled — the gate falls through
19//! to the pre-P5-1 [`crate::Config::needs_approval`] path byte-for-byte, so
20//! every existing test and today's default posture (approval=never,
21//! sandbox=none) is unchanged.
22//!
23//! **`protected_paths` coverage, honestly stated (F4, Fable-5 adversarial
24//! review; corrected by the F6/F7 delta review, the round-3 delta review,
25//! then the round-4 delta review below).**
26//! `capabilities.permissions.protected_paths` is a RULE-LAYER floor, not an
27//! OS-level one: it is enforced for file-tool calls (`read_file`/
28//! `write_file`/`edit_file`), for a bash command's direct shell redirect
29//! targets and a best-effort set of known argv-writers (`tee`, `dd of=`,
30//! `cp`/`mv`/`install`/`ln` — including their `-t DIR`/
31//! `--target-directory=DIR` form (F6) and its getopt-bundled short-flag
32//! equivalent `-ft DIR`/`-Dt DIR`/… (round-3) — `sed -i`, `truncate`,
33//! `sort -o FILE`, `split`'s PREFIX (round-3 hardening) — see
34//! `canon::known_writer_targets`'s doc comment for that heuristic's named
35//! gaps), and for `apply_patch`'s target path(s). A write this layer
36//! RECOGNIZES but can't statically resolve to a concrete destination — an
37//! opaque wrapper, a `$VAR`/`` `cmd` `` dynamic target, an unquoted glob
38//! metacharacter (`*`/`?`/`[`) that bash would pathname-expand before the
39//! write (F7), or a known argv-writer flag shape [`canon::
40//! known_writer_targets`] can't confidently resolve to a destination token
41//! (F6, including its round-3 bundled-short-flag extension, and — round-4 —
42//! the SAME bundled-short-flag extension now also covering `sed -i` and
43//! `sort -o`) — is forced to at least `Ask`, NEVER silently `Allow`. This is
44//! now true without exception for every write shape this rule layer claims
45//! to cover (the F6/F7 delta review found and closed two forms where it
46//! wasn't; the round-3 delta review found and closed a third: F6's own fix
47//! didn't yet recognize a getopt-BUNDLED short-flag `-t`, e.g.
48//! `cp -ft .git a`; the round-4 delta review found and closed a fourth:
49//! round-3's bundled-short-flag fix was only made `-t`-specific, leaving
50//! the identical blind spot open on `sed -i`/`sort -o` — `sed -ni
51//! s/../PWNED/ .env` and `sort -uo .env a` both still fell through to a
52//! silent `Allow`). Round-4 closes this as a CLASS, not a third patched
53//! instance: every flag-driven known-writer detection (`-t` for
54//! cp/mv/install/ln, `-i` for sed, `-o` for sort) now routes through one
55//! shared, generalized bundled-short-flag scan
56//! (`canon::known_writer_targets`'s doc comment names it) — a future
57//! flag-driven writer inherits the fix by construction instead of needing
58//! its own bundled-flag audit.
59//!
60//! **What "claims to cover" does NOT mean, stated plainly (round-3
61//! hardening; then STOP enumerating).** `sort -o`/`split` (round-3) are new
62//! ADDITIONS to the enumerated-writer set, not a fix to a row already
63//! claimed covered — before that change, a write via `sort -o`/`split`
64//! simply wasn't recognized AT ALL, i.e. a false-`Allow` gap of exactly the
65//! same shape every OTHER un-enumerated writer still is today: any
66//! interpreter's own file-write builtins, a compiler's `-o`, a database
67//! client's export command, or any other bash construct this rule layer
68//! doesn't specifically parse. This module does not, and does not claim to,
69//! enumerate every file-writing command that could ever appear in a bash
70//! tool call — doing so is an unbounded, always-incomplete list. What IS
71//! true, and load-bearing, is the narrower claim above: for every write
72//! SHAPE this rule layer *does* recognize (the enumerated writers, shell
73//! redirects, `apply_patch`), fail-closed holds without exception — an
74//! unresolvable target never silently resolves to `Allow`. An
75//! un-enumerated writer is a false NEGATIVE at this rule layer (nothing
76//! flagged, default policy decides), honestly named here rather than
77//! silently claimed covered — complete OS-level write confinement of
78//! arbitrary bash, which closes that gap entirely regardless of which
79//! command wrote the file, is `capabilities.permissions.sandbox`'s job (P5
80//! module 10, a later unit), not this rule-layer heuristic's. See
81//! [`crate::Config::permissions_protected_paths`]'s doc comment for the
82//! same note where the config field is defined.
83
84pub mod approval;
85pub mod canon;
86pub mod rules;
87pub mod translate;
88
89pub use approval::{
90    decision_to_approved, resolve_ask, ApprovalCache, ApprovalOutcome, ApprovalRequest,
91    PermissionsApprovalHandler,
92};
93pub use canon::{canonicalize, CanonResult, CanonSubcommand};
94pub use rules::{
95    evaluate_command, evaluate_path, evaluate_path_safe, evaluate_path_subject_safe,
96    protected_path_deny_rules, Decision, PathKind, RuleSet,
97};
98pub use translate::{
99    opencode_default_policy, translate_last_match_to_first_match, SourceRule, Translated,
100};