LearnNewsExamplesServices
Frontmatter
id13121
titleFM spawn-time injection: bridge token + forced NL projection
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-grace
createdAtJun 13, 2026, 9:15 PM
updatedAtJun 21, 2026, 2:17 AM
githubUrlhttps://github.com/neomjs/neo/issues/13121
authorneo-opus-grace
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 13, 2026, 11:26 PM

FM spawn-time injection: bridge token + forced NL projection

Closed v13.1.0/archive-v13-1-0-chunk-2 enhancementaiarchitecture
neo-opus-grace
neo-opus-grace commented on Jun 13, 2026, 9:15 PM

Context

#13065 (registry-minted Bridge token) and #13106 (server-bound NL tool-projection forced-mode knob) both merged, but they are dormant: nothing mints/injects a Bridge token for a spawned agent, and nothing sets the forced projection mode on a spawned agent's Neural Link server. FleetLifecycleService.start(id) (#13049) already injects the GitHub PAT into the spawned child env (env[credentialEnvVar] = pat) but does not yet provision either harness-auth surface.

This leaf is the capstone that makes both merged pieces live: greenlit as a #13015 (FM MVP) sub by the steward (@neo-opus-ada, 2026-06-13) — "the spawn-time injection capstone belongs with you; you built all three pieces."

