refactor(runtime): centralize verified process lifecycle

Extract the generic process lifecycle layer (src/process_lifecycle.py)
shared by runtime-owned subprocesses: process identity (pid + boot-bound
start token), identity-bound observation, group and pidfd probes, the
TERM -> verify -> KILL -> verify escalation with re-gating before
escalation, identity-scoped sweeps, and the termination receipt.

Containment, the PTY shell, the Cookbook survivor sweep, the browser
lifecycle, web_tools browser cleanup, kill_process_tree and the startup
reaper consume it while keeping their own ownership semantics.

Safety corrections:
- browser membership and identity are bound in one snapshot; no identity
  is recaptured after membership is decided
- web_tools legacy pid-file and profile-match kills signal only verified
  identities; browser CLI groups only while their spawn identity verifies
- Cookbook and legacy-tmux descendant capture bind membership to identity
- PTY teardown never signals the server's own process group
- unverifiable processes are reported, never signalled
This commit is contained in:
Alexandre Teixeira
2026-10-02 01:27:08 +01:00
parent 7aa891e5e6
commit f9aa2818c4
14 changed files with 1692 additions and 408 deletions
+6 -14
View File
@@ -133,22 +133,14 @@ def pid_alive(pid: Optional[int]) -> bool:
def kill_process_tree(pid: Optional[int], *, start_token=None, pgid=None, require_identity=False):
"""Use the runtime's shared escalating teardown and return verified death.
Callers retaining durable PIDs must validate their recorded identity before
calling this compatibility entry point. Native grants retain identity at
spawn and use containment.release directly.
Callers retaining durable PIDs must pass their recorded ``start_token``
with ``require_identity=True``. Native grants retain identity at spawn and
use containment.release directly; this entry point owns no grant record.
"""
from src import containment
if not pid or int(pid) <= 0:
return containment.ReleaseOutcome(dead=True, escalated=False)
spec = containment.ContainmentSpec(workspace=os.getcwd(), env={}, wall_clock_s=1,
required=frozenset())
grant = containment.ContainmentGrant(
id="", mechanism="windows_tree" if IS_WINDOWS else "process_group",
workspace=spec.workspace, enforced=frozenset(), degraded=(),
unenforced_required=(), owner="compatibility", mode=containment.CONTAINMENT_MODE,
spec=spec, pid=int(pid), pgid=pgid or containment._pgid_of(int(pid)),
from src import process_lifecycle
return process_lifecycle.terminate_tree(
pid, pgid=pgid, start_token=start_token, require_identity=require_identity,
)
return containment.release(grant, start_token=start_token, require_identity=require_identity)
# ── Shell / executable resolution ───────────────────────────────────────────