@bookedsolid/rea 0.37.0 → 0.38.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 (35) hide show
  1. package/hooks/_lib/shim-runtime.sh +405 -0
  2. package/hooks/architecture-review-gate.sh +11 -103
  3. package/hooks/attribution-advisory.sh +38 -209
  4. package/hooks/blocked-paths-bash-gate.sh +32 -146
  5. package/hooks/blocked-paths-enforcer.sh +32 -137
  6. package/hooks/changeset-security-gate.sh +26 -119
  7. package/hooks/dangerous-bash-interceptor.sh +46 -170
  8. package/hooks/delegation-advisory.sh +26 -144
  9. package/hooks/delegation-capture.sh +33 -139
  10. package/hooks/dependency-audit-gate.sh +29 -121
  11. package/hooks/env-file-protection.sh +30 -141
  12. package/hooks/local-review-gate.sh +117 -352
  13. package/hooks/pr-issue-link-gate.sh +16 -118
  14. package/hooks/protected-paths-bash-gate.sh +53 -152
  15. package/hooks/secret-scanner.sh +90 -213
  16. package/hooks/security-disclosure-gate.sh +32 -155
  17. package/hooks/settings-protection.sh +56 -176
  18. package/package.json +1 -1
  19. package/templates/_lib_shim-runtime.dogfood-staged.sh +405 -0
  20. package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
  21. package/templates/attribution-advisory.dogfood-staged.sh +38 -209
  22. package/templates/blocked-paths-bash-gate.dogfood-staged.sh +32 -146
  23. package/templates/blocked-paths-enforcer.dogfood-staged.sh +32 -137
  24. package/templates/changeset-security-gate.dogfood-staged.sh +26 -119
  25. package/templates/dangerous-bash-interceptor.dogfood-staged.sh +46 -170
  26. package/templates/delegation-advisory.dogfood-staged.sh +44 -0
  27. package/templates/delegation-capture.dogfood-staged.sh +52 -0
  28. package/templates/dependency-audit-gate.dogfood-staged.sh +29 -121
  29. package/templates/env-file-protection.dogfood-staged.sh +30 -141
  30. package/templates/local-review-gate.dogfood-staged.sh +117 -352
  31. package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
  32. package/templates/protected-paths-bash-gate.dogfood-staged.sh +53 -152
  33. package/templates/secret-scanner.dogfood-staged.sh +90 -213
  34. package/templates/security-disclosure-gate.dogfood-staged.sh +32 -155
  35. package/templates/settings-protection.dogfood-staged.sh +56 -176
@@ -1,229 +1,58 @@
1
1
  #!/bin/bash
2
2
  # PreToolUse hook: attribution-advisory.sh
3
3
  # 0.32.0+ — Node-binary shim for `rea hook attribution-advisory`.
4
+ # 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
4
5
  #
5
- # Pre-0.32.0 the gate's full body lived here as bash (162 LOC,
6
- # including the AI-attribution pattern catalog and segment-relevance
7
- # gating). The migration to the parser-backed Node binary moves all
8
- # of that into `src/hooks/attribution-advisory/index.ts`. This shim
9
- # is the Claude Code dispatcher's view of the hook — it forwards
10
- # stdin to the CLI and exits with whatever the CLI returns.
6
+ # Blocking-tier when `policy.block_ai_attribution: true`. Pre-port body
7
+ # was 162 LOC; full migration in `src/hooks/attribution-advisory/index.ts`.
11
8
  #
12
- # Behavioral contract is preserved byte-for-byte: exit 0 on
13
- # disabled-policy / non-relevant / clean-command, exit 2 on HALT /
14
- # attribution detected / malformed payload (fail-closed).
9
+ # # Relevance pre-gate
15
10
  #
16
- # # CLI-resolution trust boundary
11
+ # Substring match for `git commit` or `gh pr create|edit` ANYWHERE in
12
+ # the command string (allow shell prefixes). Plain substring scan is
13
+ # used instead of JSON-aware regex because escaped quotes in quoted
14
+ # env prefixes (`MODE="x" gh pr create …`) trip JSON-anchored patterns.
15
+ # Over-trigger costs one CLI spawn; the Node body handles correctness.
17
16
  #
