@bookedsolid/rea 0.41.0 → 0.43.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 +139 -0
- package/dist/cli/audit-summary.d.ts +15 -0
- package/dist/cli/audit-summary.js +94 -38
- package/dist/cli/doctor.d.ts +44 -4
- package/dist/cli/doctor.js +170 -43
- package/dist/cli/init.d.ts +102 -0
- package/dist/cli/init.js +417 -72
- package/dist/cli/upgrade-check.d.ts +38 -0
- package/dist/cli/upgrade-check.js +93 -7
- package/dist/cli/upgrade.js +42 -0
- package/package.json +1 -1
package/MIGRATING.md
CHANGED
|
@@ -389,6 +389,145 @@ per finding and produces more consistent verdicts — fewer
|
|
|
389
389
|
same-code-different-verdict round-trips. Trade-off is push-gate
|
|
390
390
|
latency.
|
|
391
391
|
|
|
392
|
+
## Node-binary hook scanner (added in 0.32.0)
|
|
393
|
+
|
|
394
|
+
Pre-0.32.0 every `.claude/hooks/*.sh` carried the full gate body in
|
|
395
|
+
bash. Adversarial review consistently caught bash-only edge cases that
|
|
396
|
+
were structurally unfixable in shell — multi-line awk encodings,
|
|
397
|
+
ANSI-C escapes, deep nested-shell decoding. 0.32.0 pivoted the entire
|
|
398
|
+
hook surface to a Node-binary scanner: hooks became thin shims (~20-80
|
|
399
|
+
LOC each) that delegate the actual gate work to `rea hook <name>` —
|
|
400
|
+
which runs the canonical scanner inside `dist/cli/index.js`.
|
|
401
|
+
|
|
402
|
+
**Consumer impact:**
|
|
403
|
+
|
|
404
|
+
- Run `pnpm install` (or `npm install`) after upgrading to 0.32.0+ so
|
|
405
|
+
`dist/cli/index.js` is built and the shims have something to call.
|
|
406
|
+
- `.claude/hooks/*.sh` files on disk are noticeably smaller after
|
|
407
|
+
`rea upgrade`; this is the canonical post-0.32.0 shape, not a
|
|
408
|
+
truncation. `rea doctor` will tell you if a shim is the wrong
|
|
409
|
+
vintage.
|
|
410
|
+
- The audit trail is unchanged: hooks still emit `rea.bash_scan`-class
|
|
411
|
+
records to `.rea/audit.jsonl` with the same field shape.
|
|
412
|
+
- Performance is materially better — single Node startup per scan
|
|
413
|
+
instead of an awk/sed pipeline per pattern.
|
|
414
|
+
|
|
415
|
+
If `rea doctor` reports `policy-reader Tier 1 (rea CLI)` as `warn:
|
|
416
|
+
dist not found`, you skipped the build step. Run `pnpm install`.
|
|
417
|
+
|
|
418
|
+
## Graceful-degradation policy reader (added in 0.37.0)
|
|
419
|
+
|
|
420
|
+
The shimmed hooks need to read `.rea/policy.yaml` from a bash context
|
|
421
|
+
that may or may not have python3, jq, or rea's CLI on PATH. 0.37.0
|
|
422
|
+
formalized a 4-tier reader ladder:
|
|
423
|
+
|
|
424
|
+
1. **Tier 1** — `rea hook policy-get` (requires `dist/cli/index.js`)
|
|
425
|
+
2. **Tier 2** — `python3 + stdlib yaml` (PyYAML) — handles flow-form
|
|
426
|
+
3. **Tier 3** — POSIX `awk` block-form parser (the always-available floor)
|
|
427
|
+
4. **Fail-closed** — every tier unreachable: shim refuses the action
|
|
428
|
+
|
|
429
|
+
Tier 1 → 2 → 3 fallthrough is silent at hook-runtime; that's
|
|
430
|
+
intentional (graceful degradation), but means an unreachable Tier 1 +
|
|
431
|
+
unreachable Tier 2 can silently downgrade flow-form policy lookups to
|
|
432
|
+
block-form-only. `rea doctor` (0.39.0+) surfaces all three tier
|
|
433
|
+
reachabilities so you can spot the gap.
|
|
434
|
+
|
|
435
|
+
**Consumer impact:**
|
|
436
|
+
|
|
437
|
+
- If you use FLOW-form YAML for any policy block (e.g.
|
|
438
|
+
`blocked_paths: [.env, ".env.*"]`), make sure either the rea CLI
|
|
439
|
+
dist is present OR `python3 + PyYAML` is installed. With ONLY awk
|
|
440
|
+
reachable, flow-form lookups silently no-op on every shim
|
|
441
|
+
fallthrough path and your declared policy isn't enforced.
|
|
442
|
+
- Install PyYAML on CI runners: `pip3 install pyyaml`. On consumer
|
|
443
|
+
developer machines, it's almost always already present (macOS ships
|
|
444
|
+
it; major Linux distros bundle it with python3).
|
|
445
|
+
- For list-valued policy keys (`blocked_paths`, `protected_writes`),
|
|
446
|
+
the loader iterates the resulting JSON via jq OR python3. Have at
|
|
447
|
+
least one on PATH or `rea doctor` (0.42.0+) will report `fail` on
|
|
448
|
+
the `policy-reader Tier 3 (awk)` row with a list-walker-specific
|
|
449
|
+
remediation message.
|
|
450
|
+
|
|
451
|
+
## Shim runtime extraction (added in 0.38.0)
|
|
452
|
+
|
|
453
|
+
Cosmetic-only refactor: every `.claude/hooks/*.sh` shim now sources
|
|
454
|
+
`hooks/_lib/shim-runtime.sh` for shared boilerplate (env loading,
|
|
455
|
+
tier classification, audit-event emission). **No consumer action
|
|
456
|
+
required** — the change is byte-equivalent at the gate surface. New
|
|
457
|
+
shims you author can adopt the same runtime by sourcing the shared
|
|
458
|
+
helper; documented in the shim authoring guide.
|
|
459
|
+
|
|
460
|
+
## Doctor health surfaces for the policy reader (added in 0.39.0)
|
|
461
|
+
|
|
462
|
+
`rea doctor` gained explicit reachability checks for the 4-tier
|
|
463
|
+
ladder, the dist invokability probe, and a sandbox-containment check
|
|
464
|
+
on the resolved `dist/cli/index.js` path. Output lines you'll see:
|
|
465
|
+
|
|
466
|
+
- `policy-reader Tier 1 (rea CLI)` — pass/warn based on dist
|
|
467
|
+
presence + actual invocation
|
|
468
|
+
- `policy-reader Tier 2 (python3 + PyYAML)` — pass/warn based on
|
|
469
|
+
python3 + import yaml succeeding
|
|
470
|
+
- `policy-reader Tier 3 (awk)` — pass when awk present; warn or
|
|
471
|
+
fail conditional on whether other tiers cover the gap (0.40.0
|
|
472
|
+
refined the verdict logic; 0.42.0 hardened the list-walker
|
|
473
|
+
predicate)
|
|
474
|
+
- `policy-reader effective floor` — summary verdict across all three
|
|
475
|
+
- `policy-reader jq (JSON accelerator)` — info-level, calls out
|
|
476
|
+
Tier 1/2 perf when jq is absent
|
|
477
|
+
|
|
478
|
+
**Consumer action:** run `rea doctor` after each upgrade. The lines
|
|
479
|
+
above accurately reflect what your shims will do at runtime — a
|
|
480
|
+
`warn` is not a hard failure but signals a posture worth knowing
|
|
481
|
+
about (e.g. flow-form policy silently no-ops). A `fail` on any tier
|
|
482
|
+
row IS a hard failure that the doctor exits non-zero on.
|
|
483
|
+
|
|
484
|
+
## Upgrade preview + audit summary (added in 0.41.0)
|
|
485
|
+
|
|
486
|
+
Two new consumer-facing commands rolled out:
|
|
487
|
+
|
|
488
|
+
### `rea upgrade --check`
|
|
489
|
+
|
|
490
|
+
Dry-run preview of what `rea upgrade` would write, file-by-file, with
|
|
491
|
+
unified diffs. JSON output via `--json`. Always exits 0 — this is a
|
|
492
|
+
preview, not a gate. Use it before any non-trivial rea upgrade to
|
|
493
|
+
sanity-check the diff:
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
rea upgrade --check # human-readable table + diffs
|
|
497
|
+
rea upgrade --check --json # machine-readable for CI
|
|
498
|
+
rea upgrade --check --no-diff # counts + paths only
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
0.42.0 added the same settings-schema validation that `rea upgrade`
|
|
502
|
+
itself runs — if the merged settings would fail schema parse (typo'd
|
|
503
|
+
hook event, malformed hook command, …), the preview surfaces the
|
|
504
|
+
`WOULD REFUSE` message rather than promising a write the real
|
|
505
|
+
upgrade would refuse. The `settings_validation` field in the JSON
|
|
506
|
+
output carries the structured outcome.
|
|
507
|
+
|
|
508
|
+
### `rea audit summary`
|
|
509
|
+
|
|
510
|
+
High-level rollup of the audit log: counts by `tool_name`, `tier`,
|
|
511
|
+
`status`, `session`, the time window covered, and a sample-verified
|
|
512
|
+
chain-integrity check. `--since <duration>` (e.g. `24h`, `7d`, `2w`)
|
|
513
|
+
narrows to a recent window:
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
rea audit summary # all time
|
|
517
|
+
rea audit summary --since 24h # last 24 hours
|
|
518
|
+
rea audit summary --since 7d --json # last week, JSON
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
0.42.0 hardened the rotated-file walk: pre-0.42.0 `--since` pruned
|
|
522
|
+
rotated audit segments by filename stamp, which is wall-clock at the
|
|
523
|
+
rotation INSTANT — not the earliest record contained. A rotated file
|
|
524
|
+
from N days ago can contain records from N+M days ago when the
|
|
525
|
+
rotation cycle was long, so pruning by filename silently dropped
|
|
526
|
+
in-window records. Post-0.42.0 the walker reads every rotated file
|
|
527
|
+
under `--since` and lets the per-record timestamp filter drop the
|
|
528
|
+
out-of-window entries. Correctness over micro-optimization;
|
|
529
|
+
`rea audit summary` performance is unchanged in practice.
|
|
530
|
+
|
|
392
531
|
## Policy knobs worth setting
|
|
393
532
|
|
|
394
533
|
For consumers with a long-running migration branch (>30 commits since
|
|
@@ -81,6 +81,21 @@ export interface AuditSummaryResult {
|
|
|
81
81
|
window_end: string | null;
|
|
82
82
|
/** Absolute paths of audit files walked. */
|
|
83
83
|
files_scanned: string[];
|
|
84
|
+
/**
|
|
85
|
+
* 0.42.0 codex round 4 P2 + round 6 P2 (2026-05-16) — reserved for
|
|
86
|
+
* future use; ALWAYS EMPTY in 0.42.0. The original intent (round 4)
|
|
87
|
+
* was to soft-skip rotated segments that the operator could not
|
|
88
|
+
* read (e.g. EACCES/EPERM after a backup restore). Round 6 showed
|
|
89
|
+
* the soft-skip was unsound: without per-segment time-range
|
|
90
|
+
* metadata we cannot prove a skipped file is out-of-scope for the
|
|
91
|
+
* `--since` window, so a silent skip risks an undercount + a
|
|
92
|
+
* misleading `chain_integrity: ok`. The current implementation
|
|
93
|
+
* therefore throws on any non-ENOENT read error; this field is
|
|
94
|
+
* kept in the public schema so a future release that ships
|
|
95
|
+
* per-segment time-range metadata can populate it without breaking
|
|
96
|
+
* JSON consumers.
|
|
97
|
+
*/
|
|
98
|
+
unreadable_segments: string[];
|
|
84
99
|
total_events: number;
|
|
85
100
|
by_tool_name: Record<string, number>;
|
|
86
101
|
by_tier: Record<string, number>;
|
|
@@ -118,48 +118,49 @@ export function parseDurationSeconds(raw) {
|
|
|
118
118
|
* PLUS the current `audit.jsonl`. Round-1 P2: the prior shape
|
|
119
119
|
* dropped rotated history silently while the header still
|
|
120
120
|
* advertised "all time", undercounting long-lived repos.
|
|
121
|
-
* - `windowStart` set: walk
|
|
122
|
-
* timestamp
|
|
123
|
-
*
|
|
124
|
-
*
|
|
121
|
+
* - `windowStart` set: walk EVERY rotated file. The per-record
|
|
122
|
+
* timestamp filter inside `computeAuditSummary` then drops
|
|
123
|
+
* out-of-window records during the scan. 0.41.0 round-3 P2 +
|
|
124
|
+
* 0.42.0 charter item 3: rotated filenames are NOT authoritative
|
|
125
|
+
* for "earliest contained record" — they are wall-clock at the
|
|
126
|
+
* ROTATION INSTANT, which can be days after the file's earliest
|
|
127
|
+
* contents when the rotation size cap is reached late. Pruning
|
|
128
|
+
* by filename therefore drops in-window records from
|
|
129
|
+
* conservatively-rotated logs (a rotated file from 7 days ago can
|
|
130
|
+
* still contain records from 14 days ago because the previous
|
|
131
|
+
* rotation event was 14 days ago). The cost of walking every
|
|
132
|
+
* rotated segment under `--since` is bounded by the rotation cap
|
|
133
|
+
* × number of segments — comfortably manageable in the
|
|
134
|
+
* summary-rollup setting where we already read every byte for
|
|
135
|
+
* the in-window scan; the win is correctness.
|
|
125
136
|
*
|
|
126
|
-
* Sort order is timestamp-ascending; the current
|
|
127
|
-
* always appended last (it is the newest segment
|
|
137
|
+
* Sort order is timestamp-ascending (by FILENAME stamp); the current
|
|
138
|
+
* `audit.jsonl` is always appended last (it is the newest segment
|
|
139
|
+
* of the chain).
|
|
128
140
|
*/
|
|
129
141
|
async function resolveSummaryFileWalk(baseDir, windowStart) {
|
|
130
142
|
const reaDir = path.join(baseDir, REA_DIR);
|
|
131
143
|
const currentAudit = path.join(reaDir, AUDIT_FILE);
|
|
132
144
|
const files = [];
|
|
133
145
|
const rotated = await listRotatedAuditFiles(reaDir);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
153
|
-
};
|
|
154
|
-
const cutoffIdx = rotated.findIndex((n) => {
|
|
155
|
-
const d = stampToDate(n);
|
|
156
|
-
return d !== null && d >= windowStart;
|
|
157
|
-
});
|
|
158
|
-
const startIdx = cutoffIdx === -1 ? Math.max(0, rotated.length - 1) : Math.max(0, cutoffIdx - 1);
|
|
159
|
-
for (const name of rotated.slice(startIdx)) {
|
|
160
|
-
files.push(path.join(reaDir, name));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
146
|
+
// Both `windowStart === null` and `windowStart` set: walk every
|
|
147
|
+
// rotated segment. Pre-0.42.0 the `windowStart` branch attempted to
|
|
148
|
+
// prune rotated files by their filename stamp ("rotated at >=
|
|
149
|
+
// windowStart minus one buffer file"). That was wrong: the filename
|
|
150
|
+
// stamp marks the ROTATION event, not the earliest record contained
|
|
151
|
+
// in the file. A rotated file's records can pre-date its filename
|
|
152
|
+
// stamp by days when the previous rotation cycle was long. Walking
|
|
153
|
+
// every rotated file and letting the per-record `timestamp >=
|
|
154
|
+
// windowStart` filter inside `computeAuditSummary` decide is the
|
|
155
|
+
// only correct approach: we never falsely drop an in-window record
|
|
156
|
+
// because of where it happens to live on disk. Reference:
|
|
157
|
+
// 0.41.0 round-3 P2 + 0.42.0 charter item 3.
|
|
158
|
+
//
|
|
159
|
+
// `windowStart === null` (no --since) already walks every rotated
|
|
160
|
+
// segment — same code path.
|
|
161
|
+
void windowStart; // intentionally unused — full-walk is correct in both modes
|
|
162
|
+
for (const name of rotated)
|
|
163
|
+
files.push(path.join(reaDir, name));
|
|
163
164
|
try {
|
|
164
165
|
const stat = await fs.stat(currentAudit);
|
|
165
166
|
if (stat.isFile())
|
|
@@ -261,16 +262,52 @@ export async function computeAuditSummary(options = {}) {
|
|
|
261
262
|
let latest = null;
|
|
262
263
|
// We only feed in-window records to the chain-sample check.
|
|
263
264
|
const inWindowRecords = [];
|
|
265
|
+
// 0.42.0 codex round 4 P2 + round 6 P2 (2026-05-16): reserved for
|
|
266
|
+
// future per-segment time-range metadata that would let us prove a
|
|
267
|
+
// skipped file is out of scope. Always empty under 0.42.0 — see
|
|
268
|
+
// the AuditSummaryResult.unreadable_segments docstring.
|
|
269
|
+
const unreadableSegments = [];
|
|
270
|
+
// We rebuild the actually-read file list as we go so the summary
|
|
271
|
+
// never claims to have scanned a file that was silently skipped.
|
|
272
|
+
// (Currently identical to `files` minus ENOENT entries since every
|
|
273
|
+
// other read error throws — kept as a separate accumulator so the
|
|
274
|
+
// shape stays correct when the future `unreadable_segments`
|
|
275
|
+
// soft-skip path lands.)
|
|
276
|
+
const actuallyScanned = [];
|
|
264
277
|
for (const filePath of files) {
|
|
265
278
|
let raw;
|
|
266
279
|
try {
|
|
267
280
|
raw = await fs.readFile(filePath, 'utf8');
|
|
268
281
|
}
|
|
269
282
|
catch (e) {
|
|
270
|
-
|
|
283
|
+
const errno = e.code;
|
|
284
|
+
if (errno === 'ENOENT')
|
|
271
285
|
continue;
|
|
272
|
-
|
|
286
|
+
// 0.42.0 codex round 4 P2 + round 5 P2 + round 6 P2 (2026-05-16):
|
|
287
|
+
// earlier rounds attempted to soft-skip unreadable rotations to
|
|
288
|
+
// accommodate backup-restore artifacts. Round 6 caught that the
|
|
289
|
+
// soft-skip is unsound: `resolveSummaryFileWalk` now enqueues
|
|
290
|
+
// every rotated segment under `--since` (filename-stamp pruning
|
|
291
|
+
// was correctly removed because the stamp marks the rotation
|
|
292
|
+
// event, not the earliest record contained), so we CANNOT prove
|
|
293
|
+
// an unreadable file is out of scope without reading it. A
|
|
294
|
+
// silent skip would mean `rea audit summary` could exit 0 with
|
|
295
|
+
// an undercount AND `chain_integrity: ok` while real in-window
|
|
296
|
+
// records went uncounted.
|
|
297
|
+
//
|
|
298
|
+
// Throwing with a precise, actionable error is the right call:
|
|
299
|
+
// the operator can chmod the file, move it out of .rea/, or
|
|
300
|
+
// delete it. `unreadable_segments` in the result is reserved
|
|
301
|
+
// for the never-reached future case where we can prove a file
|
|
302
|
+
// is genuinely out of scope (we'd need rotation start/end
|
|
303
|
+
// metadata for that — out of scope here).
|
|
304
|
+
throw new Error(`rea audit summary: cannot read ${filePath} (${errno ?? 'unknown errno'}). ` +
|
|
305
|
+
`An unreadable audit segment may contain in-window records, so the summary ` +
|
|
306
|
+
`would be silently incomplete. Fix permissions (e.g. \`chmod u+r ${filePath}\`), ` +
|
|
307
|
+
`or move the file out of \`.rea/\` if you no longer need it. The current ` +
|
|
308
|
+
`audit.jsonl is always required.`);
|
|
273
309
|
}
|
|
310
|
+
actuallyScanned.push(filePath);
|
|
274
311
|
for (const line of raw.split('\n')) {
|
|
275
312
|
if (line.length === 0)
|
|
276
313
|
continue;
|
|
@@ -322,7 +359,12 @@ export async function computeAuditSummary(options = {}) {
|
|
|
322
359
|
window_seconds: windowSeconds,
|
|
323
360
|
window_start: windowStart !== null ? windowStart.toISOString() : null,
|
|
324
361
|
window_end: windowEnd !== null ? windowEnd.toISOString() : null,
|
|
325
|
-
|
|
362
|
+
// 0.42.0 codex round 4 P2: report only the files actually read.
|
|
363
|
+
// Unreadable rotations are reported separately under
|
|
364
|
+
// `unreadable_segments` so consumers can tell the difference
|
|
365
|
+
// between "scanned and empty" and "skipped because permissions".
|
|
366
|
+
files_scanned: actuallyScanned,
|
|
367
|
+
unreadable_segments: unreadableSegments,
|
|
326
368
|
total_events: totalEvents,
|
|
327
369
|
by_tool_name: byToolName,
|
|
328
370
|
by_tier: byTier,
|
|
@@ -387,6 +429,13 @@ export function renderAuditSummary(result) {
|
|
|
387
429
|
lines.push('(no audit files found — has `rea serve` ever run?)');
|
|
388
430
|
lines.push('');
|
|
389
431
|
}
|
|
432
|
+
// 0.42.0 codex round 4 P2: even in the zero-events early-return,
|
|
433
|
+
// surface unreadable segments so the operator sees the gap.
|
|
434
|
+
if (result.unreadable_segments.length > 0) {
|
|
435
|
+
lines.push(`unreadable rotated segments: ${String(result.unreadable_segments.length)} ` +
|
|
436
|
+
`(see stderr for paths; fix permissions and re-run to include them)`);
|
|
437
|
+
lines.push('');
|
|
438
|
+
}
|
|
390
439
|
return lines.join('\n');
|
|
391
440
|
}
|
|
392
441
|
const total = result.total_events;
|
|
@@ -409,6 +458,13 @@ export function renderAuditSummary(result) {
|
|
|
409
458
|
: 'unsampled (no records in window)';
|
|
410
459
|
lines.push(`chain integrity: ${chainLabel}`);
|
|
411
460
|
lines.push(`files scanned: ${String(result.files_scanned.length)}`);
|
|
461
|
+
// 0.42.0 codex round 4 P2 (2026-05-16): surface unreadable rotated
|
|
462
|
+
// segments so an operator scanning the rendered summary doesn't
|
|
463
|
+
// miss a skipped archive that the JSON consumers can see.
|
|
464
|
+
if (result.unreadable_segments.length > 0) {
|
|
465
|
+
lines.push(`unreadable rotated segments: ${String(result.unreadable_segments.length)} ` +
|
|
466
|
+
`(see stderr for paths; fix permissions and re-run to include them)`);
|
|
467
|
+
}
|
|
412
468
|
lines.push('');
|
|
413
469
|
return lines.join('\n');
|
|
414
470
|
}
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -138,6 +138,26 @@ export interface PolicyReaderProbes {
|
|
|
138
138
|
* ignore the argument; the default production probe uses it.
|
|
139
139
|
*/
|
|
140
140
|
python3PyYamlReachable?: (baseDir: string) => boolean;
|
|
141
|
+
/**
|
|
142
|
+
* 0.42.0 codex round 5 P2 (2026-05-16) — execution probe for the
|
|
143
|
+
* python3 list-walker branch in `policy_reader_get_list`. That
|
|
144
|
+
* branch needs to spawn `python3 -c "..."` with `import json` from
|
|
145
|
+
* stdlib; PyYAML is irrelevant. The check is execution-based (not
|
|
146
|
+
* PATH-only) because a `python3` symlink can resolve on PATH but
|
|
147
|
+
* fail to start in the current sandbox (dangling pyenv/asdf stub,
|
|
148
|
+
* permission-denied interpreter, missing dynamic libs). A PATH-only
|
|
149
|
+
* check would let the doctor declare `warn` on a box where the
|
|
150
|
+
* shim will actually fall through to Tier 3 — masking a real
|
|
151
|
+
* enforcement gap for list-valued policy keys.
|
|
152
|
+
*
|
|
153
|
+
* The probe runs `python3 -c "import json; print('ok')"` with the
|
|
154
|
+
* same env scrub as the PyYAML probe (PYTHONPATH/PYTHONHOME/
|
|
155
|
+
* PYTHONSTARTUP unset, PYTHONSAFEPATH=1, sys.path scrubbed) so a
|
|
156
|
+
* malicious repo cannot plant a `./json.py` that shadows stdlib
|
|
157
|
+
* and falsely report `true` while the real loader (which scrubs)
|
|
158
|
+
* fails.
|
|
159
|
+
*/
|
|
160
|
+
python3ListWalkerReachable?: (baseDir: string) => boolean;
|
|
141
161
|
awkOnPath?: () => string | null;
|
|
142
162
|
jqOnPath?: () => string | null;
|
|
143
163
|
}
|
|
@@ -183,12 +203,12 @@ export declare function checkPolicyReaderTier2(baseDir: string, probes?: PolicyR
|
|
|
183
203
|
* Practically always present (POSIX requirement).
|
|
184
204
|
*
|
|
185
205
|
* 0.40.0 charter item 2 — conditional verdict, refined by codex
|
|
186
|
-
* round 1 P2:
|
|
206
|
+
* round 1 P2 (0.40.0) and round 2 P2 (0.42.0):
|
|
187
207
|
* - awk present → `pass`
|
|
188
208
|
* - awk absent AND Tier 2 reachable → `warn`
|
|
189
209
|
* (Tier 2 implies python3, which is a list-walker)
|
|
190
210
|
* - awk absent AND Tier 1 reachable AND a list walker
|
|
191
|
-
* (jq OR
|
|
211
|
+
* (jq OR full Tier-2 reachable) is usable → `warn`
|
|
192
212
|
* - awk absent AND Tier 1 reachable BUT no list walker → `fail`
|
|
193
213
|
* (codex round 1 P2 — list-valued policy reads silently
|
|
194
214
|
* fail-closed even though scalar reads work, so the
|
|
@@ -207,9 +227,29 @@ export declare function checkPolicyReaderTier2(baseDir: string, probes?: PolicyR
|
|
|
207
227
|
* functional box that has python3 + jq + the rea CLI all wired but
|
|
208
228
|
* happens to lack awk.
|
|
209
229
|
*
|
|
230
|
+
* List-iteration semantic (clarifying note for codex round 2 P2,
|
|
231
|
+
* 2026-05-16): `policy_reader_get_list` in
|
|
232
|
+
* `hooks/_lib/policy-reader.sh` walks the cached subtree JSON via
|
|
233
|
+
* `jq` OR `python3` (stdlib-only — `json` module, no PyYAML import).
|
|
234
|
+
* PyYAML is only needed for Tier 2 itself (YAML PARSING into JSON),
|
|
235
|
+
* NOT for iterating the already-parsed JSON arrays at list-read time.
|
|
236
|
+
*
|
|
237
|
+
* Codex round 5 P2 (2026-05-16): the "list walker" predicate uses
|
|
238
|
+
* `python3ListWalkerReachable` — an EXECUTION probe that actually
|
|
239
|
+
* spawns `python3 -c "import json"` — instead of `python3OnPath`. A
|
|
240
|
+
* PATH-only check passes for broken pyenv/asdf shims, dangling
|
|
241
|
+
* symlinks, and sandboxed environments where the interpreter cannot
|
|
242
|
+
* start; in those cases the shim's list-walker branch would actually
|
|
243
|
+
* fail and `blocked_paths`/`protected_writes` enforcement would
|
|
244
|
+
* silently break while doctor reported `warn`. The execution probe
|
|
245
|
+
* mirrors `defaultPython3PyYamlReachable` exactly but swaps the
|
|
246
|
+
* `import yaml` for `import json` so it's not gated on PyYAML
|
|
247
|
+
* availability (which is irrelevant to list iteration).
|
|
248
|
+
*
|
|
210
249
|
* Takes `baseDir` so it can evaluate Tier 1's two-stage check (dist
|
|
211
|
-
* present + CLI invokable)
|
|
212
|
-
* threaded through
|
|
250
|
+
* present + CLI invokable), Tier 2's reachability, and the
|
|
251
|
+
* list-walker execution probe. All probes are threaded through
|
|
252
|
+
* identically.
|
|
213
253
|
*/
|
|
214
254
|
export declare function checkPolicyReaderTier3(baseDir: string, probes?: PolicyReaderProbes): CheckResult;
|
|
215
255
|
/**
|