@bookedsolid/rea 0.47.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/MIGRATING.md +88 -0
- package/THREAT_MODEL.md +140 -0
- package/dist/policy/loader.d.ts +13 -0
- package/dist/policy/loader.js +36 -0
- package/dist/policy/types.d.ts +52 -0
- package/hooks/_lib/shim-cache.sh +738 -0
- package/hooks/_lib/shim-runtime.sh +339 -14
- package/package.json +1 -1
- package/scripts/profile-hooks.mjs +10 -1
- package/templates/_lib_shim-cache.dogfood-staged.sh +738 -0
- package/templates/_lib_shim-runtime.dogfood-staged.sh +339 -14
|
@@ -137,6 +137,15 @@
|
|
|
137
137
|
|
|
138
138
|
set -uo pipefail
|
|
139
139
|
|
|
140
|
+
# Source the per-session cache helper (0.48.0). This must be sourced
|
|
141
|
+
# at the top of shim-runtime.sh because `shim_run` needs all of the
|
|
142
|
+
# `shim_cache_*` functions available. The helper itself fails safe —
|
|
143
|
+
# no operations fire unless `shim_run` calls them.
|
|
144
|
+
# shellcheck source=shim-cache.sh
|
|
145
|
+
_SHIM_RUNTIME_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
146
|
+
# shellcheck source=shim-cache.sh
|
|
147
|
+
. "$_SHIM_RUNTIME_DIR/shim-cache.sh"
|
|
148
|
+
|
|
140
149
|
# -----------------------------------------------------------------------------
|
|
141
150
|
# Defaults — applied by `shim_run` when the shim hasn't set them. We use
|
|
142
151
|
# the `:=` operator to assign-if-unset so callers can override.
|
|
@@ -303,18 +312,23 @@ shim_run() {
|
|
|
303
312
|
# 4. Resolve CLI.
|
|
304
313
|
shim_resolve_cli
|
|
305
314
|
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
#
|
|
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.
|
|
313
325
|
#
|
|
314
|
-
#
|
|
315
|
-
#
|
|
316
|
-
#
|
|
317
|
-
#
|
|
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).
|
|
318
332
|
local sandbox_result=""
|
|
319
333
|
local sandbox_failed=0
|
|
320
334
|
local node_missing=0
|
|
@@ -325,7 +339,7 @@ shim_run() {
|
|
|
325
339
|
# tier shim whose policy said "disabled" still refused when node
|
|
326
340
|
# was absent (which contradicts the pre-port body's no-op-on-
|
|
327
341
|
# disabled posture). Clear REA_ARGV here so Tier 1 (rea CLI)
|
|
328
|
-
#
|
|
342
|
+
# cannot fire — the policy reader degrades to Tier 2 (python3) /
|
|
329
343
|
# Tier 3 (awk), neither of which needs node. Track node-missing
|
|
330
344
|
# separately so the CLI-required branch below can emit the right
|
|
331
345
|
# banner if the policy did NOT short-circuit us out.
|
|
@@ -347,6 +361,261 @@ shim_run() {
|
|
|
347
361
|
fi
|
|
348
362
|
fi
|
|
349
363
|
|
|
364
|
+
# 4b. Per-session cache lookup (0.48.0). When the cache is enabled
|
|
365
|
+
# AND the resolved CLI matches a recent same-session entry, the
|
|
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.
|
|
375
|
+
local _shim_cache_hit=0
|
|
376
|
+
local _shim_cache_key=""
|
|
377
|
+
local _shim_cache_cli_real=""
|
|
378
|
+
local _shim_cache_cli_mtime=""
|
|
379
|
+
local _shim_cache_cli_size=""
|
|
380
|
+
local _shim_cache_pkg_real=""
|
|
381
|
+
local _shim_cache_pkg_mtime=""
|
|
382
|
+
local _shim_cache_pkg_size=""
|
|
383
|
+
local _shim_cache_dist_mtime=""
|
|
384
|
+
local _shim_cache_node_real=""
|
|
385
|
+
local _shim_cache_node_mtime=""
|
|
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
|
|
399
|
+
local _stat_out=""
|
|
400
|
+
local _proj_real=""
|
|
401
|
+
local _euid=""
|
|
402
|
+
local _session_tok=""
|
|
403
|
+
_stat_out=$(shim_cache_mtime_size "$RESOLVED_CLI_PATH" 2>/dev/null || true)
|
|
404
|
+
# 0.48.0 codex round-4 P1 + round-7 P2: capture the ACTUAL node
|
|
405
|
+
# interpreter realpath + mtime via `process.execPath` (node's own
|
|
406
|
+
# path to itself). Pre-round-7 we resolved `command -v node` via
|
|
407
|
+
# `fs.realpathSync` — but version managers like Volta and asdf
|
|
408
|
+
# use STABLE shim scripts (e.g. ~/.volta/bin/node) that resolve
|
|
409
|
+
# to themselves; only the spawned node's `process.execPath`
|
|
410
|
+
# reveals which concrete Node binary the shim ultimately
|
|
411
|
+
# launched (e.g. /Users/foo/.volta/tools/image/node/22.x.x/bin/
|
|
412
|
+
# node). Using execPath catches `volta pin`/`nvm use` interpreter
|
|
413
|
+
# swaps correctly. The mtime field is captured at second
|
|
414
|
+
# precision (consistent with the other mtime fields) — switching
|
|
415
|
+
# Node versions changes the realpath so the mtime alone is not
|
|
416
|
+
# load-bearing.
|
|
417
|
+
_shim_cache_node_real=$(node -e 'process.stdout.write(require("fs").realpathSync(process.execPath))' 2>/dev/null || true)
|
|
418
|
+
if [ -n "$_shim_cache_node_real" ]; then
|
|
419
|
+
local _node_stat=""
|
|
420
|
+
_node_stat=$(shim_cache_mtime_size "$_shim_cache_node_real" 2>/dev/null || true)
|
|
421
|
+
if [ -n "$_node_stat" ]; then
|
|
422
|
+
_shim_cache_node_mtime="${_node_stat%% *}"
|
|
423
|
+
fi
|
|
424
|
+
fi
|
|
425
|
+
_shim_cache_cli_real=$(node -e 'try { process.stdout.write(require("fs").realpathSync(process.argv[1])); } catch (e) { process.exit(1); }' -- "$RESOLVED_CLI_PATH" 2>/dev/null || true)
|
|
426
|
+
_proj_real=$(node -e 'try { process.stdout.write(require("fs").realpathSync(process.argv[1])); } catch (e) { process.exit(1); }' -- "$proj" 2>/dev/null || true)
|
|
427
|
+
_euid=$(id -u 2>/dev/null || true)
|
|
428
|
+
_session_tok=$(shim_cache_session_token 2>/dev/null || true)
|
|
429
|
+
# 0.48.0 codex round-3 P2: ALSO capture the ancestor package.json
|
|
430
|
+
# path + mtime/size. The sandbox check walks upward to find a
|
|
431
|
+
# package.json whose `name` is `@bookedsolid/rea`; without it in
|
|
432
|
+
# the key, a same-session edit to that package.json (renaming, or
|
|
433
|
+
# removing the `name` field) would still see warm cache hits even
|
|
434
|
+
# though the uncached sandbox check would reject the new state.
|
|
435
|
+
# Codex round-3 P1: ALSO capture the dist/cli/ DIR mtime so a
|
|
436
|
+
# rebuild that adds/removes files (most fresh tsc runs after a
|
|
437
|
+
# source-tree change) invalidates the key even if dist/cli/
|
|
438
|
+
# index.js content happens to round to the same ns.
|
|
439
|
+
_shim_cache_pkg_real=$(node -e '
|
|
440
|
+
try {
|
|
441
|
+
const fs = require("fs");
|
|
442
|
+
const path = require("path");
|
|
443
|
+
const real = fs.realpathSync(process.argv[1]);
|
|
444
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
445
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i++) {
|
|
446
|
+
const pj = path.join(cur, "package.json");
|
|
447
|
+
if (fs.existsSync(pj)) {
|
|
448
|
+
try {
|
|
449
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
450
|
+
if (data && data.name === "@bookedsolid/rea") {
|
|
451
|
+
process.stdout.write(pj);
|
|
452
|
+
process.exit(0);
|
|
453
|
+
}
|
|
454
|
+
} catch (e) {}
|
|
455
|
+
}
|
|
456
|
+
cur = path.dirname(cur);
|
|
457
|
+
}
|
|
458
|
+
process.exit(1);
|
|
459
|
+
} catch (e) { process.exit(1); }
|
|
460
|
+
' -- "$RESOLVED_CLI_PATH" 2>/dev/null || true)
|
|
461
|
+
if [ -n "$_shim_cache_pkg_real" ]; then
|
|
462
|
+
local _pkg_stat=""
|
|
463
|
+
_pkg_stat=$(shim_cache_mtime_size "$_shim_cache_pkg_real" 2>/dev/null || true)
|
|
464
|
+
if [ -n "$_pkg_stat" ]; then
|
|
465
|
+
_shim_cache_pkg_mtime="${_pkg_stat%% *}"
|
|
466
|
+
_shim_cache_pkg_size="${_pkg_stat##* }"
|
|
467
|
+
fi
|
|
468
|
+
fi
|
|
469
|
+
# 0.48.0 codex round-5/7/9 — the cache key incorporates a hash of
|
|
470
|
+
# every `*.js` file's mtime across the FULL dist tree, not just
|
|
471
|
+
# dist/cli/. Pre-round-9 the hash covered only dist/cli/*.js, but
|
|
472
|
+
# `rea hook` actually executes a much larger module graph:
|
|
473
|
+
# dist/cli/hook.js imports ../hooks/**, ../policy/loader.js,
|
|
474
|
+
# ../audit/**, etc. A same-session rebuild that rewrote one of
|
|
475
|
+
# those imported files in place without touching a top-level
|
|
476
|
+
# dist/cli/*.js file would leave the hash unchanged, the warm
|
|
477
|
+
# cache would survive, and shim_run would skip the version probe
|
|
478
|
+
# against a changed CLI runtime. Hashing dist/**/*.js closes the
|
|
479
|
+
# gap. Cost: ~15ms on the rea dist (141 files) via `find -exec
|
|
480
|
+
# stat +` batched into a single subprocess call.
|
|
481
|
+
local _dist_root=""
|
|
482
|
+
# dist root is two parents above dist/cli/index.js
|
|
483
|
+
_dist_root=$(dirname "$(dirname "$RESOLVED_CLI_PATH")" 2>/dev/null || true)
|
|
484
|
+
if [ -n "$_dist_root" ] && [ -d "$_dist_root" ]; then
|
|
485
|
+
# 0.48.0 codex round-6 P2: pick a hasher that exists. macOS
|
|
486
|
+
# ships `shasum` (perl); GNU coreutils provides `sha256sum`.
|
|
487
|
+
local _hasher=""
|
|
488
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
489
|
+
_hasher="shasum -a 256"
|
|
490
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
491
|
+
_hasher="sha256sum"
|
|
492
|
+
fi
|
|
493
|
+
if [ -n "$_hasher" ]; then
|
|
494
|
+
# 0.48.0 codex round-7 P2: ns-precision mtime so a
|
|
495
|
+
# same-second rewrite is caught. Try macOS `-f` form first;
|
|
496
|
+
# fall through to GNU `-c` on failure. `find -exec stat +`
|
|
497
|
+
# batches all paths into ONE stat call (~15ms total instead
|
|
498
|
+
# of the per-file 365ms loop).
|
|
499
|
+
local _stat_macos=""
|
|
500
|
+
local _stat_gnu=""
|
|
501
|
+
_stat_macos=$(find "$_dist_root" -name '*.js' -type f -exec stat -f "%Fm %z %N" {} + 2>/dev/null || true)
|
|
502
|
+
if [ -n "$_stat_macos" ]; then
|
|
503
|
+
_shim_cache_dist_mtime=$(printf '%s' "$_stat_macos" | sort | $_hasher 2>/dev/null | awk '{print $1}' | cut -c1-32)
|
|
504
|
+
else
|
|
505
|
+
_stat_gnu=$(find "$_dist_root" -name '*.js' -type f -exec stat -c "%.Y %s %n" {} + 2>/dev/null || true)
|
|
506
|
+
if [ -n "$_stat_gnu" ]; then
|
|
507
|
+
_shim_cache_dist_mtime=$(printf '%s' "$_stat_gnu" | sort | $_hasher 2>/dev/null | awk '{print $1}' | cut -c1-32)
|
|
508
|
+
fi
|
|
509
|
+
fi
|
|
510
|
+
fi
|
|
511
|
+
# Last-ditch fallback: just the dist/cli/ dir mtime (round-3
|
|
512
|
+
# behavior). Keeps the cache functional even when find / stat /
|
|
513
|
+
# shasum / sha256sum are all unavailable (truly stripped
|
|
514
|
+
# container) — though that's already the case where the cache
|
|
515
|
+
# layer should fall back to disabled via the session token.
|
|
516
|
+
if [ -z "$_shim_cache_dist_mtime" ]; then
|
|
517
|
+
local _cli_dir=""
|
|
518
|
+
_cli_dir=$(dirname "$RESOLVED_CLI_PATH" 2>/dev/null || true)
|
|
519
|
+
if [ -n "$_cli_dir" ] && [ -d "$_cli_dir" ]; then
|
|
520
|
+
local _dir_stat=""
|
|
521
|
+
_dir_stat=$(shim_cache_mtime_size "$_cli_dir" 2>/dev/null || true)
|
|
522
|
+
if [ -n "$_dir_stat" ]; then
|
|
523
|
+
_shim_cache_dist_mtime="${_dir_stat%% *}"
|
|
524
|
+
fi
|
|
525
|
+
fi
|
|
526
|
+
fi
|
|
527
|
+
fi
|
|
528
|
+
if [ -n "$_stat_out" ] && [ -n "$_shim_cache_cli_real" ] && [ -n "$_proj_real" ] \
|
|
529
|
+
&& [ -n "$_euid" ] && [ -n "$_session_tok" ] \
|
|
530
|
+
&& [ -n "$_shim_cache_pkg_real" ] && [ -n "$_shim_cache_pkg_mtime" ] \
|
|
531
|
+
&& [ -n "$_shim_cache_dist_mtime" ] \
|
|
532
|
+
&& [ -n "$_shim_cache_node_real" ] && [ -n "$_shim_cache_node_mtime" ]; then
|
|
533
|
+
_shim_cache_cli_mtime="${_stat_out%% *}"
|
|
534
|
+
_shim_cache_cli_size="${_stat_out##* }"
|
|
535
|
+
# 0.48.0 codex round-1 P1: the key MUST include SHIM_NAME because
|
|
536
|
+
# step 8's version probe is `rea hook $SHIM_NAME --help` — it's
|
|
537
|
+
# hook-specific. Without SHIM_NAME in the key, a cache-warm shim
|
|
538
|
+
# could let a sibling shim with the SAME (session, project, CLI,
|
|
539
|
+
# mtime, size, euid, shape) skip its OWN version-skew check and
|
|
540
|
+
# forward straight to a CLI that does not implement that hook
|
|
541
|
+
# (realistic on a 0.32 CLI + newer secret-scanner shim mismatch).
|
|
542
|
+
#
|
|
543
|
+
# 0.48.0 codex round-3 P1+P2: 3 new key fields cover (a) ancestor
|
|
544
|
+
# package.json mtime/size — invalidates if the rea package.json
|
|
545
|
+
# is renamed or its `name` field is edited; (b) dist/cli/ dir
|
|
546
|
+
# mtime — invalidates when any file in that directory is
|
|
547
|
+
# added/removed (most fresh `tsc` rebuilds do both); (c) the
|
|
548
|
+
# package.json realpath is implicitly part of the key via these
|
|
549
|
+
# mtime/size fields plus the project realpath above.
|
|
550
|
+
_shim_cache_key=$(shim_cache_key "v1" "$_session_tok" "$_proj_real" "$_shim_cache_cli_real" \
|
|
551
|
+
"$_shim_cache_cli_mtime" "$_shim_cache_cli_size" "$_euid" \
|
|
552
|
+
"$SHIM_ENFORCE_CLI_SHAPE" "$SHIM_NAME" \
|
|
553
|
+
"$_shim_cache_pkg_mtime" "$_shim_cache_pkg_size" \
|
|
554
|
+
"$_shim_cache_dist_mtime" \
|
|
555
|
+
"$_shim_cache_node_real" "$_shim_cache_node_mtime" \
|
|
556
|
+
2>/dev/null || true)
|
|
557
|
+
if [ -n "$_shim_cache_key" ]; then
|
|
558
|
+
local _cache_json=""
|
|
559
|
+
_cache_json=$(shim_cache_read "$_shim_cache_key" 2>/dev/null || true)
|
|
560
|
+
if [ -n "$_cache_json" ]; then
|
|
561
|
+
# Parse + validate the entry. Failure → treat as miss.
|
|
562
|
+
local _cache_validate=""
|
|
563
|
+
_cache_validate=$(node -e '
|
|
564
|
+
try {
|
|
565
|
+
const e = JSON.parse(process.argv[1]);
|
|
566
|
+
const now = Math.floor(Date.now() / 1000);
|
|
567
|
+
const ttl = Number(e.ttl_seconds);
|
|
568
|
+
const cachedAt = Number(e.cached_at_unix);
|
|
569
|
+
const cliMtime = String(e.cli_mtime);
|
|
570
|
+
const cliSize = String(e.cli_size_bytes);
|
|
571
|
+
const cliReal = String(e.cli_realpath);
|
|
572
|
+
const pkgMtime = String(e.pkg_mtime);
|
|
573
|
+
const pkgSize = String(e.pkg_size_bytes);
|
|
574
|
+
const distMtime = String(e.dist_mtime);
|
|
575
|
+
const sandboxOk = e.sandbox_ok === true;
|
|
576
|
+
const shapeOk = e.shape_ok === true;
|
|
577
|
+
if (e.schema_version !== "v1") process.exit(1);
|
|
578
|
+
if (!Number.isFinite(ttl) || ttl <= 0 || ttl > 3600) process.exit(1);
|
|
579
|
+
if (!Number.isFinite(cachedAt)) process.exit(1);
|
|
580
|
+
if ((cachedAt + ttl) < now) process.exit(1);
|
|
581
|
+
if (cliMtime !== process.argv[2]) process.exit(1);
|
|
582
|
+
if (cliSize !== process.argv[3]) process.exit(1);
|
|
583
|
+
if (cliReal !== process.argv[4]) process.exit(1);
|
|
584
|
+
// 0.48.0 codex round-3 P1+P2: re-check the package.json
|
|
585
|
+
// mtime/size and the dist/cli/ dir mtime in addition to
|
|
586
|
+
// the CLI itself. Defense-in-depth against an entry
|
|
587
|
+
// whose key happened to collide but whose disk state
|
|
588
|
+
// has drifted.
|
|
589
|
+
if (pkgMtime !== process.argv[5]) process.exit(1);
|
|
590
|
+
if (pkgSize !== process.argv[6]) process.exit(1);
|
|
591
|
+
if (distMtime !== process.argv[7]) process.exit(1);
|
|
592
|
+
// 0.48.0 codex round-4 P1: re-check the resolved node
|
|
593
|
+
// binary realpath + mtime. A same-session interpreter
|
|
594
|
+
// swap (nvm use, volta pin) would otherwise let the
|
|
595
|
+
// warm entry silently forward through a different node.
|
|
596
|
+
const nodeReal = String(e.node_realpath);
|
|
597
|
+
const nodeMtime = String(e.node_mtime);
|
|
598
|
+
if (nodeReal !== process.argv[8]) process.exit(1);
|
|
599
|
+
if (nodeMtime !== process.argv[9]) process.exit(1);
|
|
600
|
+
if (!sandboxOk || !shapeOk) process.exit(1);
|
|
601
|
+
process.stdout.write("ok");
|
|
602
|
+
} catch (e) { process.exit(1); }
|
|
603
|
+
' -- "$_cache_json" "$_shim_cache_cli_mtime" "$_shim_cache_cli_size" "$_shim_cache_cli_real" "$_shim_cache_pkg_mtime" "$_shim_cache_pkg_size" "$_shim_cache_dist_mtime" "$_shim_cache_node_real" "$_shim_cache_node_mtime" 2>/dev/null || true)
|
|
604
|
+
if [ "$_cache_validate" = "ok" ]; then
|
|
605
|
+
_shim_cache_hit=1
|
|
606
|
+
fi
|
|
607
|
+
fi
|
|
608
|
+
fi
|
|
609
|
+
fi
|
|
610
|
+
fi
|
|
611
|
+
|
|
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.)
|
|
618
|
+
|
|
350
619
|
# 6. Policy short-circuit. Runs BEFORE the CLI-missing / node-missing
|
|
351
620
|
# banners so a shim whose policy says "disabled" exits 0 cleanly
|
|
352
621
|
# even when the CLI is unbuilt OR node is absent (matches the
|
|
@@ -399,8 +668,10 @@ shim_run() {
|
|
|
399
668
|
# 8. Version probe (skipped when SHIM_SKIP_VERSION_PROBE=1, used by
|
|
400
669
|
# delegation-capture whose pre-port body had no probe — a stale
|
|
401
670
|
# CLI drops the signal silently rather than spamming the operator
|
|
402
|
-
# on every Agent/Skill dispatch).
|
|
403
|
-
|
|
671
|
+
# on every Agent/Skill dispatch). Also skipped on cache hit — the
|
|
672
|
+
# probe answer was recorded when the entry was written and the
|
|
673
|
+
# cache key invalidates if mtime / size / realpath changes.
|
|
674
|
+
if [ "$SHIM_SKIP_VERSION_PROBE" -eq 0 ] && [ "$_shim_cache_hit" -eq 0 ]; then
|
|
404
675
|
local probe_out probe_status
|
|
405
676
|
probe_out=$("${REA_ARGV[@]}" hook "$SHIM_NAME" --help 2>&1)
|
|
406
677
|
probe_status=$?
|
|
@@ -414,6 +685,60 @@ shim_run() {
|
|
|
414
685
|
fi
|
|
415
686
|
fi
|
|
416
687
|
|
|
688
|
+
# 8b. Cache write (0.48.0). At this point sandbox + probe both
|
|
689
|
+
# succeeded — record the answers for the next fire in this
|
|
690
|
+
# session. Cache write failure NEVER blocks the gate; we ignore
|
|
691
|
+
# the return value. Skipped on a cache hit (we just used the
|
|
692
|
+
# entry; rewriting it would be wasted work AND would refresh
|
|
693
|
+
# `cached_at_unix` past the TTL ceiling, defeating the staleness
|
|
694
|
+
# bound).
|
|
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
|
|
706
|
+
local _write_payload=""
|
|
707
|
+
_write_payload=$(node -e '
|
|
708
|
+
const args = process.argv.slice(1);
|
|
709
|
+
const now = Math.floor(Date.now() / 1000);
|
|
710
|
+
const entry = {
|
|
711
|
+
schema_version: "v1",
|
|
712
|
+
cli_realpath: args[0],
|
|
713
|
+
cli_mtime: args[1],
|
|
714
|
+
cli_size_bytes: args[2],
|
|
715
|
+
// 0.48.0 codex round-3 P1+P2: record the ancestor package.json
|
|
716
|
+
// mtime/size + dist/cli/ dir mtime so the read-side validator
|
|
717
|
+
// can re-check them on every hit. The cache key includes
|
|
718
|
+
// these too, so a drifted state produces a different key —
|
|
719
|
+
// but persisting them in the entry lets the validator catch
|
|
720
|
+
// a key collision as a stale-entry miss instead of trusting
|
|
721
|
+
// it.
|
|
722
|
+
pkg_mtime: args[3],
|
|
723
|
+
pkg_size_bytes: args[4],
|
|
724
|
+
dist_mtime: args[5],
|
|
725
|
+
// 0.48.0 codex round-4 P1: record the resolved node binary
|
|
726
|
+
// realpath + mtime so the read-side validator can re-check
|
|
727
|
+
// them and refuse a hit when the interpreter swapped.
|
|
728
|
+
node_realpath: args[6],
|
|
729
|
+
node_mtime: args[7],
|
|
730
|
+
sandbox_ok: true,
|
|
731
|
+
shape_ok: true,
|
|
732
|
+
cached_at_unix: now,
|
|
733
|
+
ttl_seconds: 3600,
|
|
734
|
+
};
|
|
735
|
+
process.stdout.write(JSON.stringify(entry));
|
|
736
|
+
' -- "$_shim_cache_cli_real" "$_shim_cache_cli_mtime" "$_shim_cache_cli_size" "$_shim_cache_pkg_mtime" "$_shim_cache_pkg_size" "$_shim_cache_dist_mtime" "$_shim_cache_node_real" "$_shim_cache_node_mtime" 2>/dev/null || true)
|
|
737
|
+
if [ -n "$_write_payload" ]; then
|
|
738
|
+
shim_cache_write "$_shim_cache_key" "$_write_payload" >/dev/null 2>&1 || true
|
|
739
|
+
fi
|
|
740
|
+
fi
|
|
741
|
+
|
|
417
742
|
# 9. Forward stdin.
|
|
418
743
|
if declare -F shim_forward >/dev/null 2>&1; then
|
|
419
744
|
shim_forward
|