mosaic promote: transport reads pane_pid env but launcher spawnSync leaves lease env on the claude CHILD → 'no readable lease session' for every real seat (fomo-lin E2E) #1124

Open
opened 2026-08-08 21:40:04 +00:00 by scooby · 0 comments
Collaborator

The critical unproven link, now proven to FAIL on a real attached seat (fomo-lin). Branch feat/lease-promotion-and-harness-isolation on next.

Symptom: mosaic promote gseat against a real gated mosaic claude seat returns UNVERIFIED ... reason=RESOLVE_FAILED: Promotion seat has no readable lease session: gseat, exit 1 — before /mosaic-promote is ever injected.

Root cause (proc-level evidence):

  1. mosaic claudeexecLeaseGatedRuntimeexecRuntime() (commands/launch.js:883) runs the launcher with spawnSync, NOT an exec-replace — despite its own comment 'Use execFileSync with inherited stdio to replace the process'. Comment says replace; code spawns. So node(mosaic) stays alive as the pane's root process.
  2. Resulting tree: pane_pid = node(mosaic) (NO MOSAIC_LEASE_SESSION_ID) → child claude (HAS MOSAIC_LEASE_SESSION_ID + broker/observer sockets, set by launch-runtime.py). Verified: /proc/<pane_pid>/environ lease-var count = 0; /proc/<claude-child>/environ = 1.
  3. fleet/promotion-transport.ts resolve() reads ONLY pane.pid's /proc/environ via parseLeaseSessionId — it never walks to the claude descendant. So the lease session is unreadable and resolve throws.

This fails for EVERY seat launched via the shipping path (mosaic claude), so promotion cannot reach injection at all on a real host. Unit tests pass because they inject a mock environmentReader; only a real attached seat + real process tree exposes it — exactly the link terra could not stand up (detached pane).

Fix options (Fred's call):

  • (a) Make the launcher actually exec-replace through to claude so pane_pid == claude (matches execRuntime's own comment; cleanest — the lease-bearing process becomes the pane root).
  • (b) Have the transport resolve pane_pid → its claude descendant (walk children) before reading MOSAIC_LEASE_SESSION_ID.

Secondary: the pane-command identity check accepts {claude,node}; a bash -c seat wrapper reports bash → 'runtime identity mismatch'. Seats must be launched so the pane command is node/claude (exec, no shell wrapper). Worth a doc note or a broadened check.

E2E status: Steps 1–2 PASS (build/install; gated seat with real lease; mutator correctly DENIED MUTATOR_UNVERIFIED, file not created). Step 3 BLOCKED here. Steps 4–5 not reached (failure-path sub-case confirmed: unresolvable seat → UNVERIFIED exit 1, no hang). Verdict recorded truthfully — NOT faked. Filed by scooby (fomo-lin). Related: #1123 (activation-probe timeout, which had to be worked around to wire the gate at all).

**The critical unproven link, now proven to FAIL on a real attached seat (fomo-lin).** Branch feat/lease-promotion-and-harness-isolation on next. **Symptom:** `mosaic promote gseat` against a real gated `mosaic claude` seat returns `UNVERIFIED ... reason=RESOLVE_FAILED: Promotion seat has no readable lease session: gseat`, exit 1 — before `/mosaic-promote` is ever injected. **Root cause (proc-level evidence):** 1. `mosaic claude` → `execLeaseGatedRuntime` → `execRuntime()` (commands/launch.js:883) runs the launcher with **`spawnSync`**, NOT an exec-replace — despite its own comment 'Use execFileSync with inherited stdio to replace the process'. Comment says replace; code spawns. So `node`(mosaic) stays alive as the pane's root process. 2. Resulting tree: `pane_pid = node(mosaic)` (NO `MOSAIC_LEASE_SESSION_ID`) → child `claude` (HAS `MOSAIC_LEASE_SESSION_ID` + broker/observer sockets, set by launch-runtime.py). Verified: `/proc/<pane_pid>/environ` lease-var count = 0; `/proc/<claude-child>/environ` = 1. 3. `fleet/promotion-transport.ts` `resolve()` reads ONLY `pane.pid`'s `/proc/environ` via `parseLeaseSessionId` — it never walks to the claude descendant. So the lease session is unreadable and resolve throws. **This fails for EVERY seat launched via the shipping path** (`mosaic claude`), so promotion cannot reach injection at all on a real host. Unit tests pass because they inject a mock `environmentReader`; only a real attached seat + real process tree exposes it — exactly the link terra could not stand up (detached pane). **Fix options (Fred's call):** - (a) Make the launcher actually exec-replace through to claude so `pane_pid == claude` (matches execRuntime's own comment; cleanest — the lease-bearing process becomes the pane root). - (b) Have the transport resolve pane_pid → its claude descendant (walk children) before reading `MOSAIC_LEASE_SESSION_ID`. **Secondary:** the pane-command identity check accepts {claude,node}; a `bash -c` seat wrapper reports `bash` → 'runtime identity mismatch'. Seats must be launched so the pane command is node/claude (exec, no shell wrapper). Worth a doc note or a broadened check. **E2E status:** Steps 1–2 PASS (build/install; gated seat with real lease; mutator correctly DENIED `MUTATOR_UNVERIFIED`, file not created). Step 3 BLOCKED here. Steps 4–5 not reached (failure-path sub-case confirmed: unresolvable seat → UNVERIFIED exit 1, no hang). Verdict recorded truthfully — NOT faked. Filed by scooby (fomo-lin). Related: #1123 (activation-probe timeout, which had to be worked around to wire the gate at all).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1124