feat(workspace,cli)!: pin pidfiles to process start identity before teardown (#969) - #1069
feat(workspace,cli)!: pin pidfiles to process start identity before teardown (#969)#1069davidfarah2003 wants to merge 7 commits into
Conversation
…eardown (#969) A teardown may only signal a pid whose creation identity it can prove. The launch writes a sibling <pidfile>.identity pin (pid + process-start token, the same token the advisory lock uses); the four teardown paths - down's shared component stop (broker/web/extensions), stopManager, stopDelivery, stopAuthService - run one shared open-verify-terminate rule: a reused pid (pin mismatch) refuses and preserves, a legacy or torn record refuses loud, and only an ESRCH-proven death clears a record and its pin.
…he record shape a real launch writes verifyIdentityPin now returns gone for an ESRCH-dead pid whatever the pin's state (missing, torn, or paired): nothing can be signalled, so the stale record clears exactly as before #969 and a torn pin cannot wedge cleanup. The affected suites' fixtures write the pin a real launch writes, and the multi-space UNKNOWN-probe cell accepts the identity-gate refusal (a nonexistent pid has no token to pin, so the gate fires first - also a fail-loud preserve).
…get to the record shape a real launch writes (#969) The identity gate refuses a bare-pid (legacy) record on a live pid by design, so the two suites whose cells grade the kill/cleanup behind it plant the pin their real launchers now write.
|
Follow-up commit |
davidfarah2003
left a comment
There was a problem hiding this comment.
LANDING-QUEUE REVIEW: BLOCKERS
- Concrete rule violation:
bin/smoke/ci-suites.txt:699insertssmoke:pid-identityBEFORE the file's true tail - the pre-existingsmoke:workspace-import-exportsblock sits at line 705, after the new entry. This is not a staleness artifact:git merge-base --is-ancestor origin/main pr-1069is TRUE (branch has main's full current history) and still chose a mid-file insertion point. Move the new entry to the absolute tail before landing. - Breaking-change checks otherwise pass:
.changeset/proud-otters-identify.mdbumps both packages "minor," which is CORRECT per AGENTS.md's explicit rail ("Packages are 0.x, so a breaking change is a minor") - not a defect, and it lists exactly the two packages with first-party source changes. - Docs updated coherently:
docs/cli.mdanddocs/setup-internals.mdboth describe "teardown never signals a pid it cannot identify"; no contradiction found against SPEC.md's separate auth-plane "host belt" pidfile (atomic no-overwritelink(2)exclusivity, §13.13) - confirmed via theauth-proc.tsdiff that this PR's identity pin is written as a distinct sibling file after that existing atomic claim, leaving it unchanged. - Evidence is real:
pid-identity.smoke.tsruns against real spawned child processes (mismatch refusal, happy-path teardown, proven-dead cleanup, legacy/torn-record refusal); mutation M1/M2 targetpid.ts's comparison anddelivery-proc.ts's gate call site with namedexpectRedcells. Disclosed gaps (no Windows pin, one pre-existing red reproduced identically on main,isolated-broker.tsintentionally unchanged) are named, not hidden. - No file-level conflict detected against #1066/#1067/#1068/#1071 - this PR's file set doesn't overlap the others'.
|
Queue sequencing note: the blocking finding here is the mid-file ci-suites.txt insert (reindex hazard, #920). #1052 (approved, in CI) replaces the shared registry tail with merge-safe per-PR fragments — once it lands, rebase onto the fragment scheme rather than moving the entry to the tail. Holding until then. |
| */ | ||
| import { strict as assert } from "node:assert"; | ||
| import { spawn } from "node:child_process"; | ||
| import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
| const root = mkdtempSync(join(tmpdir(), "pid-identity-")); | ||
| mkdirSync(join(root, ".cotal"), { recursive: true }); | ||
| process.chdir(root); | ||
| const here = fileURLToPath(new URL(".", import.meta.url)); |
The ruling this PR is built on has no durable recordThis PR's description states, as the basis for its scope:
I am the current fm-mac-orch and I went looking for that ruling before treating it as settled. It
I am not saying the ruling never happened. It may have been given over the mesh, where messages are This matters beyond bookkeeping, because every seat and the repository owner share one GitHub Why this is blocking rather than a note#1057, #1069 and #1103 all edit the same four files: So exactly one of them should land, one should close, and that is a design decision rather than a What I am asking forNothing from this PR's author, who did the right thing by naming the authority they were working The ask is procedural and it applies to me as much as anyone: a design ruling that scopes a PR This PR is not blocked by any defect in its code. It is held pending that design decision, along |
|
Measured comparison of this head against #1057 posted there: #1057 (comment) Headline, against The conflicts are real rather than a phantom The point of the comparison is that these two are mutually exclusive. Both rewrite I have not graded the two designs and I am not making the call. |
Closes #969.
Scope
Stable process identity for local stack teardown, the prerequisite #969 blocks PR #880's
detached Windows lifecycle on. Ruled option (a) by fm-mac-orch, read as the creation-identity
record + atomic open-verify-terminate design (the ruling's own conditions use that vocabulary:
the cells must prove the identity check REFUSES a PID whose identity does not match).
packages/workspace/src/pid.ts): a creation-identity record ispid token(the process-start token the advisory lock already reads:
/proc/<pid>/statfield 22 onLinux,
ps -o lstart=on macOS).verifyIdentityPinis the shared open-verify verdict:match / mismatch (pid reuse) / gone (ESRCH) / unpinned, plus torn-pin and torn-pairing shapes
for the sibling file. Two shared refusal constructors keep all four teardown paths refusing
in the same words.
check in
up.ts, operator scripts, and suites keep working). The identity is a SIBLING<pidfile>.identitypin, themanager.delivery-awaremarker pattern.up.ts(all threenats.pidsites),manager-proc,delivery-proc,auth-proc. Removal sites drop the pin with the record.stopLocalProcessindown(broker, web, extension components),
stopManager,stopDelivery,stopAuthService.A reused pid is refused and preserved, never signalled. A legacy (no pin) live record and a
torn pin refuse loudly with operator NEXT steps - the issue's loud behavior for legacy
PID-only records. Only an ESRCH-proven death (or a matching pin followed by confirmed exit)
clears a record and its pin. A dead pid needs no identity proof: the verdict is
gonewhatever the pin's state, so stale-record cleanup behaves exactly as before and a torn pin
cannot wedge it.
cli.mddown section,setup-internals.md), changeset (minor on workspace + cli),plan row in
.internal.Evidence
stopDeliveryto SIGTERM a foreign process, which died exit 9; a bare-pid legacy record wassignalled the same way. With the fix: zero signals, record and pin preserved, refusal names
both starts.
smoke:pid-identity(gated, appended to ci-suites.txt): 22 cells, every onethrough the real stop entry points with real child processes. A1-A4 delivery mismatch
refusal (signal count is the graded number); B1-B5 manager (carrying a supervisor-shaped
argv so command attribution passes and the PIN is what refuses), auth, broker; C1-C2
happy-path teardown (match signalled, death confirmed, record + pin cleared); D1 dead-record
clear with no signal; E1-E4 legacy and torn-pairing refusals.
bin/smoke/mutations/pid-identity.json: M1 folds the shared mismatchcomparison to match (the bare-pid behavior verbatim); M2 deletes the delivery call-site gate
entirely. Both KILLED red-and-named on A1, re-verified after the final code state
(
node scripts/mutation-proof.mjsverdict: all 2 killed).typecheck green,
changeset statusverified.Not proven (named gaps)
DETACHED_PROCESS parent exit, and the absence of a cheap stable start token on win32 (no
pin is written there; every record is the loud legacy shape). This box has no Windows. The
PR feat(cli)!: detach and persist meshes on up #880 launcher must integrate with this seam on Windows-native runners; that integration
is out of scope here by ruling (named, not simulated).
smoke:multi-space(the doctor signer-path cell): it failsIDENTICALLY on pristine origin/main in a throwaway worktree on this box (symlinked TMPDIR
makes tmpdir() and the doctor's root resolution disagree). Environment, not this change;
156 other cells pass.
isolated-broker.ts'sstopByPidis unchanged: it signals a pid the CURRENT processspawned and still holds a Child handle for - no durable record, no cross-reopen reuse
window - so the identity seam does not apply by construction. Flagged for the PR feat(cli)!: detach and persist meshes on up #880
integration reviewer.
deterministic on demand); the mismatch cells build the truthful post-reuse state instead
(a pin whose token differs from the live process's actual start), which is byte-identical
to what a reused pid produces.