@bookedsolid/rea 0.9.1 → 0.9.3
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/push-review-core.sh +13 -12
- package/hooks/commit-review-gate.sh +11 -14
- package/package.json +1 -1
- package/scripts/tarball-smoke.sh +121 -3
|
@@ -341,15 +341,7 @@ pr_core_run() {
|
|
|
341
341
|
exit 0
|
|
342
342
|
fi
|
|
343
343
|
|
|
344
|
-
# ── 5.
|
|
345
|
-
local POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
346
|
-
if [[ -f "$POLICY_FILE" ]]; then
|
|
347
|
-
if grep -qE 'push_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
|
|
348
|
-
exit 0
|
|
349
|
-
fi
|
|
350
|
-
fi
|
|
351
|
-
|
|
352
|
-
# ── 5a. REA_SKIP_PUSH_REVIEW — whole-gate escape hatch ────────────────────
|
|
344
|
+
# ── 5. REA_SKIP_PUSH_REVIEW — whole-gate escape hatch ─────────────────────
|
|
353
345
|
# An opt-in bypass for the ENTIRE push-review gate (not just the Codex
|
|
354
346
|
# branch). Exists to unblock consumers when rea itself is broken or a
|
|
355
347
|
# corrupt policy/audit file would otherwise deadlock a push. Requires an
|
|
@@ -856,7 +848,7 @@ pr_core_run() {
|
|
|
856
848
|
} >&2
|
|
857
849
|
exit 2
|
|
858
850
|
fi
|
|
859
|
-
if printf '%s\n' "$_refspec_hits" | awk -v re='^(src/gateway/middleware/|hooks/|[.]claude/hooks/|src/policy/|[.]github/workflows/)' '
|
|
851
|
+
if printf '%s\n' "$_refspec_hits" | awk -v re='^(src/gateway/middleware/|hooks/|[.]claude/hooks/|src/policy/|[.]github/workflows/|[.]rea/|[.]husky/)' '
|
|
860
852
|
{
|
|
861
853
|
status = $1
|
|
862
854
|
if (status !~ /^[ACDMRTU]/) next
|
|
@@ -896,6 +888,8 @@ pr_core_run() {
|
|
|
896
888
|
printf ' - .claude/hooks/\n'
|
|
897
889
|
printf ' - src/policy/\n'
|
|
898
890
|
printf ' - .github/workflows/\n'
|
|
891
|
+
printf ' - .rea/\n'
|
|
892
|
+
printf ' - .husky/\n'
|
|
899
893
|
printf '\n'
|
|
900
894
|
printf ' Run /codex-review against %s, then retry the push.\n' "$local_sha"
|
|
901
895
|
printf ' The codex-adversarial agent emits the required audit entry.\n'
|
|
@@ -992,8 +986,15 @@ pr_core_run() {
|
|
|
992
986
|
|
|
993
987
|
local -a REA_CLI_ARGS
|
|
994
988
|
REA_CLI_ARGS=()
|
|
995
|
-
|
|
996
|
-
|
|
989
|
+
# node_modules/.bin/rea is a launcher (pnpm writes a POSIX shell shim, npm
|
|
990
|
+
# writes a symlink to dist/cli/index.js with its own `#!/usr/bin/env node`
|
|
991
|
+
# shebang). Either way it is NOT a plain JS file, so running `node` on it
|
|
992
|
+
# would parse shell syntax as JavaScript and SyntaxError. Execute the shim
|
|
993
|
+
# directly — it handles `exec node` itself — and only prepend `node` on the
|
|
994
|
+
# dist fallback, which is a real JS module. The `-x` guard picks up both
|
|
995
|
+
# pnpm shims (executable regular file) and npm symlinks (executable target).
|
|
996
|
+
if [[ -x "${REA_ROOT}/node_modules/.bin/rea" ]]; then
|
|
997
|
+
REA_CLI_ARGS=("${REA_ROOT}/node_modules/.bin/rea")
|
|
997
998
|
elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
|
|
998
999
|
REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
|
|
999
1000
|
fi
|
|
@@ -103,18 +103,7 @@ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--amend'; then
|
|
|
103
103
|
exit 0
|
|
104
104
|
fi
|
|
105
105
|
|
|
106
|
-
# ── 5.
|
|
107
|
-
# Fail-open if policy doesn't exist or doesn't have quality_gates
|
|
108
|
-
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
109
|
-
if [[ -f "$POLICY_FILE" ]]; then
|
|
110
|
-
if grep -qE '^quality_gates:' "$POLICY_FILE" 2>/dev/null; then
|
|
111
|
-
if grep -qE 'commit_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
|
|
112
|
-
exit 0
|
|
113
|
-
fi
|
|
114
|
-
fi
|
|
115
|
-
fi
|
|
116
|
-
|
|
117
|
-
# ── 6. Compute diff stats ────────────────────────────────────────────────────
|
|
106
|
+
# ── 5. Compute diff stats ────────────────────────────────────────────────────
|
|
118
107
|
# Get staged diff (what would be committed)
|
|
119
108
|
DIFF_OUTPUT=$(cd "$REA_ROOT" && git diff --cached --stat 2>/dev/null || echo "")
|
|
120
109
|
DIFF_FULL=$(cd "$REA_ROOT" && git diff --cached 2>/dev/null || echo "")
|
|
@@ -154,9 +143,17 @@ fi
|
|
|
154
143
|
|
|
155
144
|
# ── 9. Resolve rea CLI ────────────────────────────────────────────────────
|
|
156
145
|
# Try local installs first, then dist build, then global PATH install.
|
|
146
|
+
#
|
|
147
|
+
# node_modules/.bin/rea is a launcher (pnpm writes a POSIX shell shim, npm
|
|
148
|
+
# writes a symlink to dist/cli/index.js with its own `#!/usr/bin/env node`
|
|
149
|
+
# shebang). Either way it is NOT a plain JS file, so running `node` on it
|
|
150
|
+
# would parse shell syntax as JavaScript and SyntaxError. Execute the shim
|
|
151
|
+
# directly — it handles `exec node` itself — and only prepend `node` on the
|
|
152
|
+
# dist fallback, which is a real JS module. The `-x` guard picks up both
|
|
153
|
+
# pnpm shims (executable regular file) and npm symlinks (executable target).
|
|
157
154
|
REA_CLI_ARGS=()
|
|
158
|
-
if [[ -
|
|
159
|
-
REA_CLI_ARGS=(
|
|
155
|
+
if [[ -x "${REA_ROOT}/node_modules/.bin/rea" ]]; then
|
|
156
|
+
REA_CLI_ARGS=("${REA_ROOT}/node_modules/.bin/rea")
|
|
160
157
|
elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
|
|
161
158
|
REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
|
|
162
159
|
elif command -v rea >/dev/null 2>&1; then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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)",
|
package/scripts/tarball-smoke.sh
CHANGED
|
@@ -222,17 +222,134 @@ if [ -n "$SEC_CHANGESETS" ]; then
|
|
|
222
222
|
echo "[smoke] security-claim gate: $(printf '%s\n' "$SEC_CHANGESETS" | wc -l | awk '{print $1}') changeset(s) tagged [security]"
|
|
223
223
|
|
|
224
224
|
SEC_SRC_TESTS="$(cd "$REPO_ROOT" && find src -type f \( -name '*sanitize*.test.ts' -o -name '*security*.test.ts' \) 2>/dev/null | sort)"
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
# 0.9.3 extension — some security hotfixes touch ONLY shell hooks (no TS/dist
|
|
226
|
+
# symbols), so the compiled-symbol gate above doesn't apply. For those, the
|
|
227
|
+
# regression proof lives under __tests__/hooks/*{security,bypass,injection,
|
|
228
|
+
# sanitize}*.test.ts and the tarball must ship the hook file(s) the test
|
|
229
|
+
# exercises. This block runs IN ADDITION to the src/ symbol gate — either
|
|
230
|
+
# layer alone can satisfy a [security] changeset, but at least one MUST.
|
|
231
|
+
SEC_HOOK_TESTS="$(cd "$REPO_ROOT" && find __tests__/hooks -type f \( -name '*security*.test.ts' -o -name '*bypass*.test.ts' -o -name '*sanitize*.test.ts' -o -name '*injection*.test.ts' \) 2>/dev/null | sort)"
|
|
232
|
+
|
|
233
|
+
if [ -z "$SEC_SRC_TESTS" ] && [ -z "$SEC_HOOK_TESTS" ]; then
|
|
234
|
+
echo "[smoke] FAIL — [security] changeset present but no matching regression test found:" >&2
|
|
235
|
+
echo "[smoke] - src/**/(*sanitize*|*security*).test.ts — for compiled-symbol fixes" >&2
|
|
236
|
+
echo "[smoke] - __tests__/hooks/(*security*|*bypass*|*sanitize*|*injection*).test.ts — for hook fixes" >&2
|
|
227
237
|
echo "[smoke] a security-claim changeset with no matching regression test is a trust violation" >&2
|
|
228
238
|
exit 2
|
|
229
239
|
fi
|
|
230
240
|
|
|
231
|
-
#
|
|
241
|
+
# Hook-level gate: for each hook-security test FILE, extract the hook
|
|
242
|
+
# file path(s) it installs/exercises (relative to REPO_ROOT) and assert
|
|
243
|
+
# the tarball ships that hook under node_modules/@bookedsolid/rea/hooks/
|
|
244
|
+
# AND that `rea init` fanned it out to $SMOKE_DIR/.claude/hooks/.
|
|
245
|
+
#
|
|
246
|
+
# Known narrowness (called out honestly rather than papered over):
|
|
247
|
+
# - Granularity is per-test-file, not per-`it()` block. A file may
|
|
248
|
+
# contain multiple `it(...)` cases; the extractor scans the whole
|
|
249
|
+
# file body. In practice a [security]-claim test file should focus
|
|
250
|
+
# on one defect class; mixing unrelated `it()` cases with different
|
|
251
|
+
# hook refs dilutes the proof. PR review is the mitigation.
|
|
252
|
+
# - A [security] file with zero extractable refs fails LOUDLY (see
|
|
253
|
+
# EMPTY_REF_TESTS below). The narrowness only applies to files that
|
|
254
|
+
# do extract refs but don't scope them per `it()`.
|
|
255
|
+
if [ -n "$SEC_HOOK_TESTS" ]; then
|
|
256
|
+
HOOK_MISSING=""
|
|
257
|
+
HOOK_COUNT=0
|
|
258
|
+
EMPTY_REF_TESTS=""
|
|
259
|
+
while IFS= read -r hook_test; do
|
|
260
|
+
[ -z "$hook_test" ] && continue
|
|
261
|
+
# Pull hook paths referenced by the test. Matches forms like:
|
|
262
|
+
# 'hooks', 'push-review-gate.sh'
|
|
263
|
+
# 'hooks', '_lib', 'push-review-core.sh'
|
|
264
|
+
# 'hooks', 'commit-review-gate.sh'
|
|
265
|
+
HOOK_REFS="$(perl -0777 -ne '
|
|
266
|
+
while (/path\.join\(\s*REPO_ROOT\s*,\s*[\x27"]hooks[\x27"](?:\s*,\s*[\x27"]([^\x27"]+)[\x27"])*\s*\)/sg) {
|
|
267
|
+
my $all = $&;
|
|
268
|
+
my @parts;
|
|
269
|
+
while ($all =~ /[\x27"]([^\x27"]+)[\x27"]/g) {
|
|
270
|
+
push @parts, $1 unless $1 eq "REPO_ROOT";
|
|
271
|
+
}
|
|
272
|
+
print join("/", @parts), "\n" if @parts;
|
|
273
|
+
}
|
|
274
|
+
' "$REPO_ROOT/$hook_test" 2>/dev/null | sort -u)"
|
|
275
|
+
|
|
276
|
+
# Per-test failure: a [security] hook-test that yields zero extractable
|
|
277
|
+
# refs (e.g. uses template literals, dynamic concatenation, or helper
|
|
278
|
+
# indirection) is invisible to this gate. Fail loud so the author is
|
|
279
|
+
# forced to use the extractable path.join(REPO_ROOT, 'hooks', ...) shape,
|
|
280
|
+
# rather than having a lone extractable neighbor test silently satisfy
|
|
281
|
+
# the whole gate.
|
|
282
|
+
if [ -z "$HOOK_REFS" ]; then
|
|
283
|
+
EMPTY_REF_TESTS="$EMPTY_REF_TESTS
|
|
284
|
+
$hook_test"
|
|
285
|
+
continue
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
while IFS= read -r rel; do
|
|
289
|
+
[ -z "$rel" ] && continue
|
|
290
|
+
HOOK_COUNT=$((HOOK_COUNT + 1))
|
|
291
|
+
# `rel` already includes the leading `hooks/` segment from perl. It
|
|
292
|
+
# looks like `hooks/push-review-gate.sh` or
|
|
293
|
+
# `hooks/_lib/push-review-core.sh`. The tarball ships hooks under
|
|
294
|
+
# `node_modules/@bookedsolid/rea/hooks/` and `rea init` fans them out
|
|
295
|
+
# to `$SMOKE_DIR/.claude/hooks/`. Assert BOTH — the tarball
|
|
296
|
+
# source-of-truth AND the post-init install surface.
|
|
297
|
+
rel_no_prefix="${rel#hooks/}"
|
|
298
|
+
TARBALL_HOOK="$SMOKE_DIR/node_modules/@bookedsolid/rea/hooks/$rel_no_prefix"
|
|
299
|
+
INSTALLED_HOOK="$SMOKE_DIR/.claude/hooks/$rel_no_prefix"
|
|
300
|
+
if [ ! -f "$TARBALL_HOOK" ] || [ ! -f "$INSTALLED_HOOK" ]; then
|
|
301
|
+
HOOK_MISSING="$HOOK_MISSING
|
|
302
|
+
$rel (exercised by $hook_test)"
|
|
303
|
+
fi
|
|
304
|
+
done <<< "$HOOK_REFS"
|
|
305
|
+
done <<< "$SEC_HOOK_TESTS"
|
|
306
|
+
|
|
307
|
+
if [ -n "$HOOK_MISSING" ]; then
|
|
308
|
+
echo "[smoke] FAIL — [security] hook-test gate: hook file(s) under test are MISSING from tarball:" >&2
|
|
309
|
+
printf '%s\n' "$HOOK_MISSING" >&2
|
|
310
|
+
exit 2
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
if [ -n "$EMPTY_REF_TESTS" ]; then
|
|
314
|
+
echo "[smoke] FAIL — [security] hook-test gate: one or more hook-security tests yielded zero extractable hook references:" >&2
|
|
315
|
+
printf '%s\n' "$EMPTY_REF_TESTS" >&2
|
|
316
|
+
echo "[smoke] hook-security tests MUST reference hook files via the literal shape" >&2
|
|
317
|
+
echo "[smoke] path.join(REPO_ROOT, 'hooks', '<name>.sh') (or with a nested '_lib' arg)" >&2
|
|
318
|
+
echo "[smoke] Template literals, dynamic concatenation, and helper indirection are" >&2
|
|
319
|
+
echo "[smoke] invisible to this gate and would let hook changes ship unverified." >&2
|
|
320
|
+
exit 2
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
if [ "$HOOK_COUNT" -eq 0 ]; then
|
|
324
|
+
echo "[smoke] FAIL — [security] hook-test gate: no checkable hook references extracted" >&2
|
|
325
|
+
echo "[smoke] hook-security tests must reference hook files via path.join(REPO_ROOT, 'hooks', ...)" >&2
|
|
326
|
+
echo "[smoke] so the gate can verify the hook ships in the tarball" >&2
|
|
327
|
+
exit 2
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
echo "[smoke] → $(printf '%s\n' "$SEC_HOOK_TESTS" | wc -l | awk '{print $1}') hook-security test(s), $HOOK_COUNT hook ref(s) all present in tarball"
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# The compiled-symbol gate below only runs when src/ security tests exist.
|
|
334
|
+
# A hook-only hotfix satisfies via the block above. Flag the src/ gate to
|
|
335
|
+
# skip gracefully — the remaining smoke checks (export resolution, tree
|
|
336
|
+
# equality) still run unconditionally below.
|
|
337
|
+
SKIP_SRC_SYMBOL_GATE=0
|
|
338
|
+
if [ -z "$SEC_SRC_TESTS" ]; then
|
|
339
|
+
SKIP_SRC_SYMBOL_GATE=1
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# For each src security test, collect the named imports pulled from relative
|
|
232
343
|
# paths — those are the symbols under test and must be compiled into dist/.
|
|
233
344
|
# Example line we want to match:
|
|
234
345
|
# import { sanitizeHealthSnapshot, INJECTION_REDACTED_PLACEHOLDER } from './health';
|
|
235
346
|
# We ignore imports from bare package names ('vitest', 'node:fs', etc.).
|
|
347
|
+
#
|
|
348
|
+
# Skipped when only __tests__/hooks/ security tests exist (hook-only hotfix);
|
|
349
|
+
# the hook-test gate above is authoritative for that case.
|
|
350
|
+
if [ "$SKIP_SRC_SYMBOL_GATE" = "1" ]; then
|
|
351
|
+
echo "[smoke] → src/ symbol gate skipped (no src/*{security,sanitize}*.test.ts — hook-test gate is authoritative)"
|
|
352
|
+
else
|
|
236
353
|
MISSING_SYMBOLS=""
|
|
237
354
|
SYMBOL_COUNT=0
|
|
238
355
|
while IFS= read -r src_test; do
|
|
@@ -294,6 +411,7 @@ if [ -n "$SEC_CHANGESETS" ]; then
|
|
|
294
411
|
fi
|
|
295
412
|
|
|
296
413
|
echo "[smoke] → $(printf '%s\n' "$SEC_SRC_TESTS" | wc -l | awk '{print $1}') security regression test(s), $SYMBOL_COUNT imported symbol(s) all present in dist/"
|
|
414
|
+
fi # SKIP_SRC_SYMBOL_GATE
|
|
297
415
|
fi
|
|
298
416
|
|
|
299
417
|
# Verify every declared public export resolves. If the exports map points at a
|