@bookedsolid/rea 0.48.0 → 0.48.1
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/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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.1",
|
|
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)",
|
|
@@ -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);
|