18
- # Codex round 1 P1 (2026-05-15): realpath sandbox check + version
19
- # probe. Mirrors delegation-advisory.sh §3. Defends against
20
- # symlink-out + tarball-replacement attacks on the resolved CLI AND
21
- # stale-node_modules version skew that would otherwise turn every
22
- # Bash dispatch into a hard failure.
17
+ # # Policy short-circuit (codex round 2 P1 from 0.37.0)
23
18
  #
24
- # Codex round 2 P1 (2026-05-16): the sandbox check now runs BEFORE
25
- # the policy read. The pre-round-2 order called
26
- # `policy_reader_get block_ai_attribution` first; that read invokes
27
- # the resolved CLI through Tier 1 of the unified reader — meaning an
28
- # unsandboxed CLI executed BEFORE the sandbox guard fired. Fixed by
29
- # validating sandbox first; on failure REA_ARGV is cleared so the
30
- # reader degrades to Tier 2 / Tier 3 (both pure file-parse, no
31
- # arbitrary-code-execution).
19
+ # The block_ai_attribution policy read runs AFTER the sandbox check so
20
+ # REA_ARGV is trusted for Tier-1 reads. When the policy is disabled,
21
+ # exit 0 cleanly the pre-port bash body no-op'd when the key was
22
+ # absent or false.
32
23
 
33
24
  set -uo pipefail
34
25
 
35
- # 1. HALT check.
36
26
  # shellcheck source=_lib/halt-check.sh
37
27
  source "$(dirname "$0")/_lib/halt-check.sh"
38
28
  check_halt
39
29
  REA_ROOT=$(rea_root)
40
30
 
41
- proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
31
+ SHIM_NAME="attribution-advisory"
32
+ SHIM_INTRODUCED_IN="0.32.0"
33
+ SHIM_FAIL_OPEN=0
34
+ SHIM_REFUSAL_NOUN="attribution-policy enforcement"
42
35
 
