fix(session): route the gatewayd orphan kill through platform_compat - #7871
Conversation
The gatewayd orphan reaper signalled with raw os.kill(pid, SIGTERM/SIGKILL)
and probed liveness with a raw os.kill(pid, 0) in its TERM grace loop, while
its two siblings in the same file already route through platform_compat:
_kill_orphan_work_tree uses kill_pid(pid, SIGKILL), and the browser-daemon
reaper probes via pid_exists with its own written rationale ("correct on its
own terms rather than depending on a caller's early-out"). The raw spellings
are also the ones the cross-platform table documents as unavailable or
destructive on Windows (signal 0 terminates the target, signal.SIGKILL does
not exist), which is why the windows-kill-probe audit carried an exemption
for this function.
Route the pid-targeted paths through the shim: TERM and the escalation
SIGKILL go through platform_compat.kill_pid (identical POSIX semantics,
same exception types), and the grace-loop probe goes through
platform_compat.pid_exists (same ProcessLookupError-only liveness meaning
the loop already had). The group-targeted branches (os.getpgid/os.getpgrp/
os.killpg) stay raw on purpose: there is no shim spelling for a
process-group signal and the isolated-leader predicate guarding them is
POSIX group semantics; the sweep entry point remains IS_WINDOWS-gated.
The audit exemption for session_pid.py::_kill_orphan_gatewayd is removed
with the probe it covered, shrinking GATED_PROBES by one entry. New tests
pin the routing per function with shim spies (TERM through kill_pid,
escalation SIGKILL through kill_pid, probe through pid_exists, sweep
direct-kill through kill_pid), and the killpg branch keeps its existing
coverage in test_pid_lifecycle.py.
ee478ad to
514da2a
Compare
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of The change checks out against the base: Design-Verdict: PASS Converges a divergent reaper onto the file's own established shim pattern, retiring an audit exemption at its root rather than re-justifying it. [DESIGN-REVIEWED] 514da2a |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of All claims in the intent check out against the base tree: the four converted sites are the only raw First-Principles-Verdict: PASS Every item is mandated by the AGENTS.md What this change shipsIntent: make the gatewayd orphan reaper safe by construction on Windows instead of by a caller's gate — a FIX (latent-hazard removal).
Counts run: [FIRST-PRINCIPLES-REVIEWED] 514da2a |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
|
@bolichen97 Thanks for the relationship audit, and the coordination plan sounds right to me. Happy for this one to land first: it is conversion-only (no behavior change on POSIX), every required check is green on 514da2a and it merges clean against current main, so the only thing it is waiting on is a maintainer review. On #8304, agreed that adopting |
bolichen97
left a comment
There was a problem hiding this comment.
Verified the routing is behavior-preserving rather than merely cosmetic: platform_compat.kill_pid delegates to os.kill on POSIX and lets ProcessLookupError propagate, so the reaper's early-out on an already-dead pid still fires, and pid_exists maps only ProcessLookupError to False (EPERM and other OSError read as still-alive), which is exactly what the old grace loop's bare except ProcessLookupError did. Leaving the os.getpgid/getpgrp/killpg group-targeted branches raw is the right call since no shim spells a process-group signal, and pruning the now-dead session_pid GATED_PROBES entry keeps the audit allowlist honest instead of growing it.
Problem / Motivation
The gatewayd orphan reaper in
src/kiro_crew/session_pid.pysignalled with the raw POSIX spellings:os.kill(pid, signal.SIGTERM)and the escalationos.kill(pid, signal.SIGKILL)in_kill_orphan_gatewayd(session_pid.py:1554and:1577), a rawos.kill(pid, 0)liveness probe in its TERM grace loop (:1560), and a rawos.kill(pid, signal.SIGKILL)for the sweep's shared-pgid fallback (:2030). Two of its sibling functions in the same file already avoid exactly these spellings:_kill_orphan_work_treekills throughplatform_compat.kill_pid(target, platform_compat.SIGKILL)(:2167), and_kill_orphan_browser_daemonprobes throughplatform_compat.pid_existswith a written rationale for doing so ("routing through the shim keeps it correct on its own terms rather than depending on a caller's early-out",:1615-1618). The gatewayd reaper is the one that never got the same treatment, and it is also the function the windows-kill-probe audit carries an explicitGATED_PROBESexemption for — an exemption whose justification is precisely that the raw spellings inside cannot run on Windows.Why it matters
The raw spellings are the ones the repo's cross-platform table documents as unavailable or destructive on Windows:
signal.SIGKILLdoes not exist there (anAttributeError, not a caughtOSError), andos.kill(pid, 0)is not a liveness probe at all there — it terminates the target. None of that is reachable today, because the sweep entry pointkill_orphan_mcpsearly-outs underif platform_compat.IS_WINDOWS, and that is exactly the shape the file's own browser-daemon comment argues against: correctness that depends on remembering which caller gates what, rather than on the function itself. Thetest_windows_kill_probe_audit.pytripwire exists because this class of latent hazard keeps regressing; every entry on its allowlist is a place where "cannot happen on Windows" is load-bearing with no local proof. This PR removes one of those entries instead of adding one.What changed (motivation → approach → change)
Symptom: one orphan-reaper function uses the raw signal spellings its siblings and the audit both route around. Approach: move the pid-targeted paths onto the same shim the file already uses elsewhere, and leave the group-targeted paths alone. Concretely, in
_kill_orphan_gatewayd:platform_compat.kill_pidwithplatform_compat.SIGTERM/SIGKILL; on POSIX this is the identicalos.killcall, and the shim propagates the same exception types (ProcessLookupError,PermissionError,OSError) the existing handlers already catchplatform_compat.pid_exists, which preserves the exact meaning the loop had: onlyProcessLookupErrorreads as "gone" (the shim maps EPERM/otherOSErrorto "still exists"), so the loop's behavior is unchanged on every inputkill_orphan_mcpsroutes throughplatform_compat.kill_pidthe same wayWhat deliberately does not change: the
os.getpgid/os.getpgrp/os.killpggroup-targeted branches. There is no shim spelling for signalling a process group rather than a pid, and the isolated-leader predicate guarding those calls (pgid == pid and pgid != os.getpgrp() and pgid > 1) is POSIX process-group semantics that cannot be expressed through a pid-targeted helper. Rewriting them would change what the code protects, not just how it spells it.Because the raw signal-0 probe this function carried is gone, its
GATED_PROBESentry intest/test_windows_kill_probe_audit.pyis removed in the same change. That is not bookkeeping I noticed later — the audit's owntest_allowlist_has_no_stale_entriesgoes red the moment a listed site no longer contains a raw probe, which is how this PR found out the allowlist tracks these sites at all.GATED_PROBESshrinks from four entries to three.Tests
New file
test/test_session_pid_reaper_shim.py, four tests that pin the routing with spies on the shim rather than onos:test_term_is_sent_through_kill_pid— the TERM goes throughkill_pid, and a daemon that exits before the first poll counts as one killtest_escalation_sigkill_goes_through_kill_pid— wedged daemon past the grace window, identity recheck passing, shared pgid: the direct SIGKILL goes throughkill_pid, on both the Linux and non-Linux identity pathstest_liveness_probe_uses_pid_exists_not_signal_zero— the grace loop consultspid_existsand neveros.kill, with the raw call asserted not-calledtest_shared_pgid_orphan_killed_via_kill_pid— through the fullkill_orphan_mcpssweep, the pid-targeted branch callskill_pid(pid, SIGKILL)and notkillpgI wrote the tests first and watched them fail against the unmodified module — two of the four failures were the raw spellings executing for real on Windows (
OSError: [WinError 87]fromos.killwith a signal constant,AttributeErrorfor the absentos.getpgid), which is the failure mode this PR removes the spelling of. The killpg branch keeps its existing coverage intest_pid_lifecycle.py, and the sweep'sIS_WINDOWSgate keeps its existing pin intest_windows_kill_probe_audit.py.Manual verification
N/A — unit coverage is sufficient here. The reaper's observable behavior is entirely which function receives which signal, which is what the spy tests assert; the POSIX execution path is the identical
os.killcall by the shim's own implementation, and the Windows path cannot execute by the sweep's existing gate, which the audit test pins.The 7 failures in the second command are pre-existing on this Windows machine and unrelated: I re-ran the same set on a clean checkout of
mainwith the change stashed and got the same seven (os.getuid/os.getpgrpabsent on frozen Windowsosintest_pid_lifecycle.pyand one gatewayd socket-path test), all inside POSIX-only test paths that this change does not touch.Screenshots / video
N/A — no user-visible change.
Related Issues
No dedicated issue; this follows the cross-platform routing convention from the
platform_compattable inAGENTS.md. The audit trail for the removed exemption istest/test_windows_kill_probe_audit.pyitself.Pattern harvest
Not generalizable: the defect class already has its rule, the whole-tree AST tripwire in
test_windows_kill_probe_audit.pyplus the justifiedGATED_PROBESallowlist, and this PR is that rule working as designed (one raw probe removed, one allowlist entry pruned, the stale-entries guard forcing the cleanup). The remaining gap in the class — raw spellings that are real signals rather than probes, like the ones this PR converted — is a per-site migration with no mechanical rule to add beyond what the table and review already cover.Checklist
fix(session): route the gatewayd orphan kill through platform_compatplatform_compattable and the audit test