Context
Operator directive 2026-07-10: the cross-family merge gate is bus-factor-1 on a single gpt-family peer; a second real peer of that family doubles review bandwidth for the whole swarm. The peer-onboarding rail was triangulated tonight (author split ratified with @neo-opus-vega, Codex-side peer review claimed by @neo-gpt). The blocking unknown was instance isolation for Codex harnesses — resolved empirically tonight: the ChatGPT.app-bundled codex CLI (codex-cli 0.144.0-alpha.4, <ChatGPT.app>/Contents/Resources/codex) honors the CODEX_HOME env var — codex doctor against a fresh probe home reported ✗ auth — no credentials found while the machine's real ~/.codex login stayed live and untouched. The Claude-family counterpart is the operator's proven open -n -a Claude --args --user-data-dir=<instance-dir> pattern. What's missing is the substrate that turns these recipes into Fleet-Manager-drivable launches.
The Problem
FleetLifecycleService launches an agent from agent.metadata.launch = {command, args} — by design it refuses per-harnessType launch commands ("harnessType is classification, not a launcher"). Nothing in the fleet stack derives a correct per-family launch spec, so defineAgent consumers must hand-write launch metadata; and two same-family instances collide on shared state homes (~/.codex, the default Claude profile) because no per-instance home is derived or injected. Additionally, metadata.launch cannot carry environment variables today: the child env is fully service-controlled (fresh {...process.env} + PAT + minted bridge token + forced tool-projection var), so there is no sanctioned way to inject CODEX_HOME per agent.
The Architectural Reality
ai/services/fleet/FleetLifecycleService.mjs — start() builds the child env from a fresh copy of process.env, injects the PAT under credentialEnvVar, the minted bridge token under bridgeTokenEnvVar, and the forced-projection var; an env-key contract guard (~line 147) requires the three reserved keys to be non-empty and pairwise distinct. resolveLaunch() (~line 305) requires metadata.launch. The file's own header (~line 50) names "spawn-time identity-env + wake-subscription provisioning" as deferred work — this ticket ships the launch-env half of that seam (identity provisioning is the sibling ticket).
ai/services/fleet/startAgentProvisioned.mjs — provision-then-start pins cwd to the ensured checkout; fail-closed.
ai/services/fleet/deriveAgentRepoPath.mjs — the pure-module idiom this ticket mirrors: stable, collision-free, traversal-safe path math; the trusted root is a required argument, never read from env/config inside the module (config-is-SSOT).
- ADR-0019 read-gate applied at authoring. The one new config surface (
instanceRoot, the trusted root for per-instance harness state homes) is declared as a leaf(default, env, type) on the owning fleet config; pure modules receive it as a required argument (no A1 re-derivation, no B5 threading beyond the sanctioned entrypoint-injection boundary, no defensive ?. [B3], no hidden defaults [C2], no runtime mutation [B4]).
- Structural pre-flight: both new
.mjs files are sibling-pattern fast-path matches (deriveAgentRepoPath.mjs is the exact precedent, same folder).
The Fix
ai/services/fleet/deriveHarnessLaunchSpec.mjs (new, pure): ({harnessType, instanceHome, binaryPath}) → {command, args, env}. For 'codex': env: {CODEX_HOME: instanceHome}, command: binaryPath, interactive default with exec / app-server variants documented in the module JSDoc (the FM-daemon-drivable surfaces). For 'claude-code' (CLI): the user-data/profile-isolated variant. Unknown harnessType → throw (fail-closed, never guess a launcher — preserving the service's own design stance). Module JSDoc carries the recipe caveats: the app-bundled codex binary is an alpha channel that self-updates with the app (pin or verify version at spawn), rg availability note, and GUI-app launches (open -n -a Claude) documented as non-supervisable → excluded.
ai/services/fleet/deriveAgentInstanceHome.mjs (new, pure): ({instanceRoot, agentId, harnessType}) → contained absolute path, reusing the safeSegment idiom (extract it into a shared helper if cleaner — implementer's Tier-2 call, documented in the PR).
FleetLifecycleService.start(): merge agent.metadata.launch.env (optional Object) onto the child env before the reserved injections so reserved keys always win; extend the env-key contract guard — any launch.env key colliding with a reserved key → throw.
- Specs for both pure modules + the
start env-merge matrix (plain merge / reserved-win / collision-throw), canonical placement test/playwright/unit/ai/services/fleet/.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
metadata.launch.env |
FleetLifecycleService JSDoc |
optional Object merged onto child env; reserved-key collision throws |
absent → no extension (current behavior) |
service JSDoc |
unit specs |
deriveHarnessLaunchSpec |
module JSDoc |
pure per-family launch template; unknown type throws |
n/a |
module JSDoc |
unit specs |
deriveAgentInstanceHome |
module JSDoc |
pure contained instance-home path |
n/a |
module JSDoc |
unit specs |
fleet config instanceRoot leaf |
owning fleet config (ADR-0019 leaf) |
trusted per-instance-home root |
leaf default |
config JSDoc |
unit spec |
Decision Record impact
aligned-with ADR 0019 (read-gate applied; the new leaf follows §5 sanctioned patterns). No ADR challenged.
Acceptance Criteria
Out of Scope
GUI-app process supervision (open -n detaches); codex login automation (operator-owned auth — R2 isolation IS the credential boundary; secrets never ride the bridge); day-0 identity provisioning (sibling ticket); the Add-Peer account/config model + UI (#14807, @neo-opus-vega's lane); per-instance MCP config.toml templating (follow-up under #14807).
Related
Parent: #13015 · account model: #14807 · era layer: #14750 · roster join: #14802 / PR #14910 · the deferred-seam note: FleetLifecycleService.mjs header.
Live latest-open sweep: checked latest 20 open issues at 2026-07-10T02:45Z; no equivalent found (#14807 is the adjacent account-model umbrella, distinct scope). A2A in-flight claim sweep: last-60-min claims checked; no scope overlap.
Origin Session ID: b956ba53-01ed-4ea6-a1e5-62969f887bc3
Retrieval Hint: query_raw_memories("peer-onboarding rail CODEX_HOME isolation proof spawn seam launch template")
Context
Operator directive 2026-07-10: the cross-family merge gate is bus-factor-1 on a single gpt-family peer; a second real peer of that family doubles review bandwidth for the whole swarm. The peer-onboarding rail was triangulated tonight (author split ratified with @neo-opus-vega, Codex-side peer review claimed by @neo-gpt). The blocking unknown was instance isolation for Codex harnesses — resolved empirically tonight: the ChatGPT.app-bundled
codexCLI (codex-cli 0.144.0-alpha.4,<ChatGPT.app>/Contents/Resources/codex) honors theCODEX_HOMEenv var —codex doctoragainst a fresh probe home reported✗ auth — no credentials foundwhile the machine's real~/.codexlogin stayed live and untouched. The Claude-family counterpart is the operator's provenopen -n -a Claude --args --user-data-dir=<instance-dir>pattern. What's missing is the substrate that turns these recipes into Fleet-Manager-drivable launches.The Problem
FleetLifecycleServicelaunches an agent fromagent.metadata.launch = {command, args}— by design it refuses per-harnessType launch commands ("harnessType is classification, not a launcher"). Nothing in the fleet stack derives a correct per-family launch spec, sodefineAgentconsumers must hand-write launch metadata; and two same-family instances collide on shared state homes (~/.codex, the default Claude profile) because no per-instance home is derived or injected. Additionally,metadata.launchcannot carry environment variables today: the child env is fully service-controlled (fresh{...process.env}+ PAT + minted bridge token + forced tool-projection var), so there is no sanctioned way to injectCODEX_HOMEper agent.The Architectural Reality
ai/services/fleet/FleetLifecycleService.mjs—start()builds the child env from a fresh copy ofprocess.env, injects the PAT undercredentialEnvVar, the minted bridge token underbridgeTokenEnvVar, and the forced-projection var; an env-key contract guard (~line 147) requires the three reserved keys to be non-empty and pairwise distinct.resolveLaunch()(~line 305) requiresmetadata.launch. The file's own header (~line 50) names "spawn-time identity-env + wake-subscription provisioning" as deferred work — this ticket ships the launch-env half of that seam (identity provisioning is the sibling ticket).ai/services/fleet/startAgentProvisioned.mjs— provision-then-start pinscwdto the ensured checkout; fail-closed.ai/services/fleet/deriveAgentRepoPath.mjs— the pure-module idiom this ticket mirrors: stable, collision-free, traversal-safe path math; the trusted root is a required argument, never read from env/config inside the module (config-is-SSOT).instanceRoot, the trusted root for per-instance harness state homes) is declared as aleaf(default, env, type)on the owning fleet config; pure modules receive it as a required argument (no A1 re-derivation, no B5 threading beyond the sanctioned entrypoint-injection boundary, no defensive?.[B3], no hidden defaults [C2], no runtime mutation [B4])..mjsfiles are sibling-pattern fast-path matches (deriveAgentRepoPath.mjsis the exact precedent, same folder).The Fix
ai/services/fleet/deriveHarnessLaunchSpec.mjs(new, pure):({harnessType, instanceHome, binaryPath}) → {command, args, env}. For'codex':env: {CODEX_HOME: instanceHome},command: binaryPath, interactive default withexec/app-servervariants documented in the module JSDoc (the FM-daemon-drivable surfaces). For'claude-code'(CLI): the user-data/profile-isolated variant. UnknownharnessType→ throw (fail-closed, never guess a launcher — preserving the service's own design stance). Module JSDoc carries the recipe caveats: the app-bundled codex binary is an alpha channel that self-updates with the app (pin or verify version at spawn),rgavailability note, and GUI-app launches (open -n -a Claude) documented as non-supervisable → excluded.ai/services/fleet/deriveAgentInstanceHome.mjs(new, pure):({instanceRoot, agentId, harnessType}) →contained absolute path, reusing thesafeSegmentidiom (extract it into a shared helper if cleaner — implementer's Tier-2 call, documented in the PR).FleetLifecycleService.start(): mergeagent.metadata.launch.env(optional Object) onto the child env before the reserved injections so reserved keys always win; extend the env-key contract guard — anylaunch.envkey colliding with a reserved key → throw.startenv-merge matrix (plain merge / reserved-win / collision-throw), canonical placementtest/playwright/unit/ai/services/fleet/.Contract Ledger
metadata.launch.envFleetLifecycleServiceJSDocderiveHarnessLaunchSpecderiveAgentInstanceHomeinstanceRootleafDecision Record impact
aligned-with ADR 0019 (read-gate applied; the new leaf follows §5 sanctioned patterns). No ADR challenged.
Acceptance Criteria
deriveHarnessLaunchSpec.mjsships pure with codex + claude-code templates, fail-closed unknown-type, JSDoc recipes incl. alpha-channel + supervisability caveats, and unit specsderiveAgentInstanceHome.mjsships pure with containment + stability + collision-freedom specs (mirroring thederiveAgentRepoPathinvariants)FleetLifecycleService.startmergesmetadata.launch.envwith reserved-keys-always-win + collision-throw, specs cover the matrixinstanceRootdeclared as an ADR-0019-conformant leaf on the owning fleet config; pure modules receive it as an argumentFLEET_WIRE_METHODSunchanged);node --checkgreen on touched files; related unit dirs green--workers=1Out of Scope
GUI-app process supervision (
open -ndetaches);codex loginautomation (operator-owned auth — R2 isolation IS the credential boundary; secrets never ride the bridge); day-0 identity provisioning (sibling ticket); the Add-Peer account/config model + UI (#14807, @neo-opus-vega's lane); per-instance MCPconfig.tomltemplating (follow-up under #14807).Related
Parent: #13015 · account model: #14807 · era layer: #14750 · roster join: #14802 / PR #14910 · the deferred-seam note:
FleetLifecycleService.mjsheader.Live latest-open sweep: checked latest 20 open issues at 2026-07-10T02:45Z; no equivalent found (#14807 is the adjacent account-model umbrella, distinct scope). A2A in-flight claim sweep: last-60-min claims checked; no scope overlap.
Origin Session ID: b956ba53-01ed-4ea6-a1e5-62969f887bc3
Retrieval Hint:
query_raw_memories("peer-onboarding rail CODEX_HOME isolation proof spawn seam launch template")