43
- # 2. Relevance pre-gate (0.32.0 round-5 P1, round-6 fix). PreToolUse
44
- # Bash matchers fire on EVERY shell command, but this hook only
45
- # enforces against `git commit` / `gh pr create|edit`. Capture
46
- # stdin + check relevance FIRST so unrelated commands (ls,
47
- # pnpm test, …) exit 0 even when the CLI is missing/stale/
48
- # sandboxed-out.
49
- #
50
- # Match the pattern ANYWHERE in the command string (after the
51
- # opening quote, then `[^"]*` for any leading shell prefix —
52
- # `sudo`, `time`, env assignments like `FOO=x git commit …`).
53
- # Round-6 P1: prior round-5 pattern anchored at the start of the
54
- # JSON value and missed all prefixed forms.
55
- INPUT=$(cat)
56
- # Substring scan (NOT JSON-aware). Round-7 P2: any JSON-aware regex
57
- # anchored on `"command":"...` gets tripped by escaped quotes in
58
- # quoted env prefixes (`FOO="two words" git commit …` → the payload
59
- # carries `\"two words\"` and `[^"]*` stops at the escaped quote).
60
- # Plain substring match has no such edge: it over-triggers only on
61
- # the rare case where the pattern appears inside a quoted argument
62
- # (`echo "gh pr create"`), and the Node body handles that correctly.
63
- # This hook only fires on `tool_name=Bash`, so we don't risk matching
64
- # unrelated payload shapes.
65
- RELEVANT=0
66
- if printf '%s' "$INPUT" | grep -qE '(git[[:space:]]+commit|gh[[:space:]]+pr[[:space:]]+(create|edit))'; then
67
- RELEVANT=1
68
- fi
69
- if [ "$RELEVANT" -eq 0 ]; then
70
- # Irrelevant Bash call — nothing the pre-0.32.0 body would have
71
- # processed. Always exit 0 regardless of CLI state.
72
- exit 0
73
- fi
74
-
75
- # 2b. Resolve the rea CLI first — the unified policy reader uses
76
- # REA_ARGV (when populated) as its Tier 1 source. Reordered from
77
- # the pre-0.37.0 shape (where the CLI was resolved AFTER the policy
78
- # grep) so the policy short-circuit below can route through Tier 1
79
- # when the CLI is reachable, falling through Tier 2 (python3 +
80
- # PyYAML) and Tier 3 (awk block-form) on stale/unbuilt installs.
81
- REA_ARGV=()
82
- RESOLVED_CLI_PATH=""
83
- if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
84
- REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
85
- RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
86
- elif [ -f "$proj/dist/cli/index.js" ]; then
87
- REA_ARGV=(node "$proj/dist/cli/index.js")
88
- RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
89
- fi
90
-
91
- # 2c. Realpath sandbox check — MUST run BEFORE any policy read that
92
- # could route through Tier 1 (CLI). Codex round 2 P1: previously
93
- # the policy_reader_get call below executed the resolved CLI to
94
- # read `block_ai_attribution`. An attacker who symlinked
95
- # dist/cli/index.js → /tmp/forged-tree (or who otherwise compromised
96
- # the path) would have their forged CLI invoked during policy
97
- # lookup BEFORE the sandbox check ran — defeating the trust
98
- # boundary this shim is supposed to enforce.
99
- #
100
- # Fix: validate the CLI's sandbox shape first. On failure, clear
101
- # REA_ARGV so the unified policy reader falls back to Tier 2
102
- # (python3 + PyYAML) / Tier 3 (awk block-form) and the unsafe CLI
103
- # never runs. The shim then re-evaluates fail-closed posture at
104
- # §2e below (CLI absent + attribution-enabled → exit 2).
105
- #
106
- # Other 5 migrated shims (e.g. delegation-advisory) naturally avoid
107
- # this ordering bug because their policy reads are NESTED inside
108
- # `if [ "${#REA_ARGV[@]}" -eq 0 ]` (the CLI-absent path). This
109
- # shim's flow is different — it reads policy unconditionally to
110
- # decide whether to fail-closed at all.
111
- SANDBOX_CHECK_RESULT=""
112
- if [ "${#REA_ARGV[@]}" -gt 0 ]; then
113
- if ! command -v node >/dev/null 2>&1; then
114
- # No node on PATH — cannot run sandbox probe. Clear REA_ARGV so
115
- # the policy reader skips Tier 1; later §2e will catch the
116
- # CLI-required-but-absent state and refuse explicitly.
117
- SANDBOX_CHECK_RESULT="bad:no-node"
118
- REA_ARGV=()
119
- else
120
- SANDBOX_CHECK_RESULT=$(node -e '
121
- const fs = require("fs");
122
- const path = require("path");
123
- const cli = process.argv[1];
124
- const projDir = process.argv[2];
125
- let real, realProj;
126
- try { real = fs.realpathSync(cli); } catch (e) {
127
- process.stdout.write("bad:realpath"); process.exit(1);
128
- }
129
- try { realProj = fs.realpathSync(projDir); } catch (e) {
130
- process.stdout.write("bad:realpath-proj"); process.exit(1);
131
- }
132
- const sep = path.sep;
133
- const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
134
- if (!(real === realProj || real.startsWith(projWithSep))) {
135
- process.stdout.write("bad:cli-escapes-project"); process.exit(1);
136
- }
137
- let cur = path.dirname(path.dirname(path.dirname(real)));
138
- let found = false;
139
- for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
140
- const pj = path.join(cur, "package.json");
141
- if (fs.existsSync(pj)) {
142
- try {
143
- const data = JSON.parse(fs.readFileSync(pj, "utf8"));
144
- if (data && data.name === "@bookedsolid/rea") { found = true; break; }
145
- } catch (e) { /* keep walking */ }
146
- }
147
- cur = path.dirname(cur);
148
- }
149
- if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
150
- process.stdout.write("ok");
151
- ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
152
- if [ "$SANDBOX_CHECK_RESULT" != "ok" ]; then
153
- # Sandbox failed — drop the unsafe CLI from REA_ARGV BEFORE
154
- # reading policy. The unified reader will degrade to Tier 2 /
155
- # Tier 3, both of which only read the policy file (no
156
- # arbitrary-code-execution risk).
157
- REA_ARGV=()
158
- fi
36
+ shim_is_relevant() {
37
+ if printf '%s' "$INPUT" | grep -qE '(git[[:space:]]+commit|gh[[:space:]]+pr[[:space:]]+(create|edit))'; then
38
+ return 0
159
39
  fi
160
- fi
161
-
162
- # 2d. Policy short-circuit (round-6 P2, generalized in 0.37.0). The
163
- # pre-0.32.0 bash body no-op'd when `block_ai_attribution` was
164
- # absent or false. Without this check, an unbuilt/stale install
165
- # would refuse `git commit` even on repos that DELIBERATELY
166
- # disable the attribution gate.
167
- #
168
- # 0.37.0: route through `policy_reader_get` (4-tier ladder). The
169
- # pre-0.37.0 grep matched ONLY block-form `block_ai_attribution:
170
- # true`; inline-form (`block_ai_attribution: true` at any nesting
171
- # accident) and quoted-form variants were missed. The reader's
172
- # Tier 1 / Tier 2 paths handle both forms identically to the
173
- # canonical TS loader; Tier 3 preserves the pre-0.37.0 block-only
174
- # posture as a graceful-degrade fallback.
175
- #
176
- # Codex round 2 P1: by the time we reach this line, REA_ARGV is
177
- # EITHER (a) populated and sandbox-validated, OR (b) empty — never
178
- # populated-but-untrusted. The policy reader can safely use Tier 1.
179
- # shellcheck source=_lib/policy-reader.sh
180
- source "$(dirname "$0")/_lib/policy-reader.sh"
181
- ATTR_ENABLED=$(policy_reader_get block_ai_attribution)
182
- if [ "$ATTR_ENABLED" != "true" ]; then
183
- # Attribution blocking disabled (or unreadable on Tier 3 fallback +
184
- # missing policy file) — pre-0.32.0 bash body would have exited 0
185
- # here. Don't refuse on stale-install grounds.
186
- exit 0
187
- fi
188
-
189
- # 2e. CLI required from here on — we need the parser-backed Node binary
190
- # to scan for attribution patterns. If REA_ARGV is empty because
191
- # either (a) the CLI wasn't installed/built or (b) sandbox check
192
- # failed and we cleared it above, refuse explicitly with a tailored
193
- # message.
194
- if [ "${#REA_ARGV[@]}" -eq 0 ]; then
195
- if [ -n "$SANDBOX_CHECK_RESULT" ] && [ "$SANDBOX_CHECK_RESULT" != "ok" ]; then
196
- # Sandbox failure path — preserve forensic detail.
197
- printf 'rea: attribution-advisory FAILED sandbox check (%s) — refusing.\n' "$SANDBOX_CHECK_RESULT" >&2
198
- exit 2
40
+ return 1
41
+ }
42
+
43
+ shim_policy_short_circuit() {
44
+ # shellcheck source=_lib/policy-reader.sh
45
+ source "$(dirname "$0")/_lib/policy-reader.sh"
46
+ local attr_enabled
47
+ attr_enabled=$(policy_reader_get block_ai_attribution)
48
+ if [ "$attr_enabled" != "true" ]; then
49
+ # Attribution blocking disabled (or unreadable on Tier 3 fallback +
50
+ # missing policy file) pre-port body exit 0.
51
+ return 0
199
52
  fi
200
- # 0.32.0 round-4 P2: when `block_ai_attribution: true`, this hook is
201
- # blocking-tier — the pre-0.32.0 bash body enforced the policy
202
- # without a compiled CLI. Falling through to exit 0 would silently
203
- # let AI-attribution patterns through every git commit / gh pr
204
- # create-or-edit until the operator rebuilds. Fail closed and tell
205
- # the operator how to restore protection.
206
- printf 'rea: attribution-advisory cannot run — the rea CLI is not built.\n' >&2
207
- printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
208
- printf 'This shim fails closed because the pre-0.32.0 bash body enforced attribution policy without a CLI.\n' >&2
209
- exit 2
210
- fi
211
-
212
- # 3. Version-probe: confirm the resolved CLI implements
213
- # `hook attribution-advisory`. Codex round 1 P1.
214
- probe_out=$("${REA_ARGV[@]}" hook attribution-advisory --help 2>&1)
215
- probe_status=$?
216
- if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'attribution-advisory'; then
217
- # 0.32.0 round-4 P2: stale/older CLI without the new subcommand is
218
- # NOT advisory-tier fall-through — the bash body it replaces
219
- # enforced when policy enabled. Fail closed and tell the operator
220
- # exactly how to fix.
221
- printf 'rea: this shim requires the `rea hook attribution-advisory` subcommand (introduced in 0.32.0).\n' >&2
222
- printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
223
- printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
224
- exit 2
225
- fi
53
+ return 1
54
+ }
226
55
 
