pi-crew 0.11.53 → 0.11.54

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/CHANGELOG.md CHANGED
@@ -1,5 +1,120 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.54] — deep-review verified fix set, waves A–G + closing hardenings (2026-10-31)
4
+
5
+ **Fixed (product).** The v0.11.53 deep review's 15 findings were cold-verified by three independent verifiers
6
+ (13 CONFIRMED / 2 PARTIAL / 0 refuted); this batch implements the seven fix waves against `8e70ab6f`. Four
7
+ findings are deliberately deferred with documented backstops (closing note). Wave-by-wave:
8
+
9
+ - **wA foreground-control lock family (F1/F2)** — the producer "lock" was `mkdirSync(lockDir, { recursive: true })`:
10
+ recursive mkdir NEVER throws EEXIST, so it provided zero mutual exclusion and its contention branches were dead
11
+ code. Replaced with module-level `withForegroundControlLock` (foreground-control.ts) replicating the proven
12
+ event-log dir-lock protocol: the non-recursive mkdir IS the mutex; 5s timeout; 10s mtime-stale reclaim; dead-pid
13
+ keep-waiting fast path (never steal on a pid-lookup race); `"wx"` pid file; PID-guarded release. F2: the
14
+ interrupt-guard consumer ack did an unlocked read→mutate→whole-file write that clobbered producer appends — it
15
+ now acquires the SAME module-level lock, re-reads inside it, and acks by request `id` (tail fallback for legacy
16
+ id-less control files). Retry contends ONLY on EEXIST; any other IO error fails fast
17
+ (`foreground-control.lock-acquire-failed`) instead of spinning to the timeout. Behavior delta (per spec, event-log
18
+ pattern semantics): a dead owner no longer releases the lock immediately — only an aged mtime (>10s) reclaims.
19
+ Dedup with `withEventLogAppendLockSync` into a shared helper is a noted follow-up (parallel-wave file ownership).
20
+ - **wB event-log buffered-flush timer family (F3/F4/F5)** — F3: a flush's `finally` deleted the timer entry without
21
+ re-arming, so events appended during an in-flight flush sat un-timered until the next append; it now re-arms when
22
+ leftovers exist (identity guard so a fresh handle armed by another append is never orphaned; delay = the queue's
23
+ last-used `bufferMs` — `bufferedTimers` now stores `{ handle, bufferMs }`). F4: the in-flight tracker stored the
24
+ `.finally()` WRAPPER but compare-and-deleted the ORIGINAL promise — entries were never deleted (leak; the drain
25
+ helpers' self-delete no-op path was dead code); it now compares the stored wrapper. F5: the armed handle is
26
+ cleared at grab time (terminal-event/drain-triggered flushes no longer leave a stale timer firing a redundant
27
+ no-op flush); `flushBufferedQueuesSync` clears pending handles too. **Out-of-spec, flagged and kept**:
28
+ `flushEventLogBuffer()` now iterates the UNION of queued + in-flight paths — queue-only iteration skipped exactly
29
+ the paths its own await-inflight logic exists for, and the deterministic-read pin flaked ~1 run in 8 at baseline
30
+ HEAD (0/20 post-fix).
31
+ - **wC child-pi duplicate drain (F6)** — the final-drain 5s CEILING callback left the quiet-window poller armed:
32
+ ceiling fires first + child goes quiet-alive ⇒ the poller fired a SECOND forced drain (duplicate `final_drain` +
33
+ SIGTERM) and overwrote `hardKillTimer`, orphaning the ceiling's timer ⇒ duplicate `hard_kill`. The ceiling now
34
+ clears the poller (mirroring its own self-cancel) and hard-kill emission is idempotent at all three arm sites
35
+ (`hardKilled` gate). `settled || childExited` guards untouched; both transports arm this path (child-pi.ts /
36
+ child-pi-rpc.ts). Adjacent, left for triage: a NEW final assistant event after a quiet-drain can still re-arm one
37
+ extra `final_drain`, but hard-kill is bounded to one.
38
+ - **wD verify-gate shared-cwd untracked harness (S-1, HIGH)** — the dispatch-pinned bindings (treeOid/cleanTree/memo
39
+ key) bind TRACKED content only; in shared-cwd mode the gate ran its checks in the same cwd a WRITE-role executor
40
+ had just written to, so a tampered gitignored harness (node_modules shims) graded its own gate and skipped the LLM
41
+ verifier. New `applySharedCwdHarnessGuard` (verify-gate.ts), chained after the W2 guard at BOTH the memo-hit and
42
+ fresh-verdict PASS sites: any WRITE-role task that EXECUTED in the gate cwd (fail-closed status set incl.
43
+ `cancelled`; worktree-carrying records excluded; realpath-aware cwd compare) ⇒ refuse the deterministic skip and
44
+ force the spawn with reason `"shared-cwd-untracked-harness"`. Worktree-mode skip behavior unchanged; new guard
45
+ metric `crew.verification.verifier_gate_shared_cwd_guard_total`. Wiring deviation (deliberate, leader notified):
46
+ task-runner.ts passes host-owned task records (verifier's own excluded) into the pre-gate (+17 lines) — reading
47
+ tasks.json from the gate itself would be M1-7 self-grading ground truth. Documented residual: a worker forging
48
+ its own tasks.json record (role/status) still evades — state-store integrity lane. `worktree.linkNodeModules:
49
+ true` carries the matching trust-assumption comment (worktree-manager.ts) plus a docs/trust-model.md note.
50
+ - **wE msg.inbox run-level lane gating (S-2)** — legacy bare-runId ("runId-fallback") worker connections could read
51
+ the orchestrator's run-level inbox lane (wait.*/delegate.request already reject non-compound auth). `msg.inbox`
52
+ now rejects `taskId === undefined && authMatchKind !== "compound"` with `forbidden` + a `PI_CREW_BROKER_TASK_ID`
53
+ migrate hint (same error shape as wait-auth). The orchestrator key is compound BY CONSTRUCTION, so the legitimate
54
+ run-level reader is unaffected; undefined kind (pre-hello) fails closed; task-lane reads unchanged.
55
+ - **wF gate allowlist hardening (S-3)** — `printenv` dropped from `ALLOWED_GATE_COMMAND_PATTERNS` (it turned any
56
+ allowlisted gate into an environment-dump read primitive whose output lands in world-readable artifacts).
57
+ `grep`/`head`/`rg`/`tail` stay allowlisted only as cwd-scoped evidence extractors: new `validateGateReaderPathArgs`
58
+ rejects absolute and `~`-prefixed path args (one-quote-char strip closes the unbalanced-quote whitespace-split
59
+ bypass); runner patterns untouched. Deliberate residual: `..`-traversal and attached option values (`rg -f/etc/x`)
60
+ still pass — cheap follow-up. Known collateral (out-of-wave-ownership, escalated to leader): `verify-gate.test.ts`
61
+ "grep evidence lines" (absolute tmpdir arg; one-line relative fix) and `verification-env-sanitize.test.ts` (4
62
+ `printenv` probes; recommended tmpdir Makefile `@printenv` recipe) stay red until reworked.
63
+ - **wG pi PATH fallback validation (S-4)** — a `resolvePiCliScript()` miss returned bare `{ command: "pi" }`, spawned
64
+ via inherited PATH with NO validation, while explicit `PI_TEAMS_PI_BIN` IS validated. New `resolveCommandOnPath`
65
+ (which(1)-equivalent: PATH order, X_OK, PATHEXT) + `validatePathResolvedPi` (the same `isWithinAllowedPrefixes` +
66
+ realpath predicate as explicit-bin validation): a validated POSIX hit spawns by ABSOLUTE path (no child-side
67
+ re-resolution under a mutated env); win32 keeps bare `"pi"` (spawn cannot exec PATHEXT without a shell —
68
+ documented); an out-of-prefix hit THROWS loud diagnostics mirroring the invalid-PI_TEAMS_PI_BIN UX, while NO hit
69
+ on PATH keeps the legacy bare-`"pi"` behavior. Allowed prefixes extended minimally with `~/.pi/agent/bin` +
70
+ `~/.pi/agent/install` — the Pi-managed installer layout, live-verified legitimate and previously uncovered
71
+ (deliberately NOT all of `~/.pi/agent`, so `extensions/`/`cache/` never become auto-executable prefixes).
72
+ - **Closing hardenings (leader-approved rulings, same working tree, 2026-10-31):**
73
+ - **S-1 underscore-role bypass (HIGH follow-up):** the guard's `permissionForRole(record.role)` used the RAW role
74
+ while the tool-granting layer normalizes underscore→hyphen (agent-config.ts `normalizeRoleKey`), so
75
+ `test_engineer`/`cold_verifier`/`chain_executor` stayed write-capable at the worker while the guard
76
+ default-denied them to read_only — guard inert exactly for the roles it exists to catch. Fixed in verify-gate.ts
77
+ (mirrored `normalizeRoleKeyForGuard`; centralization TODO left at the three other un-normalized copies —
78
+ pre-execution.ts, spawn-policy.ts, prompt-builder.ts — out of this run's scope).
79
+ - **S-1 ancestor-cwd containment (M):** `taskRecordSharesGateCwd` exact-compared cwds; a WRITE-role task running
80
+ at an ANCESTOR cwd (task cwd=/ws, gate cwd=/ws/pi-crew) writes into the gate cwd all the same — a record now
81
+ counts as sharing when its realpath'd cwd equals OR CONTAINS the realpath'd gate cwd (fail-closed realpath
82
+ fallback preserved).
83
+ - **Guard-refused PASS never memoizes (M):** the memo record ran BEFORE the S-1 guard chained, so an uncertifiable
84
+ PASS could replay as a skip in a later run whose taskRecords are absent/empty. `recordVerifyGateMemo` is now
85
+ gated on the refusal reason (only `shared-cwd-untracked-harness`; the W2 refusal keeps recording — its memo-hit
86
+ path re-checks probes every replay). Fresh+memoized guard application unchanged.
87
+ - **S-4 realpath-degrade closure (LOW):** a prefix-valid PATH hit whose `realpathSync` probe failed for a
88
+ non-prefix reason (transient EACCES/TOCTOU/symlink loop) returned `undefined` → caller degraded to the
89
+ UNVALIDATED bare-`"pi"` command (the exact S-4 primitive). It now throws skip-with-diagnostic — a
90
+ found-and-prefix-valid candidate never degrades to the unvalidated fallback.
91
+ - **S-3 reader-path containment (LOW; closes wF's deliberate `..`/attached-value residual):**
92
+ `validateGateReaderPathArgs` now also rejects `..`-relative escapes (each non-flag arg resolved against the
93
+ execution cwd — threaded `executeCommand → validateGateCommand → validateGateReaderPathArgs` — with
94
+ containment enforcement), Windows absolute forms (`C:/…`, `\\host\share`), attached option values carrying a
95
+ path (`-f/etc/x`), and denies `rg --pre`/`--pre=*` outright (arbitrary preprocessor execution). Cwd-relative
96
+ evidence extraction (`grep -m1 . dist.txt`, in-cwd `sub/../x` round-trips) still passes.
97
+ - **Collateral test repairs + docs:** the 4 wave-F `printenv` env-sanitize probes reworked to the allowlisted
98
+ `make test` entry with a tmpdir Makefile `@printenv VAR` recipe (npm-run pattern; printenv NOT re-added to the
99
+ allowlist; win32-skipped like the prior fixture); the verify-gate grep-evidence test now uses the cwd-relative
100
+ `dist.txt`; trust-model.md documents the win32 spawn fallback + `~/.pi/agent/{bin,install}` same-uid residuals
101
+ and the memo/cross-run closure context.
102
+
103
+ **Deferred (documented residuals, untouched):** F7 heartbeat PID-recycle (stale-reconciler 30-min trust cap is the
104
+ designed backstop), F8 64KiB handshake cap (needs a chunked handshake protocol — feature, not a fix), S-5 unsigned
105
+ read-only RPC (backward-compat; live-control already fails closed), S-7 webhook DNS-residual SSRF (RFC1918
106
+ deliberately allowed — internal webhooks are a use case).
107
+
108
+ Tests: 29 new regression tests across the waves — wA 3 (subprocess-harness race pins: 3 producers × 16 writes, all
109
+ 48 survive; guard-cycler vs hammering producer, 0 lost; legacy id-less ack fallback), wB 3 (deterministic F3/F4/F5
110
+ pins via the exported lock seam), wC 3 (mock-timer scenarios incl. the exact F6 repro: ceiling-first + quiet ⇒
111
+ exactly one `final_drain` + one `hard_kill`), wD 8 (6 engine incl. memo-hit interplay + 2 e2e), wE 2 (replacing the
112
+ test that pinned the vulnerable behavior), wF 5 (+ reworked P-4 cap fixture to a cwd-relative temp file), wG 5
113
+ (PATH resolution/validation). Every wave: `test:critical` 127/127, typecheck, biome green; wA and wC
114
+ mutation-verified (stash-revert reproduces the bug live). Closing hardenings add 5 more (S-1 underscore-role,
115
+ ancestor-cwd, memo-suppression pins D7–D9; reader-path containment; realpath-degrade) and rework the 4 wave-F
116
+ collateral env-sanitize probes to `make test`.
117
+
3
118
  ## [0.11.53] — event-log in-flight flush await fix (macos CI catch) + Bug-030 RCA closed (2026-10-31)
4
119
 
5
120
  **Fixed (product, caught by macos CI).** `flushOneEventLogBuffer` removed the queue from