@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.
- package/hooks/_lib/policy-reader.sh +948 -0
- package/hooks/_lib/shim-runtime.sh +405 -0
- package/hooks/architecture-review-gate.sh +11 -103
- package/hooks/attribution-advisory.sh +43 -155
- package/hooks/blocked-paths-bash-gate.sh +35 -149
- package/hooks/blocked-paths-enforcer.sh +35 -140
- 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 +191 -396
- package/hooks/pr-issue-link-gate.sh +16 -118
- package/hooks/protected-paths-bash-gate.sh +57 -160
- package/hooks/secret-scanner.sh +90 -213
- package/hooks/security-disclosure-gate.sh +32 -155
- package/hooks/settings-protection.sh +56 -179
- package/package.json +1 -1
- package/templates/_lib_policy-reader.dogfood-staged.sh +948 -0
- 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 +43 -155
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +35 -149
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +35 -140
- 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 +191 -396
- package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +57 -160
- 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 -179
|
@@ -1,170 +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
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
17
|
+
# # Policy short-circuit (codex round 2 P1 from 0.37.0)
|
|
18
|
+
#
|
|
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.
|
|
23
23
|
|
|
24
24
|
set -uo pipefail
|
|
25
25
|
|
|
26
|
-
# 1. HALT check.
|
|
27
26
|
# shellcheck source=_lib/halt-check.sh
|
|
28
27
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
29
28
|
check_halt
|
|
30
29
|
REA_ROOT=$(rea_root)
|
|
31
30
|
|
|
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
|
-
if [ "$RELEVANT" -eq 0 ]; then
|
|
61
|
-
# Irrelevant Bash call — nothing the pre-0.32.0 body would have
|
|
62
|
-
# processed. Always exit 0 regardless of CLI state.
|
|
63
|
-
exit 0
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
# 2b. Policy short-circuit (round-6 P2). The pre-0.32.0 bash body
|
|
67
|
-
# no-op'd when `block_ai_attribution` was absent or false. Without
|
|
68
|
-
# this check, an unbuilt/stale install would refuse `git commit`
|
|
69
|
-
# even on repos that DELIBERATELY disable the attribution gate.
|
|
70
|
-
# Read the policy via a simple grep — the canonical loader
|
|
71
|
-
# handles inline forms but we only need block form here, and a
|
|
72
|
-
# conservative "true-and-only-true counts" rule matches the
|
|
73
|
-
# intent (false / absent / inline-only all → no enforcement).
|
|
74
|
-
POLICY_FILE="$REA_ROOT/.rea/policy.yaml"
|
|
75
|
-
if [ ! -f "$POLICY_FILE" ] || ! grep -qE '^block_ai_attribution:[[:space:]]*true([[:space:]]|$)' "$POLICY_FILE"; then
|
|
76
|
-
# Attribution blocking disabled — pre-0.32.0 bash body would have
|
|
77
|
-
# exited 0 here. Don't refuse on stale-install grounds.
|
|
78
|
-
exit 0
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
# 3. Resolve the rea CLI.
|
|
82
|
-
REA_ARGV=()
|
|
83
|
-
RESOLVED_CLI_PATH=""
|
|
84
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
85
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
86
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
87
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
88
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
89
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
90
|
-
fi
|
|
91
|
-
|
|
92
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
93
|
-
# 0.32.0 round-4 P2: when `block_ai_attribution: true`, this hook is
|
|
94
|
-
# blocking-tier — the pre-0.32.0 bash body enforced the policy
|
|
95
|
-
# without a compiled CLI. Falling through to exit 0 would silently
|
|
96
|
-
# let AI-attribution patterns through every git commit / gh pr
|
|
97
|
-
# create-or-edit until the operator rebuilds. Fail closed and tell
|
|
98
|
-
# the operator how to restore protection.
|
|
99
|
-
printf 'rea: attribution-advisory cannot run — the rea CLI is not built.\n' >&2
|
|
100
|
-
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
101
|
-
printf 'This shim fails closed because the pre-0.32.0 bash body enforced attribution policy without a CLI.\n' >&2
|
|
102
|
-
exit 2
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
# 3. Realpath sandbox check.
|
|
106
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
107
|
-
printf 'rea: attribution-advisory cannot run — `node` is not on PATH.\n' >&2
|
|
108
|
-
printf 'Install Node 22+ (engines.node) to restore enforcement.\n' >&2
|
|
109
|
-
exit 2
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
sandbox_check=$(node -e '
|
|
113
|
-
const fs = require("fs");
|
|
114
|
-
const path = require("path");
|
|
115
|
-
const cli = process.argv[1];
|
|
116
|
-
const projDir = process.argv[2];
|
|
117
|
-
let real, realProj;
|
|
118
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
119
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
122
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
const sep = path.sep;
|
|
125
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
126
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
127
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
130
|
-
let found = false;
|
|
131
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
132
|
-
const pj = path.join(cur, "package.json");
|
|
133
|
-
if (fs.existsSync(pj)) {
|
|
134
|
-
try {
|
|
135
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
136
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
137
|
-
} catch (e) { /* keep walking */ }
|
|
138
|
-
}
|
|
139
|
-
cur = path.dirname(cur);
|
|
140
|
-
}
|
|
141
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
142
|
-
process.stdout.write("ok");
|
|
143
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
144
|
-
|
|
145
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
146
|
-
# 0.32.0 round-4 P2: fail closed (blocking-tier when policy enables —
|
|
147
|
-
# see top-of-file rationale). Sandbox failure means the CLI cannot
|
|
148
|
-
# be authenticated; refuse rather than silently bypass.
|
|
149
|
-
printf 'rea: attribution-advisory FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
150
|
-
exit 2
|
|
151
|
-
fi
|
|
152
|
-
|
|
153
|
-
# 4. Version-probe: confirm the resolved CLI implements
|
|
154
|
-
# `hook attribution-advisory`. Codex round 1 P1.
|
|
155
|
-
probe_out=$("${REA_ARGV[@]}" hook attribution-advisory --help 2>&1)
|
|
156
|
-
probe_status=$?
|
|
157
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'attribution-advisory'; then
|
|
158
|
-
# 0.32.0 round-4 P2: stale/older CLI without the new subcommand is
|
|
159
|
-
# NOT advisory-tier fall-through — the bash body it replaces
|
|
160
|
-
# enforced when policy enabled. Fail closed and tell the operator
|
|
161
|
-
# exactly how to fix.
|
|
162
|
-
printf 'rea: this shim requires the `rea hook attribution-advisory` subcommand (introduced in 0.32.0).\n' >&2
|
|
163
|
-
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
164
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
165
|
-
exit 2
|
|
166
|
-
fi
|
|
167
|
-
|
|
168
|
-
# 5. Forward stdin (already captured up-front for the relevance gate).
|
|
169
|
-
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook attribution-advisory
|
|
170
|
-
exit $?
|
|
31
|
+
SHIM_NAME="attribution-advisory"
|
|
32
|
+
SHIM_INTRODUCED_IN="0.32.0"
|
|
33
|
+
SHIM_FAIL_OPEN=0
|
|
34
|
+
SHIM_REFUSAL_NOUN="attribution-policy enforcement"
|
|
35
|
+
|
|
36
|
+
shim_is_relevant() {
|
|
37
|
+
if printf '%s' "$INPUT" | grep -qE '(git[[:space:]]+commit|gh[[:space:]]+pr[[:space:]]+(create|edit))'; then
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
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
|
|
52
|
+
fi
|
|
53
|
+
return 1
|
|
54
|
+
}
|
|
55
|
+
|
|
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
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
48
|
+
# 0.37.0: route blocked_paths reads through the unified policy-reader.
|
|
49
|
+
# shellcheck source=_lib/policy-reader.sh
|
|
50
|
+
source "$(dirname "$0")/_lib/policy-reader.sh"
|
|
51
|
+
local entry
|
|
89
52
|
while IFS= read -r entry; do
|
|
90
53
|
[ -z "$entry" ] && continue
|
|
91
|
-
case "$
|
|
92
|
-
*"$entry"*)
|
|
54
|
+
case "$cli_missing_cmd" in
|
|
55
|
+
*"$entry"*) return 0 ;;
|
|
93
56
|
esac
|
|
94
|
-
done < <(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
sub(/^[[:space:]]*-[[:space:]]*/, "")
|
|
98
|
-
gsub(/^["'\'']/, "")
|
|
99
|
-
gsub(/["'\'']$/, "")
|
|
100
|
-
print
|
|
101
|
-
next
|
|
102
|
-
}
|
|
103
|
-
in_block && /^[^[:space:]-]/ { in_block=0 }
|
|
104
|
-
' "$POLICY_FILE" 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
|
|
57
|
+
done < <(policy_reader_get_list blocked_paths 2>/dev/null)
|
|
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
|
|
@@ -1,79 +1,54 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: blocked-paths-enforcer.sh
|
|
3
3
|
# 0.35.0+ — Node-binary shim for `rea hook blocked-paths-enforcer`.
|
|
4
|
+
# 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
6
|
+
# Write/Edit/MultiEdit/NotebookEdit-tier blocking gate. Full bash body
|
|
7
|
+
# preserved at
|
|
7
8
|
# `__tests__/hooks/parity/baselines/blocked-paths-enforcer.sh.pre-0.35.0`.
|
|
9
|
+
# Migration in `src/hooks/blocked-paths-enforcer/index.ts`.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# writable allow-list) into `src/hooks/blocked-paths-enforcer/index.ts`.
|
|
12
|
-
# This shim is the Claude Code dispatcher's view of the hook — it
|
|
13
|
-
# forwards stdin to the CLI and exits with whatever the CLI returns.
|
|
11
|
+
# SHIM_ENFORCE_CLI_SHAPE=1: 0.35.0 codex round-1 P1 — enforce
|
|
12
|
+
# dist/cli/index.js shape.
|
|
14
13
|
#
|
|
15
|
-
#
|
|
16
|
-
# exit 2 on HALT / blocked-paths match / malformed payload.
|
|
14
|
+
# # Relevance pre-gate (CLI-missing only)
|
|
17
15
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# Mirrors the 0.32.0 final shim shape.
|
|
21
|
-
#
|
|
22
|
-
# # Fail-closed posture
|
|
23
|
-
#
|
|
24
|
-
# blocked-paths-enforcer is a Write/Edit/MultiEdit/NotebookEdit tier
|
|
25
|
-
# security gate. The pre-0.35.0 bash body refused on uncertainty.
|
|
26
|
-
# Early-exit branches fail closed AFTER the relevance pre-gate passes.
|
|
27
|
-
#
|
|
28
|
-
# # Relevance pre-gate
|
|
29
|
-
#
|
|
30
|
-
# Extract file_path / notebook_path from the payload, substring-scan
|
|
31
|
-
# against the policy's blocked_paths entries. When CLI is missing AND
|
|
32
|
-
# no policy.blocked_paths entry matches, exit 0. Empty/missing policy
|
|
33
|
-
# → no enforcement, exit 0.
|
|
16
|
+
# Extract file_path / notebook_path; substring-scan against any
|
|
17
|
+
# policy.blocked_paths entry. Empty/missing policy → exit 0.
|
|
34
18
|
|
|
35
19
|
set -uo pipefail
|
|
36
20
|
|
|
37
|
-
# 1. HALT check.
|
|
38
21
|
# shellcheck source=_lib/halt-check.sh
|
|
39
22
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
40
23
|
check_halt
|
|
41
24
|
REA_ROOT=$(rea_root)
|
|
42
25
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
26
|
+
SHIM_NAME="blocked-paths-enforcer"
|
|
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"
|
|
47
31
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
RESOLVED_CLI_PATH=""
|
|
51
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
52
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
53
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
54
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
55
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
56
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
# 3b. Relevance pre-gate. Only used when the CLI is missing.
|
|
60
|
-
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
61
|
-
CLI_MISSING_FILE_PATH=""
|
|
32
|
+
shim_cli_missing_relevant() {
|
|
33
|
+
local cli_missing_file_path=""
|
|
62
34
|
if command -v jq >/dev/null 2>&1; then
|
|
63
|
-
|
|
35
|
+
cli_missing_file_path=$(printf '%s' "$INPUT" | jq -r '
|
|
64
36
|
(.tool_input.file_path // .tool_input.notebook_path // "") | tostring
|
|
65
37
|
' 2>/dev/null || true)
|
|
66
38
|
else
|
|
67
|
-
|
|
39
|
+
cli_missing_file_path="$INPUT"
|
|
68
40
|
fi
|
|
69
|
-
if [ -z "$
|
|
70
|
-
|
|
41
|
+
if [ -z "$cli_missing_file_path" ]; then
|
|
42
|
+
return 1
|
|
71
43
|
fi
|
|
72
|
-
|
|
73
|
-
if [ ! -f "$
|
|
74
|
-
|
|
44
|
+
local policy_file="${REA_ROOT}/.rea/policy.yaml"
|
|
45
|
+
if [ ! -f "$policy_file" ]; then
|
|
46
|
+
return 1
|
|
75
47
|
fi
|
|
76
|
-
|
|
48
|
+
# 0.37.0: route blocked_paths reads through the unified policy-reader.
|
|
49
|
+
# shellcheck source=_lib/policy-reader.sh
|
|
50
|
+
source "$(dirname "$0")/_lib/policy-reader.sh"
|
|
51
|
+
local entry base
|
|
77
52
|
while IFS= read -r entry; do
|
|
78
53
|
[ -z "$entry" ] && continue
|
|
79
54
|
# Substring scan — for directory prefixes the entry ends with /
|
|
@@ -84,97 +59,17 @@ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
|
84
59
|
case "$base" in
|
|
85
60
|
*/) base="${base%/}" ;;
|
|
86
61
|
esac
|
|
87
|
-
# Strip glob wildcards for substring testing — `src/*.ts` becomes
|
|
88
|
-
# `src/` + `.ts`. The simplest safe form is to scan the literal
|
|
89
|
-
# part before the first `*`.
|
|
90
62
|
case "$base" in
|
|
91
63
|
*'*'*) base="${base%%\**}" ;;
|
|
92
64
|
esac
|
|
93
65
|
[ -z "$base" ] && continue
|
|
94
|
-
case "$
|
|
95
|
-
*"$base"*)
|
|
66
|
+
case "$cli_missing_file_path" in
|
|
67
|
+
*"$base"*) return 0 ;;
|
|
96
68
|
esac
|
|
97
|
-
done < <(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
sub(/^[[:space:]]*-[[:space:]]*/, "")
|
|
101
|
-
gsub(/^["'\'']/, "")
|
|
102
|
-
gsub(/["'\'']$/, "")
|
|
103
|
-
print
|
|
104
|
-
next
|
|
105
|
-
}
|
|
106
|
-
in_block && /^[^[:space:]-]/ { in_block=0 }
|
|
107
|
-
' "$POLICY_FILE" 2>/dev/null)
|
|
108
|
-
if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
|
|
109
|
-
exit 0
|
|
110
|
-
fi
|
|
111
|
-
printf 'rea: blocked-paths-enforcer cannot run — the rea CLI is not built.\n' >&2
|
|
112
|
-
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
113
|
-
printf 'This shim fails closed because the pre-0.35.0 bash body enforced blocked_paths refusal without a CLI.\n' >&2
|
|
114
|
-
exit 2
|
|
115
|
-
fi
|
|
116
|
-
|
|
117
|
-
# 4. Realpath sandbox check.
|
|
118
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
119
|
-
printf 'rea: blocked-paths-enforcer cannot run — `node` is not on PATH.\n' >&2
|
|
120
|
-
printf 'Install Node 22+ (engines.node) to restore blocked_paths refusal.\n' >&2
|
|
121
|
-
exit 2
|
|
122
|
-
fi
|
|
123
|
-
|
|
124
|
-
sandbox_check=$(node -e '
|
|
125
|
-
const fs = require("fs");
|
|
126
|
-
const path = require("path");
|
|
127
|
-
const cli = process.argv[1];
|
|
128
|
-
const projDir = process.argv[2];
|
|
129
|
-
let real, realProj;
|
|
130
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
131
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
134
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
const sep = path.sep;
|
|
137
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
138
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
139
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
140
|
-
}
|
|
141
|
-
// Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
|
|
142
|
-
// settings-protection.sh).
|
|
143
|
-
const expectedEnd = path.join("dist", "cli", "index.js");
|
|
144
|
-
if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
|
|
145
|
-
process.stdout.write("bad:cli-shape"); process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
148
|
-
let found = false;
|
|
149
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
150
|
-
const pj = path.join(cur, "package.json");
|
|
151
|
-
if (fs.existsSync(pj)) {
|
|
152
|
-
try {
|
|
153
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
154
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
155
|
-
} catch (e) { /* keep walking */ }
|
|
156
|
-
}
|
|
157
|
-
cur = path.dirname(cur);
|
|
158
|
-
}
|
|
159
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
160
|
-
process.stdout.write("ok");
|
|
161
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
162
|
-
|
|
163
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
164
|
-
printf 'rea: blocked-paths-enforcer FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
165
|
-
exit 2
|
|
166
|
-
fi
|
|
167
|
-
|
|
168
|
-
# 5. Version-probe.
|
|
169
|
-
probe_out=$("${REA_ARGV[@]}" hook blocked-paths-enforcer --help 2>&1)
|
|
170
|
-
probe_status=$?
|
|
171
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'blocked-paths-enforcer'; then
|
|
172
|
-
printf 'rea: this shim requires the `rea hook blocked-paths-enforcer` subcommand (introduced in 0.35.0).\n' >&2
|
|
173
|
-
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
174
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
175
|
-
exit 2
|
|
176
|
-
fi
|
|
69
|
+
done < <(policy_reader_get_list blocked_paths 2>/dev/null)
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
177
72
|
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
73
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
74
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
75
|
+
shim_run
|