From e322ca68bef8bd0123b36c2b7937c3b1b77a4e2a Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 20 Jun 2026 16:32:39 -0500 Subject: [PATCH] fix(fleet): preserve agent env overrides on install --- packages/mosaic/package.json | 2 +- packages/mosaic/src/commands/fleet.spec.ts | 32 ++++++++++++++++++++++ packages/mosaic/src/commands/fleet.ts | 30 +++++++++++++++++--- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index b0f9430..540b814 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -1,6 +1,6 @@ { "name": "@mosaicstack/mosaic", - "version": "0.0.32", + "version": "0.0.33", "repository": { "type": "git", "url": "https://git.mosaicstack.dev/mosaicstack/stack.git", diff --git a/packages/mosaic/src/commands/fleet.spec.ts b/packages/mosaic/src/commands/fleet.spec.ts index c4ec4c3..28f348b 100644 --- a/packages/mosaic/src/commands/fleet.spec.ts +++ b/packages/mosaic/src/commands/fleet.spec.ts @@ -10,6 +10,7 @@ import { getDefaultOperatorSourceLabel, getRosterAgent, loadFleetRoster, + mergeAgentEnv, registerFleetCommand, resolveFleetPaths, type CommandRunner, @@ -121,6 +122,37 @@ describe('fleet roster parsing', () => { ); }); + it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => { + const generated = [ + 'MOSAIC_AGENT_NAME=coder0', + 'MOSAIC_AGENT_RUNTIME=codex', + 'MOSAIC_AGENT_WORKDIR=/srv/new', + 'MOSAIC_TMUX_SOCKET=mosaic-factory', + '', + ].join('\n'); + const existing = [ + 'MOSAIC_AGENT_NAME=old-name', + 'MOSAIC_AGENT_RUNTIME=old-runtime', + 'MOSAIC_AGENT_WORKDIR=/srv/old', + 'MOSAIC_TMUX_SOCKET=old-socket', + 'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh', + '# site note', + '', + ].join('\n'); + + expect(mergeAgentEnv(generated, existing)).toBe( + [ + 'MOSAIC_AGENT_NAME=coder0', + 'MOSAIC_AGENT_RUNTIME=codex', + 'MOSAIC_AGENT_WORKDIR=/srv/new', + 'MOSAIC_TMUX_SOCKET=mosaic-factory', + 'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh', + '# site note', + '', + ].join('\n'), + ); + }); + it('rejects unknown roster fields instead of silently defaulting', async () => { cleanup = await tempDir(); const rosterPath = join(cleanup, 'roster.yaml'); diff --git a/packages/mosaic/src/commands/fleet.ts b/packages/mosaic/src/commands/fleet.ts index 3eb8f61..bcfb85b 100644 --- a/packages/mosaic/src/commands/fleet.ts +++ b/packages/mosaic/src/commands/fleet.ts @@ -148,6 +148,29 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string ].join('\n'); } +export function mergeAgentEnv(generatedEnv: string, existingEnv?: string): string { + if (!existingEnv?.trim()) { + return generatedEnv; + } + const generatedKeys = new Set( + generatedEnv + .split('\n') + .map((line) => line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1]) + .filter((key): key is string => key !== undefined), + ); + const preservedLines = existingEnv.split('\n').filter((line) => { + if (!line.trim()) { + return false; + } + const key = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1]; + return key === undefined || !generatedKeys.has(key); + }); + if (preservedLines.length === 0) { + return generatedEnv; + } + return [generatedEnv.trimEnd(), ...preservedLines, ''].join('\n'); +} + export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] { const service = agentName ? `mosaic-agent@${agentName}.service` : 'mosaic-tmux-holder.service'; return ['systemctl', '--user', action, service]; @@ -477,10 +500,9 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise ); for (const agent of roster.agents) { - await writeFile( - join(activePaths.agentEnvDir, `${agent.name}.env`), - generateAgentEnv(roster, agent), - ); + const envPath = join(activePaths.agentEnvDir, `${agent.name}.env`); + const existingEnv = (await canRead(envPath)) ? await readFile(envPath, 'utf8') : undefined; + await writeFile(envPath, mergeAgentEnv(generateAgentEnv(roster, agent), existingEnv)); } console.log(`Installed fleet files for ${roster.agents.length} agent(s).`); -- 2.49.1