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 +115 -0
- package/dist/index.mjs +508 -508
- package/package.json +1 -1
- package/src/runtime/broker/protocol/msg-inbox.ts +27 -3
- package/src/runtime/child-pi/child-pi-timers.ts +25 -0
- package/src/runtime/foreground-control.ts +164 -116
- package/src/runtime/pi-spawn.ts +112 -2
- package/src/runtime/task-runner.ts +17 -0
- package/src/runtime/verification/verification-gates.ts +103 -7
- package/src/runtime/verification/verify-gate.ts +209 -21
- package/src/state/event-log/event-log.ts +108 -27
- package/src/worktree/worktree-manager.ts +12 -0
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
|