@bookedsolid/rea 0.31.0 → 0.32.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/.husky/prepare-commit-msg +80 -6
- package/MIGRATING.md +24 -15
- package/dist/cli/hook.js +32 -22
- package/dist/hooks/_lib/halt-check.d.ts +78 -0
- package/dist/hooks/_lib/halt-check.js +106 -0
- package/dist/hooks/_lib/payload.d.ts +86 -0
- package/dist/hooks/_lib/payload.js +166 -0
- package/dist/hooks/_lib/segments.d.ts +100 -0
- package/dist/hooks/_lib/segments.js +444 -0
- package/dist/hooks/attribution-advisory/index.d.ts +72 -0
- package/dist/hooks/attribution-advisory/index.js +233 -0
- package/dist/hooks/bash-scanner/protected-scan.js +14 -2
- package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
- package/dist/hooks/pr-issue-link-gate/index.js +127 -0
- package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
- package/dist/hooks/security-disclosure-gate/index.js +502 -0
- package/hooks/_lib/protected-paths.sh +10 -3
- package/hooks/attribution-advisory.sh +139 -131
- package/hooks/pr-issue-link-gate.sh +114 -45
- package/hooks/security-disclosure-gate.sh +148 -316
- package/hooks/settings-protection.sh +13 -9
- package/package.json +1 -1
- package/templates/attribution-advisory.dogfood-staged.sh +170 -0
- package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
- package/templates/prepare-commit-msg.husky.sh +80 -6
- package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
- package/templates/settings-protection.dogfood.patch +58 -0
|
@@ -1,339 +1,171 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
# security-disclosure-gate.sh
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: security-disclosure-gate.sh
|
|
3
|
+
# 0.32.0+ — Node-binary shim for `rea hook security-disclosure-gate`.
|
|
3
4
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
5
|
+
# Pre-0.32.0 the gate's full body lived here as bash (339 LOC including
|
|
6
|
+
# the awk body-file resolver, security-patterns array, and mode-aware
|
|
7
|
+
# routing). The migration to the parser-backed Node binary moves all of
|
|
8
|
+
# that into `src/hooks/security-disclosure-gate/index.ts`. This shim is
|
|
9
|
+
# the Claude Code dispatcher's view of the hook — it forwards stdin
|
|
10
|
+
# AND the REA_DISCLOSURE_MODE env var to the CLI and exits with
|
|
11
|
+
# whatever the CLI returns.
|
|
6
12
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# Use for permanently private client repos
|
|
11
|
-
# disabled — pass through (not recommended)
|
|
13
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on
|
|
14
|
+
# pass-through / no-match, exit 2 on HALT / pattern match / traversal
|
|
15
|
+
# refusal / malformed payload (fail-closed).
|
|
12
16
|
#
|
|
13
|
-
#
|
|
14
|
-
# env by rea init). Defaults to "advisory" when unset.
|
|
17
|
+
# # CLI-resolution trust boundary
|
|
15
18
|
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
# Codex round 1 P1 (2026-05-15): realpath sandbox check matches
|
|
20
|
+
# delegation-advisory.sh §3. The resolved CLI MUST live INSIDE
|
|
21
|
+
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor `package.json`
|
|
22
|
+
# whose `name` is `@bookedsolid/rea`. Defends against symlink-out
|
|
23
|
+
# and tarball-replacement attacks that could otherwise forge the
|
|
24
|
+
# pattern matcher and either suppress real findings or leak a
|
|
25
|
+
# vulnerability through the disclosure gate.
|
|
26
|
+
#
|
|
27
|
+
# Sandboxed resolution order (PATH is INTENTIONALLY OMITTED):
|
|
28
|
+
# 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side)
|
|
29
|
+
# 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (dogfood)
|
|
30
|
+
#
|
|
31
|
+
# When NO rea CLI is reachable, the hook falls through to allow —
|
|
32
|
+
# same posture as the bash-resident version, which `source`d
|
|
33
|
+
# _lib/common.sh first and exited cleanly if the lib was missing.
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
source "$(dirname "$0")/_lib/common.sh"
|
|
35
|
+
set -uo pipefail
|
|
22
36
|
|
|
37
|
+
# 1. HALT check.
|
|
38
|
+
# shellcheck source=_lib/halt-check.sh
|
|
39
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
23
40
|
check_halt
|
|
41
|
+
REA_ROOT=$(rea_root)
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
DISCLOSURE_MODE="${REA_DISCLOSURE_MODE:-advisory}"
|
|
43
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
27
44
|
|
|
28
|
-
#
|
|
29
|
-
|
|
45
|
+
# 2. Relevance pre-gate (0.32.0 round-5 P1, round-6 fix). PreToolUse
|
|
46
|
+
# Bash matchers fire on EVERY shell command, but this hook only
|
|
47
|
+
# enforces against `gh issue create` payloads carrying disclosure
|
|
48
|
+
# keywords. Capture stdin + check relevance FIRST so unrelated
|
|
49
|
+
# commands exit 0 even when the CLI is missing/stale.
|
|
50
|
+
#
|
|
51
|
+
# Match `gh issue create` ANYWHERE in the command string (allow
|
|
52
|
+
# shell prefixes — `sudo`, env assignments). Round-6 P1.
|
|
53
|
+
INPUT=$(cat)
|
|
54
|
+
# Substring scan (NOT JSON-aware). Round-7 P1: any JSON-aware regex
|
|
55
|
+
# anchored on `"command":"...` gets tripped by escaped quotes in
|
|
56
|
+
# quoted env prefixes (`MODE="internal" gh issue create …`). Plain
|
|
57
|
+
# substring match has no such edge — and false-positives just defer
|
|
58
|
+
# to the Node body which handles correctly.
|
|
59
|
+
RELEVANT=0
|
|
60
|
+
if printf '%s' "$INPUT" | grep -qE 'gh[[:space:]]+issue[[:space:]]+create'; then
|
|
61
|
+
RELEVANT=1
|
|
62
|
+
fi
|
|
63
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
30
64
|
exit 0
|
|
31
65
|
fi
|
|
32
66
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
# 2b. Mode short-circuit (round-6 P2). The pre-0.32.0 bash body
|
|
68
|
+
# no-op'd ONLY when `REA_DISCLOSURE_MODE=disabled` — `advisory`
|
|
69
|
+
# mode and the `issues` mode (default) BOTH enforced. Without
|
|
70
|
+
# this check, an unbuilt/stale install would refuse every relevant
|
|
71
|
+
# `gh issue create` even when the operator has deliberately set
|
|
72
|
+
# mode=disabled.
|
|
73
|
+
MODE="${REA_DISCLOSURE_MODE:-advisory}"
|
|
74
|
+
if [ "$MODE" = "disabled" ]; then
|
|
37
75
|
exit 0
|
|
38
76
|
fi
|
|
39
77
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# shellcheck source=_lib/cmd-segments.sh
|
|
50
|
-
if [ -f "$(dirname "$0")/_lib/cmd-segments.sh" ]; then
|
|
51
|
-
# shellcheck source=_lib/cmd-segments.sh
|
|
52
|
-
source "$(dirname "$0")/_lib/cmd-segments.sh"
|
|
53
|
-
if ! any_segment_starts_with "$COMMAND" 'gh[[:space:]]+issue[[:space:]]+create'; then
|
|
54
|
-
exit 0
|
|
55
|
-
fi
|
|
56
|
-
else
|
|
57
|
-
if ! echo "$COMMAND" | grep -qE 'gh\s+issue\s+create'; then
|
|
58
|
-
exit 0
|
|
59
|
-
fi
|
|
78
|
+
# 3. Resolve the rea CLI.
|
|
79
|
+
REA_ARGV=()
|
|
80
|
+
RESOLVED_CLI_PATH=""
|
|
81
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
82
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
83
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
84
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
85
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
86
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
60
87
|
fi
|
|
61
88
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
|
|
73
|
-
'escalat'
|
|
74
|
-
'privilege'
|
|
75
|
-
'rce'
|
|
76
|
-
'remote.code.exec'
|
|
77
|
-
'arbitrary.code'
|
|
78
|
-
'code.execution'
|
|
79
|
-
'zero.day'
|
|
80
|
-
'0day'
|
|
81
|
-
'CVE-'
|
|
82
|
-
'CVSS'
|
|
83
|
-
'GHSA-'
|
|
84
|
-
# Reagent-specific sensitive terms
|
|
85
|
-
'hook.bypass'
|
|
86
|
-
'HALT.bypass'
|
|
87
|
-
'redaction.bypass'
|
|
88
|
-
'policy.bypass'
|
|
89
|
-
'middleware.bypass'
|
|
90
|
-
'skip.*gate'
|
|
91
|
-
'evad'
|
|
92
|
-
# Credential/secret exposure
|
|
93
|
-
'secret.*leak'
|
|
94
|
-
'credential.*leak'
|
|
95
|
-
'token.*leak'
|
|
96
|
-
'key.*expos'
|
|
97
|
-
'expos.*secret'
|
|
98
|
-
# Prompt injection
|
|
99
|
-
'prompt.inject'
|
|
100
|
-
'jailbreak'
|
|
101
|
-
'jail.break'
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
# Scan the full command text (title + body + flags) for sensitive patterns.
|
|
105
|
-
#
|
|
106
|
-
# 0.16.3 discord-ops Round 9 #2 fix: pre-fix the scan only saw what was on
|
|
107
|
-
# the command line, so `gh issue create --body-file leak.md` (or `-F`)
|
|
108
|
-
# routed the body through a file the regex never read. We now resolve the
|
|
109
|
-
# named flag's path argument(s), read up to 64 KiB of each (cap covers
|
|
110
|
-
# realistic issue bodies; a multi-megabyte body is suspicious in itself),
|
|
111
|
-
# and prepend the lowercased file contents to FULL_TEXT before the
|
|
112
|
-
# pattern scan. Stdin form (`-F -` or `--body-file -`) is intentionally
|
|
113
|
-
# skipped — the hook's stdin is the tool payload, not the issue body,
|
|
114
|
-
# and re-reading is impossible. Files outside REA_ROOT (resolved via
|
|
115
|
-
# `..` traversal) are refused as a defense-in-depth measure mirroring
|
|
116
|
-
# protected-paths-bash-gate.sh's outside-root sentinel.
|
|
117
|
-
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
118
|
-
BODY_FILE_TEXT=""
|
|
119
|
-
_extract_body_file_paths() {
|
|
120
|
-
# Emit each `--body-file PATH` and `-F PATH` argument on its own line.
|
|
121
|
-
# Skips the stdin form (`-`) and emits the path verbatim from the
|
|
122
|
-
# equals-form (`--body-file=PATH` / `-F=PATH`).
|
|
123
|
-
#
|
|
124
|
-
# 0.17.0 helix-019 #2: quote-aware tokenization. The pre-fix awk split
|
|
125
|
-
# on whitespace, breaking `--body-file "security notes.md"` into three
|
|
126
|
-
# tokens — the hook then tried to read `"security` (with literal
|
|
127
|
-
# leading quote), failed, and silently skipped the body scan. Now we
|
|
128
|
-
# walk the string with quote-state awareness: whitespace inside
|
|
129
|
-
# matched `"..."` / `'...'` spans is part of the token, not a
|
|
130
|
-
# separator. Single-quote spans have no escape semantics; double-quote
|
|
131
|
-
# spans treat `\"` and `\\` as literal escapes (POSIX shell rules).
|
|
132
|
-
printf '%s' "$COMMAND" \
|
|
133
|
-
| awk '
|
|
134
|
-
BEGIN { skip_next = 0 }
|
|
135
|
-
function strip_outer_quotes(s, n, first, last) {
|
|
136
|
-
n = length(s)
|
|
137
|
-
if (n < 2) return s
|
|
138
|
-
first = substr(s, 1, 1)
|
|
139
|
-
last = substr(s, n, 1)
|
|
140
|
-
if ((first == "\"" && last == "\"") || (first == "'\''" && last == "'\''")) {
|
|
141
|
-
return substr(s, 2, n - 2)
|
|
142
|
-
}
|
|
143
|
-
return s
|
|
144
|
-
}
|
|
145
|
-
function emit_token(t) {
|
|
146
|
-
if (skip_next) {
|
|
147
|
-
skip_next = 0
|
|
148
|
-
if (t == "-" || t == "") return
|
|
149
|
-
t = strip_outer_quotes(t)
|
|
150
|
-
print t
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
if (t == "--body-file" || t == "-F") { skip_next = 1; return }
|
|
154
|
-
if (t ~ /^--body-file=/) {
|
|
155
|
-
v = substr(t, length("--body-file=") + 1)
|
|
156
|
-
v = strip_outer_quotes(v)
|
|
157
|
-
if (v != "" && v != "-") print v
|
|
158
|
-
}
|
|
159
|
-
if (t ~ /^-F=/) {
|
|
160
|
-
v = substr(t, length("-F=") + 1)
|
|
161
|
-
v = strip_outer_quotes(v)
|
|
162
|
-
if (v != "" && v != "-") print v
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
{
|
|
166
|
-
line = $0
|
|
167
|
-
n = length(line)
|
|
168
|
-
i = 1
|
|
169
|
-
tok = ""
|
|
170
|
-
mode = 0 # 0=plain, 1=double-quoted, 2=single-quoted
|
|
171
|
-
while (i <= n) {
|
|
172
|
-
ch = substr(line, i, 1)
|
|
173
|
-
if (mode == 0) {
|
|
174
|
-
# 0.18.0 helix-020 G3.B fix: in plain (unquoted) mode,
|
|
175
|
-
# `\X` (any character X) is the POSIX shell escape for
|
|
176
|
-
# the literal character X — most commonly a space in
|
|
177
|
-
# paths like `path\ with\ spaces.md`. Pre-fix the
|
|
178
|
-
# tokenizer treated the `\` as an ordinary character and
|
|
179
|
-
# truncated at the following space, dropping the rest of
|
|
180
|
-
# the path. We now consume the backslash and emit the
|
|
181
|
-
# following byte as a literal part of the current token.
|
|
182
|
-
# `\<eol>` (line-continuation) is left intact — emit the
|
|
183
|
-
# `\` and let the splitter flow into the next record on
|
|
184
|
-
# the assumption that the caller already joined the line.
|
|
185
|
-
if (ch == "\\" && i < n) {
|
|
186
|
-
nxt = substr(line, i + 1, 1)
|
|
187
|
-
tok = tok nxt
|
|
188
|
-
i += 2
|
|
189
|
-
continue
|
|
190
|
-
}
|
|
191
|
-
if (ch == " " || ch == "\t") {
|
|
192
|
-
if (tok != "") { emit_token(tok); tok = "" }
|
|
193
|
-
i++; continue
|
|
194
|
-
}
|
|
195
|
-
if (ch == "\"") { mode = 1; tok = tok ch; i++; continue }
|
|
196
|
-
if (ch == "'\''") { mode = 2; tok = tok ch; i++; continue }
|
|
197
|
-
tok = tok ch
|
|
198
|
-
i++
|
|
199
|
-
continue
|
|
200
|
-
}
|
|
201
|
-
if (mode == 1) {
|
|
202
|
-
if (ch == "\\" && i < n) {
|
|
203
|
-
nxt = substr(line, i + 1, 1)
|
|
204
|
-
tok = tok ch nxt
|
|
205
|
-
i += 2
|
|
206
|
-
continue
|
|
207
|
-
}
|
|
208
|
-
if (ch == "\"") { mode = 0; tok = tok ch; i++; continue }
|
|
209
|
-
tok = tok ch
|
|
210
|
-
i++
|
|
211
|
-
continue
|
|
212
|
-
}
|
|
213
|
-
# mode == 2
|
|
214
|
-
if (ch == "'\''") { mode = 0; tok = tok ch; i++; continue }
|
|
215
|
-
tok = tok ch
|
|
216
|
-
i++
|
|
217
|
-
}
|
|
218
|
-
if (tok != "") emit_token(tok)
|
|
219
|
-
}'
|
|
220
|
-
}
|
|
221
|
-
while IFS= read -r body_path; do
|
|
222
|
-
[[ -z "$body_path" ]] && continue
|
|
223
|
-
raw_path="$body_path"
|
|
224
|
-
# Resolve relative to the hook's cwd (the agent's project dir). gh
|
|
225
|
-
# accepts both absolute paths (e.g. tmpfiles like /var/folders/…) and
|
|
226
|
-
# cwd-relative paths; we honor both. Absolute paths NOT containing
|
|
227
|
-
# `..` are taken at face value.
|
|
228
|
-
if [[ "$body_path" != /* ]]; then
|
|
229
|
-
body_path="$(pwd)/$body_path"
|
|
230
|
-
fi
|
|
231
|
-
# Walk `..` segments. The only outside-REA_ROOT shape we refuse is one
|
|
232
|
-
# where the canonical form contains `..` (i.e. an explicit traversal
|
|
233
|
-
# by the caller). Plain absolute tmp paths are NOT refused — gh issue
|
|
234
|
-
# body-files are very commonly written to /var/folders or /tmp and
|
|
235
|
-
# rejecting those would defeat the scan in routine use.
|
|
236
|
-
had_traversal=0
|
|
237
|
-
case "/$raw_path/" in */../*) had_traversal=1 ;; esac
|
|
238
|
-
resolved="$body_path"
|
|
239
|
-
if [[ "$had_traversal" -eq 1 ]]; then
|
|
240
|
-
IFS='/' read -ra _bf_parts_raw <<<"$body_path"
|
|
241
|
-
_bf_parts=()
|
|
242
|
-
for _seg in "${_bf_parts_raw[@]}"; do
|
|
243
|
-
case "$_seg" in
|
|
244
|
-
''|.) continue ;;
|
|
245
|
-
..) [[ "${#_bf_parts[@]}" -gt 0 ]] && unset '_bf_parts[${#_bf_parts[@]}-1]' ;;
|
|
246
|
-
*) _bf_parts+=("$_seg") ;;
|
|
247
|
-
esac
|
|
248
|
-
done
|
|
249
|
-
resolved="/$(IFS=/; printf '%s' "${_bf_parts[*]}")"
|
|
250
|
-
# 0.17.0 helix-019 #1: HARD REFUSAL on traversal escaping REA_ROOT.
|
|
251
|
-
# Pre-fix the gate logged "skipping body scan" and exited 0 — every
|
|
252
|
-
# sensitive payload at the resolved external path bypassed the
|
|
253
|
-
# disclosure check. The traversal-out-of-root shape exists ONLY to
|
|
254
|
-
# obfuscate; legitimate workflows pass absolute tmpfile paths
|
|
255
|
-
# (`/tmp/...`, `/var/folders/...`) without `..` segments.
|
|
256
|
-
if [[ "$resolved" != "$REA_ROOT" && "$resolved" != "$REA_ROOT"/* ]]; then
|
|
257
|
-
{
|
|
258
|
-
printf 'SECURITY DISCLOSURE GATE: --body-file path traversal escapes project root\n'
|
|
259
|
-
printf '\n'
|
|
260
|
-
printf ' Path: %s\n' "$raw_path"
|
|
261
|
-
printf ' Resolved: %s\n' "$resolved"
|
|
262
|
-
printf '\n'
|
|
263
|
-
printf ' Rule: --body-file paths whose canonical form uses `..` segments to\n'
|
|
264
|
-
printf ' escape REA_ROOT are refused. Move the file inside the project\n'
|
|
265
|
-
printf ' tree, or paste the body inline via --body.\n'
|
|
266
|
-
} >&2
|
|
267
|
-
exit 2
|
|
268
|
-
fi
|
|
269
|
-
fi
|
|
270
|
-
if [[ ! -r "$resolved" ]]; then
|
|
271
|
-
printf 'security-disclosure-gate: --body-file %s unreadable; skipping body scan\n' "$raw_path" >&2
|
|
272
|
-
continue
|
|
273
|
-
fi
|
|
274
|
-
# Cap at 64 KiB. Lowercase to match FULL_TEXT case folding.
|
|
275
|
-
body_chunk=$(head -c 65536 "$resolved" 2>/dev/null | tr '[:upper:]' '[:lower:]') || body_chunk=""
|
|
276
|
-
if [[ -n "$body_chunk" ]]; then
|
|
277
|
-
BODY_FILE_TEXT="${BODY_FILE_TEXT}
|
|
278
|
-
${body_chunk}"
|
|
279
|
-
fi
|
|
280
|
-
done < <(_extract_body_file_paths)
|
|
281
|
-
|
|
282
|
-
FULL_TEXT="${BODY_FILE_TEXT}
|
|
283
|
-
$(echo "$COMMAND" | tr '[:upper:]' '[:lower:]')"
|
|
284
|
-
|
|
285
|
-
MATCHED_PATTERN=""
|
|
286
|
-
for PATTERN in "${SECURITY_PATTERNS[@]}"; do
|
|
287
|
-
if echo "$FULL_TEXT" | grep -qiE "$PATTERN"; then
|
|
288
|
-
MATCHED_PATTERN="$PATTERN"
|
|
289
|
-
break
|
|
290
|
-
fi
|
|
291
|
-
done
|
|
292
|
-
|
|
293
|
-
if [[ -z "$MATCHED_PATTERN" ]]; then
|
|
294
|
-
exit 0
|
|
89
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
90
|
+
# 0.32.0 round-4 P1: this is a blocking-tier gate — the pre-0.32.0
|
|
91
|
+
# bash body enforced the disclosure policy WITHOUT a compiled CLI.
|
|
92
|
+
# Falling through to exit 0 here would silently disable security-
|
|
93
|
+
# keyword blocking on `gh issue create` until the operator runs
|
|
94
|
+
# `pnpm install` / `pnpm build`. Fail closed: refuse the operation
|
|
95
|
+
# and tell the operator how to restore protection.
|
|
96
|
+
printf 'rea: security-disclosure-gate cannot run — the rea CLI is not built.\n' >&2
|
|
97
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
98
|
+
printf 'This shim fails closed because the pre-0.32.0 bash body enforced disclosure policy without a CLI.\n' >&2
|
|
99
|
+
exit 2
|
|
295
100
|
fi
|
|
296
101
|
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
This project is configured for PRIVATE disclosure (REA_DISCLOSURE_MODE=issues).
|
|
305
|
-
|
|
306
|
-
CORRECT PATH for security findings in this private repo:
|
|
307
|
-
Use: gh issue create --label 'security,internal' --title '...' --body '...'
|
|
308
|
-
|
|
309
|
-
The 'security' and 'internal' labels keep this off public project boards and
|
|
310
|
-
mark it for maintainer-only triage. Do NOT use the public issue queue without
|
|
311
|
-
these labels for security findings.
|
|
312
|
-
|
|
313
|
-
If this is NOT a security finding, rephrase the title/body to avoid triggering
|
|
314
|
-
security patterns, then retry."
|
|
315
|
-
|
|
316
|
-
else
|
|
317
|
-
# Advisory mode (default): redirect to GitHub Security Advisories
|
|
318
|
-
json_output "block" \
|
|
319
|
-
"SECURITY DISCLOSURE GATE: This issue appears to describe a security vulnerability (matched: '${MATCHED_PATTERN}'). Do NOT create a public GitHub issue for security vulnerabilities.
|
|
320
|
-
|
|
321
|
-
CORRECT DISCLOSURE PATH:
|
|
322
|
-
1. Use GitHub Security Advisories (private):
|
|
323
|
-
gh api repos/{owner}/{repo}/security-advisories --method POST --input - <<'JSON'
|
|
324
|
-
{ \"summary\": \"...\", \"description\": \"...\", \"severity\": \"medium|high|critical\",
|
|
325
|
-
\"vulnerabilities\": [{\"package\": {\"name\": \"@pkg\", \"ecosystem\": \"npm\"}}] }
|
|
326
|
-
JSON
|
|
327
|
-
2. Or navigate to: Security tab → Advisories → 'Report a vulnerability'
|
|
328
|
-
3. Or email security@bookedsolid.tech (see SECURITY.md)
|
|
329
|
-
|
|
330
|
-
The finding will be publicly disclosed AFTER a patch is released (coordinated disclosure).
|
|
102
|
+
# 3. Realpath sandbox check.
|
|
103
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
104
|
+
printf 'rea: security-disclosure-gate cannot run — `node` is not on PATH.\n' >&2
|
|
105
|
+
printf 'Install Node 22+ (engines.node) to restore disclosure-policy enforcement.\n' >&2
|
|
106
|
+
exit 2
|
|
107
|
+
fi
|
|
331
108
|
|
|
332
|
-
|
|
333
|
-
|
|
109
|
+
sandbox_check=$(node -e '
|
|
110
|
+
const fs = require("fs");
|
|
111
|
+
const path = require("path");
|
|
112
|
+
const cli = process.argv[1];
|
|
113
|
+
const projDir = process.argv[2];
|
|
114
|
+
let real, realProj;
|
|
115
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
116
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
119
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const sep = path.sep;
|
|
122
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
123
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
124
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
127
|
+
let found = false;
|
|
128
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
129
|
+
const pj = path.join(cur, "package.json");
|
|
130
|
+
if (fs.existsSync(pj)) {
|
|
131
|
+
try {
|
|
132
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
133
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
134
|
+
} catch (e) { /* keep walking */ }
|
|
135
|
+
}
|
|
136
|
+
cur = path.dirname(cur);
|
|
137
|
+
}
|
|
138
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
139
|
+
process.stdout.write("ok");
|
|
140
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
141
|
+
|
|
142
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
143
|
+
# 0.32.0 round-4 P1: fail closed (blocking-tier — see exit-0 → exit-2
|
|
144
|
+
# rationale at the top). A failed sandbox check means the CLI we
|
|
145
|
+
# would run cannot be authenticated as the rea binary; refusing is
|
|
146
|
+
# both the safest posture AND preserves the pre-0.32.0 bash-body
|
|
147
|
+
# contract that this hook always enforces policy.
|
|
148
|
+
printf 'rea: security-disclosure-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
149
|
+
exit 2
|
|
150
|
+
fi
|
|
334
151
|
|
|
335
|
-
|
|
336
|
-
security
|
|
152
|
+
# 4. Version-probe: confirm the resolved CLI implements
|
|
153
|
+
# `hook security-disclosure-gate`. Codex round 1 P1.
|
|
154
|
+
probe_out=$("${REA_ARGV[@]}" hook security-disclosure-gate --help 2>&1)
|
|
155
|
+
probe_status=$?
|
|
156
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'security-disclosure-gate'; then
|
|
157
|
+
# 0.32.0 round-4 P1: a stale/older CLI without the new subcommand is
|
|
158
|
+
# NOT a "harmless availability fallback" for this hook — the bash
|
|
159
|
+
# body it replaces always enforced. Fail closed and tell the
|
|
160
|
+
# operator exactly how to fix.
|
|
161
|
+
printf 'rea: this shim requires the `rea hook security-disclosure-gate` subcommand (introduced in 0.32.0).\n' >&2
|
|
162
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
163
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
164
|
+
exit 2
|
|
337
165
|
fi
|
|
338
166
|
|
|
339
|
-
|
|
167
|
+
# 5. Forward stdin (already captured up-front for the relevance gate).
|
|
168
|
+
# REA_DISCLOSURE_MODE is in env already; the Node binary reads it
|
|
169
|
+
# directly.
|
|
170
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook security-disclosure-gate
|
|
171
|
+
exit $?
|
|
@@ -172,10 +172,12 @@ fi
|
|
|
172
172
|
LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
|
|
173
173
|
|
|
174
174
|
# ── 5b. Extension-surface allow-list ──────────────────────────────────────────
|
|
175
|
-
# `.husky/commit-msg.d
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
175
|
+
# `.husky/commit-msg.d/*`, `.husky/pre-push.d/*`, and (0.32.0+)
|
|
176
|
+
# `.husky/prepare-commit-msg.d/*` are the documented consumer
|
|
177
|
+
# extension surface (Fix H / 0.13.0; Phase 3 / 0.32.0 for the
|
|
178
|
+
# prepare-commit-msg lane). Consumers — and the agents that govern
|
|
179
|
+
# those consumers — are expected to write here freely so they can
|
|
180
|
+
# layer commitlint, lint-staged, branch-policy, act-CI, etc. without
|
|
179
181
|
# losing rea coverage on `rea upgrade`.
|
|
180
182
|
#
|
|
181
183
|
# The §6 PROTECTED_PATTERNS list below has `.husky/` as a prefix block,
|
|
@@ -216,14 +218,14 @@ LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
|
|
|
216
218
|
# subshell pattern (no Python or readlink -f dependency required).
|
|
217
219
|
# Closes the path-string→symlink bypass completely.
|
|
218
220
|
case "$LOWER_NORM" in
|
|
219
|
-
.husky/commit-msg.d/*|.husky/pre-push.d/*)
|
|
221
|
+
.husky/commit-msg.d/*|.husky/pre-push.d/*|.husky/prepare-commit-msg.d/*)
|
|
220
222
|
if [ -L "$FILE_PATH" ]; then
|
|
221
223
|
{
|
|
222
224
|
printf 'SETTINGS PROTECTION: symlink in extension surface refused\n'
|
|
223
225
|
printf '\n'
|
|
224
226
|
printf ' File: %s\n' "$SAFE_FILE_PATH"
|
|
225
|
-
printf ' Rule: .husky/commit-msg
|
|
226
|
-
printf ' regular files (a symlink could resolve to a protected\n'
|
|
227
|
+
printf ' Rule: .husky/{commit-msg,pre-push,prepare-commit-msg}.d/* must\n'
|
|
228
|
+
printf ' be regular files (a symlink could resolve to a protected\n'
|
|
227
229
|
printf ' package-managed body and bypass §6 protection).\n'
|
|
228
230
|
} >&2
|
|
229
231
|
exit 2
|
|
@@ -245,8 +247,10 @@ case "$LOWER_NORM" in
|
|
|
245
247
|
# to `.husky/pre-push.d.bak/...` and slipped through.
|
|
246
248
|
# The trailing `/` on each pattern (and the explicit
|
|
247
249
|
# exact-match arm) requires a real directory boundary.
|
|
250
|
+
# 0.32.0 Phase 3: `.husky/prepare-commit-msg.d/` joins the
|
|
251
|
+
# allow-list (mirrors commit-msg.d/pre-push.d patterns).
|
|
248
252
|
case "$resolved_parent" in
|
|
249
|
-
*/.husky/commit-msg.d|*/.husky/commit-msg.d/*|*/.husky/pre-push.d|*/.husky/pre-push.d/*) : ;;
|
|
253
|
+
*/.husky/commit-msg.d|*/.husky/commit-msg.d/*|*/.husky/pre-push.d|*/.husky/pre-push.d/*|*/.husky/prepare-commit-msg.d|*/.husky/prepare-commit-msg.d/*) : ;;
|
|
250
254
|
*)
|
|
251
255
|
{
|
|
252
256
|
printf 'SETTINGS PROTECTION: extension path resolves outside surface\n'
|
|
@@ -254,7 +258,7 @@ case "$LOWER_NORM" in
|
|
|
254
258
|
printf ' Logical: %s\n' "$SAFE_FILE_PATH"
|
|
255
259
|
printf ' Resolved: %s\n' "$resolved_parent"
|
|
256
260
|
printf ' Rule: an intermediate directory of the extension path is a\n'
|
|
257
|
-
printf ' symlink whose target leaves .husky/{commit-msg,pre-push}.d/.\n'
|
|
261
|
+
printf ' symlink whose target leaves .husky/{commit-msg,pre-push,prepare-commit-msg}.d/.\n'
|
|
258
262
|
printf ' Refused to prevent symlinked-parent bypass of the\n'
|
|
259
263
|
printf ' package-managed body protection.\n'
|
|
260
264
|
} >&2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|