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'], });