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

View File

@@ -0,0 +1,279 @@
import {
chmod,
mkdir,
mkdtemp,
readFile,
rename,
rm,
stat,
symlink,
writeFile,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
AgentEnvBoundaryError,
parseAgentEnvironment,
renderGeneratedAgentEnvironment,
writeAgentEnvironmentProjection,
} from './generated-env-boundary.js';
const generatedValues = {
MOSAIC_AGENT_NAME: 'coder0',
MOSAIC_AGENT_CLASS: 'code',
MOSAIC_AGENT_RUNTIME: 'pi',
MOSAIC_AGENT_MODEL: 'openai-codex/gpt-5.6-sol',
MOSAIC_AGENT_REASONING: 'high',
MOSAIC_AGENT_TOOL_POLICY: 'code',
MOSAIC_AGENT_WORKDIR: '/srv/mosaic',
MOSAIC_TMUX_SOCKET: 'mosaic-fleet',
};
describe('generated fleet agent environment boundary', (): void => {
let cleanup: string | undefined;
afterEach(async (): Promise<void> => {
if (cleanup) {
await rm(cleanup, { recursive: true, force: true });
cleanup = undefined;
}
});
it('renders a deterministic complete generated projection', (): void => {
expect(renderGeneratedAgentEnvironment(generatedValues)).toBe(
[
'MOSAIC_AGENT_NAME=coder0',
'MOSAIC_AGENT_CLASS=code',
'MOSAIC_AGENT_RUNTIME=pi',
'MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol',
'MOSAIC_AGENT_REASONING=high',
'MOSAIC_AGENT_TOOL_POLICY=code',
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
'MOSAIC_TMUX_SOCKET=mosaic-fleet',
'',
].join('\n'),
);
});
it.each([
['generated-key shadowing', 'MOSAIC_AGENT_RUNTIME=codex\n'],
['unknown key', 'UNRELATED_SETTING=value\n'],
['arbitrary command', 'MOSAIC_AGENT_COMMAND=mosaic yolo codex\n'],
['sensitive key', 'MOSAIC_AGENT_TOKEN=do-not-log-me\n'],
['malformed syntax', 'export MOSAIC_RUNTIME_BIN=/opt/bin\n'],
['duplicate keys', 'MOSAIC_RUNTIME_BIN=/opt/bin\nMOSAIC_RUNTIME_BIN=/other/bin\n'],
])('rejects local %s without echoing values', (_name: string, source: string): void => {
let error: unknown;
try {
parseAgentEnvironment(source, 'local');
} catch (caught: unknown) {
error = caught;
}
expect(error).toBeInstanceOf(AgentEnvBoundaryError);
expect(String(error)).not.toContain('mosaic yolo codex');
expect(String(error)).not.toContain('do-not-log-me');
expect(String(error)).toMatch(/key=.*sha256=/);
});
it('rejects unsafe generated paths before any launch consumer can use them', (): void => {
expect((): void => {
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_AGENT_WORKDIR: '../outside',
});
}).toThrow(AgentEnvBoundaryError);
});
it('creates missing managed directories privately before writing a projection', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
const result = await writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
});
for (const directory of [mosaicHome, join(mosaicHome, 'fleet'), agentEnvDir]) {
expect((await stat(directory)).mode & 0o777).toBe(0o700);
}
expect((await stat(result.generatedPath)).mode & 0o777).toBe(0o600);
});
it('regenerates desired keys, relocates safe legacy local data, and quarantines forbidden legacy input', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await writeFile(
join(agentEnvDir, 'coder0.env'),
[
'MOSAIC_AGENT_NAME=stale-name',
'MOSAIC_AGENT_RUNTIME=codex',
'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin',
'MOSAIC_AGENT_COMMAND=mosaic yolo codex --dangerous',
'',
].join('\n'),
{ mode: 0o600 },
);
const result = await writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
});
expect(await readFile(result.generatedPath, 'utf8')).toBe(
renderGeneratedAgentEnvironment(generatedValues),
);
expect(await readFile(result.localPath, 'utf8')).toBe('MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n');
const quarantine = await readFile(result.quarantinePath!, 'utf8');
expect(quarantine).toContain('MOSAIC_AGENT_COMMAND=mosaic yolo codex --dangerous');
await expect(readFile(join(agentEnvDir, 'coder0.env'), 'utf8')).rejects.toThrow();
expect((await stat(result.generatedPath)).mode & 0o077).toBe(0);
expect((await stat(result.localPath)).mode & 0o077).toBe(0);
expect(result.diagnostics).toEqual([expect.objectContaining({ key: 'MOSAIC_AGENT_COMMAND' })]);
expect(JSON.stringify(result.diagnostics)).not.toContain('mosaic yolo codex --dangerous');
});
it('fails closed on an existing local override with unsafe permissions', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
const localPath = join(agentEnvDir, 'coder0.env.local');
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n', { mode: 0o600 });
await chmod(localPath, 0o644);
await expect(
writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
}),
).rejects.toThrow(/permissions/i);
});
it('rejects an existing group/world-accessible projection directory before reading it', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
const localPath = join(agentEnvDir, 'coder0.env.local');
const generatedPath = join(agentEnvDir, 'coder0.env.generated');
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n', { mode: 0o600 });
await chmod(agentEnvDir, 0o777);
await expect(
writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
}),
).rejects.toThrow(/unsafe-permissions/i);
await expect(readFile(localPath, 'utf8')).resolves.toBe('MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n');
await expect(readFile(generatedPath, 'utf8')).rejects.toThrow();
});
it.each([
['MOSAIC_HOME', 'symlink'],
['MOSAIC_HOME/fleet', 'symlink'],
['MOSAIC_HOME/fleet/agents', 'symlink'],
['MOSAIC_HOME', 'group/world-writable'],
['MOSAIC_HOME/fleet', 'group/world-writable'],
['MOSAIC_HOME/fleet/agents', 'group/world-writable'],
])(
'rejects a %s %s managed ancestor before any projection mutation',
async (ancestor: string, hazard: string): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const fleetDir = join(mosaicHome, 'fleet');
const agentEnvDir = join(fleetDir, 'agents');
const generatedPath = join(agentEnvDir, 'coder0.env.generated');
const localPath = join(agentEnvDir, 'coder0.env.local');
const legacyPath = join(agentEnvDir, 'coder0.env');
const quarantinePath = join(agentEnvDir, 'coder0.env.quarantine');
const managedPaths: Record<string, string> = {
MOSAIC_HOME: mosaicHome,
'MOSAIC_HOME/fleet': fleetDir,
'MOSAIC_HOME/fleet/agents': agentEnvDir,
};
const unsafePath = managedPaths[ancestor];
if (unsafePath === undefined) throw new Error(`Unknown managed ancestor: ${ancestor}`);
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await chmod(mosaicHome, 0o700);
await chmod(fleetDir, 0o700);
await chmod(agentEnvDir, 0o700);
await writeFile(generatedPath, 'generated-before\n', { mode: 0o600 });
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/safe/runtime\n', { mode: 0o600 });
await writeFile(legacyPath, 'MOSAIC_AGENT_COMMAND=legacy-command\n', { mode: 0o600 });
if (hazard === 'symlink') {
const targetPath = `${unsafePath}-target`;
await rename(unsafePath, targetPath);
await symlink(targetPath, unsafePath, 'dir');
} else {
await chmod(unsafePath, 0o777);
}
await expect(
writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
}),
).rejects.toThrow(/unsafe-(directory|permissions)/i);
await expect(readFile(generatedPath, 'utf8')).resolves.toBe('generated-before\n');
await expect(readFile(localPath, 'utf8')).resolves.toBe('MOSAIC_RUNTIME_BIN=/safe/runtime\n');
await expect(readFile(legacyPath, 'utf8')).resolves.toBe(
'MOSAIC_AGENT_COMMAND=legacy-command\n',
);
await expect(readFile(quarantinePath, 'utf8')).rejects.toThrow();
},
);
it('rejects a symlinked projection directory without mutating its target', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const targetDirectory = join(cleanup, 'target');
const linkedDirectory = join(mosaicHome, 'fleet', 'agents');
const legacyPath = join(targetDirectory, 'coder0.env');
const generatedPath = join(targetDirectory, 'coder0.env.generated');
const localPath = join(targetDirectory, 'coder0.env.local');
const quarantinePath = join(targetDirectory, 'coder0.env.quarantine');
await mkdir(join(mosaicHome, 'fleet'), { recursive: true, mode: 0o700 });
await mkdir(targetDirectory, { mode: 0o700 });
await writeFile(legacyPath, 'MOSAIC_AGENT_COMMAND=legacy-command\n', { mode: 0o600 });
await writeFile(generatedPath, 'generated-before\n', { mode: 0o600 });
await writeFile(localPath, 'local-before\n', { mode: 0o600 });
await writeFile(quarantinePath, 'quarantine-before\n', { mode: 0o600 });
await symlink(targetDirectory, linkedDirectory, 'dir');
await expect(
writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir: linkedDirectory,
agentName: 'coder0',
generated: generatedValues,
}),
).rejects.toThrow(/unsafe-directory/i);
await expect(readFile(legacyPath, 'utf8')).resolves.toBe(
'MOSAIC_AGENT_COMMAND=legacy-command\n',
);
await expect(readFile(generatedPath, 'utf8')).resolves.toBe('generated-before\n');
await expect(readFile(localPath, 'utf8')).resolves.toBe('local-before\n');
await expect(readFile(quarantinePath, 'utf8')).resolves.toBe('quarantine-before\n');
});
});

