@bookedsolid/rea 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +15 -0
  2. package/THREAT_MODEL.md +582 -0
  3. package/dist/audit/append.js +1 -1
  4. package/dist/cli/doctor.js +11 -12
  5. package/dist/cli/hook.d.ts +37 -3
  6. package/dist/cli/hook.js +167 -5
  7. package/dist/cli/init.js +14 -26
  8. package/dist/cli/install/canonical.js +18 -3
  9. package/dist/cli/install/commit-msg.js +1 -2
  10. package/dist/cli/install/copy.js +4 -13
  11. package/dist/cli/install/fs-safe.js +5 -16
  12. package/dist/cli/install/gitignore.js +1 -5
  13. package/dist/cli/install/pre-push.js +3 -8
  14. package/dist/cli/install/settings-merge.js +79 -16
  15. package/dist/cli/upgrade.js +14 -10
  16. package/dist/gateway/downstream.js +1 -2
  17. package/dist/gateway/live-state.js +3 -1
  18. package/dist/gateway/log.js +1 -3
  19. package/dist/gateway/middleware/audit.js +1 -1
  20. package/dist/gateway/middleware/injection.js +3 -9
  21. package/dist/gateway/middleware/policy.js +3 -1
  22. package/dist/gateway/middleware/redact.js +1 -1
  23. package/dist/gateway/observability/codex-telemetry.js +1 -2
  24. package/dist/gateway/reviewers/claude-self.js +10 -6
  25. package/dist/hooks/bash-scanner/blocked-scan.d.ts +26 -0
  26. package/dist/hooks/bash-scanner/blocked-scan.js +467 -0
  27. package/dist/hooks/bash-scanner/index.d.ts +41 -0
  28. package/dist/hooks/bash-scanner/index.js +62 -0
  29. package/dist/hooks/bash-scanner/parse-fail-closed.d.ts +31 -0
  30. package/dist/hooks/bash-scanner/parse-fail-closed.js +27 -0
  31. package/dist/hooks/bash-scanner/parser.d.ts +42 -0
  32. package/dist/hooks/bash-scanner/parser.js +92 -0
  33. package/dist/hooks/bash-scanner/protected-scan.d.ts +76 -0
  34. package/dist/hooks/bash-scanner/protected-scan.js +815 -0
  35. package/dist/hooks/bash-scanner/verdict.d.ts +80 -0
  36. package/dist/hooks/bash-scanner/verdict.js +49 -0
  37. package/dist/hooks/bash-scanner/walker.d.ts +165 -0
  38. package/dist/hooks/bash-scanner/walker.js +7954 -0
  39. package/dist/hooks/push-gate/base.js +2 -6
  40. package/dist/hooks/push-gate/codex-runner.js +3 -1
  41. package/dist/hooks/push-gate/index.js +9 -10
  42. package/dist/policy/loader.js +4 -1
  43. package/dist/registry/tofu-gate.js +2 -2
  44. package/hooks/blocked-paths-bash-gate.sh +142 -272
  45. package/hooks/protected-paths-bash-gate.sh +227 -511
  46. package/package.json +3 -2
  47. package/profiles/bst-internal-no-codex.yaml +1 -1
  48. package/profiles/bst-internal.yaml +1 -1
  49. package/profiles/client-engagement.yaml +1 -1
  50. package/profiles/lit-wc.yaml +1 -1
  51. package/profiles/minimal.yaml +1 -1
  52. package/profiles/open-source-no-codex.yaml +1 -1
  53. package/profiles/open-source.yaml +1 -1
  54. package/scripts/postinstall.mjs +1 -2
  55. package/scripts/run-vitest.mjs +117 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Verdict shape — the contract between the bash-shim hooks and the