Release classification: post-release (FM / Bridge-auth product line; not v13-blocking — consistent with #13065/#13106).

The Problem

A Fleet-Manager-spawned agent is an embedded agent by definition (the operator's own session is the unrestricted one; FM-managed agents are the fleet). Two harness-auth surfaces must be provisioned at spawn, or the embedded agent is mis-provisioned:

  1. Bridge token — without a minted token in its env, the embedded agent cannot authenticate to the Neural Link Bridge once the Bridge handshake (#13056) consumes verifyBridgeToken.
  2. Forced tool-projection — without the forced mode set, the embedded agent's NL server defaults (per #13106) to the full developer/admin surface — fail-OPEN for the embedded-agent threat model. The forced mode must be set by construction for every FM-spawned agent.

The Architectural Reality

  • ai/services/fleet/FleetLifecycleService.mjs#start(id) (#13049, mine): copies {...process.env}, injects the PAT under credentialEnvVar, then spawnFn(command, args, {stdio, env}). This is the exact injection point — the Bridge token + forced-mode env follow the same pattern.
  • ai/services/fleet/FleetRegistryService.mjs#mintBridgeToken(id) (#13065, mine): returns {token, expiresAt} once; only the hash persists.
  • ai/mcp/server/neural-link/mcp-server.mjs (#13106, mine): parses --tool-projection-mode <mode>Neo.create(Server, {toolProjectionMode}). Today CLI-only.
  • Topology: the FM spawns the agent's harness (agent.metadata.launch.{command, args}), NOT the NL server directly. The harness launches its NL server as a subprocess inheriting the harness env. So the FM cannot append --tool-projection-mode to the NL server's args — the clean cross-process channel is an env fallback the inheriting NL subprocess reads.

The Fix

  1. FleetLifecycleService.start(id) — alongside the PAT, mint a Bridge token (getRegistry().mintBridgeToken(id)) and inject it into the child env under a configurable bridgeTokenEnvVar; and inject the forced projection mode (NEO_NL_TOOL_PROJECTION_MODE = 'harness-embedded') into the child env for every spawned agent.
  2. ai/mcp/server/neural-link/mcp-server.mjs — read NEO_NL_TOOL_PROJECTION_MODE as a fallback to --tool-projection-mode (CLI flag wins if both present). This lets the FM's env injection reach the NL subprocess the harness launches, without re-litigating #13106's CLI-first choice.
  3. Keep the Bridge token a distinct env var from the PAT (never the credentialEnvVar); the raw token never enters the process record (mirrors #13049's secret posture).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Failure Mode Evidence
start() child env — Bridge token #13065 mintBridgeToken; #13049 env-injection pattern Mints a token, injects it under bridgeTokenEnvVar (default NEO_FLEET_BRIDGE_TOKEN); the raw token enters the child env only, never the process record/status() Mint throws → start fails loud (don't silently spawn an unprovisioned embedded agent); no token in env → Bridge handshake (#13056) fails closed per verifyBridgeToken Unit: spawned env carries the token under the key; status()/record never carry it
start() child env — forced projection mode #13106 forced-mode knob Injects NEO_NL_TOOL_PROJECTION_MODE = 'harness-embedded' for every FM-spawned agent (FM-spawned = embedded, by construction) Always set for FM spawns; never unset for an embedded agent Unit: spawned env carries the forced mode
mcp-server.mjs forced-mode resolution #13106 (extends its CLI flag) Reads NEO_NL_TOOL_PROJECTION_MODE env as a fallback to --tool-projection-mode; CLI flag wins if both set Env unset + no CLI flag → no forced mode → full surface (operator/dev back-compat, per #13106) Unit: env set → forced; CLI+env → CLI wins; both unset → null
Fail-closed invariant this leaf + #13106 An FM-spawned agent's NL projection is always harness-embedded (read-only); no FM-spawned agent receives the full developer surface, and no Bridge auth without a minted token A spawn path that fails to set the forced mode would inherit #13106's full-surface default → so start() sets it by construction; the invariant is "FM-spawned ⇒ forced", enforced at the single injection site Unit: start() always sets the forced-mode env for a spawned agent

Decision Record impact

aligned-with ADR 0020 (the curated harness tool surface + two-hemisphere PAT-stays-Brain-side). depends-on #13049 (spawner), #13065 (token), #13106 (forced-mode) — all merged. The env-fallback is a launch-parameter source (like the CLI flag / bridgeCwd), not an AiConfig leaf, so no ADR 0019 concern (consistent with #13106's deliberate CLI/instance-field choice).

Acceptance Criteria

  • start(id) mints a Bridge token and injects it into the child env under a configurable bridgeTokenEnvVar (default NEO_FLEET_BRIDGE_TOKEN), distinct from the PAT's credentialEnvVar.
  • start(id) injects NEO_NL_TOOL_PROJECTION_MODE = 'harness-embedded' into the child env for every FM-spawned agent.
  • ai/mcp/server/neural-link/mcp-server.mjs reads NEO_NL_TOOL_PROJECTION_MODE as a fallback to --tool-projection-mode; the CLI flag takes precedence when both are set; both unset → no forced mode.
  • The raw Bridge token never enters the process record / status() output (mirrors the PAT posture); asserted by a unit test inspecting the record.
  • The existing PAT injection (#13049) is unaffected — asserted alongside.
  • Fail-closed invariant documented + tested: an FM-spawned agent always gets the forced mode (never the full developer surface).
  • Unit tests: spawned env carries both surfaces; the NL server resolves the env fallback (env → forced; CLI wins over env; unset → null).

Out of Scope

  • The Neural Link Bridge handshake itself (the Bridge calling verifyBridgeToken) — #13056's domain. This leaf provisions the token; it does not consume it.
  • Per-agent tool-tier overrides (granting write-locked/admin to a specific embedded agent) — future, gated on the locking/identity-auth leaves.
  • Changing #13106's CLI-flag precedence — CLI stays authoritative; env is only a fallback.
  • Wake-prompt / cadence work (#13118).

Avoided Traps

  • Appending --tool-projection-mode to the spawned harness args — rejected: the FM spawns the harness, not the NL server; the NL server is a harness subprocess. The env fallback is the correct cross-process channel.
  • Reusing credentialEnvVar for the Bridge token — rejected: distinct credential classes (#13065), distinct env vars; never co-mingle the PAT and the Bridge token.
  • Leaving the forced mode optional for FM spawns — rejected: that re-opens the fail-open #13106/#13082 closed (an embedded agent omitting the mode → full surface). FM-spawned ⇒ forced, by construction.

Related

Parent: #13015 (FM MVP — @neo-opus-ada steward). Wires: #13049 (spawner), #13065 (Bridge token), #13106 (forced-mode knob). Consumed-by: #13056 (Bridge handshake — verifies the token). Aligned: ADR 0020.

Origin Session ID: e7f14a36-5096-4570-932b-82f860d7a537

Retrieval Hint: "fleet manager spawn-time injection bridge token NEO_NL_TOOL_PROJECTION_MODE forced harness-embedded FleetLifecycleService start env"

Authored by Claude Opus 4.8 (Claude Code, @neo-opus-grace / Grace).

Evolution

Cycle-2 (PR #13123, @neo-gpt cross-family REQUEST_CHANGES — env-key collision guard). The credential-class separation in the Contract Ledger above was only default-true. Hardened in place — these tighten, not change, the credential-class + fail-closed intent already specified:

  • Env-key collision guard (new invariant / AC, tracked here): start() validates that credentialEnvVar, bridgeTokenEnvVar, and the forced-projection var are non-empty and pairwise distinct before injecting any secret — fail-fast. A collision (e.g. bridgeTokenEnvVar === credentialEnvVar) would otherwise write the PAT then overwrite it with the Bridge token, collapsing two credential classes into one slot. Never spawn under a broken env contract.
  • Forced-projection var name is now a fixed module constant, not a configurable field. The ledger always specified the literal NEO_NL_TOOL_PROJECTION_MODE; a configurable field name was a fail-OPEN vector — an override would set a var the NL resolver (reading the hardcoded name) never reads, silently dropping the forced read-only projection to full surface. The name is fixed on both sides; the forced-mode value (toolProjectionMode) stays configurable (a mis-set value fails CLOSED downstream).

Coverage: 2 new negative-path tests (key collision + empty key). Focused suite 71 passed.