227
- # 4. Forward stdin (already captured up-front for the relevance gate).
228
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook attribution-advisory
229
- exit $?
56
+ # shellcheck source=_lib/shim-runtime.sh
57
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
58
+ shim_run
@@ -1,177 +1,63 @@
1
1
  #!/bin/bash
2
2
  # PreToolUse hook: blocked-paths-bash-gate.sh
3
3
  # 0.35.0+ — Node-binary shim for `rea hook blocked-paths-bash-gate`.
4
+ # 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
4
5
  #
5
- # Pre-0.35.0 this was a thin bash shim over `rea hook scan-bash --mode
6
- # blocked` (the parser-backed AST walker that closes 9 bypass classes
7
- # from helix-023 + discord-ops Round 13 — see `src/hooks/bash-scanner/`).
8
- # The full bash body is preserved at
6
+ # Tier-1 Bash gate. Full bash body preserved at
9
7
  # `__tests__/hooks/parity/baselines/blocked-paths-bash-gate.sh.pre-0.35.0`.
8
+ # Migration lives in `src/hooks/blocked-paths-bash-gate/index.ts`.
10
9
  #
11
- # This shim now resolves the CLI through the same 2-tier sandboxed
12
- # resolver as the 0.32.0+ pilots and calls `rea hook blocked-paths-
13
- # bash-gate` directly — eliminating the shim → CLI → scanner-module
14
- # subprocess hop entirely.
10
+ # SHIM_ENFORCE_CLI_SHAPE=1: 0.35.0 codex round-1 P1 enforce
11
+ # dist/cli/index.js shape on the resolved CLI.
15
12
  #
