@bookedsolid/rea 0.49.1 → 0.50.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/MIGRATING.md +105 -0
- package/README.md +62 -0
- package/THREAT_MODEL.md +65 -0
- package/dist/cli/doctor.d.ts +50 -0
- package/dist/cli/doctor.js +379 -0
- package/dist/cli/global-cli.d.ts +274 -0
- package/dist/cli/global-cli.js +709 -0
- package/dist/cli/hook.js +26 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/install/global.d.ts +93 -0
- package/dist/cli/install/global.js +397 -0
- package/dist/cli/trust.d.ts +70 -0
- package/dist/cli/trust.js +222 -0
- package/dist/hooks/bash-scanner/protected-scan.d.ts +24 -0
- package/dist/hooks/bash-scanner/protected-scan.js +152 -7
- package/dist/hooks/protected-paths-bash-gate/index.js +24 -1
- package/dist/policy/loader.d.ts +13 -0
- package/dist/policy/loader.js +52 -0
- package/dist/policy/types.d.ts +40 -0
- package/hooks/_lib/shim-cache.sh +42 -8
- package/hooks/_lib/shim-runtime.sh +494 -29
- package/hooks/local-review-gate.sh +51 -5
- package/package.json +1 -1
|
@@ -187,8 +187,14 @@ shim_resolve_cli() {
|
|
|
187
187
|
# 2. an ancestor package.json has `name`=`@bookedsolid/rea`
|
|
188
188
|
# 3. (when SHIM_ENFORCE_CLI_SHAPE=1) realpath ends in dist/cli/index.js
|
|
189
189
|
#
|
|
190
|
-
# Echoes "ok" on success or "bad:<reason>" on failure.
|
|
191
|
-
#
|
|
190
|
+
# Echoes "ok:<realpath>" on success or "bad:<reason>" on failure.
|
|
191
|
+
# Callers branch on the `ok:` prefix (bash 3.2: `case "$x" in ok:*)`)
|
|
192
|
+
# and use the realpath tail for realpath-exec (shim_run step 4a). The
|
|
193
|
+
# realpath is the same fs.realpathSync the sandbox already computed, so
|
|
194
|
+
# executing it shrinks the TOCTOU window to the in-place-swap residual
|
|
195
|
+
# the in-project tier already carries — it is NOT a same-inode
|
|
196
|
+
# guarantee (a rename-swap of the realpath after validation remains
|
|
197
|
+
# possible).
|
|
192
198
|
#
|
|
193
199
|
# Args:
|
|
194
200
|
# $1 — resolved CLI path
|
|
@@ -240,10 +246,310 @@ shim_sandbox_check() {
|
|
|
240
246
|
cur = path.dirname(cur);
|
|
241
247
|
}
|
|
242
248
|
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
243
|
-
|
|
249
|
+
// TOCTOU precursor: emit the validated realpath so the caller can
|
|
250
|
+
// execute it instead of the literal (possibly-symlinked) cli path.
|
|
251
|
+
// `real` is the fs.realpathSync(cli) computed at the top of this
|
|
252
|
+
// program. The `bad:<reason>` outputs above are byte-unchanged.
|
|
253
|
+
process.stdout.write("ok:" + real);
|
|
244
254
|
' -- "$cli_path" "$proj_dir" "$enforce_shape" 2>/dev/null
|
|
245
255
|
}
|
|
246
256
|
|
|
257
|
+
# -----------------------------------------------------------------------------
|
|
258
|
+
# Global tier — A5 entry gate (registry ∧ membership) + resolve. 0.49.0
|
|
259
|
+
# Phase 1b. Derives the per-user trust root from the PASSWORD DATABASE
|
|
260
|
+
# (os.userInfo().homedir → libuv getpwuid_r), NEVER from $HOME / $XDG_* —
|
|
261
|
+
# an agent can set those in-process, so an env-derived root is the N3
|
|
262
|
+
# redirect surface this tier closes. A trust root an agent can move is not
|
|
263
|
+
# a trust root.
|
|
264
|
+
#
|
|
265
|
+
# Registry-ONLY (codex design-gate P1-1): NO policy read in this pre-CLI
|
|
266
|
+
# window. policy.yaml is NOT scanner-protected while no CLI is resolved, so
|
|
267
|
+
# the optional `allow_global_cli` veto lands POST-resolution in a later
|
|
268
|
+
# phase (registry can only ENABLE; policy can only further-RESTRICT — the
|
|
269
|
+
# asymmetry holds).
|
|
270
|
+
#
|
|
271
|
+
# Every A5 / registry-unusable / not-installed condition prints
|
|
272
|
+
# "unavailable" (codex P1-3: SILENT in the hot path — an un-blessed
|
|
273
|
+
# checkout MUST stay byte-identical to feature-absent; a loud
|
|
274
|
+
# `bad:global-registry-*` banner would perturb every CLI-less checkout via
|
|
275
|
+
# shared per-user state). Loud diagnosis lives only in `rea doctor`.
|
|
276
|
+
#
|
|
277
|
+
# Output:
|
|
278
|
+
# success → "<candidate_path>\n<g_root_path>" (literal probed paths)
|
|
279
|
+
# else → "unavailable"
|
|
280
|
+
#
|
|
281
|
+
# Args: $1 — CLAUDE_PROJECT_DIR (agent-controllable; spoofing only causes
|
|
282
|
+
# the TRUSTED global CLI to run — A5 is a consent gate, not a
|
|
283
|
+
# containment gate).
|
|
284
|
+
# -----------------------------------------------------------------------------
|
|
285
|
+
shim_global_entry_gate() {
|
|
286
|
+
local proj_dir="$1"
|
|
287
|
+
node -e '
|
|
288
|
+
const fs = require("fs");
|
|
289
|
+
const path = require("path");
|
|
290
|
+
const os = require("os");
|
|
291
|
+
const projDir = process.argv[1];
|
|
292
|
+
function unavailable() { process.stdout.write("unavailable"); process.exit(0); }
|
|
293
|
+
// A5.1 — passwd-derived home. NEVER $HOME / $XDG_*.
|
|
294
|
+
let pwDir;
|
|
295
|
+
try { pwDir = os.userInfo().homedir; } catch (e) { unavailable(); }
|
|
296
|
+
if (!pwDir || typeof pwDir !== "string" || pwDir.charAt(0) !== "/") unavailable();
|
|
297
|
+
const euid = process.geteuid();
|
|
298
|
+
const reaDir = path.join(pwDir, ".rea");
|
|
299
|
+
const gRoot = path.join(reaDir, "cli");
|
|
300
|
+
const registry = path.join(reaDir, "trusted-projects");
|
|
301
|
+
// A5.3a — validate <pw_dir>/.rea (lstat; never lstat pwDir or above:
|
|
302
|
+
// firmlinks / BSD /home symlinks legitimately exist there).
|
|
303
|
+
let rs;
|
|
304
|
+
try { rs = fs.lstatSync(reaDir); } catch (e) { unavailable(); }
|
|
305
|
+
if (rs.isSymbolicLink() || !rs.isDirectory()) unavailable();
|
|
306
|
+
if (rs.uid !== euid) unavailable();
|
|
307
|
+
if ((rs.mode & 0o022) !== 0) unavailable();
|
|
308
|
+
// A5.3b — validate the registry file: regular, not a symlink, owner,
|
|
309
|
+
// strict 0600 mask (NOT the 0o022 dir mask), single link (codex P2-7:
|
|
310
|
+
// a hardlinked registry is a same-uid mutation primitive).
|
|
311
|
+
let regs;
|
|
312
|
+
try { regs = fs.lstatSync(registry); } catch (e) { unavailable(); }
|
|
313
|
+
if (regs.isSymbolicLink() || !regs.isFile()) unavailable();
|
|
314
|
+
if (regs.uid !== euid) unavailable();
|
|
315
|
+
if ((regs.mode & 0o077) !== 0) unavailable();
|
|
316
|
+
if (regs.nlink !== 1) unavailable();
|
|
317
|
+
// A5.4 — project realpath (same resolution the writer anchors trust on).
|
|
318
|
+
let projReal;
|
|
319
|
+
try { projReal = fs.realpathSync(projDir); } catch (e) { unavailable(); }
|
|
320
|
+
// A5.5 — membership: fixed-string, full-line match (grep -Fxq). No
|
|
321
|
+
// parser ever (membership is decided pre-CLI-resolution); comment/blank
|
|
322
|
+
// lines are inert because realpath queries start with "/".
|
|
323
|
+
let content;
|
|
324
|
+
try { content = fs.readFileSync(registry, "utf8"); } catch (e) { unavailable(); }
|
|
325
|
+
const lines = content.split("\n");
|
|
326
|
+
let member = false;
|
|
327
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
328
|
+
if (lines[i] === projReal) { member = true; break; }
|
|
329
|
+
}
|
|
330
|
+
if (!member) unavailable();
|
|
331
|
+
// Resolve — probe (1) node_modules shape then (2) bare-drop fallback.
|
|
332
|
+
const c1 = path.join(gRoot, "node_modules", "@bookedsolid", "rea", "dist", "cli", "index.js");
|
|
333
|
+
const c2 = path.join(gRoot, "dist", "cli", "index.js");
|
|
334
|
+
let cand = "";
|
|
335
|
+
if (fs.existsSync(c1)) cand = c1;
|
|
336
|
+
else if (fs.existsSync(c2)) cand = c2;
|
|
337
|
+
if (!cand) unavailable(); // blessed-but-not-installed
|
|
338
|
+
process.stdout.write(cand + "\n" + gRoot);
|
|
339
|
+
' -- "$proj_dir" 2>/dev/null
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# -----------------------------------------------------------------------------
|
|
343
|
+
# Global tier — A1–A4 sandbox over a resolved candidate. 0.49.0 Phase 1b.
|
|
344
|
+
# Echoes "ok:<realpath>" on success (consistent with shim_sandbox_check),
|
|
345
|
+
# "bad:global-<reason>" on a fatal tier failure (blessed-but-hostile tree),
|
|
346
|
+
# or "unavailable" when realpath throws (ENOENT / automount — NOT a
|
|
347
|
+
# refusal). Evaluation order is cheapest/most-decisive first.
|
|
348
|
+
#
|
|
349
|
+
# A2 per-component lstat walk from the candidate UP to AND INCLUDING
|
|
350
|
+
# <rea_dir> (= dirname(g_root)); STOP there (never lstat pw_dir or
|
|
351
|
+
# above). Rejects ANY symlink component (an inside-pointing symlink
|
|
352
|
+
# is still a same-uid repoint primitive), foreign owner, group/other
|
|
353
|
+
# write, OR a device-number change vs g_root (mount/bind/automount
|
|
354
|
+
# aliasing, codex P2-6). The candidate index.js must have nlink===1
|
|
355
|
+
# (codex P2-7).
|
|
356
|
+
# A1 realpath(candidate) contained in realpath(g_root).
|
|
357
|
+
# A4 realpath ends sep+"dist/cli/index.js" — ALWAYS-on for global,
|
|
358
|
+
# independent of SHIM_ENFORCE_CLI_SHAPE.
|
|
359
|
+
# A3 ancestor package.json#name==="@bookedsolid/rea" (walk ≤20); that
|
|
360
|
+
# package.json must also have nlink===1.
|
|
361
|
+
#
|
|
362
|
+
# Args:
|
|
363
|
+
# $1 — candidate CLI path (literal, as probed)
|
|
364
|
+
# $2 — g_root (<pw_dir>/.rea/cli)
|
|
365
|
+
# -----------------------------------------------------------------------------
|
|
366
|
+
shim_sandbox_check_global() {
|
|
367
|
+
local cand="$1"
|
|
368
|
+
local g_root="$2"
|
|
369
|
+
node -e '
|
|
370
|
+
const fs = require("fs");
|
|
371
|
+
const path = require("path");
|
|
372
|
+
const cand = process.argv[1];
|
|
373
|
+
const gRoot = process.argv[2];
|
|
374
|
+
function bad(r) { process.stdout.write("bad:global-" + r); process.exit(1); }
|
|
375
|
+
function unavailable() { process.stdout.write("unavailable"); process.exit(1); }
|
|
376
|
+
function ok(r) { process.stdout.write("ok:" + r); process.exit(0); }
|
|
377
|
+
const euid = process.geteuid();
|
|
378
|
+
const reaDir = path.dirname(gRoot); // walk stops here; never above
|
|
379
|
+
const sep = path.sep;
|
|
380
|
+
// Capture g_root device number for the mount/bind aliasing check.
|
|
381
|
+
let gRootDev;
|
|
382
|
+
try { gRootDev = fs.lstatSync(gRoot).dev; } catch (e) { unavailable(); }
|
|
383
|
+
// A2 — per-component lstat walk: candidate UP to AND INCLUDING reaDir.
|
|
384
|
+
let comp = cand;
|
|
385
|
+
let first = true;
|
|
386
|
+
let guard = 0;
|
|
387
|
+
for (;;) {
|
|
388
|
+
guard += 1;
|
|
389
|
+
if (guard > 128) bad("perm"); // pathological depth — fail to tier
|
|
390
|
+
let st;
|
|
391
|
+
try { st = fs.lstatSync(comp); } catch (e) { unavailable(); }
|
|
392
|
+
if (st.isSymbolicLink()) bad("symlink");
|
|
393
|
+
if (st.uid !== euid) bad("perm");
|
|
394
|
+
if ((st.mode & 0o022) !== 0) bad("perm");
|
|
395
|
+
if (st.dev !== gRootDev) bad("perm");
|
|
396
|
+
if (first) {
|
|
397
|
+
// The candidate (index.js) must be a single-link regular file.
|
|
398
|
+
if (st.nlink !== 1) bad("hardlink");
|
|
399
|
+
first = false;
|
|
400
|
+
}
|
|
401
|
+
if (comp === reaDir) break;
|
|
402
|
+
const parent = path.dirname(comp);
|
|
403
|
+
if (parent === comp) break; // reached fs root without reaDir
|
|
404
|
+
comp = parent;
|
|
405
|
+
}
|
|
406
|
+
// A1 — realpath containment.
|
|
407
|
+
let real, realRoot;
|
|
408
|
+
try { real = fs.realpathSync(cand); } catch (e) { unavailable(); }
|
|
409
|
+
try { realRoot = fs.realpathSync(gRoot); } catch (e) { unavailable(); }
|
|
410
|
+
const rootSep = realRoot.endsWith(sep) ? realRoot : realRoot + sep;
|
|
411
|
+
if (!(real === realRoot || real.startsWith(rootSep))) bad("escapes-root");
|
|
412
|
+
// A4 — dist/cli/index.js shape (ALWAYS-on for global).
|
|
413
|
+
const endWith = path.join("dist", "cli", "index.js");
|
|
414
|
+
if (!(real.endsWith(sep + endWith) || real === sep + endWith)) bad("shape");
|
|
415
|
+
// A3 — ancestor package.json with the rea name + nlink===1.
|
|
416
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
417
|
+
let pkg = "";
|
|
418
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
419
|
+
const pj = path.join(cur, "package.json");
|
|
420
|
+
if (fs.existsSync(pj)) {
|
|
421
|
+
try {
|
|
422
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
423
|
+
if (data && data.name === "@bookedsolid/rea") { pkg = pj; break; }
|
|
424
|
+
} catch (e) { /* keep walking */ }
|
|
425
|
+
}
|
|
426
|
+
cur = path.dirname(cur);
|
|
427
|
+
}
|
|
428
|
+
if (!pkg) bad("no-rea-pkg");
|
|
429
|
+
let ps;
|
|
430
|
+
try { ps = fs.lstatSync(pkg); } catch (e) { bad("no-rea-pkg"); }
|
|
431
|
+
if (ps.nlink !== 1) bad("hardlink");
|
|
432
|
+
ok(real);
|
|
433
|
+
' -- "$cand" "$g_root" 2>/dev/null
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# -----------------------------------------------------------------------------
|
|
437
|
+
# Global tier resolver. Called by shim_run ONLY when the in-project tiers
|
|
438
|
+
# missed (in-project ALWAYS wins; the registry is NEVER consulted when an
|
|
439
|
+
# in-project CLI resolved). On success sets REA_ARGV + RESOLVED_CLI_PATH +
|
|
440
|
+
# TRUST_TIER=global + _SHIM_GLOBAL_G_ROOT; on a fatal A1–A4 failure sets
|
|
441
|
+
# _SHIM_GLOBAL_BAD_REASON; on a silent A5 / registry / resolve miss leaves
|
|
442
|
+
# everything empty (byte-identical to feature-absent).
|
|
443
|
+
# -----------------------------------------------------------------------------
|
|
444
|
+
shim_resolve_cli_global() {
|
|
445
|
+
_SHIM_GLOBAL_BAD_REASON=""
|
|
446
|
+
_SHIM_GLOBAL_G_ROOT=""
|
|
447
|
+
# node is required for passwd derivation + lstat walk + realpath. No node
|
|
448
|
+
# → tier silently unavailable (same terminal as feature-absent).
|
|
449
|
+
command -v node >/dev/null 2>&1 || return 0
|
|
450
|
+
local gate=""
|
|
451
|
+
gate=$(shim_global_entry_gate "$proj" 2>/dev/null)
|
|
452
|
+
case "$gate" in
|
|
453
|
+
unavailable|"") return 0 ;; # silent A5 / registry / resolve miss
|
|
454
|
+
esac
|
|
455
|
+
# Success form is "<candidate>\n<g_root>". Parse the two lines (bash 3.2:
|
|
456
|
+
# ANSI-C $'\n' parameter-expansion trim — no `mapfile`).
|
|
457
|
+
local candidate="" g_root=""
|
|
458
|
+
candidate="${gate%%$'\n'*}"
|
|
459
|
+
g_root="${gate#*$'\n'}"
|
|
460
|
+
# Defensive: both must be non-empty absolute paths; else treat as silent.
|
|
461
|
+
case "$candidate" in /*) ;; *) return 0 ;; esac
|
|
462
|
+
case "$g_root" in /*) ;; *) return 0 ;; esac
|
|
463
|
+
local result=""
|
|
464
|
+
result=$(shim_sandbox_check_global "$candidate" "$g_root" 2>/dev/null)
|
|
465
|
+
case "$result" in
|
|
466
|
+
ok:*)
|
|
467
|
+
local real=""
|
|
468
|
+
real="${result#ok:}"
|
|
469
|
+
REA_ARGV=(node "$real")
|
|
470
|
+
RESOLVED_CLI_PATH="$real"
|
|
471
|
+
TRUST_TIER="global"
|
|
472
|
+
_SHIM_GLOBAL_G_ROOT="$g_root"
|
|
473
|
+
;;
|
|
474
|
+
bad:*)
|
|
475
|
+
_SHIM_GLOBAL_BAD_REASON="$result"
|
|
476
|
+
;;
|
|
477
|
+
*)
|
|
478
|
+
# "unavailable" (realpath threw) — silent, parity preserved.
|
|
479
|
+
;;
|
|
480
|
+
esac
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# -----------------------------------------------------------------------------
|
|
484
|
+
# Global-tier veto decision (0.50.0 Phase 2b). The A5 registry gate can only
|
|
485
|
+
# ENABLE the per-user global CLI; a project's OWN policy may further-RESTRICT
|
|
486
|
+
# it via `runtime.allow_global_cli` — the enable/restrict asymmetry the design
|
|
487
|
+
# turns on. This is the SINGLE source of truth for that decision, shared by
|
|
488
|
+
# BOTH shim_run (step-4-global-veto) and local-review-gate.sh (push/commit
|
|
489
|
+
# gate), so the 14 shims and the push gate cannot drift.
|
|
490
|
+
#
|
|
491
|
+
# Reads the veto THROUGH the sandbox-validated REA_ARGV CLI (the exact trust
|
|
492
|
+
# level shim_policy_short_circuit relies on post-sandbox). MUST be called ONLY
|
|
493
|
+
# when TRUST_TIER=global — that guarantees REA_ARGV is the non-empty validated
|
|
494
|
+
# global CLI, so "${REA_ARGV[@]}" is safe under set -u.
|
|
495
|
+
#
|
|
496
|
+
# TWO-STEP + TYPE-STRICT + fail-closed on malformed config. `rea hook
|
|
497
|
+
# policy-get` only PARSES YAML — it does NOT run the strict zod schema
|
|
498
|
+
# loadPolicy() / `rea doctor` apply, so ANY shape the strict loader would
|
|
499
|
+
# REJECT must never silently ENABLE the tier:
|
|
500
|
+
#
|
|
501
|
+
# 1. Read the PARENT `runtime` block (policy-get runtime --json). Verified
|
|
502
|
+
# shapes against the built CLI:
|
|
503
|
+
# runtime: {allow_global_cli: …} → {"allow_global_cli":…} (valid object)
|
|
504
|
+
# runtime: [] → [] (malformed)
|
|
505
|
+
# runtime: "off" → "off" (malformed)
|
|
506
|
+
# runtime: 42 → 42 (malformed)
|
|
507
|
+
# runtime absent → null
|
|
508
|
+
# malformed YAML → null, exit 1
|
|
509
|
+
# Classify:
|
|
510
|
+
# non-zero exit → fail-closed VETO
|
|
511
|
+
# null → runtime absent → ALLOW
|
|
512
|
+
# starts with `{` → valid object → step 2
|
|
513
|
+
# anything else ([, ", number …) → malformed runtime block → fail-closed VETO
|
|
514
|
+
# 2. Read the LEAF `runtime.allow_global_cli --json` (TYPE-PRESERVING: JSON
|
|
515
|
+
# boolean `true` vs the string `"true"` vs `null`):
|
|
516
|
+
# non-zero exit → fail-closed VETO
|
|
517
|
+
# true → ALLOW
|
|
518
|
+
# null → allow_global_cli absent → ALLOW
|
|
519
|
+
# anything else (false, "true", "yes", garbage) → VETO
|
|
520
|
+
#
|
|
521
|
+
# Return: 0 = VETO (refuse the global tier), 1 = ALLOW (proceed).
|
|
522
|
+
# -----------------------------------------------------------------------------
|
|
523
|
+
shim_global_tier_vetoed() {
|
|
524
|
+
local _rt_out="" _rt_status=0
|
|
525
|
+
_rt_out=$("${REA_ARGV[@]}" hook policy-get runtime --json 2>/dev/null); _rt_status=$?
|
|
526
|
+
if [ "$_rt_status" -ne 0 ]; then
|
|
527
|
+
return 0 # policy-get errored / unparseable policy → fail-closed veto
|
|
528
|
+
fi
|
|
529
|
+
case "$_rt_out" in
|
|
530
|
+
null)
|
|
531
|
+
return 1 # runtime block absent → allow
|
|
532
|
+
;;
|
|
533
|
+
'{'*)
|
|
534
|
+
: # valid object → fall through to the type-strict leaf read
|
|
535
|
+
;;
|
|
536
|
+
*)
|
|
537
|
+
# runtime present with the WRONG TYPE ([], "off", 42, …) — a shape the
|
|
538
|
+
# strict loader REJECTS. Fail closed rather than ENABLE the tier.
|
|
539
|
+
return 0
|
|
540
|
+
;;
|
|
541
|
+
esac
|
|
542
|
+
local _av_out="" _av_status=0
|
|
543
|
+
_av_out=$("${REA_ARGV[@]}" hook policy-get runtime.allow_global_cli --json 2>/dev/null); _av_status=$?
|
|
544
|
+
if [ "$_av_status" -ne 0 ]; then
|
|
545
|
+
return 0 # fail-closed veto
|
|
546
|
+
fi
|
|
547
|
+
case "$_av_out" in
|
|
548
|
+
true|null) return 1 ;; # JSON boolean true, or null (allow_global_cli absent) → allow
|
|
549
|
+
*) return 0 ;; # false | "true" | "yes" | any string / garbage → fail-closed veto
|
|
550
|
+
esac
|
|
551
|
+
}
|
|
552
|
+
|
|
247
553
|
# -----------------------------------------------------------------------------
|
|
248
554
|
# Standardized banners — keep stderr templates identical across shims.
|
|
249
555
|
# -----------------------------------------------------------------------------
|
|
@@ -268,6 +574,15 @@ shim_emit_sandbox_skip_banner() {
|
|
|
268
574
|
printf 'rea: %s skipped (sandbox check: %s)\n' "$SHIM_NAME" "$reason" >&2
|
|
269
575
|
}
|
|
270
576
|
|
|
577
|
+
# Global tier — one-line advisory when a BLESSED project resolves a global
|
|
578
|
+
# CLI tree that fails A1–A4 (hostile/malformed). Never fired by an
|
|
579
|
+
# un-blessed project (which degrades silently), so it cannot perturb the
|
|
580
|
+
# feature-absent parity surface.
|
|
581
|
+
shim_emit_global_tier_banner() {
|
|
582
|
+
local reason="$1"
|
|
583
|
+
printf 'rea: %s — global rea CLI tier rejected (%s); falling back to no-CLI.\n' "$SHIM_NAME" "$reason" >&2
|
|
584
|
+
}
|
|
585
|
+
|
|
271
586
|
shim_emit_version_skew_banner_blocking() {
|
|
272
587
|
printf 'rea: this shim requires the `rea hook %s` subcommand (introduced in %s).\n' "$SHIM_NAME" "$SHIM_INTRODUCED_IN" >&2
|
|
273
588
|
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
@@ -309,8 +624,52 @@ shim_run() {
|
|
|
309
624
|
fi
|
|
310
625
|
fi
|
|
311
626
|
|
|
312
|
-
# 4. Resolve CLI.
|
|
627
|
+
# 4. Resolve CLI. In-project tiers ALWAYS win (byte-identical to today).
|
|
313
628
|
shim_resolve_cli
|
|
629
|
+
local TRUST_TIER="project"
|
|
630
|
+
|
|
631
|
+
# 4-global (0.49.0 Phase 1b). ONLY when both in-project tiers missed do we
|
|
632
|
+
# consult the opt-in global tier. Registry membership is the SOLE gate
|
|
633
|
+
# (codex design-gate P1-1: NO policy read in this pre-CLI window). An
|
|
634
|
+
# un-blessed project leaves REA_ARGV empty + emits nothing, so the no-CLI
|
|
635
|
+
# terminal below is byte-identical to feature-absent. A5 + global
|
|
636
|
+
# resolution complete (and TRUST_TIER is final) BEFORE the cache block at
|
|
637
|
+
# step 4b builds its key — a HARD design invariant (codex P3-9): the
|
|
638
|
+
# cache's trust_tier + registry mtime/size fields are only sound if the
|
|
639
|
+
# live registry gate ran first.
|
|
640
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
641
|
+
shim_resolve_cli_global
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
# 4-global-veto (0.50.0 Phase 2b). OPTIONAL in-project veto over the global
|
|
645
|
+
# tier, via the SHARED shim_global_tier_vetoed helper (single source of
|
|
646
|
+
# truth for the decision — see its header for the two-step type-strict +
|
|
647
|
+
# fail-closed-on-malformed logic). Runs ONLY when the global tier actually
|
|
648
|
+
# resolved (TRUST_TIER=global ⇒ REA_ARGV is the sandbox-validated global
|
|
649
|
+
# CLI, so the helper's "${REA_ARGV[@]}" reads are safe under set -u).
|
|
650
|
+
#
|
|
651
|
+
# Placement is deliberate: this sits BEFORE the step-4b cache block and
|
|
652
|
+
# therefore fires on EVERY invocation, warm-cache fires included.
|
|
653
|
+
# `runtime.allow_global_cli` is intentionally kept OUT of the cache key
|
|
654
|
+
# (design memo) precisely so a mid-session edit adding
|
|
655
|
+
# `allow_global_cli: false` is honored on the very NEXT fire instead of
|
|
656
|
+
# surviving inside a warm entry. Gating this read behind the cache would
|
|
657
|
+
# reintroduce exactly the stale-consent hole the key-omission avoids. It
|
|
658
|
+
# also completes BEFORE the step-4b cache-key build, so TRUST_TIER is
|
|
659
|
+
# still final when that key is constructed (extends the step-4 invariant).
|
|
660
|
+
#
|
|
661
|
+
# On veto: behave EXACTLY like "project not in registry" / no-CLI — clear
|
|
662
|
+
# REA_ARGV, revert TRUST_TIER to project, and DO NOT set
|
|
663
|
+
# _SHIM_GLOBAL_BAD_REASON. A veto is a legitimate project CHOICE, not a
|
|
664
|
+
# security signal, so it stays SILENT — no bad:global-* advisory. The
|
|
665
|
+
# existing step-7 relevance-gated no-CLI terminal then makes a vetoed
|
|
666
|
+
# project byte-identical to an un-blessed / no-CLI project (harmless
|
|
667
|
+
# payload → exit 0 silent; relevant payload on a blocking shim →
|
|
668
|
+
# cli-missing banner + exit 2).
|
|
669
|
+
if [ "$TRUST_TIER" = "global" ] && shim_global_tier_vetoed; then
|
|
670
|
+
REA_ARGV=()
|
|
671
|
+
TRUST_TIER="project"
|
|
672
|
+
fi
|
|
314
673
|
|
|
315
674
|
# 4a. Sandbox check (0.48.1 — moved earlier from step 5). Runs BEFORE
|
|
316
675
|
# the cache-key-prep block so the dist-tree `find` walk (added in
|
|
@@ -332,7 +691,16 @@ shim_run() {
|
|
|
332
691
|
local sandbox_result=""
|
|
333
692
|
local sandbox_failed=0
|
|
334
693
|
local node_missing=0
|
|
335
|
-
|
|
694
|
+
local real=""
|
|
695
|
+
# 0.49.0 Phase 1b: gated on TRUST_TIER=project. The global tier already
|
|
696
|
+
# ran its own A1–A4 sandbox (incl. the per-component lstat walk, st_dev,
|
|
697
|
+
# nlink, realpath containment, and the always-on dist/cli/index.js shape)
|
|
698
|
+
# inside shim_resolve_cli_global, and REA_ARGV already points at the
|
|
699
|
+
# validated realpath. Re-running the IN-PROJECT check here would reject a
|
|
700
|
+
# global CLI with bad:cli-escapes-project (it legitimately lives outside
|
|
701
|
+
# CLAUDE_PROJECT_DIR). node was already required for the global resolve,
|
|
702
|
+
# so the node-missing branch below is project-tier-only too.
|
|
703
|
+
if [ "${#REA_ARGV[@]}" -gt 0 ] && [ "$TRUST_TIER" = "project" ]; then
|
|
336
704
|
if ! command -v node >/dev/null 2>&1; then
|
|
337
705
|
# 0.38.1 round-2 P2 fix: pre-fix this branch exited 0/2 IMMEDIATELY
|
|
338
706
|
# without ever calling shim_policy_short_circuit, so a blocking-
|
|
@@ -347,17 +715,33 @@ shim_run() {
|
|
|
347
715
|
REA_ARGV=()
|
|
348
716
|
else
|
|
349
717
|
sandbox_result=$(shim_sandbox_check "$RESOLVED_CLI_PATH" "$proj" "$SHIM_ENFORCE_CLI_SHAPE")
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
718
|
+
# TOCTOU precursor: shim_sandbox_check now echoes `ok:<realpath>`
|
|
719
|
+
# on success. Execute the VALIDATED realpath instead of the
|
|
720
|
+
# literal RESOLVED_CLI_PATH so the version probe (step 8) and the
|
|
721
|
+
# forward (step 9) run the exact path the sandbox vetted. This
|
|
722
|
+
# shrinks the TOCTOU window to the same in-place-swap residual the
|
|
723
|
+
# in-project tier already has — it is NOT a same-inode guarantee
|
|
724
|
+
# (a rename-swap of the realpath after validation remains
|
|
725
|
+
# possible). bash 3.2: `case` glob match, no `[[ =~ ]]`.
|
|
726
|
+
case "$sandbox_result" in
|
|
727
|
+
ok:*)
|
|
728
|
+
real="${sandbox_result#ok:}"
|
|
729
|
+
REA_ARGV=(node "$real")
|
|
730
|
+
RESOLVED_CLI_PATH="$real"
|
|
731
|
+
;;
|
|
732
|
+
*)
|
|
733
|
+
# bad:<reason> — sandbox refused.
|
|
734
|
+
sandbox_failed=1
|
|
735
|
+
if [ "$SHIM_FAIL_OPEN" -eq 1 ]; then
|
|
736
|
+
shim_emit_sandbox_skip_banner "$sandbox_result"
|
|
737
|
+
exit 0
|
|
738
|
+
fi
|
|
739
|
+
# Blocking-tier: clear REA_ARGV so Tier-1 policy reads (in
|
|
740
|
+
# shim_policy_short_circuit) degrade to Tier 2 / Tier 3 instead
|
|
741
|
+
# of invoking the untrusted CLI.
|
|
742
|
+
REA_ARGV=()
|
|
743
|
+
;;
|
|
744
|
+
esac
|
|
361
745
|
fi
|
|
362
746
|
fi
|
|
363
747
|
|
|
@@ -383,6 +767,10 @@ shim_run() {
|
|
|
383
767
|
local _shim_cache_dist_mtime=""
|
|
384
768
|
local _shim_cache_node_real=""
|
|
385
769
|
local _shim_cache_node_mtime=""
|
|
770
|
+
# 0.49.0 Phase 1b cache-key v2 fields.
|
|
771
|
+
local _registry_mtime=""
|
|
772
|
+
local _registry_size=""
|
|
773
|
+
local _effective_shape="$SHIM_ENFORCE_CLI_SHAPE"
|
|
386
774
|
# 0.48.1: gated on `sandbox_failed -eq 0` so a sandbox refusal
|
|
387
775
|
# short-circuits BEFORE the dist-tree hash walk runs (was running
|
|
388
776
|
# against a possibly-symlinked-out target pre-0.48.1).
|
|
@@ -400,6 +788,12 @@ shim_run() {
|
|
|
400
788
|
local _proj_real=""
|
|
401
789
|
local _euid=""
|
|
402
790
|
local _session_tok=""
|
|
791
|
+
# TOCTOU precursor cache-stability note: step 4a may have rewritten
|
|
792
|
+
# RESOLVED_CLI_PATH to its realpath. That does NOT drift the cache
|
|
793
|
+
# key value — `_shim_cache_cli_real` below runs realpathSync, so
|
|
794
|
+
# realpath-of-realpath is byte-identical to realpath-of-symlink, and
|
|
795
|
+
# `_stat_out` lstat reads the same underlying file (intermediate
|
|
796
|
+
# symlinks are kernel-resolved either way).
|
|
403
797
|
_stat_out=$(shim_cache_mtime_size "$RESOLVED_CLI_PATH" 2>/dev/null || true)
|
|
404
798
|
# 0.48.0 codex round-4 P1 + round-7 P2: capture the ACTUAL node
|
|
405
799
|
# interpreter realpath + mtime via `process.execPath` (node's own
|
|
@@ -426,6 +820,27 @@ shim_run() {
|
|
|
426
820
|
_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
821
|
_euid=$(id -u 2>/dev/null || true)
|
|
428
822
|
_session_tok=$(shim_cache_session_token 2>/dev/null || true)
|
|
823
|
+
# 0.49.0 Phase 1b — tier-aware key inputs. The global tier enforces the
|
|
824
|
+
# dist/cli/index.js shape unconditionally (A4), so the effective
|
|
825
|
+
# enforce_cli_shape key field is "1" there regardless of
|
|
826
|
+
# SHIM_ENFORCE_CLI_SHAPE. The registry mtime/size pin the trust source
|
|
827
|
+
# so an untrust mid-session (atomic rename → new mtime; removed line →
|
|
828
|
+
# smaller size — two invalidators defeat `touch -r`) misses a warm
|
|
829
|
+
# global entry. Stat failure → leave them empty → the completeness gate
|
|
830
|
+
# below produces a clean miss (fail-safe, NEVER fail-closed).
|
|
831
|
+
if [ "$TRUST_TIER" = "global" ]; then
|
|
832
|
+
_effective_shape="1"
|
|
833
|
+
if [ -n "${_SHIM_GLOBAL_G_ROOT:-}" ]; then
|
|
834
|
+
local _reg_path=""
|
|
835
|
+
_reg_path="$(dirname "$_SHIM_GLOBAL_G_ROOT")/trusted-projects"
|
|
836
|
+
local _reg_stat=""
|
|
837
|
+
_reg_stat=$(shim_cache_mtime_size "$_reg_path" 2>/dev/null || true)
|
|
838
|
+
if [ -n "$_reg_stat" ]; then
|
|
839
|
+
_registry_mtime="${_reg_stat%% *}"
|
|
840
|
+
_registry_size="${_reg_stat##* }"
|
|
841
|
+
fi
|
|
842
|
+
fi
|
|
843
|
+
fi
|
|
429
844
|
# 0.48.0 codex round-3 P2: ALSO capture the ancestor package.json
|
|
430
845
|
# path + mtime/size. The sandbox check walks upward to find a
|
|
431
846
|
# package.json whose `name` is `@bookedsolid/rea`; without it in
|
|
@@ -529,7 +944,8 @@ shim_run() {
|
|
|
529
944
|
&& [ -n "$_euid" ] && [ -n "$_session_tok" ] \
|
|
530
945
|
&& [ -n "$_shim_cache_pkg_real" ] && [ -n "$_shim_cache_pkg_mtime" ] \
|
|
531
946
|
&& [ -n "$_shim_cache_dist_mtime" ] \
|
|
532
|
-
&& [ -n "$_shim_cache_node_real" ] && [ -n "$_shim_cache_node_mtime" ]
|
|
947
|
+
&& [ -n "$_shim_cache_node_real" ] && [ -n "$_shim_cache_node_mtime" ] \
|
|
948
|
+
&& { [ "$TRUST_TIER" != "global" ] || { [ -n "$_registry_mtime" ] && [ -n "$_registry_size" ]; }; }; then
|
|
533
949
|
_shim_cache_cli_mtime="${_stat_out%% *}"
|
|
534
950
|
_shim_cache_cli_size="${_stat_out##* }"
|
|
535
951
|
# 0.48.0 codex round-1 P1: the key MUST include SHIM_NAME because
|
|
@@ -547,12 +963,17 @@ shim_run() {
|
|
|
547
963
|
# added/removed (most fresh `tsc` rebuilds do both); (c) the
|
|
548
964
|
# package.json realpath is implicitly part of the key via these
|
|
549
965
|
# mtime/size fields plus the project realpath above.
|
|
550
|
-
|
|
966
|
+
# 0.49.0 Phase 1b: schema "v1"→"v2"; enforce_cli_shape carries the
|
|
967
|
+
# EFFECTIVE value; trust_tier + registry mtime/size appended
|
|
968
|
+
# (positions 15-17). v1 entries become a clean miss at the read-side
|
|
969
|
+
# schema check (hard cutover).
|
|
970
|
+
_shim_cache_key=$(shim_cache_key "v2" "$_session_tok" "$_proj_real" "$_shim_cache_cli_real" \
|
|
551
971
|
"$_shim_cache_cli_mtime" "$_shim_cache_cli_size" "$_euid" \
|
|
552
|
-
"$
|
|
972
|
+
"$_effective_shape" "$SHIM_NAME" \
|
|
553
973
|
"$_shim_cache_pkg_mtime" "$_shim_cache_pkg_size" \
|
|
554
974
|
"$_shim_cache_dist_mtime" \
|
|
555
975
|
"$_shim_cache_node_real" "$_shim_cache_node_mtime" \
|
|
976
|
+
"$TRUST_TIER" "$_registry_mtime" "$_registry_size" \
|
|
556
977
|
2>/dev/null || true)
|
|
557
978
|
if [ -n "$_shim_cache_key" ]; then
|
|
558
979
|
local _cache_json=""
|
|
@@ -572,9 +993,12 @@ shim_run() {
|
|
|
572
993
|
const pkgMtime = String(e.pkg_mtime);
|
|
573
994
|
const pkgSize = String(e.pkg_size_bytes);
|
|
574
995
|
const distMtime = String(e.dist_mtime);
|
|
996
|
+
const trustTier = String(e.trust_tier);
|
|
997
|
+
const regMtime = String(e.registry_mtime);
|
|
998
|
+
const regSize = String(e.registry_size);
|
|
575
999
|
const sandboxOk = e.sandbox_ok === true;
|
|
576
1000
|
const shapeOk = e.shape_ok === true;
|
|
577
|
-
if (e.schema_version !== "
|
|
1001
|
+
if (e.schema_version !== "v2") process.exit(1);
|
|
578
1002
|
if (!Number.isFinite(ttl) || ttl <= 0 || ttl > 3600) process.exit(1);
|
|
579
1003
|
if (!Number.isFinite(cachedAt)) process.exit(1);
|
|
580
1004
|
if ((cachedAt + ttl) < now) process.exit(1);
|
|
@@ -597,10 +1021,18 @@ shim_run() {
|
|
|
597
1021
|
const nodeMtime = String(e.node_mtime);
|
|
598
1022
|
if (nodeReal !== process.argv[8]) process.exit(1);
|
|
599
1023
|
if (nodeMtime !== process.argv[9]) process.exit(1);
|
|
1024
|
+
// 0.49.0 Phase 1b: re-check trust_tier + registry mtime/size.
|
|
1025
|
+
// These are in the key too (a drifted state produces a
|
|
1026
|
+
// different key), but persisting + re-validating them catches
|
|
1027
|
+
// a key collision as a stale-entry miss instead of trusting
|
|
1028
|
+
// it — and makes the cross-tier non-collision explicit.
|
|
1029
|
+
if (trustTier !== process.argv[10]) process.exit(1);
|
|
1030
|
+
if (regMtime !== process.argv[11]) process.exit(1);
|
|
1031
|
+
if (regSize !== process.argv[12]) process.exit(1);
|
|
600
1032
|
if (!sandboxOk || !shapeOk) process.exit(1);
|
|
601
1033
|
process.stdout.write("ok");
|
|
602
1034
|
} 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)
|
|
1035
|
+
' -- "$_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" "$TRUST_TIER" "$_registry_mtime" "$_registry_size" 2>/dev/null || true)
|
|
604
1036
|
if [ "$_cache_validate" = "ok" ]; then
|
|
605
1037
|
_shim_cache_hit=1
|
|
606
1038
|
fi
|
|
@@ -648,20 +1080,46 @@ shim_run() {
|
|
|
648
1080
|
shim_emit_sandbox_failure_banner "$sandbox_result"
|
|
649
1081
|
exit 2
|
|
650
1082
|
fi
|
|
1083
|
+
# 0.49.0 Phase 1b: a BLESSED project resolved a global CLI tree that
|
|
1084
|
+
# failed A1–A4 (hostile/malformed). This MUST behave EXACTLY like "no
|
|
1085
|
+
# CLI" for the relevance decision — otherwise a broken ~/.rea/cli would
|
|
1086
|
+
# turn a harmless Bash/Write call into a repo-wide lockout for opted-in
|
|
1087
|
+
# users on the relevance-gated blocking shims (dangerous-bash-
|
|
1088
|
+
# interceptor, secret-scanner, settings-protection, blocked-paths-*,
|
|
1089
|
+
# protected-paths-bash-gate). The relevance + tier exit logic below is
|
|
1090
|
+
# SHARED between the plain CLI-missing and the bad-global-tree cases;
|
|
1091
|
+
# the only difference is that a bad global tree emits its one-line
|
|
1092
|
+
# advisory (which REPLACES the generic cli-missing banner) once we are
|
|
1093
|
+
# past the not-relevant-allow exit. An irrelevant payload is therefore
|
|
1094
|
+
# allowed SILENTLY, byte-identical to the pre-Phase-1b no-CLI path. Do
|
|
1095
|
+
# NOT escalate advisory hooks to a hard block on a global plant (A2
|
|
1096
|
+
# already proved only euid wrote the tree). Un-blessed projects never
|
|
1097
|
+
# reach here (they degrade silently). The bad reason is unset when
|
|
1098
|
+
# in-project resolved, so guard with :- for set -u.
|
|
651
1099
|
if declare -F shim_cli_missing_relevant >/dev/null 2>&1; then
|
|
652
1100
|
if ! shim_cli_missing_relevant; then
|
|
653
|
-
# CLI missing
|
|
654
|
-
#
|
|
1101
|
+
# CLI missing — or a bad global tree, treated identically — AND the
|
|
1102
|
+
# payload is not relevant per the shim's keyword scan. The pre-port
|
|
1103
|
+
# bash body would have allowed this. Silent, no advisory.
|
|
655
1104
|
exit 0
|
|
656
1105
|
fi
|
|
657
1106
|
fi
|
|
658
|
-
#
|
|
1107
|
+
# Past here the payload IS relevant (or no callback is defined) → the
|
|
1108
|
+
# gate would act. Surface the global-tier diagnostic ONCE if a bad
|
|
1109
|
+
# global tree is the cause (it replaces the generic cli-missing banner).
|
|
1110
|
+
if [ -n "${_SHIM_GLOBAL_BAD_REASON:-}" ]; then
|
|
1111
|
+
shim_emit_global_tier_banner "$_SHIM_GLOBAL_BAD_REASON"
|
|
1112
|
+
fi
|
|
659
1113
|
if [ "$SHIM_FAIL_OPEN" -eq 1 ]; then
|
|
660
|
-
# Advisory tier — drop the gate
|
|
661
|
-
# hooks are nudges, not security claims.
|
|
1114
|
+
# Advisory tier — drop the gate. Any global-bad advisory was already
|
|
1115
|
+
# emitted; advisory hooks are nudges, not security claims.
|
|
662
1116
|
exit 0
|
|
663
1117
|
fi
|
|
664
|
-
|
|
1118
|
+
# Relevant + blocking: refuse. The generic cli-missing banner only when
|
|
1119
|
+
# a bad global tree did NOT already explain the refusal.
|
|
1120
|
+
if [ -z "${_SHIM_GLOBAL_BAD_REASON:-}" ]; then
|
|
1121
|
+
shim_emit_cli_missing_banner
|
|
1122
|
+
fi
|
|
665
1123
|
exit 2
|
|
666
1124
|
fi
|
|
667
1125
|
|
|
@@ -708,7 +1166,7 @@ shim_run() {
|
|
|
708
1166
|
const args = process.argv.slice(1);
|
|
709
1167
|
const now = Math.floor(Date.now() / 1000);
|
|
710
1168
|
const entry = {
|
|
711
|
-
schema_version: "
|
|
1169
|
+
schema_version: "v2",
|
|
712
1170
|
cli_realpath: args[0],
|
|
713
1171
|
cli_mtime: args[1],
|
|
714
1172
|
cli_size_bytes: args[2],
|
|
@@ -727,13 +1185,20 @@ shim_run() {
|
|
|
727
1185
|
// them and refuse a hit when the interpreter swapped.
|
|
728
1186
|
node_realpath: args[6],
|
|
729
1187
|
node_mtime: args[7],
|
|
1188
|
+
// 0.49.0 Phase 1b: trust_tier + registry mtime/size (the read-side
|
|
1189
|
+
// validator re-checks all three). registry_* are "" on the project
|
|
1190
|
+
// tier; sandbox_ok/shape_ok are the tier-appropriate results — for
|
|
1191
|
+
// global, A4 shape was enforced unconditionally during resolution.
|
|
1192
|
+
trust_tier: args[8],
|
|
1193
|
+
registry_mtime: args[9],
|
|
1194
|
+
registry_size: args[10],
|
|
730
1195
|
sandbox_ok: true,
|
|
731
1196
|
shape_ok: true,
|
|
732
1197
|
cached_at_unix: now,
|
|
733
1198
|
ttl_seconds: 3600,
|
|
734
1199
|
};
|
|
735
1200
|
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)
|
|
1201
|
+
' -- "$_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" "$TRUST_TIER" "$_registry_mtime" "$_registry_size" 2>/dev/null || true)
|
|
737
1202
|
if [ -n "$_write_payload" ]; then
|
|
738
1203
|
shim_cache_write "$_shim_cache_key" "$_write_payload" >/dev/null 2>&1 || true
|
|
739
1204
|
fi
|