feat(fleet): enforce generated environment boundary (#772)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

This commit was merged in pull request #772.
This commit is contained in:
2026-07-15 08:40:32 +00:00
parent e9c4aa3e8b
commit 191efaefeb
18 changed files with 2282 additions and 785 deletions

View File

@@ -1,4 +1,4 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { chmod, lstat, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { Command } from 'commander';
@@ -284,6 +284,8 @@ describe('fleet roster parsing', () => {
'MOSAIC_AGENT_CLASS=implementer',
'MOSAIC_AGENT_RUNTIME=codex',
'MOSAIC_AGENT_MODEL=',
'MOSAIC_AGENT_REASONING=',
'MOSAIC_AGENT_TOOL_POLICY=',
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
'MOSAIC_TMUX_SOCKET=mosaic-fleet',
'',
@@ -325,57 +327,38 @@ 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-fleet',
'',
].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');
it('rejects legacy command overrides instead of merging them into generated authority', () => {
const generated = generateAgentEnv(
{
version: 1,
transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' },
defaults: { workingDirectory: '/srv/new' },
runtimes: {},
agents: [],
},
{ name: 'coder0', className: 'code', runtime: 'codex' },
);
expect(mergeAgentEnv(generated, existing)).toBe(
[
'MOSAIC_AGENT_NAME=coder0',
'MOSAIC_AGENT_RUNTIME=codex',
'MOSAIC_AGENT_WORKDIR=/srv/new',
'MOSAIC_TMUX_SOCKET=mosaic-fleet',
'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh',
'# site note',
'',
].join('\n'),
expect((): string => mergeAgentEnv(generated, 'MOSAIC_AGENT_COMMAND=legacy-command\n')).toThrow(
/MOSAIC_AGENT_COMMAND/,
);
});
it('updates (does not duplicate) MOSAIC_AGENT_CLASS on re-launch', () => {
const generated = [
'MOSAIC_AGENT_NAME=coder0',
'MOSAIC_AGENT_CLASS=orchestrator',
'MOSAIC_AGENT_RUNTIME=codex',
'',
].join('\n');
const existing = [
'MOSAIC_AGENT_NAME=coder0',
'MOSAIC_AGENT_CLASS=worker',
'MOSAIC_AGENT_RUNTIME=codex',
'',
].join('\n');
it('does not merge a valid local value into the generated projection', () => {
const generated = generateAgentEnv(
{
version: 1,
transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' },
defaults: { workingDirectory: '/srv/new' },
runtimes: {},
agents: [],
},
{ name: 'coder0', className: 'code', runtime: 'codex' },
);
const merged = mergeAgentEnv(generated, existing);
// mergeAgentEnv keys by VAR name, so the regenerated CLASS wins and there is
// exactly one MOSAIC_AGENT_CLASS line (no stale worker value left behind).
expect(merged).toContain('MOSAIC_AGENT_CLASS=orchestrator');
expect(merged).not.toContain('MOSAIC_AGENT_CLASS=worker');
expect(merged.match(/^MOSAIC_AGENT_CLASS=/gm)).toHaveLength(1);
expect(mergeAgentEnv(generated, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n')).toBe(generated);
});
it('rejects unknown roster fields instead of silently defaulting', async () => {
@@ -1129,6 +1112,92 @@ describe('fleet command construction', () => {
}
});
it('creates private managed directories and roster/projection files through fresh init and install under umask 022', async () => {
const originalHome = process.env.HOME;
const originalMosaicHome = process.env.MOSAIC_HOME;
const originalUmask = process.umask(0o022);
const home = await tempDir();
process.env.HOME = home;
delete process.env.MOSAIC_HOME;
const mosaicHome = join(home, '.config', 'mosaic');
const program = new Command();
program.exitOverride();
registerFleetCommand(program, { frameworkRoot: resolve(process.cwd(), 'framework') });
try {
await expect(
program.parseAsync(['node', 'mosaic', 'fleet', 'init', '--profile', 'minimal', '--write']),
).resolves.toBeDefined();
await expect(
program.parseAsync(['node', 'mosaic', 'fleet', 'install', '--no-enable']),
).resolves.toBeDefined();
for (const path of [
mosaicHome,
join(mosaicHome, 'fleet'),
join(mosaicHome, 'fleet', 'agents'),
]) {
expect((await stat(path)).mode & 0o777).toBe(0o700);
}
expect((await stat(join(mosaicHome, 'fleet', 'roster.yaml'))).mode & 0o777).toBe(0o600);
expect(
(await stat(join(mosaicHome, 'fleet', 'agents', 'canary-pi.env.generated'))).mode & 0o777,
).toBe(0o600);
} finally {
process.umask(originalUmask);
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (originalMosaicHome === undefined) delete process.env.MOSAIC_HOME;
else process.env.MOSAIC_HOME = originalMosaicHome;
await rm(home, { recursive: true, force: true });
}
});
it('creates a private real projection directory and private generated files on fresh install', async () => {
const originalHome = process.env.HOME;
const home = await tempDir();
process.env.HOME = home;
const mosaicHome = join(home, '.config', 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
const fleetDir = join(mosaicHome, 'fleet');
await mkdir(fleetDir, { recursive: true, mode: 0o700 });
await chmod(mosaicHome, 0o700);
await chmod(fleetDir, 0o700);
await writeFile(
join(mosaicHome, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'agents:',
' - name: coder0',
' runtime: pi',
' class: code',
].join('\n'),
);
const program = new Command();
program.exitOverride();
registerFleetCommand(program, {
frameworkRoot: resolve(process.cwd(), 'framework'),
mosaicHome,
});
try {
await expect(
program.parseAsync(['node', 'mosaic', 'fleet', 'install', '--no-enable']),
).resolves.toBeDefined();
const directoryMetadata = await lstat(agentEnvDir);
expect(directoryMetadata.isDirectory()).toBe(true);
expect(directoryMetadata.isSymbolicLink()).toBe(false);
expect(directoryMetadata.mode & 0o777).toBe(0o700);
expect((await stat(join(agentEnvDir, 'coder0.env.generated'))).mode & 0o777).toBe(0o600);
} finally {
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
await rm(home, { recursive: true, force: true });
}
});
it.each(['start', 'stop', 'restart', 'status'] as const)(
'rejects single-agent %s for agents outside the roster',
async (action) => {
@@ -1687,8 +1756,10 @@ describe('fleet ps — drift detection', () => {
describe('fleet-polish bundle — boot-survival symmetry', () => {
async function rosterHome(agents: string): Promise<string> {
const home = await tempDir();
await mkdir(join(home, 'fleet'), { recursive: true });
await writeFile(join(home, 'fleet', 'roster.yaml'), agents);
const fleetDir = join(home, 'fleet');
await mkdir(fleetDir, { recursive: true, mode: 0o700 });
await chmod(fleetDir, 0o700);
await writeFile(join(fleetDir, 'roster.yaml'), agents);
return home;
}
@@ -3459,7 +3530,11 @@ describe('fleet add command', () => {
async function makeHome(agents = ['orchestrator']): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'mosaic-fleet-add-'));
await mkdir(join(dir, 'fleet', 'agents'), { recursive: true });
const fleetDir = join(dir, 'fleet');
const agentEnvDir = join(fleetDir, 'agents');
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await chmod(fleetDir, 0o700);
await chmod(agentEnvDir, 0o700);
const agentLines = agents.map((name) => {
const cls = name === 'orchestrator' ? 'orchestrator' : 'worker';
return ` - name: ${name}\n runtime: claude\n class: ${cls}`;
@@ -3493,11 +3568,110 @@ describe('fleet add command', () => {
const roster = await loadFleetRoster(join(home, 'fleet', 'roster.yaml'));
expect(roster.agents.map((a) => a.name)).toContain('coder0');
const envContent = await readFile(join(home, 'fleet', 'agents', 'coder0.env'), 'utf8');
const envContent = await readFile(
join(home, 'fleet', 'agents', 'coder0.env.generated'),
'utf8',
);
expect(envContent).toContain('MOSAIC_AGENT_NAME=coder0');
expect(envContent).toContain('MOSAIC_AGENT_RUNTIME=codex');
});
it('rejects an unprojectable dogfood runtime without mutating roster or projection state', async () => {
home = await makeHome();
const rosterPath = join(home, 'fleet', 'roster.yaml');
const agentEnvDir = join(home, 'fleet', 'agents');
const trackedPaths = [
rosterPath,
join(agentEnvDir, 'dogfood.env.generated'),
join(agentEnvDir, 'dogfood.env.local'),
join(agentEnvDir, 'dogfood.env.quarantine'),
];
await writeFile(trackedPaths[1]!, 'generated-before\n');
await writeFile(trackedPaths[2]!, 'MOSAIC_RUNTIME_BIN=/safe/runtime\n');
await writeFile(trackedPaths[3]!, 'quarantine-before\n');
const beforeState = new Map<string, Buffer>();
for (const path of trackedPaths) beforeState.set(path, await readFile(path));
const calls: string[][] = [];
const runner: CommandRunner = async (command, args) => {
calls.push([command, ...args]);
return { stdout: '', stderr: '', exitCode: 0 };
};
const program = new Command();
program.exitOverride();
registerFleetCommand(program, { runner, mosaicHome: home });
await expect(
program.parseAsync([
'node',
'mosaic',
'fleet',
'add',
'dogfood',
'--runtime',
'dogfood',
'--class',
'worker',
'--no-start',
]),
).rejects.toThrow(/runtime/i);
for (const path of trackedPaths) {
expect(await readFile(path)).toEqual(beforeState.get(path));
}
expect(calls).toEqual([]);
});
it('rejects supported add with a deterministic quarantine conflict before roster persistence', async () => {
home = await makeHome();
const rosterPath = join(home, 'fleet', 'roster.yaml');
const agentEnvDir = join(home, 'fleet', 'agents');
const trackedPaths = [
rosterPath,
join(agentEnvDir, 'coder1.env.generated'),
join(agentEnvDir, 'coder1.env.local'),
join(agentEnvDir, 'coder1.env'),
join(agentEnvDir, 'coder1.env.quarantine'),
];
await writeFile(trackedPaths[1]!, 'generated-before\n', { mode: 0o600 });
await writeFile(trackedPaths[2]!, 'MOSAIC_RUNTIME_BIN=/safe/runtime\n', { mode: 0o600 });
await writeFile(trackedPaths[3]!, 'MOSAIC_AGENT_COMMAND=must-be-quarantined\n', {
mode: 0o600,
});
await writeFile(trackedPaths[4]!, 'quarantine-before\n', { mode: 0o600 });
const beforeState = new Map<string, Buffer>();
for (const path of trackedPaths) beforeState.set(path, await readFile(path));
const calls: string[][] = [];
const runner: CommandRunner = async (command, args) => {
calls.push([command, ...args]);
return { stdout: '', stderr: '', exitCode: 0 };
};
const program = new Command();
program.exitOverride();
registerFleetCommand(program, { runner, mosaicHome: home });
await expect(
program.parseAsync([
'node',
'mosaic',
'fleet',
'add',
'coder1',
'--runtime',
'codex',
'--class',
'worker',
'--no-start',
]),
).rejects.toThrow('quarantine-exists');
for (const path of trackedPaths) {
expect(await readFile(path)).toEqual(beforeState.get(path));
}
expect(calls).toEqual([]);
});
it('--no-start skips the start command', async () => {
home = await makeHome();
const calls: string[][] = [];
@@ -3659,7 +3833,10 @@ describe('fleet remove command', () => {
].join('\n'),
);
// Create env and heartbeat files for coder0
await writeFile(join(dir, 'fleet', 'agents', 'coder0.env'), 'MOSAIC_AGENT_NAME=coder0\n');
await writeFile(
join(dir, 'fleet', 'agents', 'coder0.env.generated'),
'MOSAIC_AGENT_NAME=coder0\n',
);
await writeFile(join(dir, 'fleet', 'run', 'coder0.hb'), 'ts=2026-01-01T00:00:00.000Z\n');
return dir;
}
@@ -3738,12 +3915,15 @@ describe('fleet remove command', () => {
await program.parseAsync(['node', 'mosaic', 'fleet', 'remove', 'coder0', '--keep-files']);
// Env file should still exist
const envContent = await readFile(join(home, 'fleet', 'agents', 'coder0.env'), 'utf8');
// Generated projection should still exist.
const envContent = await readFile(
join(home, 'fleet', 'agents', 'coder0.env.generated'),
'utf8',
);
expect(envContent).toContain('MOSAIC_AGENT_NAME=coder0');
});
it('env file is removed by default (no --keep-files)', async () => {
it('generated projection is removed by default (no --keep-files)', async () => {
home = await makeHome();
const runner: CommandRunner = async () => ({ stdout: '', stderr: '', exitCode: 0 });
const program = new Command();
@@ -3752,7 +3932,9 @@ describe('fleet remove command', () => {
await program.parseAsync(['node', 'mosaic', 'fleet', 'remove', 'coder0']);
await expect(readFile(join(home, 'fleet', 'agents', 'coder0.env'), 'utf8')).rejects.toThrow();
await expect(
readFile(join(home, 'fleet', 'agents', 'coder0.env.generated'), 'utf8'),
).rejects.toThrow();
});
it('removing the sole orchestrator throws with a clear error about the guard', async () => {

View File

@@ -19,6 +19,16 @@ import * as readline from 'node:readline';
import type { Command } from 'commander';
import YAML from 'yaml';
import { resolveCommsBlock } from '../fleet/comms-onboarding.js';
import {
applyPreparedAgentEnvironmentProjection,
ensureFleetHolderIdentity,
GENERATED_AGENT_ENV_SUPPORTED_RUNTIMES,
parseAgentEnvironment,
prepareAgentEnvironmentProjection,
renderGeneratedAgentEnvironment,
writeAgentEnvironmentProjection,
writeManagedFleetRoster,
} from '../fleet/generated-env-boundary.js';
import { registerFleetBacklogCommand } from './fleet-backlog.js';
import { registerFleetPersonaCommand } from './fleet-personas.js';
import { registerFleetProfileCommand } from './fleet-profiles.js';
@@ -509,50 +519,35 @@ export async function generateNorthStarMarkdown(repoRoot?: string): Promise<stri
}
export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string {
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
return [
`MOSAIC_AGENT_NAME=${shellEnvValue(agent.name)}`,
// Per-agent class → start-agent-session.sh / launcher reads this to inject the
// matching persona contract for the agent's class (default `worker`).
`MOSAIC_AGENT_CLASS=${shellEnvValue(agent.className)}`,
`MOSAIC_AGENT_RUNTIME=${shellEnvValue(agent.runtime)}`,
// Per-agent model hint → start-agent-session.sh appends `--model <hint>` to
// the `mosaic yolo` launch so workers run on the roster's model (e.g. pi on
// openai-codex/gpt-5.5:high). Empty when the agent declares no model_hint.
`MOSAIC_AGENT_MODEL=${shellEnvValue(agent.modelHint ?? '')}`,
...(agent.reasoningLevel !== undefined
? [`MOSAIC_AGENT_REASONING=${shellEnvValue(agent.reasoningLevel)}`]
: []),
...(agent.toolPolicy !== undefined
? [`MOSAIC_AGENT_TOOL_POLICY=${shellEnvValue(agent.toolPolicy)}`]
: []),
`MOSAIC_AGENT_WORKDIR=${shellEnvValue(expandHome(workingDirectory))}`,
`MOSAIC_TMUX_SOCKET=${shellEnvValue(roster.tmux.socketName)}`,
'',
].join('\n');
return renderGeneratedAgentEnvironment(generateAgentEnvValues(roster, agent));
}
/**
* Compatibility shim for callers that previously merged site-owned data into a
* generated file. Local data is validated but never appended to the generated
* projection, preventing a local override from becoming hidden authority.
*/
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');
parseAgentEnvironment(generatedEnv, 'generated');
if (existingEnv?.trim()) parseAgentEnvironment(existingEnv, 'local');
return generatedEnv;
}
function generateAgentEnvValues(
roster: FleetRoster,
agent: FleetAgent,
): Readonly<Record<string, string>> {
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
return {
MOSAIC_AGENT_NAME: agent.name,
MOSAIC_AGENT_CLASS: agent.className,
MOSAIC_AGENT_RUNTIME: agent.runtime,
MOSAIC_AGENT_MODEL: agent.modelHint ?? '',
MOSAIC_AGENT_REASONING: agent.reasoningLevel ?? '',
MOSAIC_AGENT_TOOL_POLICY: agent.toolPolicy ?? '',
MOSAIC_AGENT_WORKDIR: expandHome(workingDirectory),
MOSAIC_TMUX_SOCKET: roster.tmux.socketName,
};
}
export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] {
@@ -1544,8 +1539,12 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
`Fleet roster already exists: ${destination}. Re-run with --force to overwrite.`,
);
}
await mkdir(dirname(destination), { recursive: true });
await writeFile(destination, content);
if (resolve(destination) === resolve(activePaths.rosterPath)) {
await writeManagedFleetRoster(activePaths.mosaicHome, destination, content);
} else {
await mkdir(dirname(destination), { recursive: true });
await writeFile(destination, content);
}
// Guarantee the two-agent floor: exactly one orchestrator AND at least
// one enhancer for every profile except the sanctioned no-orchestrator
@@ -1948,15 +1947,17 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
};
const updatedRoster = addAgentToRoster(roster, newAgent);
const generated = generateAgentEnvValues(updatedRoster, newAgent);
// Preflight every deterministic projection, local, legacy, quarantine,
// and managed-directory condition before roster authority changes.
const preparedProjection = await prepareAgentEnvironmentProjection({
mosaicHome: activePaths.mosaicHome,
agentEnvDir: activePaths.agentEnvDir,
agentName: name,
generated,
});
await writeFile(rosterPath, serializeRosterToYaml(updatedRoster));
const envPath = join(activePaths.agentEnvDir, `${name}.env`);
const existingEnv = (await canRead(envPath)) ? await readFile(envPath, 'utf8') : undefined;
await mkdir(activePaths.agentEnvDir, { recursive: true });
await writeFile(
envPath,
mergeAgentEnv(generateAgentEnv(updatedRoster, newAgent), existingEnv),
);
await applyPreparedAgentEnvironmentProjection(preparedProjection);
console.log(`Added ${name} (${opts.runtime}/${opts.class}) to the fleet.`);
@@ -2037,10 +2038,11 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps =
// Write updated roster
await writeFile(rosterPath, serializeRosterToYaml(updatedRoster));
// Delete env and heartbeat files (best-effort, non-fatal)
// Delete the non-authoritative generated projection and heartbeat files
// (best-effort, non-fatal). Operator local/quarantine data is retained.
if (!opts.keepFiles) {
try {
await unlink(join(activePaths.agentEnvDir, `${name}.env`));
await unlink(join(activePaths.agentEnvDir, `${name}.env.generated`));
} catch {
// best-effort
}
@@ -2328,16 +2330,17 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
const activePaths = resolveFleetPaths(cmd.opts<{ mosaicHome: string }>().mosaicHome);
assertDefaultMosaicHomeForSystemd(activePaths.mosaicHome);
const roster = await loadRosterForCommand(cmd);
await ensureFleetHolderIdentity(activePaths.mosaicHome);
await mkdir(activePaths.fleetToolsDir, { recursive: true });
await mkdir(activePaths.tmuxToolsDir, { recursive: true });
await mkdir(activePaths.systemdUserDir, { recursive: true });
await mkdir(activePaths.agentEnvDir, { recursive: true });
const startAgentSessionPath = join(activePaths.fleetToolsDir, 'start-agent-session.sh');
const startInteractionServicePath = join(
activePaths.fleetToolsDir,
'start-interaction-service.sh',
);
const startTmuxHolderPath = join(activePaths.fleetToolsDir, 'start-tmux-holder.sh');
const printInteractionPolicyPath = join(
activePaths.fleetToolsDir,
'print-interaction-effective-policy.sh',
@@ -2347,6 +2350,7 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
const executableToolPaths = [
startAgentSessionPath,
startInteractionServicePath,
startTmuxHolderPath,
printInteractionPolicyPath,
sendMessagePath,
agentSendPath,
@@ -2359,6 +2363,10 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
join(frameworkRoot, 'tools', 'fleet', 'start-interaction-service.sh'),
startInteractionServicePath,
);
await copyFile(
join(frameworkRoot, 'tools', 'fleet', 'start-tmux-holder.sh'),
startTmuxHolderPath,
);
await copyFile(
join(frameworkRoot, 'tools', 'fleet', 'print-interaction-effective-policy.sh'),
printInteractionPolicyPath,
@@ -2382,9 +2390,12 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
);
for (const agent of roster.agents) {
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));
await writeAgentEnvironmentProjection({
mosaicHome: activePaths.mosaicHome,
agentEnvDir: activePaths.agentEnvDir,
agentName: agent.name,
generated: generateAgentEnvValues(roster, agent),
});
}
console.log(`Installed fleet files for ${roster.agents.length} agent(s).`);
@@ -2656,19 +2667,6 @@ function expandHome(path: string): string {
return path === '~' || path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
}
function shellEnvValue(value: string): string {
// Empty ⇒ a bare `VAR=` (unambiguous empty in a systemd EnvironmentFile and
// when shell-sourced). Quoting it as '' risks a literal two-char value (e.g.
// a tmux socket named "''"), which would defeat the default-socket behavior.
if (value === '') {
return '';
}
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
return value;
}
return `'${value.replaceAll("'", "'\"'\"'")}'`;
}
async function stopFleetBestEffort(runner: CommandRunner, agentNames: string[]): Promise<void> {
const failures: string[] = [];
for (const agentName of agentNames) {
@@ -2770,13 +2768,8 @@ export function countEnhancers(roster: FleetRoster): number {
}
/** Valid runtime identifiers for fleet agents. */
export const VALID_FLEET_RUNTIMES: readonly string[] = [
'pi',
'claude',
'codex',
'opencode',
'dogfood',
];
/** Runtime launchers accepted by `fleet add`; shared with the generated projection validator. */
export const VALID_FLEET_RUNTIMES: readonly string[] = GENERATED_AGENT_ENV_SUPPORTED_RUNTIMES;
/**
* Add a new agent to a fleet roster (immutable — returns a new FleetRoster).