@bookedsolid/rea 0.34.0 → 0.35.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/dist/cli/hook.js +28 -0
- package/dist/hooks/_lib/path-normalize.d.ts +81 -0
- package/dist/hooks/_lib/path-normalize.js +171 -0
- package/dist/hooks/_lib/payload.js +1 -1
- package/dist/hooks/_lib/protected-paths.d.ts +0 -0
- package/dist/hooks/_lib/protected-paths.js +232 -0
- package/dist/hooks/blocked-paths-bash-gate/index.d.ts +55 -0
- package/dist/hooks/blocked-paths-bash-gate/index.js +175 -0
- package/dist/hooks/blocked-paths-enforcer/index.d.ts +51 -0
- package/dist/hooks/blocked-paths-enforcer/index.js +287 -0
- package/dist/hooks/protected-paths-bash-gate/index.d.ts +47 -0
- package/dist/hooks/protected-paths-bash-gate/index.js +168 -0
- package/dist/hooks/settings-protection/index.d.ts +74 -0
- package/dist/hooks/settings-protection/index.js +485 -0
- package/hooks/blocked-paths-bash-gate.sh +118 -116
- package/hooks/blocked-paths-enforcer.sh +152 -256
- package/hooks/protected-paths-bash-gate.sh +123 -210
- package/hooks/settings-protection.sh +171 -549
- package/package.json +1 -1
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +177 -0
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +180 -0
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +186 -0
- package/templates/settings-protection.dogfood-staged.sh +204 -0
|
@@ -1,284 +1,180 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: blocked-paths-enforcer.sh
|
|
3
|
-
#
|
|
4
|
-
# Reads blocked_paths from .rea/policy.yaml and blocks matching writes.
|
|
3
|
+
# 0.35.0+ — Node-binary shim for `rea hook blocked-paths-enforcer`.
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Pre-0.35.0 the gate's full body lived here as bash (284 LOC). The
|
|
6
|
+
# full bash body is preserved at
|
|
7
|
+
# `__tests__/hooks/parity/baselines/blocked-paths-enforcer.sh.pre-0.35.0`.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# Migration moves the enforcement logic (path normalization, traversal
|
|
10
|
+
# reject, glob/prefix/exact matching, symlink resolution, agent-
|
|
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.
|
|
14
|
+
#
|
|
15
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on allow,
|
|
16
|
+
# exit 2 on HALT / blocked-paths match / malformed payload.
|
|
17
|
+
#
|
|
18
|
+
# # CLI-resolution trust boundary
|
|
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.
|
|
12
34
|
|
|
13
35
|
set -uo pipefail
|
|
14
36
|
|
|
15
|
-
#
|
|
16
|
-
INPUT=$(cat)
|
|
17
|
-
|
|
18
|
-
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
19
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
20
|
-
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
21
|
-
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
22
|
-
exit 2
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
26
|
-
# 0.16.0: HALT check sourced from shared _lib/halt-check.sh.
|
|
37
|
+
# 1. HALT check.
|
|
27
38
|
# shellcheck source=_lib/halt-check.sh
|
|
28
39
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
29
40
|
check_halt
|
|
30
41
|
REA_ROOT=$(rea_root)
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
34
|
-
|
|
35
|
-
if [[ -z "$FILE_PATH" ]]; then
|
|
36
|
-
exit 0
|
|
37
|
-
fi
|
|
43
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
38
44
|
|
|
39
|
-
#
|
|
40
|
-
|
|
45
|
+
# 2. Capture stdin once.
|
|
46
|
+
INPUT=$(cat)
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
# 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
49
|
+
REA_ARGV=()
|
|
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"
|
|
44
57
|
fi
|
|
45
58
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
break
|
|
56
|
-
fi
|
|
57
|
-
# Check for inline array with values
|
|
58
|
-
if printf '%s' "$line" | grep -qE 'blocked_paths:[[:space:]]*\['; then
|
|
59
|
-
# Extract inline array items
|
|
60
|
-
items=$(printf '%s' "$line" | sed 's/.*\[//; s/\].*//; s/,/ /g')
|
|
61
|
-
for item in $items; do
|
|
62
|
-
cleaned=$(printf '%s' "$item" | sed "s/^[[:space:]]*[\"']//; s/[\"'][[:space:]]*$//")
|
|
63
|
-
if [[ -n "$cleaned" ]]; then
|
|
64
|
-
BLOCKED_PATHS+=("$cleaned")
|
|
65
|
-
fi
|
|
66
|
-
done
|
|
67
|
-
break
|
|
68
|
-
fi
|
|
69
|
-
IN_BLOCK=1
|
|
70
|
-
continue
|
|
59
|
+
# 3b. Relevance pre-gate. Only used when the CLI is missing.
|
|
60
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
61
|
+
CLI_MISSING_FILE_PATH=""
|
|
62
|
+
if command -v jq >/dev/null 2>&1; then
|
|
63
|
+
CLI_MISSING_FILE_PATH=$(printf '%s' "$INPUT" | jq -r '
|
|
64
|
+
(.tool_input.file_path // .tool_input.notebook_path // "") | tostring
|
|
65
|
+
' 2>/dev/null || true)
|
|
66
|
+
else
|
|
67
|
+
CLI_MISSING_FILE_PATH="$INPUT"
|
|
71
68
|
fi
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# Block sequence items start with " - "
|
|
75
|
-
if printf '%s' "$line" | grep -qE '^[[:space:]]+-'; then
|
|
76
|
-
cleaned=$(printf '%s' "$line" | sed 's/^[[:space:]]*-[[:space:]]*//; s/^"//; s/"$//; s/^'"'"'//; s/'"'"'$//')
|
|
77
|
-
if [[ -n "$cleaned" ]]; then
|
|
78
|
-
BLOCKED_PATHS+=("$cleaned")
|
|
79
|
-
fi
|
|
80
|
-
else
|
|
81
|
-
# Non-indented line means we've left the block
|
|
82
|
-
break
|
|
83
|
-
fi
|
|
69
|
+
if [ -z "$CLI_MISSING_FILE_PATH" ]; then
|
|
70
|
+
exit 0
|
|
84
71
|
fi
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
72
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
73
|
+
if [ ! -f "$POLICY_FILE" ]; then
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
CLI_MISSING_RELEVANT=0
|
|
77
|
+
while IFS= read -r entry; do
|
|
78
|
+
[ -z "$entry" ] && continue
|
|
79
|
+
# Substring scan — for directory prefixes the entry ends with /
|
|
80
|
+
# and any file_path under it matches. Glob entries fall back to
|
|
81
|
+
# the same substring test (over-trigger is fine — the CLI does
|
|
82
|
+
# the precise evaluation when reachable).
|
|
83
|
+
base="$entry"
|
|
84
|
+
case "$base" in
|
|
85
|
+
*/) base="${base%/}" ;;
|
|
86
|
+
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
|
+
case "$base" in
|
|
91
|
+
*'*'*) base="${base%%\**}" ;;
|
|
92
|
+
esac
|
|
93
|
+
[ -z "$base" ] && continue
|
|
94
|
+
case "$CLI_MISSING_FILE_PATH" in
|
|
95
|
+
*"$base"*) CLI_MISSING_RELEVANT=1; break ;;
|
|
96
|
+
esac
|
|
97
|
+
done < <(awk '
|
|
98
|
+
/^blocked_paths:/ { in_block=1; next }
|
|
99
|
+
in_block && /^[[:space:]]*-/ {
|
|
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
|
|
89
115
|
fi
|
|
90
116
|
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# write there. Settings-protection.sh guards the sensitive files explicitly.
|
|
96
|
-
AGENT_WRITABLE=(
|
|
97
|
-
'.rea/tasks.jsonl'
|
|
98
|
-
'.rea/audit/'
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
# 0.16.0: normalize_path migrated to shared `_lib/path-normalize.sh`.
|
|
102
|
-
# Both this hook AND settings-protection.sh consume the same helper
|
|
103
|
-
# so URL-decoding / backslash-translation / `./`-stripping cannot
|
|
104
|
-
# drift between them again.
|
|
105
|
-
# shellcheck source=_lib/path-normalize.sh
|
|
106
|
-
source "$(dirname "$0")/_lib/path-normalize.sh"
|
|
107
|
-
|
|
108
|
-
NORMALIZED=$(normalize_path "$FILE_PATH")
|
|
109
|
-
|
|
110
|
-
# ── 5a. Path-traversal rejection (0.14.0 iron-gate fix) ───────────────────────
|
|
111
|
-
# Reject any path containing a `..` segment BEFORE the literal-match below.
|
|
112
|
-
# Without this, `foo/../CODEOWNERS` would get past `normalize_path()` (which
|
|
113
|
-
# only strips leading project root + URL-decodes) and the literal-match
|
|
114
|
-
# loop would compare `foo/../CODEOWNERS` against the literal `CODEOWNERS`
|
|
115
|
-
# entry — which doesn't match, so the policy lets the write through. The
|
|
116
|
-
# downstream Write/Edit tool then resolves the traversal and writes to
|
|
117
|
-
# `CODEOWNERS` anyway, defeating the gate.
|
|
118
|
-
#
|
|
119
|
-
# Mirrors settings-protection.sh §5a (which has had this guard since
|
|
120
|
-
# 0.10.x). Both pre- and post-decode forms are checked because
|
|
121
|
-
# normalize_path() URL-decodes earlier and an attacker could split the
|
|
122
|
-
# traversal across encodings (`%2E%2E/`, `..%2F`, etc.).
|
|
123
|
-
raw_has_traversal=0
|
|
124
|
-
norm_has_traversal=0
|
|
125
|
-
case "/$FILE_PATH/" in
|
|
126
|
-
*/../*) raw_has_traversal=1 ;;
|
|
127
|
-
esac
|
|
128
|
-
case "/$NORMALIZED/" in
|
|
129
|
-
*/../*) norm_has_traversal=1 ;;
|
|
130
|
-
esac
|
|
131
|
-
# Also catch URL-encoded traversal in case some tool routes raw-encoded
|
|
132
|
-
# paths through here (e.g. file:// inputs). normalize_path()'s decoder
|
|
133
|
-
# only handles a fixed set; an unrecognized encoding would slip past.
|
|
134
|
-
case "$FILE_PATH" in
|
|
135
|
-
*%2[Ee]%2[Ee]*|*%2[Ee].*|*.%2[Ee]*) raw_has_traversal=1 ;;
|
|
136
|
-
esac
|
|
137
|
-
if [[ "$raw_has_traversal" -eq 1 ]] || [[ "$norm_has_traversal" -eq 1 ]]; then
|
|
138
|
-
{
|
|
139
|
-
printf 'BLOCKED PATH: path traversal rejected\n'
|
|
140
|
-
printf '\n'
|
|
141
|
-
printf ' File: %s\n' "$FILE_PATH"
|
|
142
|
-
printf " Rule: path contains a '..' segment; rewrite to a canonical\n"
|
|
143
|
-
printf ' project-relative path without traversal.\n'
|
|
144
|
-
} >&2
|
|
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
|
|
145
121
|
exit 2
|
|
146
122
|
fi
|
|
147
123
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
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
|
|
184
165
|
exit 2
|
|
185
166
|
fi
|
|
186
167
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
for blocked in "${BLOCKED_PATHS[@]}"; do
|
|
197
|
-
LOWER_BLOCKED=$(printf '%s' "$blocked" | tr '[:upper:]' '[:lower:]')
|
|
198
|
-
|
|
199
|
-
# Directory match (blocked path ends with /)
|
|
200
|
-
if [[ "$LOWER_BLOCKED" == */ ]]; then
|
|
201
|
-
if [[ "$LOWER_NORM" == "$LOWER_BLOCKED"* ]] || [[ "$LOWER_NORM" == "${LOWER_BLOCKED%/}" ]]; then
|
|
202
|
-
{
|
|
203
|
-
printf 'BLOCKED PATH: Write denied by policy\n'
|
|
204
|
-
printf '\n'
|
|
205
|
-
printf ' File: %s\n' "$FILE_PATH"
|
|
206
|
-
printf ' Blocked by: %s\n' "$blocked"
|
|
207
|
-
printf ' Source: .rea/policy.yaml → blocked_paths\n'
|
|
208
|
-
printf '\n'
|
|
209
|
-
printf ' This path is protected by policy. To modify it, a human must\n'
|
|
210
|
-
printf ' either update blocked_paths in policy.yaml or edit the file directly.\n'
|
|
211
|
-
} >&2
|
|
212
|
-
exit 2
|
|
213
|
-
fi
|
|
214
|
-
continue
|
|
215
|
-
fi
|
|
216
|
-
|
|
217
|
-
# Glob pattern match (contains *)
|
|
218
|
-
if [[ "$blocked" == *'*'* ]]; then
|
|
219
|
-
# Convert glob to regex: . → \., * → .*
|
|
220
|
-
regex=$(printf '%s' "$LOWER_BLOCKED" | sed 's/\./\\./g; s/\*/.*/g')
|
|
221
|
-
if printf '%s' "$LOWER_NORM" | grep -qE "^${regex}$"; then
|
|
222
|
-
{
|
|
223
|
-
printf 'BLOCKED PATH: Write denied by policy\n'
|
|
224
|
-
printf '\n'
|
|
225
|
-
printf ' File: %s\n' "$FILE_PATH"
|
|
226
|
-
printf ' Blocked by: %s (glob pattern)\n' "$blocked"
|
|
227
|
-
printf ' Source: .rea/policy.yaml → blocked_paths\n'
|
|
228
|
-
} >&2
|
|
229
|
-
exit 2
|
|
230
|
-
fi
|
|
231
|
-
continue
|
|
232
|
-
fi
|
|
233
|
-
|
|
234
|
-
# Exact match
|
|
235
|
-
if [[ "$LOWER_NORM" == "$LOWER_BLOCKED" ]]; then
|
|
236
|
-
{
|
|
237
|
-
printf 'BLOCKED PATH: Write denied by policy\n'
|
|
238
|
-
printf '\n'
|
|
239
|
-
printf ' File: %s\n' "$FILE_PATH"
|
|
240
|
-
printf ' Blocked by: %s\n' "$blocked"
|
|
241
|
-
printf ' Source: .rea/policy.yaml → blocked_paths\n'
|
|
242
|
-
} >&2
|
|
243
|
-
exit 2
|
|
244
|
-
fi
|
|
245
|
-
done
|
|
246
|
-
|
|
247
|
-
# ── 0.16.0 fix H.2: intermediate-symlink resolution ──────────────────────────
|
|
248
|
-
# Same shape as Helix Finding 2 against blocked_paths policy entries.
|
|
249
|
-
# If `secrets/` is in blocked_paths and an attacker creates
|
|
250
|
-
# `pretty/ -> ../secrets/`, then writes `pretty/foo`, the literal-match
|
|
251
|
-
# loop above sees `pretty/foo` (no match) and exits 0 — the downstream
|
|
252
|
-
# Write tool follows the symlink and lands the body in `secrets/foo`.
|
|
253
|
-
# Mirrors settings-protection.sh §6c.
|
|
254
|
-
if [[ -e "$FILE_PATH" || -d "$(dirname -- "$FILE_PATH")" ]]; then
|
|
255
|
-
parent_dir=$(dirname -- "$FILE_PATH")
|
|
256
|
-
if [[ -d "$parent_dir" ]]; then
|
|
257
|
-
resolved_parent=$(cd -P -- "$parent_dir" 2>/dev/null && pwd -P 2>/dev/null) || resolved_parent=""
|
|
258
|
-
if [[ -n "$resolved_parent" && "$resolved_parent" == "$REA_ROOT"/* ]]; then
|
|
259
|
-
relative_resolved="${resolved_parent#"$REA_ROOT"/}"
|
|
260
|
-
resolved_target="${relative_resolved}/$(basename -- "$FILE_PATH")"
|
|
261
|
-
resolved_target_lc=$(printf '%s' "$resolved_target" | tr '[:upper:]' '[:lower:]')
|
|
262
|
-
for blocked in "${BLOCKED_PATHS[@]}"; do
|
|
263
|
-
blocked_lc=$(printf '%s' "$blocked" | tr '[:upper:]' '[:lower:]')
|
|
264
|
-
if [[ "$resolved_target_lc" == "$blocked_lc" ]] || \
|
|
265
|
-
{ [[ "$blocked_lc" == */ ]] && [[ "$resolved_target_lc" == "$blocked_lc"* ]]; }; then
|
|
266
|
-
{
|
|
267
|
-
printf 'BLOCKED PATH: intermediate-symlink resolution blocked\n'
|
|
268
|
-
printf '\n'
|
|
269
|
-
printf ' Logical: %s\n' "$FILE_PATH"
|
|
270
|
-
printf ' Resolved: %s\n' "$resolved_target"
|
|
271
|
-
printf ' Blocked by: %s\n' "$blocked"
|
|
272
|
-
printf ' Source: .rea/policy.yaml → blocked_paths\n'
|
|
273
|
-
printf '\n'
|
|
274
|
-
printf ' Rule: an intermediate directory of the path is a symlink\n'
|
|
275
|
-
printf ' whose target falls inside a blocked policy entry.\n'
|
|
276
|
-
} >&2
|
|
277
|
-
exit 2
|
|
278
|
-
fi
|
|
279
|
-
done
|
|
280
|
-
fi
|
|
281
|
-
fi
|
|
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
|
|
282
176
|
fi
|
|
283
177
|
|
|
284
|
-
|
|
178
|
+
# 6. Forward stdin (already captured up-front).
|
|
179
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook blocked-paths-enforcer
|
|
180
|
+
exit $?
|