3
+ * `rea hook scan-bash` CLI.
4
+ *
5
+ * Stability: this JSON shape is part of the public hook protocol from
6
+ * 0.23.0 onward. The shim hooks under `hooks/protected-paths-bash-gate.sh`
7
+ * and `hooks/blocked-paths-bash-gate.sh` shell out to `rea hook scan-bash`
8
+ * and parse this exact shape with `jq`. Any change here that the bash
9
+ * shims don't survive is a breaking change — bump the major or stage
10
+ * with a fallback shape.
11
+ *
12
+ * Snapshot tests in `__tests__/hooks/bash-scanner/verdict-shape.test.ts`
13
+ * lock the wire format to keep this honest.
14
+ */
15
+ /**
16
+ * The form of write the walker detected. New shapes can land at the
17
+ * end; the bash shims do not branch on these — they only forward the
18
+ * verdict — so adding a new tag is non-breaking.
19
+ *
20
+ * Naming convention: `<utility>_<role>` for argv-driven detections,
21
+ * `redirect` for shell I/O redirects (covers `>`, `>>`, `>|`, `&>`,
22
+ * fd-prefixed variants), `nested_shell_inner` for unwrapped
23
+ * `bash -c`/`sh -c` payloads.
24
+ */
25
+ export type DetectedForm = 'redirect' | 'cp_dest' | 'cp_t_flag' | 'mv_dest' | 'mv_t_flag' | 'tee_arg' | 'sed_i' | 'dd_of' | 'truncate_arg' | 'install_dest' | 'ln_dest' | 'awk_inplace' | 'awk_source' | 'ed_target' | 'ex_target' | 'find_exec_inner' | 'find_exec_placeholder_unresolvable' | 'xargs_unresolvable' | 'parallel_stdin_unresolvable' | 'git_filter_branch_inner' | 'git_rebase_exec_inner' | 'git_bisect_run_inner' | 'git_commit_template' | 'git_rm_dest' | 'git_mv_src' | 'archive_extract_dest' | 'archive_extract_unresolvable' | 'archive_member_dest' | 'archive_create_dest' | 'gzip_compress_dest' | 'cmake_e_dest' | 'mkfifo_dest' | 'mknod_dest' | 'node_e_path' | 'python_c_path' | 'ruby_e_path' | 'perl_e_path' | 'php_r_path' | 'process_subst_inner' | 'nested_shell_inner';
26
+ /**
27
+ * Source position for a detected write. 1-indexed (matches the parser's
28
+ * convention) so the operator-facing error message reads naturally.
29
+ */
30
+ export interface SourcePosition {
31
+ line: number;
32
+ col: number;
33
+ }
34
+ /**
35
+ * The single verdict the scanner returns to its caller. Allow paths
36
+ * leave reason/hit_pattern/detected_form/source_position unset; block
37
+ * paths set all four where determinable.
38
+ *
39
+ * `parse_failure_reason` is set ONLY when the parser itself rejected
40
+ * the input — distinct from a successfully-parsed but policy-violating
41
+ * command. Lets the operator-facing error tell the difference between
42
+ * "you wrote bad bash" and "you tried to write to .rea/HALT".
43
+ */
44
+ export interface Verdict {
45
+ verdict: 'allow' | 'block';
46
+ reason?: string;
47
+ hit_pattern?: string;
48
+ detected_form?: DetectedForm;
49
+ source_position?: SourcePosition;
50
+ /**
51
+ * Set on parse-failure blocks only. Format: parser library's raw
52
+ * error message verbatim, prefixed with "parser: ". Operators can
53
+ * paste this into a bug report without further redaction.
54
+ */
55
+ parse_failure_reason?: string;
56
+ }
57
+ /** Construct a uniform allow verdict. */
58
+ export declare function allowVerdict(): Verdict;
59
+ /**
60
+ * Construct a uniform block verdict for a successful-parse + policy
61
+ * violation. All four explanation fields are required so the operator
62
+ * gets actionable context.
63
+ */
64
+ export declare function blockVerdict(args: {
65
+ reason: string;
66
+ hitPattern: string;
67
+ detectedForm: DetectedForm;
68
+ sourcePosition?: SourcePosition;
69
+ }): Verdict;
70
+ /**
71
+ * Construct a uniform block verdict for a parse-failure event. We
72
+ * always block — the alternative is "scanner can't tell, assume safe"
73
+ * which is the entire bug class this rewrite exists to close.
74
+ *
75
+ * `parserMessage` flows through to the operator. We DO NOT sanitize
76
+ * it — the parser's messages are static (no user-controlled
77
+ * interpolation in modern mvdan-sh) and including them helps debug
78
+ * malformed payloads in the field.
79
+ */
80
+ export declare function parseFailureVerdict(parserMessage: string): Verdict;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Verdict shape — the contract between the bash-shim hooks and the
3
+ * `rea hook scan-bash` CLI.
4
+ *
5
+ * Stability: this JSON shape is part of the public hook protocol from
6
+ * 0.23.0 onward. The shim hooks under `hooks/protected-paths-bash-gate.sh`
7
+ * and `hooks/blocked-paths-bash-gate.sh` shell out to `rea hook scan-bash`
8
+ * and parse this exact shape with `jq`. Any change here that the bash
9
+ * shims don't survive is a breaking change — bump the major or stage
10
+ * with a fallback shape.
11
+ *
12
+ * Snapshot tests in `__tests__/hooks/bash-scanner/verdict-shape.test.ts`
13
+ * lock the wire format to keep this honest.
14
+ */
15
+ /** Construct a uniform allow verdict. */
16
+ export function allowVerdict() {
17
+ return { verdict: 'allow' };
18
+ }
19
+ /**
20
+ * Construct a uniform block verdict for a successful-parse + policy
21
+ * violation. All four explanation fields are required so the operator
22
+ * gets actionable context.
23
+ */
24
+ export function blockVerdict(args) {
25
+ return {
26
+ verdict: 'block',
27
+ reason: args.reason,
28
+ hit_pattern: args.hitPattern,
29
+ detected_form: args.detectedForm,
30
+ ...(args.sourcePosition !== undefined ? { source_position: args.sourcePosition } : {}),
31
+ };
32
+ }
33
+ /**
34
+ * Construct a uniform block verdict for a parse-failure event. We
35
+ * always block — the alternative is "scanner can't tell, assume safe"
36
+ * which is the entire bug class this rewrite exists to close.
37
+ *
38
+ * `parserMessage` flows through to the operator. We DO NOT sanitize
39
+ * it — the parser's messages are static (no user-controlled
40
+ * interpolation in modern mvdan-sh) and including them helps debug
41
+ * malformed payloads in the field.
42
+ */
43
+ export function parseFailureVerdict(parserMessage) {
44
+ return {
45
+ verdict: 'block',
46
+ reason: 'rea: bash parser failed; refusing on uncertainty',
47
+ parse_failure_reason: `parser: ${parserMessage}`,
48
+ };
49
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * AST walker — given a parsed `BashFile`, yield every detected write
3
+ * target with its form classification and source position.
4
+ *
5
+ * The walker is the closed surface this scanner is built around. Every
6
+ * regex/heuristic the bash gates used (and there were many; see
7
+ * `hooks/_lib/cmd-segments.sh` and `hooks/_lib/interpreter-scanner.sh`
8
+ * pre-0.23.0) is replaced here by an AST-driven match. The argument
9
+ * grammar is already correct because the parser rebuilt the tree from
10
+ * shell tokenization rules; we never re-tokenize a string.
11
+ *
12
+ * Detection forms covered (matching `verdict.ts::DetectedForm`):
13
+ *
14
+ * - `redirect` — Stmt-level Redirs whose Op is a write
15
+ * - `cp_dest` / `cp_t_flag` — POSIX-cp tail destination, plus -t / --target-directory
16
+ * - `mv_dest` / `mv_t_flag` — same for mv
17
+ * - `tee_arg` — every non-flag arg to tee
18
+ * - `sed_i` — sed -i / -i'' /-iEXT trailing target
19
+ * - `dd_of` — `of=` named arg
20
+ * - `truncate_arg` — first non-flag arg
21
+ * - `install_dest` / `ln_dest` — last positional
22
+ * - `awk_inplace` — awk/gawk -i inplace target
23
+ * - `ed_target` / `ex_target` — first non-flag positional
24
+ * - `find_exec_inner` — recurse the inner -exec / -execdir / -ok cmd
25
+ * - `xargs_unresolvable` — xargs is destination-via-stdin; refuse
26
+ * - `node_e_path` — node -e payload string-scanned for fs.write*
27
+ * - `python_c_path` — python -c payload scanned for open(...,'w'/'a')
28
+ * - `ruby_e_path` — ruby -e File.write/.open(...'w')
29
+ * - `perl_e_path` — perl -e open(FH,'>FILE')
30
+ * - `process_subst_inner` — recurse `>(...)` / `<(...)` inner stmts
31
+ * - `nested_shell_inner` — recurse bash -c / sh -c / zsh -c payloads
32
+ *
33
+ * Dynamic targets (containing $VAR, `cmd`, $(cmd), arithmetic, etc.)
34
+ * are emitted with `dynamic: true`. The compositor refuses on dynamic
35
+ * by default — fail-closed parity with the 0.21.2/0.22.0 sentinel
36
+ * `__rea_unresolved_expansion__`. Process substitutions and nested
37
+ * shells are NOT considered dynamic targets themselves; they're
38
+ * recursed.
39
+ */
40
+ import type { BashFile } from 'mvdan-sh';
41
+ import type { DetectedForm, SourcePosition } from './verdict.js';
42
+ /**
43
+ * One detected write. The compositor pairs `path` against the policy
44
+ * (protected or blocked) to decide allow/block.
45
+ *
46
+ * `dynamic: true` means the target's value depends on shell expansion
47
+ * (`$VAR`, `$(cmd)`, backticks, arithmetic, brace expansion) we did not
48
+ * fully resolve. Fail-closed semantics: the compositor always BLOCKS
49
+ * dynamic targets. The detected_form on a dynamic emit is whatever
50
+ * shape was in argv position.
51
+ *
52
+ * `isDirTarget: true` means the target is semantically a directory —
53
+ * `cp -t DIR ...` / `cp --target-directory=DIR ...` / `install -t DIR
54
+ * ...` / `mv -t DIR ...` / `ln -t DIR ...`. The matcher treats dir-
55
+ * targets as `<DIR>/`-shaped: writes INTO that directory may hit any
56
+ * file under it, so a protected file inside the dir matches even when
57
+ * the input lacks a trailing slash. Codex round 1 F-7.
58
+ *
59
+ * `originSrc` is the bash source-substring for the offending node —
60
+ * useful when the operator-facing error message wants to show
61
+ * "Segment: ..." like the bash gates did. It is NOT guaranteed
62
+ * verbatim from the input (the parser may normalize whitespace);
63
+ * treat it as best-effort.
64
+ */
65
+ export interface DetectedWrite {
66
+ path: string;
67
+ form: DetectedForm;
68
+ position: SourcePosition;
69
+ dynamic: boolean;
70
+ /**
71
+ * The target is a directory (the write semantics are "into this
72
+ * directory"). Set on `cp -t`, `mv -t`, `install -t`, `ln -t`,
73
+ * `--target-directory=`. False for ordinary file destinations.
74
+ */
75
+ isDirTarget?: boolean;
76
+ /**
77
+ * The detection is a destructive operation (recursive removal,
78
+ * unlink, rmdir, find -delete, FileUtils.rm_rf, shutil.rmtree, etc).
79
+ * Codex round 4 Finding 1: when set, a target that is an ANCESTOR
80
+ * directory of any protected file matches via protected-ancestry —
81
+ * `rm -rf .rea` is treated as a write to every file under .rea/.
82
+ * Without this flag, the scanner only matches exact patterns or
83
+ * dir-shape inputs (-t flags, trailing slash); plain `.rea` argv
84
+ * positionals walked unchecked because they are neither.
85
+ */
86
+ isDestructive?: boolean;
87
+ originSrc?: string;
88
+ }
89
+ /**
90
+ * Walk the AST and return every detected write.
91
+ *
92
+ * 0.23.0 round-6 architectural refactor: deny-by-default generic
93
+ * `syntax.Walk()` traversal. Pre-refactor the walker dispatched on
94
+ * specific Cmd kinds (`case 'WhileClause':`, `case 'ForClause':`,
95
+ * `case 'DeclClause':`, etc.) and manually traversed each kind's
96
+ * specific fields. Any field NOT enumerated in the case branch was
97
+ * silently dropped — that pattern produced six rounds of P0 bypasses
98
+ * (DeclClause.Args round 5, CaseClause.Word round 5, WhileClause.Cond
99
+ * round 6, ForClause.CStyleLoop.Init/Cond/Post round 6, etc).
100
+ *
101
+ * The round-6 design closes OUR-DISPATCH field-omission structurally.
102
+ * We use `mvdan-sh`'s built-in `syntax.Walk(node, visit)` for
103
+ * traversal — every Cmd kind's inner Stmts / CallExprs / BinaryCmds
104
+ * reach our dispatcher when Walk descends into them. Our dispatch is
105
+ * preserved (per-utility cp/mv/sed/find/etc.), but the TRAVERSAL is
106
+ * no longer a denylist of OUR shapes. A new mvdan-sh Cmd type, or a
107
+ * new field on an existing Cmd, automatically gets visited from our
108
+ * side.
109
+ *
110
+ * 0.23.0 round-7 P0 closure: the round-6 framing "Walk visits every
111
+ * field" was OVERCLAIM. mvdan-sh@0.10.1's `syntax.Walk` itself has
112
+ * field gaps. Empirically verified (see `walk-probe-slice.mjs`):
113
+ * `ParamExp.Slice.Offset` and `ParamExp.Slice.Length` (Word nodes)
114
+ * are NOT recursed into by Walk. Pre-fix every `${X:$(...)}` /
115
+ * `${X:0:$(...)}` / `${arr[@]:$(...)}` / `${@:$(...)}` form bypassed
116
+ * every detector — a regression vs 0.22.0's bash regex.
117
+ *
118
+ * Round-7 fix: this function declares its visitor up front and
119
+ * manually re-enters `syntax.Walk` on the missed Slice subtrees via
120
+ * `recurseParamExpSlice` whenever the visitor sees a `ParamExp`. The
121
+ * re-entry uses the SAME visitor, so nested forms (e.g.
122
+ * `${X:${Y:$(rm)}}`) recurse to fixed point.
123
+ *
124
+ * Round-7 structural pin: the Class O exhaustiveness contract test
125
+ * (`__tests__/hooks/bash-scanner/walker-exhaustiveness.contract.test.ts`)
126
+ * names every (node-type, field) Word-bearing position mvdan-sh
127
+ * populates and asserts the walker reaches each one. If
128
+ * mvdan-sh@0.11.0+ adds a new node type or field that Walk skips,
129
+ * that test fails CI before any runtime regression — the fix is
130
+ * always a one-line manual recursion in the visit callback below.
131
+ *
132
+ * What this closes by construction:
133
+ * - WhileClause / UntilClause `.Cond` → visited (round 6)
134
+ * - ForClause `.CStyleLoop.{Init,Cond,Post}` → visited (round 6)
135
+ * - DeclClause `.Args[*].Value` → visited (round 5 redux)
136
+ * - CaseClause `.Word` and `.Items[*].Patterns` → visited (round 5 redux)
137
+ * - TestClause arbitrary nesting → visited
138
+ * - ArithmCmd / LetClause / SelectClause → visited
139
+ * - Stmt.Redirs[*].Word with embedded CmdSubst → visited
140
+ * - Function bodies, if/else branches, subshells → visited
141
+ * - Anything we add in mvdan-sh@0.11.0+ → reaches our dispatcher
142
+ * IF Walk visits it; if Walk skips it, Class O fails CI first
143
+ *
144
+ * What we still maintain explicitly (because they require argv-level
145
+ * inspection or string-level re-parse):
146
+ * - CallExpr → detector dispatch on the command head
147
+ * - Stmt-level redirects (`>`, `>>`, etc.) → emit per redirect Op
148
+ * - BinaryCmd pipe-into-bare-shell detection
149
+ * - heredoc-into-shell payload re-parse
150
+ * - eval / trap / nested-shell payload re-parse
151
+ * - ParamExp.Slice.{Offset,Length} → manual Walk re-entry
152
+ * (round 7 P0 — Walk's own gap, NOT our dispatch gap)
153
+ *
154
+ * The walker NEVER throws on shape oddities — it gracefully ignores
155
+ * nodes whose shape doesn't match what we expect. Parse-level failures
156
+ * are upstream (the parser wrapper). Walker-level failures would be a
157
+ * bug in this file; defensively continue.
158
+ *
159
+ * Performance: O(N) over AST nodes; allocation-light. `syntax.Walk`
160
+ * is a pure-traversal pass that calls our visitor once per node.
161
+ * Visited-node-set tracking is unnecessary because mvdan-sh's
162
+ * tree is acyclic by construction (re-parsed payloads are separate
163
+ * BashFile trees walked via fresh `walkForWrites` invocations).
164
+ */
165
+ export declare function walkForWrites(file: BashFile): DetectedWrite[];