16
- # Behavioral contract is preserved byte-for-byte: exit 0 on allow,
17
- # exit 2 on HALT / verdict block / malformed payload / sandbox fail.
13
+ # # Relevance pre-gate (CLI-missing only)
18
14
  #
19
- # # CLI-resolution trust boundary
20
- #
21
- # Mirrors the 0.32.0 final shim shape. The resolved CLI MUST live
22
- # INSIDE realpath(CLAUDE_PROJECT_DIR) AND have an ancestor
23
- # `package.json` whose `name` is `@bookedsolid/rea`. Defends against
24
- # symlink-out and tarball-replacement attacks on the resolved CLI.
25
- #
26
- # # Fail-closed posture
27
- #
28
- # blocked-paths-bash-gate is a Tier-1 security gate (PreToolUse Bash).
29
- # The pre-0.35.0 bash body refused on uncertainty for every failure
30
- # class. Early-exit branches (CLI missing, node missing, sandbox failed,
31
- # version skew) fail closed AFTER the relevance pre-gate passes.
32
- # Irrelevant Bash calls exit 0 regardless of CLI state.
33
- #
34
- # # Relevance pre-gate
35
- #
36
- # Same posture as 0.34.0 dangerous-bash + secret-scanner. When the CLI
37
- # is missing, refuse only when the extracted command MENTIONS a path
38
- # from `policy.blocked_paths`. Empty policy → no enforcement, exit 0.
39
- # This unblocks the install path itself: `npx rea init`, pre-`pnpm build`
40
- # checkouts can still run benign Bash like `ls`/`mkdir`/`pnpm install`.
15
+ # Substring scan over the extracted command against any
16
+ # policy.blocked_paths entry. Empty/missing policy → no enforcement,
17
+ # exit 0 (matches the pre-port bash body's allow-on-no-policy posture).
41
18
 
