@bookedsolid/rea 0.36.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 (37) hide show
  1. package/hooks/_lib/policy-reader.sh +948 -0
  2. package/hooks/_lib/shim-runtime.sh +405 -0
  3. package/hooks/architecture-review-gate.sh +11 -103
  4. package/hooks/attribution-advisory.sh +43 -155
  5. package/hooks/blocked-paths-bash-gate.sh +35 -149
  6. package/hooks/blocked-paths-enforcer.sh +35 -140
  7. package/hooks/changeset-security-gate.sh +26 -119
  8. package/hooks/dangerous-bash-interceptor.sh +46 -170
  9. package/hooks/delegation-advisory.sh +26 -144
  10. package/hooks/delegation-capture.sh +33 -139
  11. package/hooks/dependency-audit-gate.sh +29 -121
  12. package/hooks/env-file-protection.sh +30 -141
  13. package/hooks/local-review-gate.sh +191 -396
  14. package/hooks/pr-issue-link-gate.sh +16 -118
  15. package/hooks/protected-paths-bash-gate.sh +57 -160
  16. package/hooks/secret-scanner.sh +90 -213
  17. package/hooks/security-disclosure-gate.sh +32 -155
  18. package/hooks/settings-protection.sh +56 -179
  19. package/package.json +1 -1
  20. package/templates/_lib_policy-reader.dogfood-staged.sh +948 -0
  21. package/templates/_lib_shim-runtime.dogfood-staged.sh +405 -0
  22. package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
  23. package/templates/attribution-advisory.dogfood-staged.sh +43 -155
  24. package/templates/blocked-paths-bash-gate.dogfood-staged.sh +35 -149
  25. package/templates/blocked-paths-enforcer.dogfood-staged.sh +35 -140
  26. package/templates/changeset-security-gate.dogfood-staged.sh +26 -119
  27. package/templates/dangerous-bash-interceptor.dogfood-staged.sh +46 -170
  28. package/templates/delegation-advisory.dogfood-staged.sh +44 -0
  29. package/templates/delegation-capture.dogfood-staged.sh +52 -0
  30. package/templates/dependency-audit-gate.dogfood-staged.sh +29 -121
  31. package/templates/env-file-protection.dogfood-staged.sh +30 -141
  32. package/templates/local-review-gate.dogfood-staged.sh +191 -396
  33. package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
  34. package/templates/protected-paths-bash-gate.dogfood-staged.sh +57 -160
  35. package/templates/secret-scanner.dogfood-staged.sh +90 -213
  36. package/templates/security-disclosure-gate.dogfood-staged.sh +32 -155
  37. package/templates/settings-protection.dogfood-staged.sh +56 -179
@@ -1,204 +1,81 @@
1
1
  #!/bin/bash
2
2
  # PreToolUse hook: settings-protection.sh
3
3
  # 0.35.0+ — Node-binary shim for `rea hook settings-protection`.
4
+ # 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
4
5
  #
5
- # Pre-0.35.0 this was the LARGEST hook in the repo at 582 LOC of bash:
6
- # §5a `..` traversal reject, §5a-bis interior `/./` reject, §5b
7
- # extension-surface allow-list (with final-component + intermediate-
8
- # directory symlink refusal), §6 hard-protected pattern resolution
9
- # (PROTECTED_PATTERNS sourced from `_lib/protected-paths.sh` with
10
- # `protected_writes` override + `protected_paths_relax` subtractor),
11
- # §6c intermediate-symlink resolution against the hard-protected list,
12
- # §6b REA_HOOK_PATCH_SESSION unlock for .claude/hooks/ with hash-
13
- # chained audit append (fail-closed). The full bash body is preserved
14
- # at `__tests__/hooks/parity/baselines/settings-protection.sh.pre-0.35.0`.
6
+ # Pre-0.35.0 this was the LARGEST hook in the repo at 582 LOC of bash;
7
+ # the full bash body is preserved at
8
+ # `__tests__/hooks/parity/baselines/settings-protection.sh.pre-0.35.0`.
9
+ # Migration in `src/hooks/settings-protection/index.ts`.
15
10
  #
