diff --git a/docs/superpowers/plans/2026-08-17-abi43-history-replay.md b/docs/superpowers/plans/2026-08-17-abi43-history-replay.md new file mode 100644 index 0000000000..e12870bbe4 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-abi43-history-replay.md @@ -0,0 +1,460 @@ +# ABI 43 History Replay Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the ABI 43 squash merge with one rebase-merged pull request +whose linear history contains conceptually complete ABI 43 commits and the +postcommit spawn-liveness repair. + +**Architecture:** Build a grouped replay line on the squash parent, prove that +line recreates the squash tree exactly, then assemble one public branch that +reverts the squash and cherry-picks the grouped line. Add replay documentation +and the already validated Node/browser liveness repair only after exact tree +equality is proven. + +**Tech Stack:** Git, GitHub CLI, Bash, Rust/TypeScript repository checks, +Vitest, Kandelo dev shell. + +## Global Constraints + +- Never force-push or rewrite `main`. +- Open one pull request and merge it only with GitHub **Rebase and merge**. +- Keep the pull request below GitHub's 100-commit rebase limit. +- Preserve original authors and every original commit through explicit + provenance. +- Preserve the two protected source-test changes that landed after the + original ABI 43 branch point. +- The grouped replay tree must equal + `bd28679cd2452f24e5c3ea2c245ed3dfcace1e05`. +- The final net source change is limited to replay docs and the postcommit + spawn-liveness repair. +- Run build and validation commands through `scripts/dev-shell.sh` where the + repository provides a declared environment. + +--- + +### Task 1: Audit the original range and planning documents + +**Files:** +- Create: + `docs/superpowers/specs/2026-08-17-abi43-history-replay-design.md` +- Create: `docs/superpowers/plans/2026-08-17-abi43-history-replay.md` + +**Interfaces:** +- Consumes: merge base `847875deeb07d2be4039485a7c3287ad5edf400f`, + original head `c218d35225c411859d067133242316ddf07cb08e`, squash + parent `c1ff05541a7d9eaa4f295acb065f735cdb0bd272`, and squash + commit `bd28679cd2452f24e5c3ea2c245ed3dfcace1e05`. +- Produces: the exact 63-group authority used by Tasks 2-4. + +- [ ] **Step 1: Verify range size, authors, and trailers** + +Run: + +```bash +git rev-list --count \ + 847875deeb07d2be4039485a7c3287ad5edf400f..\ +c218d35225c411859d067133242316ddf07cb08e +git shortlog -sne \ + 847875deeb07d2be4039485a7c3287ad5edf400f..\ +c218d35225c411859d067133242316ddf07cb08e +git log --format=%B \ + 847875deeb07d2be4039485a7c3287ad5edf400f..\ +c218d35225c411859d067133242316ddf07cb08e | \ + rg '^Co-authored-by:' || true +``` + +Expected: 135 commits; 131 by Brandon Payton, three by Dependabot, one by +`mho22`; any co-author trailers are visible for explicit preservation. + +- [ ] **Step 2: Verify the intervening-main delta** + +Run: + +```bash +git diff --name-status \ + c218d35225c411859d067133242316ddf07cb08e \ + bd28679cd2452f24e5c3ea2c245ed3dfcace1e05 +``` + +Expected: exactly the protected publisher trust checker, exact source-test +packer, and packer regression named in the design. + +- [ ] **Step 3: Commit the design and this plan separately** + +Run: + +```bash +git add docs/superpowers/specs/2026-08-17-abi43-history-replay-design.md +git commit -m "docs: design the ABI 43 history replay" +git add docs/superpowers/plans/2026-08-17-abi43-history-replay.md +git commit -m "docs: plan the ABI 43 history replay" +``` + +### Task 2: Build the grouped replay line + +**Files:** +- Modify: Git commit topology only; no final source edits. + +**Interfaces:** +- Consumes: the 135 original commits and the design grouping table. +- Produces: temporary branch `abi43-replay-grouped`, based on + `c1ff05541a7d9eaa4f295acb065f735cdb0bd272`, with 63 commits. + +- [ ] **Step 1: Create the temporary grouped branch** + +Run: + +```bash +git switch -c abi43-replay-grouped \ + c1ff05541a7d9eaa4f295acb065f735cdb0bd272 +``` + +- [ ] **Step 2: Define the bounded grouping helper** + +Run in the Bash session used for grouped commits: + +```bash +replay_group() { + first="$1" + last="$2" + subject="$3" + author="$(git show -s --format='%an <%ae>' "$first")" + provenance="$(git log --reverse --format='Original-commit: %H' \ + "$first^..$last")" + git cherry-pick --no-commit "$first^..$last" + git commit --author="$author" \ + -m "$subject" \ + -m "This concept commit consolidates its complete contiguous change from the original ABI 43 batch." \ + -m "$provenance" +} +``` + +- [ ] **Step 3: Replay independent commits 1-41** + +Run: + +```bash +git cherry-pick -x \ + 4efcab85fe10dd9abcc3b1c74cc78ba9b0850703^..\ +0b21ee6b3 +``` + +Expected: 41 commits with `mho22` and Dependabot authors unchanged. + +- [ ] **Step 4: Replay ownership, audio, and vfork groups** + +Run: + +```bash +replay_group 4968cbca2 774eec589 \ + "ABI: Move lifecycle metadata ownership into Rust" +replay_group 5d3317bbc 78b64f174 \ + "Audio: Provide process-safe OSS PCM across hosts and packages" +replay_group eebf350ae 691b5ede7 \ + "ABI: Establish the production vfork mechanism" +git cherry-pick -x 0f33598ea +``` + +- [ ] **Step 5: Replay credential, login, and runtime groups** + +Run: + +```bash +replay_group 7d0a7074f 91d6be903 \ + "POSIX: Make credentials and set-ID execution authoritative" +replay_group 8abe5ef8b 6144a4ea8 \ + "POSIX: Complete interrupted waits and login integration" +replay_group 6e4a37b52 c2b572cc8 \ + "Build: Initialize the reentrant Node runtime" +replay_group a272497b5 cbd148ebb \ + "Homebrew: Integrate ABI 43 descriptors and products" +``` + +- [ ] **Step 6: Replay CI and candidate-authority groups** + +Run: + +```bash +replay_group 6254851d6 e61878bcd \ + "CI: Repair ABI 43 product contract fixtures" +replay_group 7b81eebb7 819882228 \ + "Homebrew: Make Formula candidate provenance authoritative" +replay_group 4b4127b35 446749856 \ + "Homebrew: Preserve candidate Formula tests and program projections" +replay_group b81889e01 b4b55a183 \ + "Homebrew: Preserve candidate bottle identity" +``` + +- [ ] **Step 7: Replay publication and verification groups** + +Run: + +```bash +replay_group b38598451 7a80d1248 \ + "ABI: Unblock first-wave candidate publication" +replay_group 34112844b fdb8cb04b \ + "ABI: Enforce qualified candidate metadata" +replay_group e33081997 4dd2ae433 \ + "Homebrew: Pour authenticated candidates in bounded realms" +replay_group 9b7844fe7 8f46de6cb \ + "Homebrew: Preserve Chromium across Formula tests" +``` + +- [ ] **Step 8: Replay dependency and final execution groups** + +Run: + +```bash +replay_group 20b3f97b5 6ac709107 \ + "ABI: Materialize candidate dependency bottles" +git cherry-pick -x c66ccdb0b +git cherry-pick -x 66468b600 +git cherry-pick -x d6b381482 +replay_group 7d01b329a ae23d0dc0 \ + "ABI: Authorize artifact-independent staging validation" +replay_group bfd069791 c218d3522 \ + "Host: Carry secure exec and retirement through commit" +``` + +If either final group conflicts with the protected source-test changes, inspect +each conflict with `git diff --cc` and resolve it to the exact content in +`bd28679cd2452f24e5c3ea2c245ed3dfcace1e05`. Do not use a blanket side +selection. + +- [ ] **Step 9: Prove grouped tree equality** + +Run: + +```bash +git diff --exit-code \ + bd28679cd2452f24e5c3ea2c245ed3dfcace1e05 HEAD +git rev-list --count \ + c1ff05541a7d9eaa4f295acb065f735cdb0bd272..HEAD +``` + +Expected: no tree diff and 63 commits. + +### Task 3: Assemble the public repair branch + +**Files:** +- Modify: Git commit topology only until replay equality is proven. + +**Interfaces:** +- Consumes: grouped line plus design and plan commits. +- Produces: `emdash/abi43-history-replay-final`, based on current `main`. + +- [ ] **Step 1: Record the prepared heads** + +Run: + +```bash +grouped_head="$(git rev-parse HEAD)" +design_head="$(git rev-parse emdash/abi43-history-replay~1)" +plan_head="$(git rev-parse emdash/abi43-history-replay)" +``` + +- [ ] **Step 2: Revert the squash on the final branch** + +Run: + +```bash +git switch -c emdash/abi43-history-replay-final origin/main +git revert --no-edit bd28679cd2452f24e5c3ea2c245ed3dfcace1e05 +git diff --exit-code \ + c1ff05541a7d9eaa4f295acb065f735cdb0bd272 HEAD +``` + +Expected: the revert tree equals the squash parent. + +- [ ] **Step 3: Append the grouped replay** + +Run: + +```bash +git cherry-pick \ + c1ff05541a7d9eaa4f295acb065f735cdb0bd272.."$grouped_head" +git diff --exit-code \ + bd28679cd2452f24e5c3ea2c245ed3dfcace1e05 HEAD +``` + +Expected: the replay tree equals the squash tree exactly. + +- [ ] **Step 4: Append design and plan** + +Run: + +```bash +git cherry-pick "$design_head" "$plan_head" +``` + +### Task 4: Append the postcommit spawn-liveness repair + +**Files:** +- Create: + `docs/superpowers/specs/2026-08-17-posix-spawn-postcommit-liveness-design.md` +- Create: + `docs/superpowers/plans/2026-08-17-posix-spawn-postcommit-liveness.md` +- Modify: `host/src/node-kernel-worker-entry.ts` +- Modify: `host/src/browser-kernel-worker-entry.ts` +- Modify: `host/test/spawn-host-parity.test.ts` + +**Interfaces:** +- Consumes: validated commits `b7a42cfd2`, `93880b52a`, and + `108f47fea`. +- Produces: one complete Node/browser runtime repair commit. + +- [ ] **Step 1: Apply all validated changes without committing** + +Run: + +```bash +git cherry-pick --no-commit b7a42cfd2^..108f47fea +``` + +- [ ] **Step 2: Commit the complete repair** + +Run: + +```bash +git commit --author="Brandon Payton " \ + -m "Host: Defer spawn liveness until after allocation" \ + -m "Keep the single legal post-allocation child-liveness fence in both Node and browser hosts, with the reviewed design, plan, and parity regression." +``` + +### Task 5: Verify history, attribution, and behavior + +**Files:** +- Verify only. + +**Interfaces:** +- Consumes: final repair branch. +- Produces: evidence for the one rebase merge. + +- [ ] **Step 1: Verify count and linearity** + +Run: + +```bash +git rev-list --count origin/main..HEAD +git rev-list --merges origin/main..HEAD +test "$(git rev-list --count origin/main..HEAD)" -lt 100 +``` + +Expected: 67 commits and no merge commits. + +- [ ] **Step 2: Verify every original SHA exactly once** + +Run: + +```bash +git rev-list --reverse \ + 847875deeb07d2be4039485a7c3287ad5edf400f..\ +c218d35225c411859d067133242316ddf07cb08e | sort \ + > /tmp/abi43-original-shas +git log --format=%B origin/main..HEAD | \ + sed -nE \ + -e 's/^Original-commit: ([0-9a-f]{40})$/\1/p' \ + -e 's/^\(cherry picked from commit ([0-9a-f]{40})\)$/\1/p' | \ + sort > /tmp/abi43-replayed-shas +diff -u /tmp/abi43-original-shas /tmp/abi43-replayed-shas +``` + +Expected: no differences or duplicates. + +- [ ] **Step 3: Verify special authors and final diff** + +Run: + +```bash +git log --format='%an <%ae>%x09%s' origin/main..HEAD | \ + rg 'mho22|dependabot\[bot\]' +git diff --name-only origin/main..HEAD +git diff --check origin/main..HEAD +``` + +Expected: one `mho22` commit, three Dependabot commits, and only the four +replay/spawn docs plus three spawn implementation/test files in the final diff. + +- [ ] **Step 4: Run focused host validation** + +Run: + +```bash +scripts/dev-shell.sh bash -lc ' + cd host && npx --no-install vitest run \ + test/spawn-host-parity.test.ts \ + test/exec-state-tracking.test.ts \ + test/deferred-worker-start.test.ts \ + test/spawn-pid-authority.test.ts +' +``` + +Expected: 85 tests pass. + +- [ ] **Step 5: Run ABI and full Vitest validation** + +Run: + +```bash +scripts/dev-shell.sh bash scripts/check-abi-version.sh +scripts/dev-shell.sh bash scripts/ci-run-test-suite.sh vitest +``` + +Expected: ABI version/snapshot checks and the full Vitest lane pass. + +### Task 6: Publish and rebase-merge one pull request + +**Files:** +- No source changes after Task 5. + +**Interfaces:** +- Consumes: final verified branch. +- Produces: repaired linear `main` and a new immutable staging source. + +- [ ] **Step 1: Push and create the pull request** + +Run: + +```bash +git push -u origin emdash/abi43-history-replay-final +gh pr create --repo Automattic/kandelo \ + --base main \ + --head emdash/abi43-history-replay-final \ + --title "ABI: Replay ABI 43 as reviewable history" +``` + +The PR body must lead with why squash history prevents review and bisecting, +explain the one-revert/63-replay topology, list tree and attribution evidence, +and document the final spawn-liveness repair. + +- [ ] **Step 2: Revalidate immediately before merge** + +Run: + +```bash +git fetch origin main +test "$(git rev-parse origin/main)" = \ + bd28679cd2452f24e5c3ea2c245ed3dfcace1e05 +gh pr view --repo Automattic/kandelo --json \ + mergeable,mergeStateStatus,statusCheckRollup,headRefOid,baseRefOid +``` + +If `main` moved, rebuild from Task 3. Do not force a stale branch. + +- [ ] **Step 3: Rebase-merge and verify public history** + +Run: + +```bash +gh pr merge --repo Automattic/kandelo --rebase --delete-branch +git fetch origin main +git log --format=fuller --reverse \ + bd28679cd2452f24e5c3ea2c245ed3dfcace1e05..origin/main +``` + +Expected: revert, 63 replay commits, replay docs, and final spawn repair appear +linearly with preserved authors. diff --git a/docs/superpowers/plans/2026-08-17-posix-spawn-postcommit-liveness.md b/docs/superpowers/plans/2026-08-17-posix-spawn-postcommit-liveness.md new file mode 100644 index 0000000000..470a1856e0 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-posix-spawn-postcommit-liveness.md @@ -0,0 +1,138 @@ +# POSIX Spawn Post-Commit Liveness Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let a committed `posix_spawn` child reach host Worker allocation without re-entering the kernel while its post-commit transaction is still draining. + +**Architecture:** Remove the redundant pre-allocation child-liveness query from both host entries. Keep the existing post-allocation query as the single authoritative fence before registration, and lock that ordering into the shared spawn parity test. + +**Tech Stack:** TypeScript, Vitest, Node worker host, browser worker host + +## Global Constraints + +- Preserve the kernel-entry reentrancy guard unchanged. +- Preserve Node/browser observable process behavior. +- Do not add an msmtpd-specific path. +- Do not register a Worker after a child exits during an asynchronous allocation. + +--- + +### Task 1: Enforce post-allocation liveness ordering + +**Files:** +- Modify: `host/test/spawn-host-parity.test.ts` +- Modify: `host/src/node-kernel-worker-entry.ts` +- Modify: `host/src/browser-kernel-worker-entry.ts` + +**Interfaces:** +- Consumes: `CentralizedKernelWorker.shouldLaunchPendingChild(pid: number): boolean` +- Produces: one post-allocation liveness fence in each `handlePosixSpawn` + +- [ ] **Step 1: Write the failing ordering regression** + +Extract each `handlePosixSpawn` body with the test's existing helper. Locate +`createFreshProcessMemory`, `shouldLaunchPendingChild`, and `registerProcess`. +Assert that `shouldLaunchPendingChild` occurs exactly once, after allocation +and before registration: + +```ts +expect(spawn.match(/shouldLaunchPendingChild/g)).toHaveLength(1); +expect(spawn.indexOf("createFreshProcessMemory")).toBeLessThan( + spawn.indexOf("shouldLaunchPendingChild"), +); +expect(spawn.indexOf("shouldLaunchPendingChild")).toBeLessThan( + spawn.indexOf("registerProcess"), +); +``` + +- [ ] **Step 2: Run the regression and verify RED** + +Run: + +```bash +scripts/dev-shell.sh bash -lc \ + 'cd host && npx --no-install vitest run test/spawn-host-parity.test.ts' +``` + +Expected: FAIL for both Node and browser because each entry contains two +liveness queries and the first precedes allocation. + +- [ ] **Step 3: Implement the minimal shared fix** + +Delete only the first `shouldLaunchPendingChild` branch from each +`handlePosixSpawn`. Add a short comment explaining that the successful +prepared-target commit proves the child at callback entry and that the retained +post-allocation check owns kill-during-yield handling. + +- [ ] **Step 4: Run focused spawn and lifecycle tests** + +Run: + +```bash +scripts/dev-shell.sh bash -lc \ + 'cd host && npx --no-install vitest run \ + test/spawn-host-parity.test.ts \ + test/exec-state-tracking.test.ts \ + test/deferred-worker-start.test.ts \ + test/spawn-pid-authority.test.ts' +``` + +Expected: all selected tests pass. + +- [ ] **Step 5: Commit the implementation** + +```bash +git add host/test/spawn-host-parity.test.ts \ + host/src/node-kernel-worker-entry.ts \ + host/src/browser-kernel-worker-entry.ts +git commit -m "fix: defer spawn child liveness until after allocation" +``` + +### Task 2: Validate and publish the runtime repair + +**Files:** +- Verify: all files from Task 1 +- Verify: `abi/snapshot.json` + +**Interfaces:** +- Consumes: the Task 1 branch +- Produces: a reviewable Kandelo pull request and hosted msmtpd retry input + +- [ ] **Step 1: Run the complete host suite** + +```bash +scripts/dev-shell.sh bash scripts/ci-run-test-suite.sh vitest +``` + +Expected: all host tests pass, including every resource-isolated case. + +- [ ] **Step 2: Verify the ABI snapshot is unchanged** + +```bash +scripts/dev-shell.sh bash scripts/check-abi-version.sh +``` + +Expected: the ABI check passes without changing `ABI_VERSION` or the snapshot. + +- [ ] **Step 3: Review the exact branch diff** + +```bash +git diff --check origin/main...HEAD +git status --short +git log --format=fuller origin/main..HEAD +``` + +Expected: only the approved design/plan, two host entries, and parity test are +changed; known submodule dirt remains unstaged. + +- [ ] **Step 4: Open the purpose-first pull request** + +Use a title describing the process-lifecycle outcome. Put `## Why` before +`## What changed`, list the exact validation, and state that hosted msmtpd is +not yet proven until its staging retry succeeds. + +- [ ] **Step 5: Retry msmtpd after merge** + +Dispatch the immutable ABI 43 request against the new Kandelo source only after +the runtime PR merges. Require the original standalone msmtpd service test to +build and pass before calling the end-to-end defect fixed. diff --git a/docs/superpowers/specs/2026-08-17-abi43-history-replay-design.md b/docs/superpowers/specs/2026-08-17-abi43-history-replay-design.md new file mode 100644 index 0000000000..2b0dc2c861 --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-abi43-history-replay-design.md @@ -0,0 +1,156 @@ +# ABI 43 History Replay Design + +## Purpose + +Pull request #1264 was squash-merged as `bd28679cd2452f24e5c3ea2c245ed3dfcace1e05`. +That preserved the final source tree but discarded the reviewable ABI 43 commit +sequence from `c218d35225c411859d067133242316ddf07cb08e`. Repair the public history +without rewriting or force-pushing `main`. + +The repair is one pull request merged with GitHub's **Rebase and merge** method. +It first reverts the squash, then replays a conceptually complete ABI 43 +sequence, and finally adds the postcommit `posix_spawn` liveness repair needed +by the `msmtpd` bottle test. + +## Repository facts + +- Squash commit: `bd28679cd2452f24e5c3ea2c245ed3dfcace1e05` +- Squash parent/current pre-ABI tree: + `c1ff05541a7d9eaa4f295acb065f735cdb0bd272` +- Original PR head: `c218d35225c411859d067133242316ddf07cb08e` +- Original PR merge base: + `847875deeb07d2be4039485a7c3287ad5edf400f` +- Original PR range: 135 commits +- GitHub rebase-merge limit: 100 commits + +The original PR head is not byte-identical to the squash commit. Two protected +source-test preparation commits landed on `main` after the original branch +point. GitHub's squash result correctly combined those intervening changes +with the ABI 43 diff, affecting three protected workflow-support files. The +replay must therefore be applied onto the squash parent, not copied from the +old PR head as a complete tree. + +## One-PR topology + +The repair branch starts at the current squash-merged `main` and contains this +linear sequence: + +1. One revert of the squash commit. +2. Sixty-three conceptually complete ABI 43 replay commits. +3. The reviewed design and implementation plan. +4. One postcommit spawn-liveness commit containing its contract docs, + Node/browser implementation, and parity regression. + +The tree immediately after the revert must equal the squash parent. The tree +immediately after the ABI replay must equal the squash commit exactly. The +final pull-request diff may then contain only the replay documentation and the +spawn-liveness repair. + +## Consolidation policy + +Keep a commit separate when it represents an independently reviewable platform +contract, has a distinct upstream author, or is an independent dependency +update. Consolidate contiguous commits when later commits complete or repair +the same contract, regenerate artifacts for that contract, or record its +validation and rollout. + +Each consolidated commit records every original SHA and subject in its body. +The resulting commit author is the author shared by the grouped commits. +`mho22` remains the author of the Windows mount-permissions commit. Each of the +three Dependabot commits remains separate with Dependabot as author. Any +original co-author trailer is retained on the consolidated commit. + +The following table is the complete grouping authority. Ordinals refer to +`git log --reverse 847875dee..c218d3522`. + +| Original ordinals | Replay treatment | Concept | +|---|---|---| +| 1-38 | Keep individually | Independent fork, host, POSIX, network, SDK, CI, and browser contracts | +| 39-41 | Keep individually | Three independent Dependabot updates | +| 42-49 | Consolidate | Move lifecycle and host metadata ownership into Rust | +| 50-52 | Consolidate | Provide process-safe OSS audio across hosts and packages | +| 53-57 | Consolidate | Establish the ABI 43 vfork mechanism and build artifacts | +| 58 | Keep individually | Default executable mounts to `nosuid` | +| 59-66 | Consolidate | Make credentials, set-ID execution, and PTY metadata authoritative | +| 67-72 | Consolidate | Complete interrupted-wait, login, sudo-lite, and vfork integration | +| 73-75 | Consolidate | Pin build tools and initialize the reentrant Node runtime | +| 76-83 | Consolidate | Integrate ABI 43 Homebrew descriptors and product projections | +| 84-86 | Consolidate | Repair CI and product contract fixtures for ABI 43 | +| 87-93 | Consolidate | Make Formulae and candidate provenance authoritative | +| 94-97 | Consolidate | Preserve candidate Formula tests and program projections | +| 98-102 | Consolidate | Preserve keg, launcher, rebuild, schema, and public clone identity | +| 103-111 | Consolidate | Unblock and validate first-wave candidate publication | +| 112-117 | Consolidate | Enforce qualified metadata and close first-wave platform drift | +| 118-120 | Consolidate | Pour authenticated candidates in bounded verification realms | +| 121-124 | Consolidate | Preserve and project Chromium across Formula tests | +| 125-127 | Consolidate | Materialize exact candidate dependency bottles | +| 128 | Keep individually | Keep publisher integration out of PR staging | +| 129 | Keep individually | Export PHP side-module ABI identity | +| 130 | Keep individually | Preserve worker output process identity | +| 131-132 | Consolidate | Make exact ABI validation artifact-independent and authorize the builder | +| 133-135 | Consolidate | Carry secure exec and retirement through commit and refresh projections | + +This produces 63 replay commits. With the revert, two documentation commits, +and the final runtime repair, the pull request has 67 commits. + +## Replay mechanics + +Build a temporary replay line from the squash parent. Apply each group in +original order with `git cherry-pick --no-commit`, resolve only the protected +source-test overlap introduced after the original merge base, and commit the +group once its complete final state is present. Never select conflict sides by +blanket `ours` or `theirs`; compare each conflict against the squash tree. + +After the grouped line is complete, require: + +```text +git diff --exit-code bd28679cd2452f24e5c3ea2c245ed3dfcace1e05 +``` + +Then, on the public repair branch, revert the squash and cherry-pick the 63 +grouped commits. Re-run the same tree-equality check before adding the runtime +repair. + +## Merge and attribution rules + +- No force push to `main`. +- No squash merge. +- No merge commit. +- The pull request must be merged using GitHub **Rebase and merge**. +- The pull request must contain fewer than 100 commits. +- Every replay commit must have the intended original author. +- Consolidated commit bodies must list their complete original SHA set. +- Compare original and replay author inventories before merge. + +GitHub rewrites committer identity and commit SHAs during rebase merge while +retaining authors. That is acceptable; losing original authors or conceptual +boundaries is not. + +## Validation + +Before opening the pull request: + +1. Prove the post-revert tree equals `c1ff05541...`. +2. Prove the post-replay tree equals `bd28679cd...`. +3. Prove the final diff contains only the design/plan and spawn-liveness files. +4. Prove the branch is linear and contains fewer than 100 commits. +5. Compare author inventories and original-SHA coverage. +6. Run `scripts/check-abi-version.sh`. +7. Run the focused spawn/exec host tests. +8. Run the full Vitest lane used by the merged ABI tree. +9. Run `git diff --check`. + +Before merging, re-fetch `main`, require the pull-request base to remain the +expected squash commit, and re-run the tree, attribution, and commit-count +checks. If `main` moves, rebuild the replay against the new base rather than +force-merging stale history. + +## Staging interaction + +The active tap workflow may continue validating the immutable original ABI 43 +request while this history repair is prepared. Its candidate bytes remain +useful. The repaired Kandelo history produces a new source commit identity, so +after the history pull request lands the staging coordinator must either prove +reuse against the new request or rebuild only contracts changed by the final +spawn-liveness repair. It must not pretend the old source commit is the new +one. diff --git a/docs/superpowers/specs/2026-08-17-posix-spawn-postcommit-liveness-design.md b/docs/superpowers/specs/2026-08-17-posix-spawn-postcommit-liveness-design.md new file mode 100644 index 0000000000..604e7cc3f4 --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-posix-spawn-postcommit-liveness-design.md @@ -0,0 +1,45 @@ +# POSIX Spawn Post-Commit Liveness Design + +## Problem + +The ABI 43 kernel-entry gate correctly forbids a second kernel WebAssembly +export while a serialized ingress or detached protocol transaction is still +draining. Node's `handlePosixSpawn` violates that boundary immediately after a +successful prepared-target commit: it calls `shouldLaunchPendingChild`, which +enters `kernel_get_process_exit_signal`. The msmtpd standalone service test +reaches this path and fails with `KernelReentrantEntryError` before its child +Worker can be attached. + +This is shared host-runtime behavior, not an msmtpd Formula defect. A Formula +workaround would hide a general `posix_spawn` sequencing error. + +## Design + +Remove the pre-allocation `shouldLaunchPendingChild` call from both Node and +browser `handlePosixSpawn` implementations. The callback is reached only after +Rust has committed the exact pending child's prepared target. Node has not +yielded since that commit, so another ingress cannot have killed the child. +Browser may have yielded while draining unrelated process teardowns, but it +still performs the authoritative liveness check after memory allocation and +before registration, so removing the earlier optimization cannot resurrect a +dead child. + +Retain the post-allocation check in both hosts. It runs after an asynchronous +allocation boundary, releases the unused memory lease when the child is no +longer live, and prevents Worker registration for an exited child. + +Do not weaken `KernelEntryGate`, expose an entry capability to detached host +callbacks, special-case msmtpd, or delay Node launch merely to make the +forbidden query legal. + +## Validation + +Extend the existing spawn host-parity contract to require that neither entry +calls `shouldLaunchPendingChild` before `createFreshProcessMemory` completes, +and that both retain exactly one check after allocation and before process +registration. Run that test red before implementation and green afterward. + +Then run the focused spawn/lifecycle host tests, the complete host Vitest +suite, and the ABI snapshot check. The hosted msmtpd Formula retry remains the +end-to-end proof because it exercises the exact Node worker, kernel, shell, +network, and `posix_spawn` path that exposed the defect. diff --git a/host/src/browser-kernel-worker-entry.ts b/host/src/browser-kernel-worker-entry.ts index abc992570c..7298d4dc67 100644 --- a/host/src/browser-kernel-worker-entry.ts +++ b/host/src/browser-kernel-worker-entry.ts @@ -3056,9 +3056,10 @@ async function handlePosixSpawn( const secureExec = kernelWorker.takeCommittedExecSecureExec(childPid); await waitForProcessTeardowns(); - // Unrelated teardown waits yield to the event loop. Keep a successfully - // created zombie, but never resurrect it with a new Worker. - if (!kernelWorker.shouldLaunchPendingChild(childPid)) return 0; + // The shared launcher already committed the exact pending child. The + // post-allocation fence below owns any exit observed across this teardown + // wait or allocation yield; do not add a separate kernel entry while the + // postcommit transaction is still draining. post({ type: "proc_event", kind: "spawn", pid: childPid, ppid: parentPid }); const { programBytes, programModule, argv } = program; diff --git a/host/src/node-kernel-worker-entry.ts b/host/src/node-kernel-worker-entry.ts index b7196efdef..3c39f7e331 100644 --- a/host/src/node-kernel-worker-entry.ts +++ b/host/src/node-kernel-worker-entry.ts @@ -2710,9 +2710,10 @@ async function handlePosixSpawn( envp: string[], ): Promise { const secureExec = kernelWorker.takeCommittedExecSecureExec(childPid); - // Preserve a child that became a zombie before launch, but do not resurrect - // it by registering a new execution generation. - if (!kernelWorker.shouldLaunchPendingChild(childPid)) return 0; + // The shared launcher invokes this callback only after Rust committed the + // exact pending child. Do not re-enter the kernel while that postcommit + // transaction is still draining; the first legal liveness fence follows + // the asynchronous memory allocation below. post({ type: "proc_event", kind: "spawn", pid: childPid, ppid: parentPid }); const { programBytes, programModule, argv } = program; diff --git a/host/test/spawn-host-parity.test.ts b/host/test/spawn-host-parity.test.ts index 95801307ba..7e829249ba 100644 --- a/host/test/spawn-host-parity.test.ts +++ b/host/test/spawn-host-parity.test.ts @@ -181,6 +181,29 @@ describe("spawn host parity", () => { } }); + it("both spawn adapters check child liveness only after allocation yields", () => { + for (const entry of [nodeEntry, browserEntry]) { + const handler = posixSpawnHandlerSource(readFileSync(entry, "utf8")); + const allocation = handler.indexOf("createFreshProcessMemory("); + const liveness = handler.indexOf("shouldLaunchPendingChild(childPid)"); + const registration = handler.indexOf("registerProcess(childPid"); + expect( + handler.match(/shouldLaunchPendingChild\(childPid\)/g) ?? [], + `${entry} must retain exactly one post-allocation liveness fence`, + ).toHaveLength(1); + expect(allocation, `${entry} must allocate process memory`) + .toBeGreaterThanOrEqual(0); + expect( + liveness, + `${entry} must check the child after allocation yields`, + ).toBeGreaterThan(allocation); + expect( + registration, + `${entry} must check liveness before registering the Worker generation`, + ).toBeGreaterThan(liveness); + } + }); + it("both exec adapters consume the complete commit-captured transition", () => { for (const entry of [nodeEntry, browserEntry]) { const handler = execHandlerSource(readFileSync(entry, "utf8")); diff --git a/packages/registry/program-packages.json b/packages/registry/program-packages.json index 9b24f96457..ab1b44487f 100644 --- a/packages/registry/program-packages.json +++ b/packages/registry/program-packages.json @@ -158,8 +158,8 @@ "lamp": { "manifestSha256": "250b64635d64f8537178188fb5488dbfd2980d79a1c2ffbf346af67008088ba0", "cacheKeys": { - "wasm32": "9f39600d109c032edd900740c72028212e29c87defd91fff9c166d0d4e3f1f76", - "wasm64": "7940409328ca59d7bf6314fc0498837d22348c3904de9ddf902eb2ca0eb2e26d" + "wasm32": "6d8bc40c085627ca90f6620cfa9d61f1fcbedf4c3c087ea1441d1136f09f5f7a", + "wasm64": "ea01ebbe426b78e13903c36816e3abbadc7a48e02feed9e327e88204f7325700" } }, "less": { @@ -312,8 +312,8 @@ "nginx-php-vfs": { "manifestSha256": "97976410cb02f8ba710d856b4ac904bcf976d677b50eefdee39fb64176070d4b", "cacheKeys": { - "wasm32": "f8c5abb969412ee57cfeee1e820e584429040ee2c17157ddc84d6716f97e924c", - "wasm64": "e9f0afea90eb0d8a325e4c06051b65341bea369b71aba64bcd77ed784b2fd728" + "wasm32": "051823a3007192df66b6d0f42529f0e4b3c17d5226a80591d235ad842853c862", + "wasm64": "26e1638db2436d3c0f516f8e476cdb1a1e487f0b194a99c94998e9a6a9f7ee21" } }, "nginx-vfs": { @@ -543,8 +543,8 @@ "wordpress": { "manifestSha256": "36465e0596a06e855a8524eecfd87da98643bc13b37ee12939f5aab44bf33418", "cacheKeys": { - "wasm32": "bff9dd02e957f308c75595260314e91f2a246187ec53fafcddfa31be0b8145d8", - "wasm64": "c12d22029539427ae7f5f1fcb83688280f0dbbc8417bd927203e439296346755" + "wasm32": "e97e12283c5b9c258a51f876f0204a4c7e9487869ff9a354a18a50671a2e267c", + "wasm64": "e0b05cbf52f713504a2511b1ded8972a655f7358e0e71738a6191823b75a9d3a" } }, "xz": { @@ -1121,7 +1121,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "9f39600d109c032edd900740c72028212e29c87defd91fff9c166d0d4e3f1f76" + "wasm32": "6d8bc40c085627ca90f6620cfa9d61f1fcbedf4c3c087ea1441d1136f09f5f7a" }, "dependencyClosures": { "wasm32": [ @@ -1751,7 +1751,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "f8c5abb969412ee57cfeee1e820e584429040ee2c17157ddc84d6716f97e924c" + "wasm32": "051823a3007192df66b6d0f42529f0e4b3c17d5226a80591d235ad842853c862" }, "dependencyClosures": { "wasm32": [ @@ -3046,7 +3046,7 @@ "wasm32" ], "cacheKeys": { - "wasm32": "bff9dd02e957f308c75595260314e91f2a246187ec53fafcddfa31be0b8145d8" + "wasm32": "e97e12283c5b9c258a51f876f0204a4c7e9487869ff9a354a18a50671a2e267c" }, "dependencyClosures": { "wasm32": [