@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.
- package/hooks/_lib/shim-runtime.sh +405 -0
- package/hooks/architecture-review-gate.sh +11 -103
- package/hooks/attribution-advisory.sh +38 -209
- package/hooks/blocked-paths-bash-gate.sh +32 -146
- package/hooks/blocked-paths-enforcer.sh +32 -137
- package/hooks/changeset-security-gate.sh +26 -119
- package/hooks/dangerous-bash-interceptor.sh +46 -170
- package/hooks/delegation-advisory.sh +26 -144
- package/hooks/delegation-capture.sh +33 -139
- package/hooks/dependency-audit-gate.sh +29 -121
- package/hooks/env-file-protection.sh +30 -141
- package/hooks/local-review-gate.sh +117 -352
- package/hooks/pr-issue-link-gate.sh +16 -118
- package/hooks/protected-paths-bash-gate.sh +53 -152
- package/hooks/secret-scanner.sh +90 -213
- package/hooks/security-disclosure-gate.sh +32 -155
- package/hooks/settings-protection.sh +56 -176
- package/package.json +1 -1
- package/templates/_lib_shim-runtime.dogfood-staged.sh +405 -0
- package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
- package/templates/attribution-advisory.dogfood-staged.sh +38 -209
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +32 -146
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +32 -137
- package/templates/changeset-security-gate.dogfood-staged.sh +26 -119
- package/templates/dangerous-bash-interceptor.dogfood-staged.sh +46 -170
- package/templates/delegation-advisory.dogfood-staged.sh +44 -0
- package/templates/delegation-capture.dogfood-staged.sh +52 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +29 -121
- package/templates/env-file-protection.dogfood-staged.sh +30 -141
- package/templates/local-review-gate.dogfood-staged.sh +117 -352
- package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +53 -152
- package/templates/secret-scanner.dogfood-staged.sh +90 -213
- package/templates/security-disclosure-gate.dogfood-staged.sh +32 -155
- 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
|
-
#
|
|
6
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
#
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
12
|
-
#
|
|
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
|
-
#
|
|
17
|
-
# exit 2 on HALT / verdict block / malformed payload / sandbox fail.
|
|
13
|
+
# # Relevance pre-gate (CLI-missing only)
|
|
18
14
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
cli_missing_cmd="$INPUT"
|
|
75
40
|
fi
|
|
76
|
-
if [ -z "$
|
|
77
|
-
|
|
78
|
-
exit 0
|
|
41
|
+
if [ -z "$cli_missing_cmd" ]; then
|
|
42
|
+
return 1
|
|
79
43
|
fi
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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 "$
|
|
102
|
-
*"$entry"*)
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
#
|
|
176
|
-
|
|
177
|
-
|
|
61
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
62
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
63
|
+
shim_run
|