16
- # The migration moves every section into
17
- # `src/hooks/settings-protection/index.ts`. This shim is the Claude Code
18
- # dispatcher's view of the hook it forwards stdin to the CLI and
19
- # exits with whatever the CLI returns.
11
+ # THE gate protecting the entire governance layer from agent self-
12
+ # disable. SHIM_ENFORCE_CLI_SHAPE=1 closes the 0.35.0 codex round-1 P1
13
+ # (forged in-project JS as the trusted gate CLI).
20
14
  #
21
- # Behavioral contract is preserved byte-for-byte: exit 0 on allow,
22
- # exit 2 on HALT / traversal-reject / interior-dot-reject / protected
23
- # match / patch-session-mismatch / malformed payload.
15
+ # # Relevance pre-gate (CLI-missing only)
24
16
  #
25
- # # CLI-resolution trust boundary
26
- #
27
- # Mirrors the 0.32.0 final shim shape.
28
- #
29
- # # Fail-closed posture
30
- #
31
- # settings-protection is THE gate protecting the entire governance layer
32
- # from agent self-disable. Pre-0.35.0 the bash body enforced refusal
33
- # without any compiled CLI; the Node-binary port preserves that — early-
34
- # exit branches fail closed AFTER the relevance pre-gate passes.
35
- #
36
- # # Relevance pre-gate
37
- #
38
- # Substring scan over the extracted file_path / notebook_path for the
39
- # protected-path markers (.claude/, .husky/, .rea/policy.yaml, .rea/HALT,
40
- # the verdict cache paths, plus any policy.blocked_paths entry). When
41
- # CLI is missing AND none of these substrings appear in the payload's
42
- # file path, exit 0. The pre-0.35.0 bash body would have allowed.
43
- #
44
- # # Bootstrap safety
45
- #
46
- # This shim is ITSELF protected by `settings-protection.sh`. The new
47
- # shim must not block legitimate writes — the `bash -n` syntax check
48
- # in the test:bash-syntax script catches parse errors BEFORE the
49
- # install lands them. The relevance pre-gate keeps benign writes (like
50
- # editing `src/foo.ts`) exiting 0 even when the CLI is missing.
17
+ # Substring scan over file_path / notebook_path for protected-path
18
+ # markers (.claude/, .husky/, .rea/policy.yaml, .rea/HALT, the
19
+ # verdict cache paths), plus any policy.protected_writes entry. Empty
20
+ # / missing policy is OK — the static marker set still catches the
21
+ # canonical protected paths.
51
22
 
52
23
  set -uo pipefail
53
24
 
54
- # 1. HALT check.
55
25
  # shellcheck source=_lib/halt-check.sh
56
26
  source "$(dirname "$0")/_lib/halt-check.sh"
57
27
  check_halt
58
28
  REA_ROOT=$(rea_root)
59
29
 
60
- proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
30
+ SHIM_NAME="settings-protection"
31
+ SHIM_INTRODUCED_IN="0.35.0"
32
+ SHIM_FAIL_OPEN=0
33
+ SHIM_ENFORCE_CLI_SHAPE=1
34
+ SHIM_REFUSAL_NOUN="protected-path refusal"
61
35
 
