1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! 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 -o` — `sed -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.
pub use ;
pub use ;
pub use ;
pub use ;
/// BP-5: the deny→ask→allow [`RuleSet`] a resolved [`crate::Config`] means,
/// with the protected-path floor already folded into the deny tier.
///
/// ONE construction, so every surface that has to ask "is this allowed?"
/// asks the same engine the same way: the agent's tool-dispatch gate
/// (`Agent::permissions_gate_denial`, which additionally narrows it with
/// plan mode's own deny rules) and the `` !`cmd` `` expansion in a
/// skill/command body ([`crate::skills::ShellInjection`]). A second,
/// hand-assembled rule set anywhere is a silent privilege split.
/// BP-5: the baseline decision for `tool` when NO rule in
/// [`rules_for_config`] matches — derived from
/// [`crate::config::ApprovalPolicy`] exactly as the agent's gate derives it
/// (see `Agent::permissions_gate_denial_impl`, which calls this).