42
19
  set -uo pipefail
43
20
 
44
- # 1. HALT check.
45
21
  # shellcheck source=_lib/halt-check.sh
46
22
  source "$(dirname "$0")/_lib/halt-check.sh"
47
23
  check_halt
48
24
  REA_ROOT=$(rea_root)
49
25
 
50
- proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
51
-
52
- # 2. Capture stdin once.
53
- INPUT=$(cat)
26
+ SHIM_NAME="blocked-paths-bash-gate"
27
+ SHIM_INTRODUCED_IN="0.35.0"
28
+ SHIM_FAIL_OPEN=0
29
+ SHIM_ENFORCE_CLI_SHAPE=1
30
+ SHIM_REFUSAL_NOUN="blocked_paths refusal"
54
31
 
55
- # 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
56
- REA_ARGV=()
57
- RESOLVED_CLI_PATH=""
58
- if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
59
- REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
60
- RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
61
- elif [ -f "$proj/dist/cli/index.js" ]; then
62
- REA_ARGV=(node "$proj/dist/cli/index.js")
63
- RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
64
- fi
65
-
66
- # 3b. Relevance pre-gate. Only used when the CLI is missing.
67
- if [ "${#REA_ARGV[@]}" -eq 0 ]; then
68
- CLI_MISSING_CMD=""
32
+ shim_cli_missing_relevant() {
33
+ local cli_missing_cmd=""
69
34
  if command -v jq >/dev/null 2>&1; then
70
- CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
35
+ cli_missing_cmd=$(printf '%s' "$INPUT" | jq -r '
71
36
  (.tool_input.command // "") | tostring
72
37
  ' 2>/dev/null || true)
73
38
  else
74
- CLI_MISSING_CMD="$INPUT"
39
+ cli_missing_cmd="$INPUT"
75
40
  fi
76
- if [ -z "$CLI_MISSING_CMD" ]; then
77
- # Empty/non-Bash payload → pre-0.35.0 body would have exited 0.
78
- exit 0
41
+ if [ -z "$cli_missing_cmd" ]; then
42
+ return 1
79
43
  fi
80
- # Empty policy.blocked_paths → no enforcement, exit 0.
81
- POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
82
- if [ ! -f "$POLICY_FILE" ]; then
83
- exit 0
44
+ local policy_file="${REA_ROOT}/.rea/policy.yaml"
45
+ if [ ! -f "$policy_file" ]; then
46
+ return 1
84
47
  fi
85
- # 0.37.0: route blocked_paths reads through the unified
86
- # policy-reader (Tier 1 CLI → Tier 2 python3 → Tier 3 awk
87
- # block-form). Pre-0.37.0 the per-shim awk parser missed flow-form
88
- # arrays (`blocked_paths: [.env, .env.*, ...]`), silently exiting 0
89
- # on relevant Bash calls when the CLI was unreachable. The
90
- # 4-tier ladder closes that bypass via Tier 2 whenever python3 +
91
- # PyYAML are available (common on macOS Homebrew + most Linux
92
- # distros); falls through to Tier 3 (block-form only) otherwise.
48
+ # 0.37.0: route blocked_paths reads through the unified policy-reader.
93
49
  # shellcheck source=_lib/policy-reader.sh
94
50
  source "$(dirname "$0")/_lib/policy-reader.sh"
95
- # Substring scan: does the command mention any blocked_paths entry?
96
- # Coarse — over-trigger is fine, under-trigger is the bypass we MUST
97
- # avoid.
98
- CLI_MISSING_RELEVANT=0
51
+ local entry
99
52
  while IFS= read -r entry; do
100
53
  [ -z "$entry" ] && continue
101
- case "$CLI_MISSING_CMD" in
102
- *"$entry"*) CLI_MISSING_RELEVANT=1; break ;;
54
+ case "$cli_missing_cmd" in
55
+ *"$entry"*) return 0 ;;
103
56
  esac
104
57
  done < <(policy_reader_get_list blocked_paths 2>/dev/null)
105
- if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
106
- exit 0
107
- fi
108
- printf 'rea: blocked-paths-bash-gate cannot run — the rea CLI is not built.\n' >&2
109
- printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
110
- printf 'This shim fails closed because the pre-0.35.0 bash body enforced blocked_paths refusal without a CLI.\n' >&2
111
- exit 2
112
- fi
113
-
114
- # 4. Realpath sandbox check.
115
- if ! command -v node >/dev/null 2>&1; then
116
- printf 'rea: blocked-paths-bash-gate cannot run — `node` is not on PATH.\n' >&2
117
- printf 'Install Node 22+ (engines.node) to restore blocked_paths refusal.\n' >&2
118
- exit 2
119
- fi
120
-
121
- sandbox_check=$(node -e '
122
- const fs = require("fs");
123
- const path = require("path");
124
- const cli = process.argv[1];
125
- const projDir = process.argv[2];
126
- let real, realProj;
127
- try { real = fs.realpathSync(cli); } catch (e) {
128
- process.stdout.write("bad:realpath"); process.exit(1);
129
- }
130
- try { realProj = fs.realpathSync(projDir); } catch (e) {
131
- process.stdout.write("bad:realpath-proj"); process.exit(1);
132
- }
133
- const sep = path.sep;
134
- const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
135
- if (!(real === realProj || real.startsWith(projWithSep))) {
136
- process.stdout.write("bad:cli-escapes-project"); process.exit(1);
137
- }
138
- // Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
139
- // settings-protection.sh).
140
- const expectedEnd = path.join("dist", "cli", "index.js");
141
- if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
142
- process.stdout.write("bad:cli-shape"); process.exit(1);
143
- }
144
- let cur = path.dirname(path.dirname(path.dirname(real)));
145
- let found = false;
146
- for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
147
- const pj = path.join(cur, "package.json");
148
- if (fs.existsSync(pj)) {
149
- try {
150
- const data = JSON.parse(fs.readFileSync(pj, "utf8"));
151
- if (data && data.name === "@bookedsolid/rea") { found = true; break; }
152
- } catch (e) { /* keep walking */ }
153
- }
154
- cur = path.dirname(cur);
155
- }
156
- if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
157
- process.stdout.write("ok");
158
- ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
159
-
160
- if [ "$sandbox_check" != "ok" ]; then
161
- printf 'rea: blocked-paths-bash-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
162
- exit 2
163
- fi
164
-
165
- # 5. Version-probe.
166
- probe_out=$("${REA_ARGV[@]}" hook blocked-paths-bash-gate --help 2>&1)
167
- probe_status=$?
168
- if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'blocked-paths-bash-gate'; then
169
- printf 'rea: this shim requires the `rea hook blocked-paths-bash-gate` subcommand (introduced in 0.35.0).\n' >&2
170
- printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
171
- printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
172
- exit 2
173
- fi
58
+ return 1
59
+ }
174
60
 
175
- # 6. Forward stdin (already captured up-front).
176
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook blocked-paths-bash-gate
177
- exit $?
61
+ # shellcheck source=_lib/shim-runtime.sh
62
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
63
+ shim_run