LearnNewsExamplesServices
Frontmatter
id14914
titleFleet: per-family launch templates + per-agent child-env extension
stateClosed
labels
enhancementaiarchitecture
assigneesneo-fable
createdAt4:41 AM
updatedAt8:15 AM
githubUrlhttps://github.com/neomjs/neo/issues/14914
authorneo-fable
commentsCount3
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAt8:15 AM

Fleet: per-family launch templates + per-agent child-env extension

Closed Backlog/active-chunk-4 enhancementaiarchitecture
neo-fable
neo-fable commented on 4:41 AM

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.mjsstart() 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

  1. 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 harnessTypethrow (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.
  2. 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).
  3. 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.
  4. 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

  • deriveHarnessLaunchSpec.mjs ships pure with codex + claude-code templates, fail-closed unknown-type, JSDoc recipes incl. alpha-channel + supervisability caveats, and unit specs
  • deriveAgentInstanceHome.mjs ships pure with containment + stability + collision-freedom specs (mirroring the deriveAgentRepoPath invariants)
  • FleetLifecycleService.start merges metadata.launch.env with reserved-keys-always-win + collision-throw, specs cover the matrix
  • instanceRoot declared as an ADR-0019-conformant leaf on the owning fleet config; pure modules receive it as an argument
  • Zero new wire verbs (FLEET_WIRE_METHODS unchanged); node --check green on touched files; related unit dirs green --workers=1

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")

tobiu referenced in commit 01556e4 - "feat(fleet): per-family harness launch templates + per-agent child env (#14914) (#14918) on 8:15 AM
tobiu closed this issue on 8:15 AM