export interface MojoTreeMember { pid: number; ppid: number; pgid: number; /** Which signal matched, for operator-facing logs. */ via: 'pgid' | 'env' | 'ppid'; /** * True only when `/proc//stat` field 3 is exactly `Z`. * * A zombie has already been reaped by the kernel: it executes no instructions * and cannot use the injected credential, it merely waits for its parent to * collect the exit status. Counting one as a survivor blocks the close forever * for a process that can do nothing — a safe direction, but an unrecoverable * one. Anything OTHER than a definite `Z` (including a state we could not read) * is treated as executing, so the discount can never be a free pass. */ zombie: boolean; } /** * Why a scan could not be completed. Every variant is fail-closed at the call * site; the split exists so an operator can tell a permanently unscannable host * (`unsupported-platform`) from a transient or partial read failure. */ export type MojoTreeScanFailure = { kind: 'unsupported-platform'; platform: string; } | { kind: 'proc-unreadable'; detail: string; } | { kind: 'proc-entry-unreadable'; pid: number; detail: string; } | { kind: 'proc-entry-unparsable'; pid: number; detail: string; }; export type MojoTreeScan = { ok: true; /** * Never 'proof'. Present so a reader cannot mistake `ok: true` for a * credential-boundary guarantee — see the trust-domain note above. */ evidence: 'diagnostic'; members: MojoTreeMember[]; /** * Pids whose `environ` was unreadable for a benign reason, so the env-nonce * signal could not speak for them. Surfaced rather than swallowed. */ envBlindSpots: number[]; } | { ok: false; failure: MojoTreeScanFailure; reason: string; }; /** * Whether a turn subtree is gone, and — crucially — whether that answer is * strong enough to drop a credential blocker. * * `boundaryProof` is the field a blocker decision consults, and it is true * exclusively for `contained-proven`, which this module never produces: it is * reserved for the kernel-level containment handle. * * It is NOT consumed directly by callers. It is projected into * `TerminationOutcome.boundaryProven` by `terminationOutcomeFromQuiescence()` * below, and that projected field is what the production close path reads -- * see the `boundaryProven` check in MojoBackend.destroySession(). The previous * wording here claimed this was "the ONLY field a blocker decision may * consult" while no conditional anywhere consumed it; the claim is now * restricted to a statement about the projection, which has a real consumer. */ export type TurnQuiescence = { kind: 'contained-proven'; boundaryProof: true; } | { kind: 'diagnostic-clean'; boundaryProof: false; } | { kind: 'alive'; boundaryProof: false; pids: number[]; } | { kind: 'unscannable'; boundaryProof: false; reason: string; } | { kind: 'unsupported-platform'; boundaryProof: false; platform: string; }; /** * Why a termination attempt believes what it believes. * * `esrch` is reserved for a future "the pid is provably gone because signalling * it returned ESRCH" observation. Nothing synthesises it today, and it is * deliberately NOT treated as boundary proof if it ever appears: a vanished * root pid says nothing about a descendant that setsid'd away from it. */ export type TerminationEvidence = 'members-empty' | 'esrch' | 'diagnostic-clean' | 'timeout' | 'unknown'; /** * What is still owed after a termination attempt that did not establish a * boundary. * * `deviceIsolation: true` means the session's device-isolation blocker must be * retained even if the session row itself is allowed to close. */ export interface TerminationResidual { deviceIsolation: boolean; pids?: number[]; reason?: string; } /** * Structured result of a termination attempt. Replaces the bare boolean that * `terminateChildProven()` used to return, which collapsed "the signalling * ladder finished" and "a credential boundary was established" into one bit -- * so `diagnostic-clean` (an empty scan on Linux, which proves nothing) was * indistinguishable from a real cgroup-backed proof and authorised a plain * closed row. * * Field semantics, all load-bearing: * * * `ok` the ladder ran to completion without evidence of a live * member. It does NOT mean the boundary is proven. * `ok: true` with `boundaryProven: false` is legal and, on * Linux weak handles, the COMMON case. * * `boundaryProven` the gate. Only this may authorise dropping a credential * blocker or deleting a containment handle. True only for * kernel-level containment. * * `residual` non-null whenever the boundary was not proven, so the * caller has something concrete to retain rather than an * absence to overlook. * * `signalsStopped` whether it is pointless to keep signalling. A clean scan * earns this and nothing else. */ export interface TerminationOutcome { ok: boolean; boundaryProven: boolean; evidence: TerminationEvidence; residual: TerminationResidual | null; signalsStopped: boolean; } /** * Project a scan/containment verdict onto the termination contract. * * This is the ONLY place a `TerminationOutcome` is constructed, so the * invariants above cannot be bypassed by a caller assembling the object by * hand. */ export declare function terminationOutcomeFromQuiescence(q: TurnQuiescence): TerminationOutcome; /** * Identity of a pid that is stable across pid recycling. * * A bare pid is NOT a stable handle: the kernel recycles pids, so between * remembering `rootPid` and signalling it the number can belong to an unrelated * process — and `kill(-rootPid, SIGKILL)` would then take down a stranger's * process group. `starttime` (field 22 of /proc//stat, in clock ticks since * boot) distinguishes a recycled pid, and `bootId` makes the pair meaningful * across a reboot, after which every starttime restarts from zero and a * persisted record would otherwise appear to match. */ export interface MojoProcessIdentity { pid: number; bootId: string; starttime: number; } export type MojoIdentityRead = { ok: true; identity: MojoProcessIdentity; } | { ok: false; failure: MojoTreeScanFailure; reason: string; }; /** Injected into every turn child; inherited by the whole subtree. */ export declare const MOJO_TREE_NONCE_ENV = "BOTMUX_MOJO_TREE_NONCE"; /** The real kernel interface. Anything else is, by definition, an override. */ export declare const DEFAULT_PROC_ROOT = "/proc"; /** * Is this `procRoot` a SUBSTITUTE for the kernel's own /proc? * * The distinction is the whole gate. The previous test was * `procRoot !== undefined`, and every production caller passes a procRoot -- the * backend's getter returns the string '/proc' -- so the override branch was * unconditionally taken and the non-Linux refusal below could never fire in * production. Passing the real path is not an override; only pointing the scanner * somewhere else is. */ export declare function isProcRootOverridden(procRoot: string | undefined): boolean; /** * `/proc` is a Linux interface. On any other platform the layout either does not * exist or does not mean the same thing, so enumeration is refused OUTRIGHT * rather than allowed to fail its way into a misleading "nothing is running". * * Refusing OUTRIGHT is not pedantry, it is the difference between two very * different closes. `unsupported-platform` routes to a residual close, which * publishes the row, keeps the device-isolation blocker on the durable handle and * lets the remote cancel proceed. Any other failure -- including `unscannable` -- * routes to a fence, which latches write admission and returns a failed close. * A fence is right when a retry might yet produce proof; on a host that can NEVER * enumerate, it is a permanent wedge, which is exactly the behaviour this module * was fixed not to have. * * A synthetic `procRoot` opts back in: that is how a test points the scanner at a * fake tree. The previous version of this comment claimed such an override "is * never set in production", which was the reverse of the truth -- production * always sets it, to the real /proc, which is why the gate was dead. See * `isProcRootOverridden`. */ export declare function mojoTreeScanSupported(opts?: { platform?: string; procRootOverridden?: boolean; }): boolean; /** * Every live process still belonging to the turn rooted at `rootPid`. * * `excludePids` MUST contain the current process (and anything else that must * never be signalled): the daemon shares neither the nonce nor the group, but an * explicit guard is cheaper than trusting that invariant while sending SIGKILL. * * PGID membership (`p.pid === rootPid || p.pgid === rootPid`) is a bare numeric * comparison against a remembered pid, and this module's own identity primitive * exists because bare pids are not stable handles: once the root is reaped, a * recycled pid that becomes a new group leader wears the same number, and the * ppid closure would then claim that stranger's whole subtree — consumers * SIGKILL every claimed member. PGID claiming therefore requires * `opts.rootIdentity` (the identity recorded when the root was spawned) to * still verify against the live process. Without it — not passed, root gone, or * recycled — only the env nonce plus its ppid closure claim members, both of * which are positive evidence of THIS turn's membership. * * A successful result is a diagnostic signal only; see `quiescenceFromScan`. */ export declare function scanMojoTree(rootPid: number, nonce: string, opts?: { procRoot?: string; excludePids?: readonly number[]; platform?: string; /** Recorded identity of `rootPid` at spawn; gates PGID-based claiming. */ rootIdentity?: MojoProcessIdentity; }): MojoTreeScan; /** * Map a scan onto a quiescence verdict — WITHOUT ever minting boundary proof. * * An empty member list becomes `diagnostic-clean`, whose `boundaryProof` is * false, because enumeration cannot see a descendant that setsid'd and scrubbed * its environ. Callers keep the blocker unless `boundaryProof` is true. */ export declare function quiescenceFromScan(scan: MojoTreeScan): TurnQuiescence; /** Read the recycle-proof identity of a pid. */ export declare function readProcessIdentity(pid: number, opts?: { procRoot?: string; platform?: string; }): MojoIdentityRead; /** Same process, or a recycled pid wearing its number? */ export declare function sameProcessIdentity(a: MojoProcessIdentity, b: MojoProcessIdentity): boolean; export type TreeGroupSignalOutcome = { kind: 'signalled'; } /** The pid now belongs to a DIFFERENT process; nothing was signalled. */ | { kind: 'identity-mismatch'; expected: MojoProcessIdentity; actual: MojoProcessIdentity; } /** The root pid is already gone; nothing to signal, and nothing was. */ | { kind: 'gone'; } /** Identity could not be established, so signalling was refused. */ | { kind: 'unverifiable'; reason: string; } | { kind: 'unsupported-platform'; platform: string; }; /** * `kill(-rootPid, signal)` — but ONLY after proving the pid is still the process * we spawned. * * A bare group kill on a remembered pid is a footgun: pids recycle, so after the * turn root exits the number can name an unrelated process, and negating it * signals THAT process's whole group. Everything except a verified match * therefore signals nothing at all; refusing to kill is always recoverable, * killing the wrong group is not. */ export declare function signalTurnTreeGroup(expected: MojoProcessIdentity, signal: NodeJS.Signals, opts?: { procRoot?: string; platform?: string; kill?: (target: number, sig: NodeJS.Signals) => void; }): TreeGroupSignalOutcome; //# sourceMappingURL=mojo-process-tree.d.ts.map