@bookedsolid/rea 0.48.0 → 0.49.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/THREAT_MODEL.md +70 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +241 -0
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +161 -0
- package/dist/cli/install/self-pin.d.ts +440 -0
- package/dist/cli/install/self-pin.js +853 -0
- package/dist/cli/upgrade.js +134 -0
- package/dist/policy/loader.d.ts +13 -0
- package/dist/policy/loader.js +36 -0
- package/dist/policy/profiles.d.ts +13 -0
- package/dist/policy/profiles.js +12 -0
- package/dist/policy/types.d.ts +38 -0
- package/hooks/_lib/bootstrap-allowlist.sh +1075 -0
- package/hooks/_lib/shim-cache.sh +96 -8
- package/hooks/_lib/shim-runtime.sh +88 -53
- package/hooks/blocked-paths-bash-gate.sh +35 -12
- package/hooks/protected-paths-bash-gate.sh +30 -12
- package/package.json +3 -1
- package/profiles/bst-internal-no-codex.yaml +4 -0
- package/profiles/bst-internal.yaml +28 -0
- package/profiles/client-engagement.yaml +9 -0
- package/profiles/lit-wc.yaml +6 -0
- package/profiles/minimal.yaml +11 -0
- package/profiles/open-source-no-codex.yaml +4 -0
- package/profiles/open-source.yaml +11 -0
- package/templates/_lib_shim-cache.dogfood-staged.sh +96 -8
- package/templates/_lib_shim-runtime.dogfood-staged.sh +88 -53
package/hooks/_lib/shim-cache.sh
CHANGED
|
@@ -250,10 +250,24 @@ shim_cache_disabled() {
|
|
|
250
250
|
# but valid YAML). Pre-fix the bash matcher pinned column 0.
|
|
251
251
|
# We strip leading whitespace from the line for matching
|
|
252
252
|
# purposes; for the block-form sub-block scan we ALSO track the
|
|
253
|
-
# opener
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
253
|
+
# opener indent depth so we do not mistake a deeper-indented
|
|
254
|
+
# sibling block enabled: false for ours. The block-form-end
|
|
255
|
+
# heuristic is now first non-empty line at or below the opener
|
|
256
|
+
# indent level.
|
|
257
|
+
#
|
|
258
|
+
# 0.48.1 R10 P3: pure-comment lines (matching ^[[:space:]]*#) MUST
|
|
259
|
+
# NOT close the block. Pre-fix a top-level comment like
|
|
260
|
+
# shim_cache:\n# note\n enabled: false closed the block on the
|
|
261
|
+
# comment line (non-empty, indent 0 <= opener_indent 0) and the
|
|
262
|
+
# subsequent enabled: false was treated as a top-level key with
|
|
263
|
+
# no parent block, so the disable was silently ignored.
|
|
264
|
+
#
|
|
265
|
+
# 0.48.1 multi-line flow-form: shim_cache: {\n enabled: false\n}
|
|
266
|
+
# is valid YAML the TS loader accepts. We add a flow-block state
|
|
267
|
+
# that opens on shim_cache: { (with the { unmatched on the same
|
|
268
|
+
# line), accumulates body until }, and matches enabled: false in
|
|
269
|
+
# the assembled buffer. The single-line flow-form rule above
|
|
270
|
+
# still wins for shim_cache: { enabled: false } on one line.
|
|
257
271
|
result=$(awk '
|
|
258
272
|
{
|
|
259
273
|
lc = tolower($0)
|
|
@@ -262,9 +276,82 @@ shim_cache_disabled() {
|
|
|
262
276
|
indent_of_line = match(lc, /[^[:space:]]/) - 1
|
|
263
277
|
if (indent_of_line < 0) indent_of_line = 0
|
|
264
278
|
}
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
279
|
+
# Pure-comment line: skip without affecting state. Must come
|
|
280
|
+
# before BOTH the flow-block accumulator AND the block-end
|
|
281
|
+
# heuristic so a comment inside either context is transparent.
|
|
282
|
+
lc ~ /^[[:space:]]*#/ {
|
|
283
|
+
next
|
|
284
|
+
}
|
|
285
|
+
# 0.48.1 round-2 P2: removed the narrow single-line flow regex
|
|
286
|
+
# that matched shim_cache: { enabled: false } with [^}]* — it
|
|
287
|
+
# had no concept of quoted scalars or trailing comments and
|
|
288
|
+
# mis-fired on shim_cache: { note: "enabled: false", enabled:
|
|
289
|
+
# true }. The single-line case now flows through the brace-
|
|
290
|
+
# depth path below (which strips quotes + trailing comments
|
|
291
|
+
# per line); the only behavior change is that the inline form
|
|
292
|
+
# gets the same sanitization the multi-line form already does.
|
|
293
|
+
# Flow-form multi-line opener: shim_cache: { with the first {
|
|
294
|
+
# unmatched on the same line. Start accumulating until the
|
|
295
|
+
# matching close brace.
|
|
296
|
+
#
|
|
297
|
+
# 0.48.1 round-1 P2-B: track BRACE DEPTH across the buffer
|
|
298
|
+
# instead of closing on the first }. Valid YAML such as
|
|
299
|
+
# shim_cache: { meta: { foo: bar }, enabled: false } has
|
|
300
|
+
# nested {} pairs; a quoted scalar like note: "}" embeds a
|
|
301
|
+
# brace inside a string. We strip "..." and *...* (single-quote
|
|
302
|
+
# placeholder is \047 to keep this awk single-quoted body
|
|
303
|
+
# apostrophe-clean) before counting so quoted braces do not
|
|
304
|
+
# affect depth. Approximate but matches every shape the TS
|
|
305
|
+
# loader accepts; on a malformed policy the worst case is
|
|
306
|
+
# cache-stays-on (the safe default).
|
|
307
|
+
in_flow == 0 && lc ~ /^[[:space:]]*shim_cache:[[:space:]]*\{/ {
|
|
308
|
+
# Build a sanitized line: strip quoted scalars first so
|
|
309
|
+
# quoted braces / quoted comments / quoted enabled: false
|
|
310
|
+
# tokens cannot pollute brace-depth or value detection;
|
|
311
|
+
# then strip trailing #-comments so they cannot either.
|
|
312
|
+
# 0.48.1 round-2 P2 fix.
|
|
313
|
+
line_stripped = lc
|
|
314
|
+
gsub(/"[^"]*"/, "", line_stripped)
|
|
315
|
+
gsub(/\047[^\047]*\047/, "", line_stripped)
|
|
316
|
+
gsub(/[[:space:]]*#.*$/, "", line_stripped)
|
|
317
|
+
opens = gsub(/\{/, "{", line_stripped)
|
|
318
|
+
closes = gsub(/\}/, "}", line_stripped)
|
|
319
|
+
flow_depth = opens - closes
|
|
320
|
+
if (flow_depth <= 0) {
|
|
321
|
+
# Already balanced on this line — single-line flow form,
|
|
322
|
+
# potentially with nested braces (e.g. { meta: { foo: bar
|
|
323
|
+
# }, enabled: false }). The narrow single-line rule above
|
|
324
|
+
# cannot match nested-brace shapes (its [^}]* fails on the
|
|
325
|
+
# inner closing brace), so we check the SANITIZED line
|
|
326
|
+
# (quotes + comments stripped) here. Anchoring on a token
|
|
327
|
+
# boundary defends against accidental substring noise
|
|
328
|
+
# like enabled-false-something.
|
|
329
|
+
if (line_stripped ~ /enabled[[:space:]]*:[[:space:]]*false([^[:alnum:]_]|$)/) {
|
|
330
|
+
print "off"; exit
|
|
331
|
+
}
|
|
332
|
+
next
|
|
333
|
+
}
|
|
334
|
+
in_flow = 1
|
|
335
|
+
flow_buf = line_stripped
|
|
336
|
+
next
|
|
337
|
+
}
|
|
338
|
+
# Flow-form continuation: accumulate sanitized + maintain depth.
|
|
339
|
+
in_flow == 1 {
|
|
340
|
+
line_stripped = lc
|
|
341
|
+
gsub(/"[^"]*"/, "", line_stripped)
|
|
342
|
+
gsub(/\047[^\047]*\047/, "", line_stripped)
|
|
343
|
+
gsub(/[[:space:]]*#.*$/, "", line_stripped)
|
|
344
|
+
flow_buf = flow_buf " " line_stripped
|
|
345
|
+
opens = gsub(/\{/, "{", line_stripped)
|
|
346
|
+
closes = gsub(/\}/, "}", line_stripped)
|
|
347
|
+
flow_depth += opens - closes
|
|
348
|
+
if (flow_depth <= 0) {
|
|
349
|
+
in_flow = 0
|
|
350
|
+
if (flow_buf ~ /enabled[[:space:]]*:[[:space:]]*false([^[:alnum:]_]|$)/) {
|
|
351
|
+
print "off"; exit
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
next
|
|
268
355
|
}
|
|
269
356
|
# Block-form opener: leading whitespace allowed.
|
|
270
357
|
lc ~ /^[[:space:]]*shim_cache:[[:space:]]*(#.*)?$/ {
|
|
@@ -273,7 +360,8 @@ shim_cache_disabled() {
|
|
|
273
360
|
next
|
|
274
361
|
}
|
|
275
362
|
# End the block when we see a non-empty line at or below the
|
|
276
|
-
# opener indent (a sibling YAML key at the same level).
|
|
363
|
+
# opener indent (a sibling YAML key at the same level). Comment
|
|
364
|
+
# lines were already filtered above so they cannot close the block.
|
|
277
365
|
in_block && lc !~ /^[[:space:]]*$/ && indent_of_line <= opener_indent {
|
|
278
366
|
in_block = 0
|
|
279
367
|
}
|
|
@@ -312,16 +312,66 @@ shim_run() {
|
|
|
312
312
|
# 4. Resolve CLI.
|
|
313
313
|
shim_resolve_cli
|
|
314
314
|
|
|
315
|
+
# 4a. Sandbox check (0.48.1 — moved earlier from step 5). Runs BEFORE
|
|
316
|
+
# the cache-key-prep block so the dist-tree `find` walk (added in
|
|
317
|
+
# 0.48.0) never recurses an out-of-project symlink target. Pre-
|
|
318
|
+
# 0.48.1 ordering was: cache prep → cache lookup → sandbox check;
|
|
319
|
+
# codex 0.48.0 round-10 P2 caught that a hostile workspace whose
|
|
320
|
+
# `node_modules/@bookedsolid/rea` (or `dist`) symlinks outside
|
|
321
|
+
# CLAUDE_PROJECT_DIR caused the find walk to traverse the external
|
|
322
|
+
# tree before step 5 refused at `bad:cli-escapes-project`. Moving
|
|
323
|
+
# sandbox earlier preserves the cheap-refusal posture the
|
|
324
|
+
# pre-0.48.0 hot path had.
|
|
325
|
+
#
|
|
326
|
+
# Tradeoff vs 0.48.0: a warm cache hit no longer skips sandbox
|
|
327
|
+
# (pre-fix the `_shim_cache_hit` guard at this site bypassed it).
|
|
328
|
+
# Sandbox is a single `node -e` (~30ms warm) so the lost
|
|
329
|
+
# optimization is acceptable; the cache's primary win is skipping
|
|
330
|
+
# the version probe at step 8 (which is a full CLI spawn,
|
|
331
|
+
# materially more expensive).
|
|
332
|
+
local sandbox_result=""
|
|
333
|
+
local sandbox_failed=0
|
|
334
|
+
local node_missing=0
|
|
335
|
+
if [ "${#REA_ARGV[@]}" -gt 0 ]; then
|
|
336
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
337
|
+
# 0.38.1 round-2 P2 fix: pre-fix this branch exited 0/2 IMMEDIATELY
|
|
338
|
+
# without ever calling shim_policy_short_circuit, so a blocking-
|
|
339
|
+
# tier shim whose policy said "disabled" still refused when node
|
|
340
|
+
# was absent (which contradicts the pre-port body's no-op-on-
|
|
341
|
+
# disabled posture). Clear REA_ARGV here so Tier 1 (rea CLI)
|
|
342
|
+
# cannot fire — the policy reader degrades to Tier 2 (python3) /
|
|
343
|
+
# Tier 3 (awk), neither of which needs node. Track node-missing
|
|
344
|
+
# separately so the CLI-required branch below can emit the right
|
|
345
|
+
# banner if the policy did NOT short-circuit us out.
|
|
346
|
+
node_missing=1
|
|
347
|
+
REA_ARGV=()
|
|
348
|
+
else
|
|
349
|
+
sandbox_result=$(shim_sandbox_check "$RESOLVED_CLI_PATH" "$proj" "$SHIM_ENFORCE_CLI_SHAPE")
|
|
350
|
+
if [ "$sandbox_result" != "ok" ]; then
|
|
351
|
+
sandbox_failed=1
|
|
352
|
+
if [ "$SHIM_FAIL_OPEN" -eq 1 ]; then
|
|
353
|
+
shim_emit_sandbox_skip_banner "$sandbox_result"
|
|
354
|
+
exit 0
|
|
355
|
+
fi
|
|
356
|
+
# Blocking-tier: clear REA_ARGV so Tier-1 policy reads (in
|
|
357
|
+
# shim_policy_short_circuit) degrade to Tier 2 / Tier 3 instead
|
|
358
|
+
# of invoking the untrusted CLI.
|
|
359
|
+
REA_ARGV=()
|
|
360
|
+
fi
|
|
361
|
+
fi
|
|
362
|
+
fi
|
|
363
|
+
|
|
315
364
|
# 4b. Per-session cache lookup (0.48.0). When the cache is enabled
|
|
316
365
|
# AND the resolved CLI matches a recent same-session entry, the
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
366
|
+
# version probe (step 8) can be skipped — that answer does not
|
|
367
|
+
# change for a stable CLI inside a stable session. Cache MISS /
|
|
368
|
+
# disabled / corrupt → fall through to the existing uncached hot
|
|
369
|
+
# path. NEVER fail closed on a cache error (see
|
|
370
|
+
# hooks/_lib/shim-cache.sh header for the security contract). The
|
|
371
|
+
# cache check runs AFTER `shim_is_relevant` (per design memo
|
|
372
|
+
# concern #3) so we never pay a stat-per-fire cost for irrelevant
|
|
373
|
+
# payloads. 0.48.1: also runs AFTER step 4a sandbox check so the
|
|
374
|
+
# dist-tree hash walk never traverses a symlinked-out CLI tree.
|
|
325
375
|
local _shim_cache_hit=0
|
|
326
376
|
local _shim_cache_key=""
|
|
327
377
|
local _shim_cache_cli_real=""
|
|
@@ -333,7 +383,19 @@ shim_run() {
|
|
|
333
383
|
local _shim_cache_dist_mtime=""
|
|
334
384
|
local _shim_cache_node_real=""
|
|
335
385
|
local _shim_cache_node_mtime=""
|
|
336
|
-
|
|
386
|
+
# 0.48.1: gated on `sandbox_failed -eq 0` so a sandbox refusal
|
|
387
|
+
# short-circuits BEFORE the dist-tree hash walk runs (was running
|
|
388
|
+
# against a possibly-symlinked-out target pre-0.48.1).
|
|
389
|
+
#
|
|
390
|
+
# 0.48.1 round-1 P2-A: also gated on SHIM_SKIP_VERSION_PROBE -eq 0.
|
|
391
|
+
# Skip-probe shims (delegation-advisory, delegation-capture) cannot
|
|
392
|
+
# write a cache entry (the step-8b write block also gates on
|
|
393
|
+
# SHIM_SKIP_VERSION_PROBE -eq 0 — 0.48.1 SOUNDNESS fix), so any
|
|
394
|
+
# cache-key prep + lookup is pure overhead with zero possible hit.
|
|
395
|
+
# Pre-fix the highest-frequency hooks paid dist-tree find/stat/hash
|
|
396
|
+
# + several `node -e` calls on EVERY write-class fire for nothing.
|
|
397
|
+
if [ "${#REA_ARGV[@]}" -gt 0 ] && [ "$sandbox_failed" -eq 0 ] \
|
|
398
|
+
&& [ "$SHIM_SKIP_VERSION_PROBE" -eq 0 ] && ! shim_cache_disabled; then
|
|
337
399
|
local _stat_out=""
|
|
338
400
|
local _proj_real=""
|
|
339
401
|
local _euid=""
|
|
@@ -547,49 +609,12 @@ shim_run() {
|
|
|
547
609
|
fi
|
|
548
610
|
fi
|
|
549
611
|
|
|
550
|
-
# 5.
|
|
551
|
-
#
|
|
552
|
-
#
|
|
553
|
-
# short-circuit
|
|
554
|
-
#
|
|
555
|
-
#
|
|
556
|
-
# use `policy_reader_get`).
|
|
557
|
-
#
|
|
558
|
-
# Advisory-tier: a sandbox failure exits 0 with the skip banner —
|
|
559
|
-
# nothing to enforce for nudges. Blocking-tier: deferred to the
|
|
560
|
-
# CLI-required branch below so we emit ONE banner per refusal
|
|
561
|
-
# (instead of double-emitting sandbox + cli-missing).
|
|
562
|
-
local sandbox_result=""
|
|
563
|
-
local sandbox_failed=0
|
|
564
|
-
local node_missing=0
|
|
565
|
-
if [ "${#REA_ARGV[@]}" -gt 0 ] && [ "$_shim_cache_hit" -eq 0 ]; then
|
|
566
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
567
|
-
# 0.38.1 round-2 P2 fix: pre-fix this branch exited 0/2 IMMEDIATELY
|
|
568
|
-
# without ever calling shim_policy_short_circuit, so a blocking-
|
|
569
|
-
# tier shim whose policy said "disabled" still refused when node
|
|
570
|
-
# was absent (which contradicts the pre-port body's no-op-on-
|
|
571
|
-
# disabled posture). Clear REA_ARGV here so Tier 1 (rea CLI)
|
|
572
|
-
# can't fire — the policy reader degrades to Tier 2 (python3) /
|
|
573
|
-
# Tier 3 (awk), neither of which needs node. Track node-missing
|
|
574
|
-
# separately so the CLI-required branch below can emit the right
|
|
575
|
-
# banner if the policy did NOT short-circuit us out.
|
|
576
|
-
node_missing=1
|
|
577
|
-
REA_ARGV=()
|
|
578
|
-
else
|
|
579
|
-
sandbox_result=$(shim_sandbox_check "$RESOLVED_CLI_PATH" "$proj" "$SHIM_ENFORCE_CLI_SHAPE")
|
|
580
|
-
if [ "$sandbox_result" != "ok" ]; then
|
|
581
|
-
sandbox_failed=1
|
|
582
|
-
if [ "$SHIM_FAIL_OPEN" -eq 1 ]; then
|
|
583
|
-
shim_emit_sandbox_skip_banner "$sandbox_result"
|
|
584
|
-
exit 0
|
|
585
|
-
fi
|
|
586
|
-
# Blocking-tier: clear REA_ARGV so Tier-1 policy reads (in
|
|
587
|
-
# shim_policy_short_circuit) degrade to Tier 2 / Tier 3 instead
|
|
588
|
-
# of invoking the untrusted CLI.
|
|
589
|
-
REA_ARGV=()
|
|
590
|
-
fi
|
|
591
|
-
fi
|
|
592
|
-
fi
|
|
612
|
+
# 5. (0.48.1: sandbox check moved to step 4a — before cache prep — so
|
|
613
|
+
# a hostile workspace cannot make the dist-tree hash walk traverse
|
|
614
|
+
# a symlinked-out target. The original step-5 block lived between
|
|
615
|
+
# cache prep and policy short-circuit; that location was sound for
|
|
616
|
+
# correctness but the cache code regressed the cheap-refusal
|
|
617
|
+
# posture against symlink workspaces. See step 4a comment.)
|
|
593
618
|
|
|
594
619
|
# 6. Policy short-circuit. Runs BEFORE the CLI-missing / node-missing
|
|
595
620
|
# banners so a shim whose policy says "disabled" exits 0 cleanly
|
|
@@ -667,7 +692,17 @@ shim_run() {
|
|
|
667
692
|
# entry; rewriting it would be wasted work AND would refresh
|
|
668
693
|
# `cached_at_unix` past the TTL ceiling, defeating the staleness
|
|
669
694
|
# bound).
|
|
670
|
-
|
|
695
|
+
#
|
|
696
|
+
# 0.48.1 SOUNDNESS: also skipped when SHIM_SKIP_VERSION_PROBE=1
|
|
697
|
+
# (delegation-capture path). Pre-fix the cache write proceeded
|
|
698
|
+
# after a skipped probe, recording `shape_ok: true` from
|
|
699
|
+
# defaulted-true logic without the probe having actually run.
|
|
700
|
+
# On the next fire a cache hit would read that entry and trust
|
|
701
|
+
# a version-probe answer that was never produced. The cache MUST
|
|
702
|
+
# only persist real probe results; if the probe was bypassed,
|
|
703
|
+
# the next fire pays the cost of running it for real.
|
|
704
|
+
if [ "$_shim_cache_hit" -eq 0 ] && [ -n "$_shim_cache_key" ] \
|
|
705
|
+
&& [ "$SHIM_SKIP_VERSION_PROBE" -eq 0 ]; then
|
|
671
706
|
local _write_payload=""
|
|
672
707
|
_write_payload=$(node -e '
|
|
673
708
|
const args = process.argv.slice(1);
|
|
@@ -41,21 +41,44 @@ shim_cli_missing_relevant() {
|
|
|
41
41
|
if [ -z "$cli_missing_cmd" ]; then
|
|
42
42
|
return 1
|
|
43
43
|
fi
|
|
44
|
+
|
|
45
|
+
# R5-P1: substring scan DETERMINES refusal; allowlist OPENS gates
|
|
46
|
+
# only. R7-P1: PM-route can ALSO return 2 (audit-integrity fail)
|
|
47
|
+
# which MUST refuse via banner regardless of substring scan.
|
|
48
|
+
local matched_blocked=0
|
|
44
49
|
local policy_file="${REA_ROOT}/.rea/policy.yaml"
|
|
45
|
-
if [
|
|
50
|
+
if [ -f "$policy_file" ]; then
|
|
51
|
+
# shellcheck source=_lib/policy-reader.sh
|
|
52
|
+
source "$(dirname "$0")/_lib/policy-reader.sh"
|
|
53
|
+
local entry
|
|
54
|
+
while IFS= read -r entry; do
|
|
55
|
+
[ -z "$entry" ] && continue
|
|
56
|
+
case "$cli_missing_cmd" in
|
|
57
|
+
*"$entry"*) matched_blocked=1; break ;;
|
|
58
|
+
esac
|
|
59
|
+
done < <(policy_reader_get_list blocked_paths 2>/dev/null)
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# shellcheck source=_lib/bootstrap-allowlist.sh
|
|
63
|
+
source "$(dirname "$0")/_lib/bootstrap-allowlist.sh"
|
|
64
|
+
|
|
65
|
+
# R7-P1 (codex round 7): 3-state PM-route return.
|
|
66
|
+
# 0 = auditable allow → exit 0 immediately.
|
|
67
|
+
# 2 = refuse-HARD (audit-integrity fail) → banner regardless
|
|
68
|
+
# of substring scan (the helper printed an explainer to
|
|
69
|
+
# stderr; we must NOT silently allow a payload whose audit
|
|
70
|
+
# record could not be written).
|
|
71
|
+
# * = refuse-fallthrough → defer to substring-scan verdict.
|
|
72
|
+
_bootstrap_shim_pm_route "blocked-paths-bash-gate" "$cli_missing_cmd" "$REA_ROOT"
|
|
73
|
+
case "$?" in
|
|
74
|
+
0) exit 0 ;;
|
|
75
|
+
2) return 0 ;;
|
|
76
|
+
esac
|
|
77
|
+
|
|
78
|
+
if [ "$matched_blocked" -eq 0 ]; then
|
|
46
79
|
return 1
|
|
47
80
|
fi
|
|
48
|
-
|
|
49
|
-
# shellcheck source=_lib/policy-reader.sh
|
|
50
|
-
source "$(dirname "$0")/_lib/policy-reader.sh"
|
|
51
|
-
local entry
|
|
52
|
-
while IFS= read -r entry; do
|
|
53
|
-
[ -z "$entry" ] && continue
|
|
54
|
-
case "$cli_missing_cmd" in
|
|
55
|
-
*"$entry"*) return 0 ;;
|
|
56
|
-
esac
|
|
57
|
-
done < <(policy_reader_get_list blocked_paths 2>/dev/null)
|
|
58
|
-
return 1
|
|
81
|
+
return 0
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
# shellcheck source=_lib/shim-runtime.sh
|
|
@@ -45,21 +45,24 @@ shim_cli_missing_relevant() {
|
|
|
45
45
|
if [ -z "$cli_missing_cmd" ]; then
|
|
46
46
|
return 1
|
|
47
47
|
fi
|
|
48
|
+
|
|
49
|
+
# R5-P1 (codex round 5): substring scan is DETERMINATIVE for
|
|
50
|
+
# refusal. Allowlist opens an audited-allow gate but never closes
|
|
51
|
+
# one.
|
|
52
|
+
local matched_protected=0
|
|
48
53
|
case "$cli_missing_cmd" in
|
|
49
|
-
*".claude/"*)
|
|
50
|
-
*".husky/"*)
|
|
51
|
-
*".rea/policy.yaml"*)
|
|
52
|
-
*".rea/HALT"*)
|
|
53
|
-
*".rea/last-review"*)
|
|
54
|
-
*".claude\\"*|*".husky\\"*|*".rea\\"*)
|
|
54
|
+
*".claude/"*) matched_protected=1 ;;
|
|
55
|
+
*".husky/"*) matched_protected=1 ;;
|
|
56
|
+
*".rea/policy.yaml"*) matched_protected=1 ;;
|
|
57
|
+
*".rea/HALT"*) matched_protected=1 ;;
|
|
58
|
+
*".rea/last-review"*) matched_protected=1 ;;
|
|
59
|
+
*".claude\\"*|*".husky\\"*|*".rea\\"*) matched_protected=1 ;;
|
|
55
60
|
esac
|
|
56
61
|
# 0.37.0: route protected_writes reads through the unified
|
|
57
62
|
# policy-reader (Tier 1 CLI → Tier 2 python3 → Tier 3 awk
|
|
58
|
-
# block-form).
|
|
59
|
-
# arrays (`protected_writes: [path/a, path/b]`) on CLI-missing
|
|
60
|
-
# installs.
|
|
63
|
+
# block-form).
|
|
61
64
|
local policy_file="${REA_ROOT}/.rea/policy.yaml"
|
|
62
|
-
if [ -f "$policy_file" ]; then
|
|
65
|
+
if [ "$matched_protected" -eq 0 ] && [ -f "$policy_file" ]; then
|
|
63
66
|
# shellcheck source=_lib/policy-reader.sh
|
|
64
67
|
source "$(dirname "$0")/_lib/policy-reader.sh"
|
|
65
68
|
local entry base
|
|
@@ -71,11 +74,26 @@ shim_cli_missing_relevant() {
|
|
|
71
74
|
esac
|
|
72
75
|
[ -z "$base" ] && continue
|
|
73
76
|
case "$cli_missing_cmd" in
|
|
74
|
-
*"$base"*)
|
|
77
|
+
*"$base"*) matched_protected=1; break ;;
|
|
75
78
|
esac
|
|
76
79
|
done < <(policy_reader_get_list protected_writes 2>/dev/null)
|
|
77
80
|
fi
|
|
78
|
-
|
|
81
|
+
|
|
82
|
+
# shellcheck source=_lib/bootstrap-allowlist.sh
|
|
83
|
+
source "$(dirname "$0")/_lib/bootstrap-allowlist.sh"
|
|
84
|
+
|
|
85
|
+
# R7-P1 (codex round 7): 3-state PM-route return. See the
|
|
86
|
+
# blocked-paths shim for the contract.
|
|
87
|
+
_bootstrap_shim_pm_route "protected-paths-bash-gate" "$cli_missing_cmd" "$REA_ROOT"
|
|
88
|
+
case "$?" in
|
|
89
|
+
0) exit 0 ;;
|
|
90
|
+
2) return 0 ;;
|
|
91
|
+
esac
|
|
92
|
+
|
|
93
|
+
if [ "$matched_protected" -eq 0 ]; then
|
|
94
|
+
return 1
|
|
95
|
+
fi
|
|
96
|
+
return 0
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
# shellcheck source=_lib/shim-runtime.sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.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)",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"mvdan-sh": "0.10.1",
|
|
76
76
|
"proper-lockfile": "^4.1.2",
|
|
77
77
|
"safe-regex": "^2.1.1",
|
|
78
|
+
"semver": "^7.7.4",
|
|
78
79
|
"yaml": "^2.7.0",
|
|
79
80
|
"zod": "^3.23.0"
|
|
80
81
|
},
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"@types/node": "^25.5.2",
|
|
84
85
|
"@types/proper-lockfile": "^4.1.4",
|
|
85
86
|
"@types/safe-regex": "^1.1.6",
|
|
87
|
+
"@types/semver": "^7.7.1",
|
|
86
88
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
87
89
|
"@typescript-eslint/parser": "^8.0.0",
|
|
88
90
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -31,6 +31,10 @@ blocked_paths:
|
|
|
31
31
|
- .github/workflows/release.yml
|
|
32
32
|
- SECURITY.md
|
|
33
33
|
- THREAT_MODEL.md
|
|
34
|
+
# 0.49.0 — gates Write/Edit tool calls targeting `package.json`.
|
|
35
|
+
# See `bst-internal.yaml` for the rationale and explicit
|
|
36
|
+
# out-of-scope statement on PM-induced writes.
|
|
37
|
+
- package.json
|
|
34
38
|
notification_channel: ''
|
|
35
39
|
# G9: Booked-internal consumers retain the stricter 0.2.x posture — a single
|
|
36
40
|
# literal injection match at write/destructive tier denies (does not merely
|
|
@@ -14,6 +14,21 @@ blocked_paths:
|
|
|
14
14
|
- .github/workflows/release.yml
|
|
15
15
|
- SECURITY.md
|
|
16
16
|
- THREAT_MODEL.md
|
|
17
|
+
# 0.49.0 — gates Write/Edit/MultiEdit/NotebookEdit tool calls
|
|
18
|
+
# targeting `package.json`. The bootstrap allowlist passes a
|
|
19
|
+
# CLI-missing PM payload through ONLY when `package.json` already
|
|
20
|
+
# declares `@bookedsolid/rea`, so blocking AGENT edits to the
|
|
21
|
+
# manifest narrows the Edit/Write-tier forge surface (an attacker
|
|
22
|
+
# can no longer add the declaration via Edit then invoke the
|
|
23
|
+
# allowed PM command). PM-induced writes to the manifest are
|
|
24
|
+
# explicitly out of scope (see THREAT_MODEL.md §5.23 "Out of
|
|
25
|
+
# scope"): a path-based blocklist cannot distinguish agent edits
|
|
26
|
+
# from PM-tool writes. The static manifest-write detector that
|
|
27
|
+
# would close that gap was deliberately removed in R17;
|
|
28
|
+
# defending against agent-initiated PM mutations needs a
|
|
29
|
+
# different abstraction (process-tree-aware policy / per-tool
|
|
30
|
+
# capability tokens) — not shipped in 0.49.0.
|
|
31
|
+
- package.json
|
|
17
32
|
notification_channel: ''
|
|
18
33
|
# G9: Booked-internal consumers retain the stricter 0.2.x posture — a single
|
|
19
34
|
# literal injection match at write/destructive tier denies (does not merely
|
|
@@ -80,3 +95,16 @@ attribution:
|
|
|
80
95
|
delegation_advisory:
|
|
81
96
|
enabled: true
|
|
82
97
|
threshold: 25
|
|
98
|
+
# 0.49.0 bootstrap allowlist (P3-1) — explicit declaration for parity
|
|
99
|
+
# with the dogfood `.rea/policy.yaml`. The other five shipped profiles
|
|
100
|
+
# (open-source*, client-engagement, lit-wc, minimal) inherit the zod
|
|
101
|
+
# schema default (`enabled: true`), so they need no entry here. The
|
|
102
|
+
# bst-internal profile pins it so the operator-visible policy file
|
|
103
|
+
# clearly states the position rather than relying on schema defaults.
|
|
104
|
+
# The allowlist passes a single-segment pnpm install / npm ci / yarn /
|
|
105
|
+
# corepack invocation through when (a) the rea CLI is unreachable, (b)
|
|
106
|
+
# the consumer's package.json declares `@bookedsolid/rea`, and (c)
|
|
107
|
+
# every other precondition (single-segment payload, exact argv shape,
|
|
108
|
+
# audit emission OK) holds. See hooks/_lib/bootstrap-allowlist.sh.
|
|
109
|
+
bootstrap_allowlist:
|
|
110
|
+
enabled: true
|
|
@@ -15,6 +15,15 @@ blocked_paths:
|
|
|
15
15
|
- THREAT_MODEL.md
|
|
16
16
|
- secrets/
|
|
17
17
|
- credentials/
|
|
18
|
+
# 0.49.0 — gates Write/Edit tool calls targeting `package.json`.
|
|
19
|
+
# Pairs with the always-on `bootstrap_allowlist` (schema default):
|
|
20
|
+
# the allowlist trusts a CLI-missing PM command only when
|
|
21
|
+
# `package.json` declares `@bookedsolid/rea`, so blocking AGENT
|
|
22
|
+
# edits to the manifest narrows the Edit/Write-tier forge surface.
|
|
23
|
+
# PM-induced writes are explicitly out of scope (see
|
|
24
|
+
# THREAT_MODEL.md §5.23 "Out of scope") — a path-based blocklist
|
|
25
|
+
# cannot distinguish agent edits from PM-tool writes.
|
|
26
|
+
- package.json
|
|
18
27
|
notification_channel: ''
|
|
19
28
|
context_protection:
|
|
20
29
|
delegate_to_subagent:
|
package/profiles/lit-wc.yaml
CHANGED
|
@@ -14,6 +14,12 @@ blocked_paths:
|
|
|
14
14
|
- .github/workflows/release.yml
|
|
15
15
|
- .github/workflows/publish.yml
|
|
16
16
|
- tokens/
|
|
17
|
+
# 0.49.0 — gates Write/Edit tool calls targeting `package.json`.
|
|
18
|
+
# Pairs with the always-on `bootstrap_allowlist` so the
|
|
19
|
+
# "consumer declares @bookedsolid/rea" precondition is meaningful.
|
|
20
|
+
# PM-induced writes are explicitly out of scope (THREAT_MODEL.md
|
|
21
|
+
# §5.23 "Out of scope").
|
|
22
|
+
- package.json
|
|
17
23
|
notification_channel: ''
|
|
18
24
|
# 0.30.0 attribution augmenter — opt-in.
|
|
19
25
|
# Husky prepare-commit-msg hook appends a Co-Authored-By trailer to
|
package/profiles/minimal.yaml
CHANGED
|
@@ -8,6 +8,17 @@ block_ai_attribution: true
|
|
|
8
8
|
blocked_paths:
|
|
9
9
|
- .env
|
|
10
10
|
- .env.*
|
|
11
|
+
# 0.49.0 — gates Write/Edit tool calls targeting `package.json`.
|
|
12
|
+
# The always-on `bootstrap_allowlist` (schema default) trusts a
|
|
13
|
+
# CLI-missing PM payload only when `package.json` declares
|
|
14
|
+
# `@bookedsolid/rea`; without an Edit/Write gate on the manifest,
|
|
15
|
+
# an agent could ADD that declaration first and then route an
|
|
16
|
+
# otherwise-disallowed PM command through the allowlist. PM-
|
|
17
|
+
# induced writes are explicitly out of scope (THREAT_MODEL.md
|
|
18
|
+
# §5.23 "Out of scope"). Operators who deliberately want agent
|
|
19
|
+
# freedom to edit `package.json` should remove this entry AND
|
|
20
|
+
# set `bootstrap_allowlist.enabled: false` in tandem.
|
|
21
|
+
- package.json
|
|
11
22
|
notification_channel: ''
|
|
12
23
|
# 0.30.0 attribution augmenter — opt-in.
|
|
13
24
|
# When enabled: true, the husky prepare-commit-msg hook appends a
|
|
@@ -30,6 +30,10 @@ blocked_paths:
|
|
|
30
30
|
- SECURITY.md
|
|
31
31
|
- .github/workflows/release.yml
|
|
32
32
|
- .github/workflows/publish.yml
|
|
33
|
+
# 0.49.0 — gates Write/Edit tool calls targeting `package.json`.
|
|
34
|
+
# See `open-source.yaml` for the rationale; this variant inherits
|
|
35
|
+
# the same posture sans Codex review.
|
|
36
|
+
- package.json
|
|
33
37
|
notification_channel: ''
|
|
34
38
|
# 0.30.0 attribution augmenter — opt-in.
|
|
35
39
|
# Husky prepare-commit-msg hook appends a Co-Authored-By trailer to
|
|
@@ -15,6 +15,17 @@ blocked_paths:
|
|
|
15
15
|
- SECURITY.md
|
|
16
16
|
- .github/workflows/release.yml
|
|
17
17
|
- .github/workflows/publish.yml
|
|
18
|
+
# 0.49.0 — gates Write/Edit tool calls targeting `package.json`.
|
|
19
|
+
# The always-on `bootstrap_allowlist` (schema default) trusts a
|
|
20
|
+
# CLI-missing PM payload only when `package.json` declares
|
|
21
|
+
# `@bookedsolid/rea`. Without an Edit/Write-tier gate on the
|
|
22
|
+
# manifest, an agent could ADD that declaration first and then
|
|
23
|
+
# invoke the allowed PM command, defeating the precondition.
|
|
24
|
+
# PM-induced writes are explicitly out of scope (THREAT_MODEL.md
|
|
25
|
+
# §5.23 "Out of scope"). Operators who need agent freedom to edit
|
|
26
|
+
# package.json should remove this entry in `.rea/policy.yaml`
|
|
27
|
+
# AND set `bootstrap_allowlist.enabled: false` in tandem.
|
|
28
|
+
- package.json
|
|
18
29
|
notification_channel: ''
|
|
19
30
|
# 0.30.0 attribution augmenter — opt-in.
|
|
20
31
|
# Husky prepare-commit-msg hook appends a Co-Authored-By trailer to
|