62
- # 2. Capture stdin once.
63
- INPUT=$(cat)
64
-
65
- # 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
66
- REA_ARGV=()
67
- RESOLVED_CLI_PATH=""
68
- if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
69
- REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
70
- RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
71
- elif [ -f "$proj/dist/cli/index.js" ]; then
72
- REA_ARGV=(node "$proj/dist/cli/index.js")
73
- RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
74
- fi
75
-
76
- # 3b. Relevance pre-gate. Only used when the CLI is missing.
77
- if [ "${#REA_ARGV[@]}" -eq 0 ]; then
78
- CLI_MISSING_FILE_PATH=""
36
+ shim_cli_missing_relevant() {
37
+ local cli_missing_file_path=""
79
38
  if command -v jq >/dev/null 2>&1; then
80
- CLI_MISSING_FILE_PATH=$(printf '%s' "$INPUT" | jq -r '
39
+ cli_missing_file_path=$(printf '%s' "$INPUT" | jq -r '
81
40
  (.tool_input.file_path // .tool_input.notebook_path // "") | tostring
82
41
  ' 2>/dev/null || true)
83
42
  else
84
- CLI_MISSING_FILE_PATH="$INPUT"
43
+ cli_missing_file_path="$INPUT"
85
44
  fi
86
- if [ -z "$CLI_MISSING_FILE_PATH" ]; then
87
- exit 0
45
+ if [ -z "$cli_missing_file_path" ]; then
46
+ return 1
88
47
  fi
89
- CLI_MISSING_RELEVANT=0
90
- case "$CLI_MISSING_FILE_PATH" in
91
- *".claude/settings"*) CLI_MISSING_RELEVANT=1 ;;
92
- *".claude/hooks/"*) CLI_MISSING_RELEVANT=1 ;;
93
- *".husky/"*) CLI_MISSING_RELEVANT=1 ;;
94
- *".rea/policy.yaml"*) CLI_MISSING_RELEVANT=1 ;;
95
- *".rea/HALT"*) CLI_MISSING_RELEVANT=1 ;;
96
- *".rea/last-review"*) CLI_MISSING_RELEVANT=1 ;;
97
- *".claude\\"*|*".husky\\"*|*".rea\\"*) CLI_MISSING_RELEVANT=1 ;;
98
- *"..%2F"*|*"%2E%2E"*) CLI_MISSING_RELEVANT=1 ;;
48
+ case "$cli_missing_file_path" in
49
+ *".claude/settings"*) return 0 ;;
50
+ *".claude/hooks/"*) return 0 ;;
51
+ *".husky/"*) return 0 ;;
52
+ *".rea/policy.yaml"*) return 0 ;;
53
+ *".rea/HALT"*) return 0 ;;
54
+ *".rea/last-review"*) return 0 ;;
55
+ *".claude\\"*|*".husky\\"*|*".rea\\"*) return 0 ;;
56
+ *"..%2F"*|*"%2E%2E"*) return 0 ;;
99
57
  esac