View File

@@ -0,0 +1,509 @@
import { createHash, randomUUID } from 'node:crypto';
import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
export type AgentEnvironmentKind = 'generated' | 'local';
export interface AgentEnvironmentDiagnostic {
readonly code: string;
readonly key: string;
readonly sha256: string;
}
export interface AgentEnvironmentProjectionOptions {
readonly mosaicHome: string;
readonly agentEnvDir: string;
readonly agentName: string;
readonly generated: Readonly<Record<string, string>>;
}
export interface AgentEnvironmentProjectionResult {
readonly generatedPath: string;
readonly localPath: string;
readonly quarantinePath?: string;
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
/** A projection fully validated without changing managed files. */
export interface PreparedAgentEnvironmentProjection {
readonly mosaicHome: string;
readonly agentEnvDir: string;
readonly generatedPath: string;
readonly localPath: string;
readonly legacyPath: string;
readonly generated: string;
readonly local: string;
readonly legacy?: string;
readonly quarantinePath?: string;
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
export class AgentEnvBoundaryError extends Error {
readonly diagnostic: AgentEnvironmentDiagnostic;
constructor(code: string, key: string, value: string) {
const diagnostic: AgentEnvironmentDiagnostic = {
code,
key,
sha256: hashValue(value),
};
super(`Agent environment rejected: code=${code} key=${key} sha256=${diagnostic.sha256}`);
this.name = 'AgentEnvBoundaryError';
this.diagnostic = diagnostic;
}
}
export const GENERATED_AGENT_ENV_KEYS = [
'MOSAIC_AGENT_NAME',
'MOSAIC_AGENT_CLASS',
'MOSAIC_AGENT_RUNTIME',
'MOSAIC_AGENT_MODEL',
'MOSAIC_AGENT_REASONING',
'MOSAIC_AGENT_TOOL_POLICY',
'MOSAIC_AGENT_WORKDIR',
'MOSAIC_TMUX_SOCKET',
] as const;
export const LOCAL_AGENT_ENV_KEYS = [
'MOSAIC_RUNTIME_BIN',
'MOSAIC_HEARTBEAT_RUN_DIR',
'MOSAIC_HEARTBEAT_INTERVAL',
'MOSAIC_CLAUDE_JSON',
'CLAUDE_CONFIG_DIR',
] as const;
const GENERATED_KEY_SET = new Set<string>(GENERATED_AGENT_ENV_KEYS);
const LOCAL_KEY_SET = new Set<string>(LOCAL_AGENT_ENV_KEYS);
const SENSITIVE_KEY = /(?:API[_-]?KEY|AUTH|CREDENTIAL|PASSWORD|PRIVATE|SECRET|TOKEN)/i;
const AGENT_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const POLICY_NAME = /^[a-z][a-z0-9-]*$/;
const TMUX_SOCKET = /^[A-Za-z0-9_.-]*$/;
const MODEL = /^[A-Za-z0-9._/:+-]*$/;
/** Runtime launchers supported by the generated environment contract. */
export const GENERATED_AGENT_ENV_SUPPORTED_RUNTIMES = [
'claude',
'codex',
'opencode',
'pi',
] as const;
const SUPPORTED_RUNTIMES = new Set<string>(GENERATED_AGENT_ENV_SUPPORTED_RUNTIMES);
const REASONING = new Set(['', 'low', 'medium', 'high']);
/** Parses a strict data-only generated or local environment file without shell evaluation. */
export function parseAgentEnvironment(
source: string,
kind: AgentEnvironmentKind,
): Readonly<Record<string, string>> {
const values: Record<string, string> = {};
const seen = new Set<string>();
for (const line of source.split('\n')) {
if (line === '') continue;
const match = /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
if (!match) throw new AgentEnvBoundaryError('malformed-line', '(malformed)', line);
const [, key, value] = match;
if (key === undefined || value === undefined) {
throw new AgentEnvBoundaryError('malformed-line', '(malformed)', line);
}
if (seen.has(key)) throw new AgentEnvBoundaryError('duplicate-key', key, value);
seen.add(key);
if (SENSITIVE_KEY.test(key)) throw new AgentEnvBoundaryError('sensitive-key', key, value);
if (containsShellSyntax(value)) throw new AgentEnvBoundaryError('unsafe-value', key, value);
if (kind === 'generated') {
if (!GENERATED_KEY_SET.has(key)) throw new AgentEnvBoundaryError('unknown-key', key, value);
} else {
if (GENERATED_KEY_SET.has(key))
throw new AgentEnvBoundaryError('generated-key-shadow', key, value);
if (!LOCAL_KEY_SET.has(key)) throw new AgentEnvBoundaryError('unknown-key', key, value);
}
values[key] = value;
}
if (kind === 'generated') assertGeneratedValues(values);
else assertLocalValues(values);
return Object.freeze(values);
}
/** Renders the roster-derived generated projection in a stable, complete key order. */
export function renderGeneratedAgentEnvironment(values: Readonly<Record<string, string>>): string {
const normalized = normalizeGeneratedValues(values);
return `${GENERATED_AGENT_ENV_KEYS.map((key): string => `${key}=${normalized[key]}`).join('\n')}\n`;
}
/** Validates all deterministic projection inputs and target state without mutation. */
export async function prepareAgentEnvironmentProjection(
options: AgentEnvironmentProjectionOptions,
): Promise<PreparedAgentEnvironmentProjection> {
if (!AGENT_NAME.test(options.agentName)) {
throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', options.agentName);
}
await validatePrivateProjectionDirectory(options.mosaicHome, options.agentEnvDir);
const generatedPath = join(options.agentEnvDir, `${options.agentName}.env.generated`);
const localPath = join(options.agentEnvDir, `${options.agentName}.env.local`);
const legacyPath = join(options.agentEnvDir, `${options.agentName}.env`);
const generated = renderGeneratedAgentEnvironment(options.generated);
await assertPrivateRegularFileIfPresent(generatedPath);
const currentLocal = await readOptionalPrivateFile(localPath);
const parsedLocal =
currentLocal === undefined ? {} : parseAgentEnvironment(currentLocal, 'local');
const legacy = await readOptionalPrivateFile(legacyPath);
const legacyDisposition =
legacy === undefined ? emptyLegacyDisposition() : classifyLegacyEnvironment(legacy);
for (const [key, value] of Object.entries(legacyDisposition.localValues)) {
if (parsedLocal[key] !== undefined && parsedLocal[key] !== value) {
throw new AgentEnvBoundaryError('legacy-local-conflict', key, value);
}
}
const local = renderLocalAgentEnvironment({ ...legacyDisposition.localValues, ...parsedLocal });
const quarantinePath = legacyDisposition.diagnostics.length
? join(options.agentEnvDir, `${options.agentName}.env.quarantine`)
: undefined;
if (quarantinePath !== undefined) {
const existingQuarantine = await readOptionalPrivateFile(quarantinePath);
if (existingQuarantine !== undefined) {
throw new AgentEnvBoundaryError('quarantine-exists', '(quarantine)', existingQuarantine);
}
}
return {
mosaicHome: options.mosaicHome,
agentEnvDir: options.agentEnvDir,
generatedPath,
localPath,
legacyPath,
generated,
local,
...(legacy === undefined ? {} : { legacy }),
...(quarantinePath === undefined ? {} : { quarantinePath }),
diagnostics: legacyDisposition.diagnostics,
};
}
/** Applies a previously prepared deterministic projection. */
export async function applyPreparedAgentEnvironmentProjection(
prepared: PreparedAgentEnvironmentProjection,
): Promise<AgentEnvironmentProjectionResult> {
await ensurePrivateProjectionDirectory(prepared.mosaicHome, prepared.agentEnvDir);
await writePrivateAtomically(prepared.generatedPath, prepared.generated);
if (prepared.local !== '') await writePrivateAtomically(prepared.localPath, prepared.local);
if (prepared.quarantinePath !== undefined && prepared.legacy !== undefined) {
await writePrivateAtomically(prepared.quarantinePath, prepared.legacy);
}
if (prepared.legacy !== undefined) await unlink(prepared.legacyPath);
return {
generatedPath: prepared.generatedPath,
localPath: prepared.localPath,
...(prepared.quarantinePath === undefined ? {} : { quarantinePath: prepared.quarantinePath }),
diagnostics: prepared.diagnostics,
};
}
/** Writes deterministic projection files and explicitly removes legacy `.env` authority. */
export async function writeAgentEnvironmentProjection(
options: AgentEnvironmentProjectionOptions,
): Promise<AgentEnvironmentProjectionResult> {
return applyPreparedAgentEnvironmentProjection(await prepareAgentEnvironmentProjection(options));
}
/** Creates the canonical managed roster path with private fresh-chain permissions. */
export async function writeManagedFleetRoster(
mosaicHome: string,
rosterPath: string,
content: string,
): Promise<void> {
const fleetDir = join(mosaicHome, 'fleet');
const expectedRosterPath = join(fleetDir, 'roster.yaml');
if (resolve(rosterPath) !== resolve(expectedRosterPath)) {
throw new AgentEnvBoundaryError('unsafe-file', '(roster)', rosterPath);
}
await ensureManagedDirectory(mosaicHome, false);
await ensureManagedDirectory(fleetDir, false);
await writePrivateAtomically(rosterPath, content);
}
/** Ensures the private install-derived identity used to own a named tmux server. */
export async function ensureFleetHolderIdentity(mosaicHome: string): Promise<string> {
const fleetDir = join(mosaicHome, 'fleet');
const runDir = join(fleetDir, 'run');
const identityPath = join(runDir, 'holder-owner');
await ensureManagedDirectory(mosaicHome, false);
await ensureManagedDirectory(fleetDir, false);
await ensureManagedDirectory(runDir, true);
const existing = await readOptionalPrivateFile(identityPath);
if (existing !== undefined) {
const identity = existing.trim();
if (!/^[a-f0-9-]{36}$/.test(identity)) {
throw new AgentEnvBoundaryError('unsafe-owner-identity', '(holder-owner)', existing);
}
return identity;
}
const identity = randomUUID();
await writePrivateAtomically(identityPath, `${identity}\n`);
return identity;
}
function normalizeGeneratedValues(
values: Readonly<Record<string, string>>,
): Readonly<Record<string, string>> {
const normalized: Record<string, string> = {};
for (const key of GENERATED_AGENT_ENV_KEYS) {
const value = values[key];
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
normalized[key] = value;
}
for (const [key, value] of Object.entries(values)) {
if (!GENERATED_KEY_SET.has(key)) throw new AgentEnvBoundaryError('unknown-key', key, value);
}
assertGeneratedValues(normalized);
return Object.freeze(normalized);
}
function renderLocalAgentEnvironment(values: Readonly<Record<string, string>>): string {
if (Object.keys(values).length === 0) return '';
const parsed = parseAgentEnvironment(
Object.entries(values)
.sort(([left], [right]): number => left.localeCompare(right))
.map(([key, value]): string => `${key}=${value}`)
.join('\n'),
'local',
);
return `${Object.entries(parsed)
.sort(([left], [right]): number => left.localeCompare(right))
.map(([key, value]): string => `${key}=${value}`)
.join('\n')}\n`;
}
function assertGeneratedValues(values: Readonly<Record<string, string>>): void {
for (const key of GENERATED_AGENT_ENV_KEYS) {
const value = values[key];
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
}
const name = requiredGeneratedValue(values, 'MOSAIC_AGENT_NAME');
const className = requiredGeneratedValue(values, 'MOSAIC_AGENT_CLASS');
const runtime = requiredGeneratedValue(values, 'MOSAIC_AGENT_RUNTIME');
const model = requiredGeneratedValue(values, 'MOSAIC_AGENT_MODEL');
const reasoning = requiredGeneratedValue(values, 'MOSAIC_AGENT_REASONING');
const toolPolicy = requiredGeneratedValue(values, 'MOSAIC_AGENT_TOOL_POLICY');
const workingDirectory = requiredGeneratedValue(values, 'MOSAIC_AGENT_WORKDIR');
const socket = requiredGeneratedValue(values, 'MOSAIC_TMUX_SOCKET');
if (!AGENT_NAME.test(name))
throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', name);
if (!POLICY_NAME.test(className)) {
throw new AgentEnvBoundaryError('unsafe-class', 'MOSAIC_AGENT_CLASS', className);
}
if (!SUPPORTED_RUNTIMES.has(runtime)) {
throw new AgentEnvBoundaryError('unsupported-runtime', 'MOSAIC_AGENT_RUNTIME', runtime);
}
if (!MODEL.test(model))
throw new AgentEnvBoundaryError('unsafe-model', 'MOSAIC_AGENT_MODEL', model);
if (!REASONING.has(reasoning)) {
throw new AgentEnvBoundaryError('unsupported-reasoning', 'MOSAIC_AGENT_REASONING', reasoning);
}
if (toolPolicy !== '' && !POLICY_NAME.test(toolPolicy)) {
throw new AgentEnvBoundaryError('unsafe-tool-policy', 'MOSAIC_AGENT_TOOL_POLICY', toolPolicy);
}
if (!isSafeAbsolutePath(workingDirectory)) {
throw new AgentEnvBoundaryError('unsafe-path', 'MOSAIC_AGENT_WORKDIR', workingDirectory);
}
if (!TMUX_SOCKET.test(socket)) {
throw new AgentEnvBoundaryError('unsafe-socket', 'MOSAIC_TMUX_SOCKET', socket);
}
}
function requiredGeneratedValue(values: Readonly<Record<string, string>>, key: string): string {
const value = values[key];
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
return value;
}
function assertLocalValues(values: Readonly<Record<string, string>>): void {
for (const [key, value] of Object.entries(values)) {
if (key === 'MOSAIC_HEARTBEAT_INTERVAL') {
if (!/^[1-9][0-9]*$/.test(value)) {
throw new AgentEnvBoundaryError('invalid-interval', key, value);
}
continue;
}
if (!isSafeAbsolutePath(value)) throw new AgentEnvBoundaryError('unsafe-path', key, value);
}
}
function isSafeAbsolutePath(value: string): boolean {
return (
value.startsWith('/') && !value.split('/').includes('..') && !/[\s"'`$\\;&|<>(){}]/.test(value)
);
}
function containsShellSyntax(value: string): boolean {
return /["'`$\\;&|<>(){}]/.test(value);
}
interface LegacyDisposition {
readonly localValues: Readonly<Record<string, string>>;
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
function emptyLegacyDisposition(): LegacyDisposition {
return { localValues: {}, diagnostics: [] };
}
function classifyLegacyEnvironment(source: string): LegacyDisposition {
const localValues: Record<string, string> = {};
const diagnostics: AgentEnvironmentDiagnostic[] = [];
const seen = new Set<string>();
for (const line of source.split('\n')) {
if (line === '') continue;
const match = /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
if (!match || match[1] === undefined || match[2] === undefined) {
diagnostics.push(diagnostic('malformed-line', '(malformed)', line));
continue;
}
const [, key, value] = match;
if (seen.has(key)) {
diagnostics.push(diagnostic('duplicate-key', key, value));
continue;
}
seen.add(key);
if (GENERATED_KEY_SET.has(key)) continue;
try {
parseAgentEnvironment(`${key}=${value}\n`, 'local');
localValues[key] = value;
} catch (error: unknown) {
if (error instanceof AgentEnvBoundaryError) diagnostics.push(error.diagnostic);
else throw error;
}
}
return { localValues: Object.freeze(localValues), diagnostics: Object.freeze(diagnostics) };
}
function diagnostic(code: string, key: string, value: string): AgentEnvironmentDiagnostic {
return { code, key, sha256: hashValue(value) };
}
function hashValue(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
async function readOptionalPrivateFile(path: string): Promise<string | undefined> {
try {
await assertPrivateRegularFile(path);
return await readFile(path, 'utf8');
} catch (error: unknown) {
if (isMissingFile(error)) return undefined;
throw error;
}
}
function isMissingFile(error: unknown): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
}
async function validatePrivateProjectionDirectory(
mosaicHome: string,
agentEnvDir: string,
): Promise<void> {
const fleetDir = join(mosaicHome, 'fleet');
const expectedAgentEnvDir = join(fleetDir, 'agents');
if (resolve(agentEnvDir) !== resolve(expectedAgentEnvDir)) {
throw new AgentEnvBoundaryError('unsafe-directory', '(directory)', agentEnvDir);
}
await assertManagedDirectoryIfPresent(mosaicHome, false);
await assertManagedDirectoryIfPresent(fleetDir, false);
await assertManagedDirectoryIfPresent(agentEnvDir, true);
}
async function ensurePrivateProjectionDirectory(
mosaicHome: string,
agentEnvDir: string,
): Promise<void> {
await validatePrivateProjectionDirectory(mosaicHome, agentEnvDir);
const fleetDir = join(mosaicHome, 'fleet');
await ensureManagedDirectory(mosaicHome, false);
await ensureManagedDirectory(fleetDir, false);
await ensureManagedDirectory(agentEnvDir, true);
}
async function ensureManagedDirectory(path: string, privateDirectory: boolean): Promise<void> {
let created = false;
try {
await lstat(path);
} catch (error: unknown) {
if (!isMissingFile(error)) throw error;
try {
await mkdir(path, { recursive: true, mode: 0o700 });
created = true;
} catch (mkdirError: unknown) {
if (!isAlreadyExists(mkdirError)) throw mkdirError;
}
}
await assertManagedDirectory(path, privateDirectory);
if (created) {
await chmod(path, 0o700);
await assertManagedDirectory(path, privateDirectory);
}
}
function isAlreadyExists(error: unknown): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST';
}
async function assertManagedDirectoryIfPresent(
path: string,
privateDirectory: boolean,
): Promise<void> {
try {
await assertManagedDirectory(path, privateDirectory);
} catch (error: unknown) {
if (isMissingFile(error)) return;
throw error;
}
}
async function assertManagedDirectory(path: string, privateDirectory: boolean): Promise<void> {
const metadata = await lstat(path);
if (!metadata.isDirectory()) {
throw new AgentEnvBoundaryError('unsafe-directory', '(directory)', path);
}
if ((metadata.mode & 0o022) !== 0 || (privateDirectory && (metadata.mode & 0o077) !== 0)) {
throw new AgentEnvBoundaryError('unsafe-permissions', '(directory)', path);
}
}
async function assertPrivateRegularFileIfPresent(path: string): Promise<void> {
try {
await assertPrivateRegularFile(path);
} catch (error: unknown) {
if (isMissingFile(error)) return;
throw error;
}
}
async function assertPrivateRegularFile(path: string): Promise<void> {
const metadata = await lstat(path);
if (!metadata.isFile()) throw new AgentEnvBoundaryError('unsafe-file', '(file)', path);
if ((metadata.mode & 0o077) !== 0) {
throw new AgentEnvBoundaryError('unsafe-permissions', '(file)', path);
}
}
async function writePrivateAtomically(path: string, content: string): Promise<void> {
try {
await assertPrivateRegularFile(path);
} catch (error: unknown) {
if (!isMissingFile(error)) throw error;
}
const temporaryPath = join(dirname(path), `.${randomUUID()}.tmp`);
await writeFile(temporaryPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
await rename(temporaryPath, path);
}