From 2373a5ad345fb316ad2460f6390baab1f45ba08f Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 13 Aug 2026 10:53:46 -0500 Subject: [PATCH] fix(lease): raise capability-probe timeout to 10s on both halves (#869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The activation/enforcement capability probes (#869 C1/C4) budget 2.0s for an out-of-process launch of the mosaic CLI, but the CLI's Node cold start alone measures 2.2-2.3s on a mid-range workstation (sb-it-1-dt, 2026-08-13). Result: every probe timed out, was treated as NO capability (fail-closed), and every `mosaic ` launch on such hosts died with the misleading version-skew message even though the capability matched exactly. 10s costs nothing on healthy hosts — the happy path returns as soon as the probe exits; the timeout only bounds hangs. Also fixes a latent test-hermeticity bug the new budget exposed: _resolve_probe_command() ignored the provided environ and resolved `mosaic` against the ambient os.environ PATH, so the "not resolvable on PATH" unittest actually spawned the host's real CLI — and only passed on hosts where that real probe happened to exceed the old 2s timeout. Resolution now honors the provided environment's PATH (fail-closed when absent); the unittest suite drops from ~2.1s to ~0.004s, confirming no real process is spawned. Verified: version_coupling_unittest.py 15/15; lease-activation-probe spec 15/15; lease-doctor + mutator-gate specs unchanged vs clean next (4 acceptance failures pre-exist on 216cd722, unrelated seam); eslint + prettier clean; tsc --noEmit emits the identical pre-existing error set as clean next. End-to-end on the affected host: patched gate passes the real probe in 2.19s and `mosaic yolo claude -p` launches successfully. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh --- .../lease-broker/activation_version_gate.py | 17 +++++++++++++++-- .../src/commands/lease-activation-probe.ts | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/mosaic/framework/tools/lease-broker/activation_version_gate.py b/packages/mosaic/framework/tools/lease-broker/activation_version_gate.py index b1d151b2..4afd38ae 100644 --- a/packages/mosaic/framework/tools/lease-broker/activation_version_gate.py +++ b/packages/mosaic/framework/tools/lease-broker/activation_version_gate.py @@ -62,7 +62,14 @@ EXPECTED_ACTIVATION_CAPABILITY: Final[ActivationCapability] = { # capability as compact JSON. LEASE_CAPABILITY_PROBE_COMMAND: Final = "__lease-capability" -PROBE_TIMEOUT_SECONDS: Final = 2.0 +# Budget for the out-of-process `mosaic __lease-capability` probe. The CLI +# is a Node program whose cold start alone measures 2.2-2.3s on a mid-range +# workstation (sb-it-1-dt, 2026-08-13), so a 2s budget made every launch on +# such hosts fail closed with the #869 skew message even though the +# capability matched. The timeout only bounds the pathological hang case — +# the happy path returns as soon as the probe exits — so a generous budget +# costs nothing on healthy hosts. +PROBE_TIMEOUT_SECONDS: Final = 10.0 # Override hook: a full shell-style command line (parsed with `shlex.split`) # to run INSTEAD of resolving `mosaic` on PATH and appending the probe @@ -88,7 +95,13 @@ def _resolve_probe_command(environ: Mapping[str, str]) -> list[str] | None: if override: parsed = shlex.split(override) return parsed or None - resolved = shutil.which("mosaic") + # Resolve against the PROVIDED environment's PATH, not the ambient + # os.environ. Before this, a test passing a hermetic environ still + # resolved (and spawned) the host's real `mosaic` — masked only on hosts + # where the real probe happened to exceed the old 2s timeout. No PATH in + # the provided environment means nothing is resolvable (fail-closed), + # matching the probe's overall contract. + resolved = shutil.which("mosaic", path=environ.get("PATH", "")) if resolved is None: return None return [resolved, LEASE_CAPABILITY_PROBE_COMMAND] diff --git a/packages/mosaic/src/commands/lease-activation-probe.ts b/packages/mosaic/src/commands/lease-activation-probe.ts index 7c634d22..c3987cb1 100644 --- a/packages/mosaic/src/commands/lease-activation-probe.ts +++ b/packages/mosaic/src/commands/lease-activation-probe.ts @@ -55,6 +55,19 @@ export const LEASE_ACTIVATION_CAPABILITY: LeaseActivationCapability = { /** Hidden CLI probe subcommand name — wired via {@link registerLeaseCapabilityProbe}. */ export const LEASE_CAPABILITY_PROBE_COMMAND = '__lease-capability'; +/** + * Budget for the out-of-process capability probe. The probe launches a fresh + * Node process on the built CLI entrypoint, whose cold start alone measures + * 2.2-2.3s on a mid-range workstation (sb-it-1-dt, 2026-08-13) — so the + * previous 2s budget made the probe time out and report NO capability on + * such hosts, failing every launch with the #869 skew message even though + * the capability matched. The timeout only bounds the pathological hang + * case; the happy path returns as soon as the probe exits. Mirrors + * PROBE_TIMEOUT_SECONDS in the enforcement half + * (framework/tools/lease-broker/activation_version_gate.py). + */ +export const LEASE_CAPABILITY_PROBE_TIMEOUT_MS = 10_000; + function capabilityMatches(candidate: LeaseActivationCapability | null): boolean { return ( candidate !== null && @@ -141,7 +154,7 @@ export function defaultCapabilityProbe( const output = execFileSync(process.execPath, [cliEntry, LEASE_CAPABILITY_PROBE_COMMAND], { encoding: 'utf-8', - timeout: 2000, + timeout: LEASE_CAPABILITY_PROBE_TIMEOUT_MS, stdio: ['ignore', 'pipe', 'ignore'], });