100
- # Codex round-1 P2 fix: scan policy.protected_writes entries too so a
101
- # consumer-defined protected path isn't silently allowed when the CLI
102
- # is missing.
103
- if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
104
- POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
105
- if [ -f "$POLICY_FILE" ]; then
106
- while IFS= read -r entry; do
107
- [ -z "$entry" ] && continue
108
- base="$entry"
109
- case "$base" in
110
- */) base="${base%/}" ;;
111
- esac
112
- [ -z "$base" ] && continue
113
- case "$CLI_MISSING_FILE_PATH" in
114
- *"$base"*) CLI_MISSING_RELEVANT=1; break ;;
115
- esac
116
- done < <(awk '
117
- /^protected_writes:/ { in_block=1; next }
118
- in_block && /^[[:space:]]*-/ {
119
- sub(/^[[:space:]]*-[[:space:]]*/, "")
120
- gsub(/^["'\'']/, "")
121
- gsub(/["'\'']$/, "")
122
- print
123
- next
124
- }
125
- in_block && /^[^[:space:]-]/ { in_block=0 }
126
- ' "$POLICY_FILE" 2>/dev/null)
127
- fi
58
+ # 0.37.0: route protected_writes reads through the unified policy-reader.
59
+ local policy_file="${REA_ROOT}/.rea/policy.yaml"
60
+ if [ -f "$policy_file" ]; then
61
+ # shellcheck source=_lib/policy-reader.sh
62
+ source "$(dirname "$0")/_lib/policy-reader.sh"
63
+ local entry base
64
+ while IFS= read -r entry; do
65
+ [ -z "$entry" ] && continue
66
+ base="$entry"
67
+ case "$base" in
68
+ */) base="${base%/}" ;;
69
+ esac
70
+ [ -z "$base" ] && continue
71
+ case "$cli_missing_file_path" in
72
+ *"$base"*) return 0 ;;
73
+ esac
74
+ done < <(policy_reader_get_list protected_writes 2>/dev/null)
128
75
  fi
129
- if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
130
- exit 0
131
- fi
132
- printf 'rea: settings-protection cannot run — the rea CLI is not built.\n' >&2
133
- printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
134
- printf 'This shim fails closed because the pre-0.35.0 bash body enforced protected-path refusal without a CLI.\n' >&2
135
- exit 2
136
- fi
137
-
138
- # 4. Realpath sandbox check.
139
- if ! command -v node >/dev/null 2>&1; then
140
- printf 'rea: settings-protection cannot run — `node` is not on PATH.\n' >&2
141
- printf 'Install Node 22+ (engines.node) to restore protected-path refusal.\n' >&2
142
- exit 2
143
- fi
144
-
145
- sandbox_check=$(node -e '
146
- const fs = require("fs");
147
- const path = require("path");
148
- const cli = process.argv[1];
149
- const projDir = process.argv[2];
150
- let real, realProj;
151
- try { real = fs.realpathSync(cli); } catch (e) {
152
- process.stdout.write("bad:realpath"); process.exit(1);
153
- }
154
- try { realProj = fs.realpathSync(projDir); } catch (e) {
155
- process.stdout.write("bad:realpath-proj"); process.exit(1);
156
- }
157
- const sep = path.sep;
158
- const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
159
- if (!(real === realProj || real.startsWith(projWithSep))) {
160
- process.stdout.write("bad:cli-escapes-project"); process.exit(1);
161
- }
162
- // Codex round-1 P1 fix: enforce dist/cli/index.js shape so a
163
- // workspace attacker who repoints node_modules/@bookedsolid/rea or
164
- // dist at an arbitrary in-project JS file cannot execute it as the
165
- // trusted gate CLI. Pre-0.35.0 shims had this check; the 0.34.0
166
- // round-8 template dropped it; restored here.
167
- const expectedEnd = path.join("dist", "cli", "index.js");
168
- if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
169
- process.stdout.write("bad:cli-shape"); process.exit(1);
170
- }
171
- let cur = path.dirname(path.dirname(path.dirname(real)));
172
- let found = false;
173
- for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
174
- const pj = path.join(cur, "package.json");
175
- if (fs.existsSync(pj)) {
176
- try {
177
- const data = JSON.parse(fs.readFileSync(pj, "utf8"));
178
- if (data && data.name === "@bookedsolid/rea") { found = true; break; }
179
- } catch (e) { /* keep walking */ }
180
- }
181
- cur = path.dirname(cur);
182
- }
183
- if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
184
- process.stdout.write("ok");
185
- ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
186
-
187
- if [ "$sandbox_check" != "ok" ]; then
188
- printf 'rea: settings-protection FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
189
- exit 2
190
- fi
191
-
192
- # 5. Version-probe.
193
- probe_out=$("${REA_ARGV[@]}" hook settings-protection --help 2>&1)
194
- probe_status=$?
195
- if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'settings-protection'; then
196
- printf 'rea: this shim requires the `rea hook settings-protection` subcommand (introduced in 0.35.0).\n' >&2
197
- printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
198
- printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
199
- exit 2
200
- fi
76
+ return 1
77
+ }
201
78
 
202
- # 6. Forward stdin (already captured up-front).
203
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook settings-protection
204
- exit $?
79
+ # shellcheck source=_lib/shim-runtime.sh
80
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
81
+ shim_run