pi-crew 0.11.53 → 0.11.55

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