@bookedsolid/rea 0.39.0 → 0.40.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/dist/cli/delegation-advisory.d.ts +40 -0
- package/dist/cli/delegation-advisory.js +86 -8
- package/dist/cli/doctor.d.ts +46 -7
- package/dist/cli/doctor.js +101 -11
- package/package.json +1 -1
|
@@ -89,7 +89,47 @@ export interface HookDelegationAdvisoryOptions {
|
|
|
89
89
|
* Production omits.
|
|
90
90
|
*/
|
|
91
91
|
stdinOverride?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Test seam — override the sleep used by
|
|
94
|
+
* `sessionHasRealDelegation`'s poll-and-backoff loop. Production
|
|
95
|
+
* omits and uses real `setTimeout`-backed sleeps; tests pass a
|
|
96
|
+
* controllable fake so the race-coverage tests don't wall-clock on
|
|
97
|
+
* the real 500ms budget.
|
|
98
|
+
*
|
|
99
|
+
* 0.40.0 charter item 1.
|
|
100
|
+
*/
|
|
101
|
+
sleepOverride?: (ms: number) => Promise<void>;
|
|
92
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Backoff schedule (in milliseconds) for `sessionHasRealDelegation`'s
|
|
105
|
+
* poll-and-backoff loop.
|
|
106
|
+
*
|
|
107
|
+
* 0.40.0 charter item 1 — closes the `& disown` race between
|
|
108
|
+
* `delegation-capture.sh` (which fire-and-forgets `rea hook
|
|
109
|
+
* delegation-signal --detach &` for sub-50ms PreToolUse latency) and the
|
|
110
|
+
* `delegation-advisory.sh` PostToolUse path (which reads the audit log
|
|
111
|
+
* to decide whether the session has delegated). A `git commit` landing
|
|
112
|
+
* within the narrow window between an Agent dispatch and the audit
|
|
113
|
+
* append-on-disk would read the stale chain, see no delegation, fire
|
|
114
|
+
* the nudge spuriously, AND write the `.fired` sentinel — silencing
|
|
115
|
+
* every future advisory in the session even though delegation DID
|
|
116
|
+
* happen.
|
|
117
|
+
*
|
|
118
|
+
* The schedule is delays BETWEEN re-reads (NOT cumulative): 50ms,
|
|
119
|
+
* 150ms, 300ms. Total worst-case 500ms — acceptable hot-path budget
|
|
120
|
+
* for a PostToolUse hook running on `Bash|Edit|Write|MultiEdit|
|
|
121
|
+
* NotebookEdit`. The first read is immediate (no upfront delay), so a
|
|
122
|
+
* session that DID delegate before threshold-crossing pays zero extra
|
|
123
|
+
* latency. Only the rare "we crossed threshold while a recent
|
|
124
|
+
* delegation signal hasn't yet hit disk" case pays the full budget,
|
|
125
|
+
* and only ONCE per session (the `.fired` sentinel suppresses future
|
|
126
|
+
* scans).
|
|
127
|
+
*
|
|
128
|
+
* Exported for the race-coverage test in
|
|
129
|
+
* `delegation-advisory.test.ts` so it can assert on the schedule
|
|
130
|
+
* without duplicating the constant.
|
|
131
|
+
*/
|
|
132
|
+
export declare const DELEGATION_POLL_BACKOFF_MS: readonly number[];
|
|
93
133
|
/**
|
|
94
134
|
* Derive a filesystem-safe, **collision-free** per-session state-key
|
|
95
135
|
* basename from an untrusted session id.
|
|
@@ -70,6 +70,36 @@ import { loadDelegationRecords, listRotatedAuditFiles, } from './audit-specialis
|
|
|
70
70
|
import { discoverRoster, countsAsRealDelegation, DEFAULT_EXEMPT_SUBAGENTS, } from './roster.js';
|
|
71
71
|
import { REA_DIR } from './utils.js';
|
|
72
72
|
const DEFAULT_THRESHOLD = 25;
|
|
73
|
+
/**
|
|
74
|
+
* Backoff schedule (in milliseconds) for `sessionHasRealDelegation`'s
|
|
75
|
+
* poll-and-backoff loop.
|
|
76
|
+
*
|
|
77
|
+
* 0.40.0 charter item 1 — closes the `& disown` race between
|
|
78
|
+
* `delegation-capture.sh` (which fire-and-forgets `rea hook
|
|
79
|
+
* delegation-signal --detach &` for sub-50ms PreToolUse latency) and the
|
|
80
|
+
* `delegation-advisory.sh` PostToolUse path (which reads the audit log
|
|
81
|
+
* to decide whether the session has delegated). A `git commit` landing
|
|
82
|
+
* within the narrow window between an Agent dispatch and the audit
|
|
83
|
+
* append-on-disk would read the stale chain, see no delegation, fire
|
|
84
|
+
* the nudge spuriously, AND write the `.fired` sentinel — silencing
|
|
85
|
+
* every future advisory in the session even though delegation DID
|
|
86
|
+
* happen.
|
|
87
|
+
*
|
|
88
|
+
* The schedule is delays BETWEEN re-reads (NOT cumulative): 50ms,
|
|
89
|
+
* 150ms, 300ms. Total worst-case 500ms — acceptable hot-path budget
|
|
90
|
+
* for a PostToolUse hook running on `Bash|Edit|Write|MultiEdit|
|
|
91
|
+
* NotebookEdit`. The first read is immediate (no upfront delay), so a
|
|
92
|
+
* session that DID delegate before threshold-crossing pays zero extra
|
|
93
|
+
* latency. Only the rare "we crossed threshold while a recent
|
|
94
|
+
* delegation signal hasn't yet hit disk" case pays the full budget,
|
|
95
|
+
* and only ONCE per session (the `.fired` sentinel suppresses future
|
|
96
|
+
* scans).
|
|
97
|
+
*
|
|
98
|
+
* Exported for the race-coverage test in
|
|
99
|
+
* `delegation-advisory.test.ts` so it can assert on the schedule
|
|
100
|
+
* without duplicating the constant.
|
|
101
|
+
*/
|
|
102
|
+
export const DELEGATION_POLL_BACKOFF_MS = [50, 150, 300];
|
|
73
103
|
/**
|
|
74
104
|
* Maximum length of the human-readable prefix in a state key. The full
|
|
75
105
|
* key is `<prefix>-<16-hex-hash>`, so a 64-char cap keeps basenames well
|
|
@@ -250,7 +280,22 @@ export function advisoryMessage(count, threshold) {
|
|
|
250
280
|
* `since` anchor is `undefined` and behavior is the pre-0.31.0
|
|
251
281
|
* single-file walk.
|
|
252
282
|
*/
|
|
253
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Real-clock sleep used by the production poll-and-backoff loop.
|
|
285
|
+
* Factored out so tests can swap it for a fake controllable scheduler
|
|
286
|
+
* via `HookDelegationAdvisoryOptions.sleepOverride`.
|
|
287
|
+
*/
|
|
288
|
+
function realSleep(ms) {
|
|
289
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Single audit-chain scan: returns `'delegated'` when a real
|
|
293
|
+
* delegation signal is found, `'not-delegated'` when scanning succeeds
|
|
294
|
+
* but no real signal is in the chain, and `'unreadable'` when audit
|
|
295
|
+
* loading throws (the chain is missing / unreadable). Split out from
|
|
296
|
+
* the polling loop so each retry runs identical scan logic.
|
|
297
|
+
*/
|
|
298
|
+
async function scanForRealDelegationOnce(reaRoot, sessionId, exemptSubagents) {
|
|
254
299
|
let records;
|
|
255
300
|
try {
|
|
256
301
|
// Resolve the rotated-file set the same way `rea audit specialists`
|
|
@@ -263,13 +308,10 @@ async function sessionHasRealDelegation(reaRoot, sessionId, exemptSubagents) {
|
|
|
263
308
|
records = loaded.records;
|
|
264
309
|
}
|
|
265
310
|
catch {
|
|
266
|
-
|
|
267
|
-
// we DON'T fire (fail toward silence, not toward a false-positive
|
|
268
|
-
// nudge). Returning `true` here suppresses the advisory.
|
|
269
|
-
return true;
|
|
311
|
+
return 'unreadable';
|
|
270
312
|
}
|
|
271
313
|
if (records.length === 0)
|
|
272
|
-
return
|
|
314
|
+
return 'not-delegated';
|
|
273
315
|
const roster = discoverRoster(reaRoot);
|
|
274
316
|
for (const rec of records) {
|
|
275
317
|
if (countsAsRealDelegation({
|
|
@@ -278,9 +320,45 @@ async function sessionHasRealDelegation(reaRoot, sessionId, exemptSubagents) {
|
|
|
278
320
|
roster,
|
|
279
321
|
exempt: exemptSubagents,
|
|
280
322
|
})) {
|
|
281
|
-
return
|
|
323
|
+
return 'delegated';
|
|
282
324
|
}
|
|
283
325
|
}
|
|
326
|
+
return 'not-delegated';
|
|
327
|
+
}
|
|
328
|
+
async function sessionHasRealDelegation(reaRoot, sessionId, exemptSubagents, sleep = realSleep) {
|
|
329
|
+
// 0.40.0 charter item 1 — poll-and-backoff before declaring
|
|
330
|
+
// "no delegation in this session".
|
|
331
|
+
//
|
|
332
|
+
// The producer (`delegation-capture.sh`) calls `rea hook
|
|
333
|
+
// delegation-signal --detach &` to fire-and-forget the audit append.
|
|
334
|
+
// For sub-50ms PreToolUse latency this is the right call, but it
|
|
335
|
+
// opens a narrow race: a write-class call (Bash/Edit/Write/…)
|
|
336
|
+
// landing in the same tick as an Agent/Skill dispatch can run this
|
|
337
|
+
// predicate BEFORE the audit append commits to disk. Pre-fix, the
|
|
338
|
+
// function then returned `false`, the caller fired the advisory,
|
|
339
|
+
// wrote the `.fired` sentinel, and silenced every future nudge in
|
|
340
|
+
// the session — even though delegation DID happen.
|
|
341
|
+
//
|
|
342
|
+
// Each retry runs a full audit scan. The first scan is immediate
|
|
343
|
+
// (no upfront delay); subsequent scans wait per
|
|
344
|
+
// `DELEGATION_POLL_BACKOFF_MS`. Worst-case total: 50+150+300 = 500ms
|
|
345
|
+
// for the four-scan path. We exit early as soon as a delegation is
|
|
346
|
+
// observed OR the chain becomes unreadable (preserving the pre-fix
|
|
347
|
+
// "audit log unreadable → suppress the advisory" posture so a
|
|
348
|
+
// missing chain never produces a false-positive nudge).
|
|
349
|
+
let outcome = await scanForRealDelegationOnce(reaRoot, sessionId, exemptSubagents);
|
|
350
|
+
if (outcome === 'delegated')
|
|
351
|
+
return true;
|
|
352
|
+
if (outcome === 'unreadable')
|
|
353
|
+
return true;
|
|
354
|
+
for (const waitMs of DELEGATION_POLL_BACKOFF_MS) {
|
|
355
|
+
await sleep(waitMs);
|
|
356
|
+
outcome = await scanForRealDelegationOnce(reaRoot, sessionId, exemptSubagents);
|
|
357
|
+
if (outcome === 'delegated')
|
|
358
|
+
return true;
|
|
359
|
+
if (outcome === 'unreadable')
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
284
362
|
return false;
|
|
285
363
|
}
|
|
286
364
|
/**
|
|
@@ -379,7 +457,7 @@ export async function computeDelegationAdvisory(options) {
|
|
|
379
457
|
// verbatim (or `'unknown'` for untagged sessions), so the `stateKey`
|
|
380
458
|
// filesystem form would never match (see the comment at the
|
|
381
459
|
// `auditSessionId` / `stateKey` split above).
|
|
382
|
-
const delegated = await sessionHasRealDelegation(reaRoot, auditSessionId, policy.exemptSubagents);
|
|
460
|
+
const delegated = await sessionHasRealDelegation(reaRoot, auditSessionId, policy.exemptSubagents, options.sleepOverride);
|
|
383
461
|
if (delegated) {
|
|
384
462
|
// Session DID delegate to a real specialist — no nudge warranted.
|
|
385
463
|
// We deliberately do NOT write the `.fired` sentinel here: if the
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -124,7 +124,20 @@ export interface PolicyReaderProbes {
|
|
|
124
124
|
cliDistExists?: (baseDir: string) => boolean;
|
|
125
125
|
cliInvokable?: (baseDir: string) => boolean;
|
|
126
126
|
python3OnPath?: () => string | null;
|
|
127
|
-
|
|
127
|
+
/**
|
|
128
|
+
* 0.40.0 charter item 3 — accepts the consumer's `baseDir` so the
|
|
129
|
+
* probe can thread it as `cwd` to the spawned `python3 -c` process.
|
|
130
|
+
* Pre-fix, the spawn happened in doctor's own cwd, which meant the
|
|
131
|
+
* sys.path scrub (which removes "", ".", CWD, realpath(CWD) to
|
|
132
|
+
* mirror policy-reader.sh's defense against a malicious repo-local
|
|
133
|
+
* `./yaml.py`) operated on the wrong directory when `rea doctor`
|
|
134
|
+
* was invoked from outside the consumer tree (e.g. `cd /tmp && rea
|
|
135
|
+
* doctor --base-dir /Users/.../consumer-repo`).
|
|
136
|
+
*
|
|
137
|
+
* Probes that don't care about cwd (test stubs, fakes) can simply
|
|
138
|
+
* ignore the argument; the default production probe uses it.
|
|
139
|
+
*/
|
|
140
|
+
python3PyYamlReachable?: (baseDir: string) => boolean;
|
|
128
141
|
awkOnPath?: () => string | null;
|
|
129
142
|
jqOnPath?: () => string | null;
|
|
130
143
|
}
|
|
@@ -164,15 +177,41 @@ export declare function checkPolicyReaderTier1(baseDir: string, probes?: PolicyR
|
|
|
164
177
|
* The warning highlights the silent no-op risk for flow-form lookups
|
|
165
178
|
* when CLI is also unreachable.
|
|
166
179
|
*/
|
|
167
|
-
export declare function checkPolicyReaderTier2(probes?: PolicyReaderProbes): CheckResult;
|
|
180
|
+
export declare function checkPolicyReaderTier2(baseDir: string, probes?: PolicyReaderProbes): CheckResult;
|
|
168
181
|
/**
|
|
169
182
|
* Tier 3 — awk block-form parser. Last-resort no-dep fallback.
|
|
170
|
-
* Practically always present (POSIX requirement)
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
183
|
+
* Practically always present (POSIX requirement).
|
|
184
|
+
*
|
|
185
|
+
* 0.40.0 charter item 2 — conditional verdict, refined by codex
|
|
186
|
+
* round 1 P2:
|
|
187
|
+
* - awk present → `pass`
|
|
188
|
+
* - awk absent AND Tier 2 reachable → `warn`
|
|
189
|
+
* (Tier 2 implies python3, which is a list-walker)
|
|
190
|
+
* - awk absent AND Tier 1 reachable AND a list walker
|
|
191
|
+
* (jq OR python3) is on PATH → `warn`
|
|
192
|
+
* - awk absent AND Tier 1 reachable BUT no list walker → `fail`
|
|
193
|
+
* (codex round 1 P2 — list-valued policy reads silently
|
|
194
|
+
* fail-closed even though scalar reads work, so the
|
|
195
|
+
* downgrade-to-warn is misleading; doctor would exit 0 on a
|
|
196
|
+
* broken install)
|
|
197
|
+
* - awk absent AND no other tier reachable → `fail`
|
|
198
|
+
*
|
|
199
|
+
* Pre-fix the absent-awk branch always returned `fail` — but when
|
|
200
|
+
* Tier 1 (rea CLI) AND/OR Tier 2 (python3+PyYAML) are reachable AND
|
|
201
|
+
* the list walker exists, the operator's effective floor is fine even
|
|
202
|
+
* without awk; Tier 3 is the LAST fallback, not a hard requirement.
|
|
203
|
+
* The summary check (`checkPolicyReaderTierSummary`) already
|
|
204
|
+
* aggregates correctly; this per-tier verdict now reflects the same
|
|
205
|
+
* severity logic so an operator who reads ONLY the Tier 3 row isn't
|
|
206
|
+
* misled into thinking the install is broken on a perfectly-
|
|
207
|
+
* functional box that has python3 + jq + the rea CLI all wired but
|
|
208
|
+
* happens to lack awk.
|
|
209
|
+
*
|
|
210
|
+
* Takes `baseDir` so it can evaluate Tier 1's two-stage check (dist
|
|
211
|
+
* present + CLI invokable) and Tier 2's reachability. Probes are
|
|
212
|
+
* threaded through identically.
|
|
174
213
|
*/
|
|
175
|
-
export declare function checkPolicyReaderTier3(probes?: PolicyReaderProbes): CheckResult;
|
|
214
|
+
export declare function checkPolicyReaderTier3(baseDir: string, probes?: PolicyReaderProbes): CheckResult;
|
|
176
215
|
/**
|
|
177
216
|
* jq — optional accelerator used by Tier 1/2's JSON subtree parsing.
|
|
178
217
|
* Per the 0.37.0 round-1 P2 fix the helper falls back to a python3
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1202,7 +1202,7 @@ function defaultCliInvokable(baseDir) {
|
|
|
1202
1202
|
return false;
|
|
1203
1203
|
}
|
|
1204
1204
|
}
|
|
1205
|
-
function defaultPython3PyYamlReachable() {
|
|
1205
|
+
function defaultPython3PyYamlReachable(baseDir) {
|
|
1206
1206
|
// The Tier 2 loader runs `python3 -c "import yaml"`. We mirror that
|
|
1207
1207
|
// probe verbatim so a `yaml`-installable-but-broken interpreter is
|
|
1208
1208
|
// not falsely reported as "reachable". Apply the SAME env scrub
|
|
@@ -1244,7 +1244,16 @@ function defaultPython3PyYamlReachable() {
|
|
|
1244
1244
|
'sys.path[:] = [p for p in sys.path if p not in ("", ".", _cwd, _cwd_real)]',
|
|
1245
1245
|
'import yaml',
|
|
1246
1246
|
].join('\n');
|
|
1247
|
+
// 0.40.0 charter item 3 — thread `baseDir` as cwd so the sys.path
|
|
1248
|
+
// scrub above strips THIS consumer's repo root (the directory the
|
|
1249
|
+
// production shim chain runs from), not doctor's own cwd. Pre-fix,
|
|
1250
|
+
// `rea doctor --base-dir <consumer>` invoked from `/tmp/foo` would
|
|
1251
|
+
// scrub against `/tmp/foo`, leaving any `<consumer>/yaml.py`
|
|
1252
|
+
// shadowing potential undetected — exactly the multi-repo workflow
|
|
1253
|
+
// every other doctor probe (cliInvokable, …) already handles by
|
|
1254
|
+
// setting cwd to baseDir.
|
|
1247
1255
|
const res = spawnSync('python3', ['-c', probeBody], {
|
|
1256
|
+
cwd: baseDir,
|
|
1248
1257
|
env: probeEnv,
|
|
1249
1258
|
timeout: 5_000,
|
|
1250
1259
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
@@ -1332,7 +1341,7 @@ export function checkPolicyReaderTier1(baseDir, probes) {
|
|
|
1332
1341
|
* The warning highlights the silent no-op risk for flow-form lookups
|
|
1333
1342
|
* when CLI is also unreachable.
|
|
1334
1343
|
*/
|
|
1335
|
-
export function checkPolicyReaderTier2(probes) {
|
|
1344
|
+
export function checkPolicyReaderTier2(baseDir, probes) {
|
|
1336
1345
|
const label = 'policy-reader Tier 2 (python3 + PyYAML)';
|
|
1337
1346
|
const p = resolveProbes(probes);
|
|
1338
1347
|
const py = p.python3OnPath();
|
|
@@ -1345,7 +1354,7 @@ export function checkPolicyReaderTier2(probes) {
|
|
|
1345
1354
|
'no-ops when the rea CLI is also unreachable. Install python3 to close this gap.',
|
|
1346
1355
|
};
|
|
1347
1356
|
}
|
|
1348
|
-
if (!p.python3PyYamlReachable()) {
|
|
1357
|
+
if (!p.python3PyYamlReachable(baseDir)) {
|
|
1349
1358
|
return {
|
|
1350
1359
|
label,
|
|
1351
1360
|
status: 'warn',
|
|
@@ -1362,12 +1371,38 @@ export function checkPolicyReaderTier2(probes) {
|
|
|
1362
1371
|
}
|
|
1363
1372
|
/**
|
|
1364
1373
|
* Tier 3 — awk block-form parser. Last-resort no-dep fallback.
|
|
1365
|
-
* Practically always present (POSIX requirement)
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1374
|
+
* Practically always present (POSIX requirement).
|
|
1375
|
+
*
|
|
1376
|
+
* 0.40.0 charter item 2 — conditional verdict, refined by codex
|
|
1377
|
+
* round 1 P2:
|
|
1378
|
+
* - awk present → `pass`
|
|
1379
|
+
* - awk absent AND Tier 2 reachable → `warn`
|
|
1380
|
+
* (Tier 2 implies python3, which is a list-walker)
|
|
1381
|
+
* - awk absent AND Tier 1 reachable AND a list walker
|
|
1382
|
+
* (jq OR python3) is on PATH → `warn`
|
|
1383
|
+
* - awk absent AND Tier 1 reachable BUT no list walker → `fail`
|
|
1384
|
+
* (codex round 1 P2 — list-valued policy reads silently
|
|
1385
|
+
* fail-closed even though scalar reads work, so the
|
|
1386
|
+
* downgrade-to-warn is misleading; doctor would exit 0 on a
|
|
1387
|
+
* broken install)
|
|
1388
|
+
* - awk absent AND no other tier reachable → `fail`
|
|
1389
|
+
*
|
|
1390
|
+
* Pre-fix the absent-awk branch always returned `fail` — but when
|
|
1391
|
+
* Tier 1 (rea CLI) AND/OR Tier 2 (python3+PyYAML) are reachable AND
|
|
1392
|
+
* the list walker exists, the operator's effective floor is fine even
|
|
1393
|
+
* without awk; Tier 3 is the LAST fallback, not a hard requirement.
|
|
1394
|
+
* The summary check (`checkPolicyReaderTierSummary`) already
|
|
1395
|
+
* aggregates correctly; this per-tier verdict now reflects the same
|
|
1396
|
+
* severity logic so an operator who reads ONLY the Tier 3 row isn't
|
|
1397
|
+
* misled into thinking the install is broken on a perfectly-
|
|
1398
|
+
* functional box that has python3 + jq + the rea CLI all wired but
|
|
1399
|
+
* happens to lack awk.
|
|
1400
|
+
*
|
|
1401
|
+
* Takes `baseDir` so it can evaluate Tier 1's two-stage check (dist
|
|
1402
|
+
* present + CLI invokable) and Tier 2's reachability. Probes are
|
|
1403
|
+
* threaded through identically.
|
|
1369
1404
|
*/
|
|
1370
|
-
export function checkPolicyReaderTier3(probes) {
|
|
1405
|
+
export function checkPolicyReaderTier3(baseDir, probes) {
|
|
1371
1406
|
const label = 'policy-reader Tier 3 (awk)';
|
|
1372
1407
|
const p = resolveProbes(probes);
|
|
1373
1408
|
const awk = p.awkOnPath();
|
|
@@ -1378,6 +1413,61 @@ export function checkPolicyReaderTier3(probes) {
|
|
|
1378
1413
|
detail: `awk at ${awk} — block-form fallback available`,
|
|
1379
1414
|
};
|
|
1380
1415
|
}
|
|
1416
|
+
// 0.40.0 — awk is absent. Decide whether this is `warn` (other tiers
|
|
1417
|
+
// cover) or `fail` (catastrophic — no working policy lookup tier).
|
|
1418
|
+
// Mirror Tier 1's two-stage check (dist + invokable) and Tier 2's
|
|
1419
|
+
// python3 + PyYAML pair so the verdict here matches what the shim
|
|
1420
|
+
// ladder would actually do at runtime.
|
|
1421
|
+
const tier1 = p.cliDistExists(baseDir) && p.cliInvokable(baseDir);
|
|
1422
|
+
const tier2 = p.python3OnPath() !== null && p.python3PyYamlReachable(baseDir);
|
|
1423
|
+
// Codex round 1 P2 (2026-05-16): the downgrade-to-warn branch needs
|
|
1424
|
+
// a list walker too. `policy_reader_get_list` (the helper that reads
|
|
1425
|
+
// list-valued keys like `blocked_paths`) iterates the parsed JSON
|
|
1426
|
+
// array via jq OR python3, falling back to Tier 3 awk for inline
|
|
1427
|
+
// arrays. With awk gone AND no jq AND no python3, list-valued
|
|
1428
|
+
// policy reads silently fail-closed even when Tier 1 is reachable —
|
|
1429
|
+
// `blocked-paths-bash-gate.sh` etc. would see an EMPTY blocked-paths
|
|
1430
|
+
// set and stop enforcing entries the operator declared. Pre-fix
|
|
1431
|
+
// this concrete shape (cliInvokable + no python3 + no jq + no awk)
|
|
1432
|
+
// returned `warn` and the doctor exited 0 on a broken install.
|
|
1433
|
+
// Post-fix the downgrade requires a list walker; otherwise we stay
|
|
1434
|
+
// on `fail`. Tier 2 implies python3 on PATH (the interpreter that
|
|
1435
|
+
// ran PyYAML), so Tier 2 always brings list-walker support — no
|
|
1436
|
+
// additional check needed for the Tier-2 branch.
|
|
1437
|
+
const py = p.python3OnPath();
|
|
1438
|
+
const listWalker = p.jqOnPath() !== null || py !== null;
|
|
1439
|
+
if (tier2 || (tier1 && listWalker)) {
|
|
1440
|
+
const reachable = [];
|
|
1441
|
+
if (tier1)
|
|
1442
|
+
reachable.push('Tier 1 (rea CLI)');
|
|
1443
|
+
if (tier2)
|
|
1444
|
+
reachable.push('Tier 2 (python3+PyYAML)');
|
|
1445
|
+
return {
|
|
1446
|
+
label,
|
|
1447
|
+
status: 'warn',
|
|
1448
|
+
detail: `awk not on PATH — Tier 3 (block-form fallback) unreachable. ${reachable.join(' and ')} ` +
|
|
1449
|
+
'still cover the shim ladder, so policy lookups continue to work; this is a ' +
|
|
1450
|
+
'soft degradation, not a hard failure. Install awk (`mawk`, `gawk`, or `nawk`) ' +
|
|
1451
|
+
'to restore the last-resort fallback.',
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
// Codex round 1 P2: separate "no list walker" diagnosis from the
|
|
1455
|
+
// catastrophic "no tier at all" case. Tier 1 reachable but no jq
|
|
1456
|
+
// AND no python3 AND no awk means list-valued policy reads
|
|
1457
|
+
// fail-closed silently — distinct from the truly-empty
|
|
1458
|
+
// no-CLI-no-python-no-awk shape, and worth a precise remediation.
|
|
1459
|
+
if (tier1) {
|
|
1460
|
+
return {
|
|
1461
|
+
label,
|
|
1462
|
+
status: 'fail',
|
|
1463
|
+
detail: 'awk not on PATH AND neither jq nor python3 is on PATH — Tier 1 (rea CLI) parses ' +
|
|
1464
|
+
'flow-form scalars, but `policy_reader_get_list` cannot iterate list-valued keys ' +
|
|
1465
|
+
'(e.g. `blocked_paths: [.env, ...]`) without jq, python3, OR awk to walk the ' +
|
|
1466
|
+
'resulting JSON arrays. Affected hooks (`blocked-paths-bash-gate.sh`, ' +
|
|
1467
|
+
'`blocked-paths-enforcer.sh`, …) see an EMPTY list and silently stop enforcing. ' +
|
|
1468
|
+
'Install awk OR jq OR python3 to restore list-iteration.',
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1381
1471
|
return {
|
|
1382
1472
|
label,
|
|
1383
1473
|
status: 'fail',
|
|
@@ -1449,7 +1539,7 @@ export function checkPolicyReaderTierSummary(baseDir, probes) {
|
|
|
1449
1539
|
// ladder would actually do at runtime.
|
|
1450
1540
|
const tier1 = p.cliDistExists(baseDir) && p.cliInvokable(baseDir);
|
|
1451
1541
|
const py = p.python3OnPath();
|
|
1452
|
-
const tier2 = py !== null && p.python3PyYamlReachable();
|
|
1542
|
+
const tier2 = py !== null && p.python3PyYamlReachable(baseDir);
|
|
1453
1543
|
const tier3 = p.awkOnPath() !== null;
|
|
1454
1544
|
const jq = p.jqOnPath();
|
|
1455
1545
|
// List iteration after Tier 1/2 needs jq OR python3 to walk the
|
|
@@ -2059,8 +2149,8 @@ export function collectChecks(baseDir, codexProbeState, prePushState, options =
|
|
|
2059
2149
|
...(policyParsesResult.status === 'pass'
|
|
2060
2150
|
? [
|
|
2061
2151
|
checkPolicyReaderTier1(baseDir),
|
|
2062
|
-
checkPolicyReaderTier2(),
|
|
2063
|
-
checkPolicyReaderTier3(),
|
|
2152
|
+
checkPolicyReaderTier2(baseDir),
|
|
2153
|
+
checkPolicyReaderTier3(baseDir),
|
|
2064
2154
|
checkPolicyReaderJq(),
|
|
2065
2155
|
checkPolicyReaderTierSummary(baseDir),
|
|
2066
2156
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|