From ab360d443cf88c1edeea72c18b77d73d0023fd88 Mon Sep 17 00:00:00 2001 From: be-coder-06 Date: Wed, 5 Aug 2026 23:28:28 -0500 Subject: [PATCH] fix(fleet): propagate roster git identity (#1043) --- docs/PRD.md | 30 +++++ docs/fleet/FLEET-LAUNCH.md | 23 ++-- .../concepts/generated-env-launch-chain.md | 7 +- .../fleet/reference/generated-env-boundary.md | 6 +- docs/scratchpads/1043-pane-git-identity.md | 111 ++++++++++++++++++ .../systemd/user/test-fleet-units.sh | 1 + .../tools/fleet/start-agent-session.sh | 9 +- .../tools/fleet/test-start-agent-session.sh | 50 ++++++++ .../src/commands/fleet-regen-command.spec.ts | 3 +- packages/mosaic/src/commands/fleet.spec.ts | 2 + packages/mosaic/src/commands/fleet.ts | 1 + packages/mosaic/src/fleet/fleet-agent-crud.ts | 1 + .../src/fleet/fleet-documentation.spec.ts | 8 +- packages/mosaic/src/fleet/fleet-reconciler.ts | 1 + .../src/fleet/generated-env-boundary.spec.ts | 18 +++ .../src/fleet/generated-env-boundary.ts | 8 ++ packages/mosaic/src/fleet/v1-v2-migration.ts | 1 + 17 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 docs/scratchpads/1043-pane-git-identity.md diff --git a/docs/PRD.md b/docs/PRD.md index 77ccd609..454f66cb 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -146,6 +146,36 @@ lands. M0 consists only of these normative requirements, the complete task DAG, documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards are defined in [docs/TASKS.md](./TASKS.md) and must remain one card/one PR. +### Fleet git identity launch propagation (#1043) + +#### Problem and objective + +A fleet seat can have a registered per-agent Git credential while its launched runtime process lacks +`MOSAIC_GIT_IDENTITY`. The credential resolver then cannot select the seat identity reliably, which +blocks repository operations on fail-closed estates and can fall through to an unrelated identity on +estates where that refusal is not active. The objective is to make Git identity a deterministic, +roster-derived part of the generated launch projection and prove it reaches the launched process. + +#### Normative requirements + +1. `FGI-REQ-01`: Every generated fleet agent projection SHALL declare + `MOSAIC_GIT_IDENTITY=`; a differing or unsafe identity SHALL fail closed before + tmux launch. +2. `FGI-REQ-02`: The clean `/usr/bin/env -i` pane boundary SHALL pass every variable declared by the + generated projection, including `MOSAIC_GIT_IDENTITY`, to the launched runtime process. +3. `FGI-REQ-03`: A behavioral integration test SHALL set-compare the complete generated projection + against the launched process environment. Source-text/string-presence assertions are insufficient. +4. `FGI-REQ-04`: Verification SHALL include RED-first evidence and a delete-the-subject mutation that + removes Git-identity pane propagation and makes the behavioral test fail. + +#### Acceptance criteria + +1. `AC-FGI-01`: A launched seat process contains every key/value pair declared by its generated + environment projection, including the roster-derived Git identity. +2. `AC-FGI-02`: Missing, unsafe, or split Git identity is rejected before a tmux session is created. +3. `AC-FGI-03`: Focused launcher and generated-environment tests, repository quality gates, + independent review, and the required RED/green/R7 evidence are recorded before push. + --- ## Exact Cross-Harness Fleet Communications Contract (#766) diff --git a/docs/fleet/FLEET-LAUNCH.md b/docs/fleet/FLEET-LAUNCH.md index 758a1e99..419372e7 100644 --- a/docs/fleet/FLEET-LAUNCH.md +++ b/docs/fleet/FLEET-LAUNCH.md @@ -5,14 +5,14 @@ Generated environment files are rebuildable projections, not an operator-editabl ## Launch chain -| Layer | Responsibility | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| Roster | `fleet/roster.yaml` supplies the agent name, class, supported runtime, model, reasoning, tool policy, workdir, and tmux socket. | -| Projection writer | Renders deterministic fleet/agents/.env.generated from the roster. | -| Optional local data | Reads a strict, data-only fleet/agents/.env.local; it cannot shadow generated keys. | -| systemd | Starts the launcher with env -i and fixed bootstrap data. It does not preload either environment file. | -| session launcher | Validates generated and local data before it queries, creates, or stops an exact tmux session. | -| runtime launch | Derives the fixed mosaic yolo argument array from validated roster data, then seeds the runtime contract. | +| Layer | Responsibility | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Roster | `fleet/roster.yaml` supplies the agent name, class, supported runtime, model, reasoning, tool policy, workdir, and tmux socket; Git identity is derived from the exact agent name. | +| Projection writer | Renders deterministic fleet/agents/.env.generated from the roster. | +| Optional local data | Reads a strict, data-only fleet/agents/.env.local; it cannot shadow generated keys. | +| systemd | Starts the launcher with env -i and fixed bootstrap data. It does not preload either environment file. | +| session launcher | Validates generated and local data before it queries, creates, or stops an exact tmux session. | +| runtime launch | Derives the fixed mosaic yolo argument array from validated roster data, then seeds the runtime contract. | The launcher never `source`s or `eval`s an environment file and never accepts an environment-supplied command. `MOSAIC_AGENT_COMMAND`, command/channel overrides, unknown keys, generated-key shadowing, @@ -24,6 +24,7 @@ secret-like key names, duplicate keys, comments, quoted/export syntax, and unsaf ```dotenv MOSAIC_AGENT_NAME= +MOSAIC_GIT_IDENTITY= MOSAIC_AGENT_CLASS= MOSAIC_AGENT_RUNTIME= MOSAIC_AGENT_MODEL= @@ -33,8 +34,10 @@ MOSAIC_AGENT_WORKDIR= MOSAIC_TMUX_SOCKET= ``` -The generated launch contract supports `claude`, `codex`, `opencode`, and `pi`. mosaic fleet add -rejects another runtime before it writes the roster or modifies generated, local, or quarantine state. +`MOSAIC_GIT_IDENTITY` is not independently configurable: it must equal `MOSAIC_AGENT_NAME`, preventing +split runtime and repository identity authority. The generated launch contract supports `claude`, +`codex`, `opencode`, and `pi`. mosaic fleet add rejects another runtime before it writes the roster or +modifies generated, local, or quarantine state. The legacy dogfood stub remains an observability-only canary on its separate `mosaic-factory` socket; it has no generated-launch adapter and cannot be added through this path. diff --git a/docs/fleet/concepts/generated-env-launch-chain.md b/docs/fleet/concepts/generated-env-launch-chain.md index 65cb582a..fc221b8f 100644 --- a/docs/fleet/concepts/generated-env-launch-chain.md +++ b/docs/fleet/concepts/generated-env-launch-chain.md @@ -3,11 +3,12 @@ The launcher consumes validated data, not shell configuration. 1. Read and validate the canonical roster. -2. Render deterministic .env.generated data from that roster. +2. Render deterministic .env.generated data from that roster, including `MOSAIC_GIT_IDENTITY` derived exactly from the roster agent name. 3. Parse optional .env.local through a strict allowlist. 4. Reject generated-key shadowing, unknown or sensitive-looking keys, unsafe paths/values, duplicates, malformed lines, shell syntax, and command overrides. -5. Derive the runtime command from validated runtime/model/reasoning data. -6. Target only the exact configured tmux socket and roster session after ownership checks. +5. Reject a Git identity that is unsafe or differs from the generated agent name. +6. Derive the runtime command from validated runtime/model/reasoning data and pass every generated projection entry through the clean process environment boundary. +7. Target only the exact configured tmux socket and roster session after ownership checks. ## File precedence and ownership diff --git a/docs/fleet/reference/generated-env-boundary.md b/docs/fleet/reference/generated-env-boundary.md index 5894da5e..7d97e39b 100644 --- a/docs/fleet/reference/generated-env-boundary.md +++ b/docs/fleet/reference/generated-env-boundary.md @@ -35,6 +35,7 @@ values, credential material, or command text. ```dotenv MOSAIC_AGENT_NAME= +MOSAIC_GIT_IDENTITY= MOSAIC_AGENT_CLASS= MOSAIC_AGENT_RUNTIME= MOSAIC_AGENT_MODEL= @@ -44,8 +45,9 @@ MOSAIC_AGENT_WORKDIR= MOSAIC_TMUX_SOCKET= ``` -The generated launch contract supports only `claude`, `codex`, `opencode`, and `pi`. fleet add -uses that same runtime authority and rejects any other runtime before it writes the roster or changes +`MOSAIC_GIT_IDENTITY` is derived from and must equal `MOSAIC_AGENT_NAME`; it is not a separate +operator-controlled identity authority. The generated launch contract supports only `claude`, `codex`, +`opencode`, and `pi`. fleet add uses that same runtime authority and rejects any other runtime before it writes the roster or changes projection, local, or quarantine files. The legacy dogfood stub on its separate `mosaic-factory` socket remains an observability canary; it has no generated-launch adapter and cannot be added through this projection path. diff --git a/docs/scratchpads/1043-pane-git-identity.md b/docs/scratchpads/1043-pane-git-identity.md new file mode 100644 index 00000000..3668a082 --- /dev/null +++ b/docs/scratchpads/1043-pane-git-identity.md @@ -0,0 +1,111 @@ +# #1043 — Fleet pane git-identity propagation + +## Objective + +Ensure a fleet seat's launched runtime process receives its roster-derived `MOSAIC_GIT_IDENTITY`, and lock the complete generated-environment propagation boundary with an enumerated set comparison. + +## Tracking + +- External issue: `mosaicstack/stack#1043` +- Branch: `fix/1043-pane-git-identity` +- Coordinator: `tl-mosaic` +- `docs/TASKS.md`: read-only by project worker contract; not modified. + +## Constraints + +- RED-first bug reproducer is mandatory. +- R7 delete-the-subject mutation must turn the behavioral test red. +- Assert launched-process environment, not source text. +- One push only; do not poll CI after push. +- Run the CI queue guard immediately before push and report its `state=` line as state, not evidence. +- Do not modify a live host launcher or obtain/copy another credential. +- Self-post the PR, verify provider attribution, then stop. +- Final status wording: `believed-fixed, pending jarvis validation`. + +## Scope inventory + +Re-derived against `origin/main` at `85d2108e`: + +- Launch consumer: `packages/mosaic/framework/tools/fleet/start-agent-session.sh` +- Behavioral launch test: `packages/mosaic/framework/tools/fleet/test-start-agent-session.sh` +- Generated-environment contract/parser: `packages/mosaic/src/fleet/generated-env-boundary.ts` +- Roster projection producers: + - `packages/mosaic/src/commands/fleet.ts` + - `packages/mosaic/src/fleet/fleet-reconciler.ts` + - `packages/mosaic/src/fleet/fleet-agent-crud.ts` + - `packages/mosaic/src/fleet/v1-v2-migration.ts` +- Contract and producer tests discovered by repository search. +- Generated-environment operator/developer docs and their executable documentation contract test. + +Discrepancy sent to `tl-mosaic`: current main no longer contains the charter's `PANE_SHELL_SNIPPET`; #772 replaced it with an `/usr/bin/env -i` argv launch boundary, and current generated projections do not declare git identity. Code-read inventory is **NOT MEASURED** behavior. + +## Plan + +1. Add the process-environment set-comparison regression first and record RED. +2. Add roster-derived `MOSAIC_GIT_IDENTITY=` to the complete generated projection contract. +3. Validate identity syntax and equality with `MOSAIC_AGENT_NAME`; pass it through the clean pane environment. +4. Update affected projection tests and generated-environment docs. +5. Run focused and baseline gates. +6. Perform R7 by deleting the pane propagation entry, prove RED, restore, and prove GREEN. +7. Run independent review, remediate, commit, queue guard, one push, self-post PR, verify provider attribution, and stop without CI polling. + +## Budget + +No explicit token cap was provided. Working cap: one narrow logical unit, no dependency installation unless existing tooling requires it, no unrelated refactor. + +## Evidence log + +### TDD and mutation evidence + +- RED-first, repository launcher: `bash packages/mosaic/framework/tools/fleet/test-start-agent-session.sh` exited 64 on pre-fix source with `code=unknown-key key=MOSAIC_GIT_IDENTITY`. The generated seat could not launch with the required declared identity. +- GREEN: the same repository launcher test emitted `ok - start-agent-session generated environment boundary`. +- R7 delete-the-subject: removed only `"MOSAIC_GIT_IDENTITY=$MOSAIC_GIT_IDENTITY"` from the repository launch array; the same test exited 1 with `FAIL: runtime pane omitted or changed generated environment keys: MOSAIC_GIT_IDENTITY`. +- R7 restoration: restored that launch entry; the same test returned green. +- Launcher under test is explicitly `packages/mosaic/framework/tools/fleet/start-agent-session.sh` through the test's `$START`, **not** the stale installed host copy. + +### Situational and focused tests + +- Repository launcher boundary: green, including set comparison of all nine generated projection entries and fail-before-tmux cases for missing, unsafe, mismatched, and local-shadow Git identity. +- Fleet systemd launcher integration: `bash packages/mosaic/framework/systemd/user/test-fleet-units.sh` — green. +- Focused Mosaic Vitest set: 6 files, 311 tests — green. +- `bash -n` on changed shell files — green. +- `git diff --check` — green. + +### Baseline gates + +- `pnpm typecheck` — 45/45 tasks green. +- `pnpm lint` — 25/25 tasks green. +- `pnpm format:check` — green. +- `pnpm test:checkout` — green. +- Repository-wide Vitest under a hermetic current-version npm prefix: Mosaic 81/81 files and 1510/1510 tests green; other workspace test tasks shown green before the framework-shell phase. +- Canonical `pnpm test` is not fully green on this host for unrelated environment-sensitive gates: + 1. the first two runs exposed the globally installed Mosaic 0.0.48 update banner in three CLI smoke tests expecting empty stderr; + 2. after isolating that global-version input, the framework wake assertion aborted at the known `#973` Bash `BASH_LINENO` convention check (exit 97; observed `[3 5]`, expected `[3 4]`). + No tests were weakened or bypassed; focused changed-surface tests are green. CI remains the canonical clean-environment result and is intentionally not polled after push per charter. + +### Independent review + +- Codex code review first pass: request changes for missing shell rejection-path coverage. +- Remediation: added table-driven missing/unsafe/mismatch/local-shadow launcher cases, each asserting no tmux call. +- Codex code re-review: **approve**, no findings, confidence 0.88. +- Codex security review: risk `none`, no findings, confidence 0.97. + +### Acceptance criteria mapping + +| Acceptance criterion | Evidence | +| --- | --- | +| AC-FGI-01: launched process receives every generated key/value | Repository launcher process-environment `comm -23` set comparison; GREEN and R7 RED evidence above | +| AC-FGI-02: missing, unsafe, or split identity fails before tmux | Table-driven shell cases plus TypeScript generated-boundary tests | +| AC-FGI-03: focused/baseline/review evidence recorded | Commands and review outcomes above; host-sensitive full-suite limitations stated explicitly | + +### Documentation checklist + +- PRD updated with #1043 requirements and acceptance criteria. +- Fleet launch runbook, generated-env concept, and generated-env reference updated. +- No API/OpenAPI, sitemap, user publishing target, deployment, or external docs publication change applies. +- `docs/TASKS.md` remains unmodified per its single-writer project contract. + +## Residual risk + +- Landing on `main` does not update the currently installed host launcher. Host framework installation/reseed and Jarvis live-seat validation are separate downstream events. +- Canonical CI result is pending and will not be polled by this seat. diff --git a/packages/mosaic/framework/systemd/user/test-fleet-units.sh b/packages/mosaic/framework/systemd/user/test-fleet-units.sh index 6973a9ce..91b4a34e 100755 --- a/packages/mosaic/framework/systemd/user/test-fleet-units.sh +++ b/packages/mosaic/framework/systemd/user/test-fleet-units.sh @@ -112,6 +112,7 @@ EOF chmod 700 "$AGENT_HOME/fleet/agents" cat > "$AGENT_HOME/fleet/agents/$AGENT_NAME.env.generated" < "$home/fleet/agents/$agent.env.generated" < "$home/fleet/agents/$agent.env.local" + chmod 600 "$home/fleet/agents/$agent.env.local" + ;; + *) fail "unknown Git identity rejection case: $case_name" ;; + esac + chmod 600 "$generated" + + : > "$TMUX_CALLS" + if output=$(run_start "$home" "$agent" 2>&1); then + fail "Git identity case $case_name was accepted" + fi + [ ! -s "$TMUX_CALLS" ] || fail "tmux ran before Git identity $case_name rejection" + echo "$output" | grep -qF "code=$expected_code" || \ + fail "Git identity $case_name diagnostic omitted code $expected_code" +} + +assert_git_identity_rejected missing missing-key +assert_git_identity_rejected unsafe unsafe-git-identity +assert_git_identity_rejected mismatch git-identity-mismatch +assert_git_identity_rejected local-shadow generated-key-shadow + # The generated-file parent is a security boundary too: even a private regular # file is untrusted if its parent can be replaced or written by another user. # Validation must happen before fake tmux receives even a has-session call. @@ -274,6 +311,18 @@ after_pane_env=$(printf '%s\n' "$pane_args" | grep -n -m1 -F '/usr/bin/env' | cu printf '%s\n' "$pane_args" | tail -n +"$after_pane_env" | grep -qxF -- '-i' || \ fail "pane command did not clear its environment" pane_environment=$(tr '\0' '\n' < "$HOME_PANE_BOUNDARY/fleet/pane-environment") +# Exercise the repository launcher at $START, not the independently installed +# host copy. Set-compare every declared generated projection entry with the +# launched process environment so a newly declared identity cannot be omitted +# by a hand-maintained per-variable assertion. +declared_generated_environment=$(sort "$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.generated") +missing_or_changed_generated_environment=$(comm -23 \ + <(printf '%s\n' "$declared_generated_environment") \ + <(printf '%s\n' "$pane_environment" | sort)) +if [ -n "$missing_or_changed_generated_environment" ]; then + missing_or_changed_keys=$(printf '%s\n' "$missing_or_changed_generated_environment" | cut -d= -f1 | paste -sd, -) + fail "runtime pane omitted or changed generated environment keys: $missing_or_changed_keys" +fi echo "$pane_environment" | grep -qxF "HOME=$PANE_TRUSTED_HOME" || \ fail "runtime pane did not receive trusted HOME" echo "$pane_environment" | grep -qF "$PANE_STALE_PATH" && fail "runtime pane received stale PATH" @@ -290,6 +339,7 @@ write_interaction_generated() { chmod 600 "$home/fleet/run/holder-owner" cat > "$home/fleet/agents/$agent.env.generated" < { } describe('projectRosterV2AgentGeneratedEnv', (): void => { - it('maps a roster-v2 agent to exactly the eight generated projection keys', (): void => { + it('maps a roster-v2 agent to exactly the nine generated projection keys', (): void => { const roster = parseRosterV2(rosterYaml, 'yaml'); const agent = roster.agents.find((candidate) => candidate.name === 'coder0'); expect(agent).toBeDefined(); const values = projectRosterV2AgentGeneratedEnv(roster, agent!); expect(values).toEqual({ MOSAIC_AGENT_NAME: 'coder0', + MOSAIC_GIT_IDENTITY: 'coder0', MOSAIC_AGENT_CLASS: 'code', MOSAIC_AGENT_RUNTIME: 'pi', MOSAIC_AGENT_MODEL: 'gpt-5.6-sol', diff --git a/packages/mosaic/src/commands/fleet.spec.ts b/packages/mosaic/src/commands/fleet.spec.ts index 9aaaf055..f0a468da 100644 --- a/packages/mosaic/src/commands/fleet.spec.ts +++ b/packages/mosaic/src/commands/fleet.spec.ts @@ -422,6 +422,7 @@ describe('fleet roster parsing', () => { expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe( [ 'MOSAIC_AGENT_NAME=coder0', + 'MOSAIC_GIT_IDENTITY=coder0', // Reflects the roster's canonicalized compatibility class (A3a). 'MOSAIC_AGENT_CLASS=code', 'MOSAIC_AGENT_RUNTIME=codex', @@ -3799,6 +3800,7 @@ describe('fleet add command', () => { 'utf8', ); expect(envContent).toContain('MOSAIC_AGENT_NAME=coder0'); + expect(envContent).toContain('MOSAIC_GIT_IDENTITY=coder0'); expect(envContent).toContain('MOSAIC_AGENT_RUNTIME=codex'); }); diff --git a/packages/mosaic/src/commands/fleet.ts b/packages/mosaic/src/commands/fleet.ts index c344660a..9e392604 100644 --- a/packages/mosaic/src/commands/fleet.ts +++ b/packages/mosaic/src/commands/fleet.ts @@ -484,6 +484,7 @@ function generateAgentEnvValues( const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory; return { MOSAIC_AGENT_NAME: agent.name, + MOSAIC_GIT_IDENTITY: agent.name, MOSAIC_AGENT_CLASS: agent.className, MOSAIC_AGENT_RUNTIME: agent.runtime, MOSAIC_AGENT_MODEL: agent.modelHint ?? '', diff --git a/packages/mosaic/src/fleet/fleet-agent-crud.ts b/packages/mosaic/src/fleet/fleet-agent-crud.ts index 9795a406..51bf7ed5 100644 --- a/packages/mosaic/src/fleet/fleet-agent-crud.ts +++ b/packages/mosaic/src/fleet/fleet-agent-crud.ts @@ -358,6 +358,7 @@ function generatedValues( ): Readonly> { return { MOSAIC_AGENT_NAME: agent.name, + MOSAIC_GIT_IDENTITY: agent.name, MOSAIC_AGENT_CLASS: agent.className, MOSAIC_AGENT_RUNTIME: agent.runtime, MOSAIC_AGENT_MODEL: agent.model, diff --git a/packages/mosaic/src/fleet/fleet-documentation.spec.ts b/packages/mosaic/src/fleet/fleet-documentation.spec.ts index 2e5a108e..3254f19c 100644 --- a/packages/mosaic/src/fleet/fleet-documentation.spec.ts +++ b/packages/mosaic/src/fleet/fleet-documentation.spec.ts @@ -380,7 +380,7 @@ const COMMAND_RECORDS: Readonly> = { const DATA_PROFILE_BODIES: Readonly> = { 'DATA.DOTENV.FLEET_LAUNCH': - 'MOSAIC_AGENT_NAME=\nMOSAIC_AGENT_CLASS=\nMOSAIC_AGENT_RUNTIME=\nMOSAIC_AGENT_MODEL=\nMOSAIC_AGENT_REASONING=\nMOSAIC_AGENT_TOOL_POLICY=\nMOSAIC_AGENT_WORKDIR=\nMOSAIC_TMUX_SOCKET=', + 'MOSAIC_AGENT_NAME=\nMOSAIC_GIT_IDENTITY=\nMOSAIC_AGENT_CLASS=\nMOSAIC_AGENT_RUNTIME=\nMOSAIC_AGENT_MODEL=\nMOSAIC_AGENT_REASONING=\nMOSAIC_AGENT_TOOL_POLICY=\nMOSAIC_AGENT_WORKDIR=\nMOSAIC_TMUX_SOCKET=', 'DATA.TEXT_TABLE.FLEET_TASKS': '| W-FLEET | in-progress | Fleet (agent-session execution layer) | Phase 2/5 | docs/fleet/TASKS.md | observability dogfooded on live stub fleet; control plane rides federation (W1) |', 'DATA.TEXT_DIAGRAM.BACKLOG_FLOW': @@ -406,7 +406,7 @@ const DATA_PROFILE_BODIES: Readonly> = { 'DATA.JSON.MUTATION_RESULT': '{\n "applied": false,\n "authoritativeRoster": "committed",\n "projections": "incomplete",\n "recovery": {\n "code": "projection-apply-failed",\n "action": "regenerate-projections-from-roster"\n }\n}', 'DATA.DOTENV.GENERATED_ENV': - 'MOSAIC_AGENT_NAME=\nMOSAIC_AGENT_CLASS=\nMOSAIC_AGENT_RUNTIME=\nMOSAIC_AGENT_MODEL=\nMOSAIC_AGENT_REASONING=\nMOSAIC_AGENT_TOOL_POLICY=\nMOSAIC_AGENT_WORKDIR=\nMOSAIC_TMUX_SOCKET=', + 'MOSAIC_AGENT_NAME=\nMOSAIC_GIT_IDENTITY=\nMOSAIC_AGENT_CLASS=\nMOSAIC_AGENT_RUNTIME=\nMOSAIC_AGENT_MODEL=\nMOSAIC_AGENT_REASONING=\nMOSAIC_AGENT_TOOL_POLICY=\nMOSAIC_AGENT_WORKDIR=\nMOSAIC_TMUX_SOCKET=', 'DATA.YAML.ROSTER_FIELDS': 'version: 2\ngeneration: 1\ntransport: tmux\ntmux:\n socket_name: mosaic-fleet\n holder_session: _holder\ndefaults:\n working_directory: ~/src\n runtime: pi\nruntimes:\n pi:\n reset_command: /new\nagents:\n - name: coder0\n alias: Coder 0\n class: code\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: high\n tool_policy: code\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true', }; @@ -922,8 +922,8 @@ describe('fleet operator documentation', (): void => { ); expect( surfaces.filter((surface): boolean => surface.category === 'InlineLiteral'), - ).toHaveLength(858); - expect(surfaces).toHaveLength(882); + ).toHaveLength(863); + expect(surfaces).toHaveLength(887); const rosterSource = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8'); const auxiliary: CodeSurface = { diff --git a/packages/mosaic/src/fleet/fleet-reconciler.ts b/packages/mosaic/src/fleet/fleet-reconciler.ts index 732c878d..21578e25 100644 --- a/packages/mosaic/src/fleet/fleet-reconciler.ts +++ b/packages/mosaic/src/fleet/fleet-reconciler.ts @@ -597,6 +597,7 @@ export function projectRosterV2AgentGeneratedEnv( ): Readonly> { return { MOSAIC_AGENT_NAME: agent.name, + MOSAIC_GIT_IDENTITY: agent.name, MOSAIC_AGENT_CLASS: agent.className, MOSAIC_AGENT_RUNTIME: agent.runtime, MOSAIC_AGENT_MODEL: agent.model, diff --git a/packages/mosaic/src/fleet/generated-env-boundary.spec.ts b/packages/mosaic/src/fleet/generated-env-boundary.spec.ts index c0c8a96e..bf76a70b 100644 --- a/packages/mosaic/src/fleet/generated-env-boundary.spec.ts +++ b/packages/mosaic/src/fleet/generated-env-boundary.spec.ts @@ -22,6 +22,7 @@ import { const generatedValues = { MOSAIC_AGENT_NAME: 'coder0', + MOSAIC_GIT_IDENTITY: 'coder0', MOSAIC_AGENT_CLASS: 'code', MOSAIC_AGENT_RUNTIME: 'pi', MOSAIC_AGENT_MODEL: 'openai-codex/gpt-5.6-sol', @@ -45,6 +46,7 @@ describe('generated fleet agent environment boundary', (): void => { expect(renderGeneratedAgentEnvironment(generatedValues)).toBe( [ 'MOSAIC_AGENT_NAME=coder0', + 'MOSAIC_GIT_IDENTITY=coder0', 'MOSAIC_AGENT_CLASS=code', 'MOSAIC_AGENT_RUNTIME=pi', 'MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol', @@ -78,6 +80,22 @@ describe('generated fleet agent environment boundary', (): void => { expect(String(error)).toMatch(/key=.*sha256=/); }); + it.each([ + ['unsafe-git-identity', 'other/identity'], + ['git-identity-mismatch', 'reviewer0'], + ])('rejects %s before any launch consumer can use it', (code: string, identity: string): void => { + expect((): void => { + renderGeneratedAgentEnvironment({ + ...generatedValues, + MOSAIC_GIT_IDENTITY: identity, + }); + }).toThrow( + expect.objectContaining({ + diagnostic: expect.objectContaining({ code, key: 'MOSAIC_GIT_IDENTITY' }), + }), + ); + }); + it('rejects unsafe generated paths before any launch consumer can use them', (): void => { expect((): void => { renderGeneratedAgentEnvironment({ diff --git a/packages/mosaic/src/fleet/generated-env-boundary.ts b/packages/mosaic/src/fleet/generated-env-boundary.ts index cd849440..54d68415 100644 --- a/packages/mosaic/src/fleet/generated-env-boundary.ts +++ b/packages/mosaic/src/fleet/generated-env-boundary.ts @@ -73,6 +73,7 @@ export class AgentEnvBoundaryError extends Error { export const GENERATED_AGENT_ENV_KEYS = [ 'MOSAIC_AGENT_NAME', + 'MOSAIC_GIT_IDENTITY', 'MOSAIC_AGENT_CLASS', 'MOSAIC_AGENT_RUNTIME', 'MOSAIC_AGENT_MODEL', @@ -402,6 +403,7 @@ function assertGeneratedValues(values: Readonly>): void { if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, ''); } const name = requiredGeneratedValue(values, 'MOSAIC_AGENT_NAME'); + const gitIdentity = requiredGeneratedValue(values, 'MOSAIC_GIT_IDENTITY'); const className = requiredGeneratedValue(values, 'MOSAIC_AGENT_CLASS'); const runtime = requiredGeneratedValue(values, 'MOSAIC_AGENT_RUNTIME'); const model = requiredGeneratedValue(values, 'MOSAIC_AGENT_MODEL'); @@ -412,6 +414,12 @@ function assertGeneratedValues(values: Readonly>): void { if (!AGENT_NAME.test(name)) throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', name); + if (!AGENT_NAME.test(gitIdentity)) { + throw new AgentEnvBoundaryError('unsafe-git-identity', 'MOSAIC_GIT_IDENTITY', gitIdentity); + } + if (gitIdentity !== name) { + throw new AgentEnvBoundaryError('git-identity-mismatch', 'MOSAIC_GIT_IDENTITY', gitIdentity); + } if (!POLICY_NAME.test(className)) { throw new AgentEnvBoundaryError('unsafe-class', 'MOSAIC_AGENT_CLASS', className); } diff --git a/packages/mosaic/src/fleet/v1-v2-migration.ts b/packages/mosaic/src/fleet/v1-v2-migration.ts index 69961d28..4f078b10 100644 --- a/packages/mosaic/src/fleet/v1-v2-migration.ts +++ b/packages/mosaic/src/fleet/v1-v2-migration.ts @@ -1405,6 +1405,7 @@ function generatedValues( ): Readonly> { return { MOSAIC_AGENT_NAME: agent.name, + MOSAIC_GIT_IDENTITY: agent.name, MOSAIC_AGENT_CLASS: agent.className, MOSAIC_AGENT_RUNTIME: agent.runtime, MOSAIC_AGENT_MODEL: agent.model,