import { createHash, randomUUID } from 'node:crypto'; import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { compareCodePoints } from './deterministic-order.js'; 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>; } export interface AgentGeneratedProjectionDeletionOptions { readonly mosaicHome: string; readonly agentEnvDir: string; readonly agentName: string; } export interface AgentEnvironmentProjectionResult { readonly generatedPath: string; readonly localPath: string; readonly quarantinePath?: string; readonly diagnostics: readonly AgentEnvironmentDiagnostic[]; } /** Sanitized, non-mutating projection evidence safe for migration output. */ export interface AgentEnvironmentProjectionPreview { readonly agentName: string; readonly generated: 'rebuild'; readonly legacy: 'absent' | 'regenerate-only' | 'relocate-local' | 'quarantine'; readonly relocatedKeys: readonly 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 legacyRelocatedKeys: readonly 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(GENERATED_AGENT_ENV_KEYS); const LOCAL_KEY_SET = new Set(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(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> { const values: Record = {}; const seen = new Set(); 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>): 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 { 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 }), legacyRelocatedKeys: Object.keys(legacyDisposition.localValues).sort(compareCodePoints), ...(quarantinePath === undefined ? {} : { quarantinePath }), diagnostics: legacyDisposition.diagnostics, }; } /** * Validates only the exact generated projection eligible for a roster delete. * Local overrides, legacy input, and quarantine records are operator-retained and * intentionally excluded from delete validation and cleanup. */ export async function prepareAgentGeneratedProjectionDeletion( options: AgentGeneratedProjectionDeletionOptions, ): Promise { 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`); await assertPrivateRegularFileIfPresent(generatedPath); return generatedPath; } export async function previewAgentEnvironmentProjection( options: AgentEnvironmentProjectionOptions, ): Promise { const prepared = await prepareAgentEnvironmentProjection(options); const relocatedKeys = prepared.legacyRelocatedKeys; const legacy = prepared.legacy === undefined ? 'absent' : prepared.diagnostics.length > 0 ? 'quarantine' : relocatedKeys.length > 0 ? 'relocate-local' : 'regenerate-only'; return { agentName: options.agentName, generated: 'rebuild', legacy, relocatedKeys, diagnostics: [...prepared.diagnostics].sort((left, right): number => compareCodePoints( `${left.key}:${left.code}:${left.sha256}`, `${right.key}:${right.code}:${right.sha256}`, ), ), }; } /** Applies a previously prepared deterministic projection. */ export async function applyPreparedAgentEnvironmentProjection( prepared: PreparedAgentEnvironmentProjection, ): Promise { 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 { 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 { 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 { 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>, ): Readonly> { const normalized: Record = {}; for (const key of GENERATED_AGENT_ENV_KEYS) { const value = values[key]; if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, ''); normalized[key] = key === 'MOSAIC_AGENT_WORKDIR' ? expandHomeDirectory(value) : 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 expandHomeDirectory(path: string): string { if (path === '~') return homedir(); if (!path.startsWith('~/') || path.split('/').includes('..')) return path; return join(homedir(), path.slice(2)); } function renderLocalAgentEnvironment(values: Readonly>): string { if (Object.keys(values).length === 0) return ''; const parsed = parseAgentEnvironment( Object.entries(values) .sort(([left], [right]): number => compareCodePoints(left, right)) .map(([key, value]): string => `${key}=${value}`) .join('\n'), 'local', ); return `${Object.entries(parsed) .sort(([left], [right]): number => compareCodePoints(left, right)) .map(([key, value]): string => `${key}=${value}`) .join('\n')}\n`; } function assertGeneratedValues(values: Readonly>): 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>, key: string): string { const value = values[key]; if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, ''); return value; } function assertLocalValues(values: Readonly>): 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>; readonly diagnostics: readonly AgentEnvironmentDiagnostic[]; } function emptyLegacyDisposition(): LegacyDisposition { return { localValues: {}, diagnostics: [] }; } function classifyLegacyEnvironment(source: string): LegacyDisposition { const localValues: Record = {}; const diagnostics: AgentEnvironmentDiagnostic[] = []; const seen = new Set(); 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 { 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 { 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 { 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 { 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 { try { await assertManagedDirectory(path, privateDirectory); } catch (error: unknown) { if (isMissingFile(error)) return; throw error; } } async function assertManagedDirectory(path: string, privateDirectory: boolean): Promise { 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 { try { await assertPrivateRegularFile(path); } catch (error: unknown) { if (isMissingFile(error)) return; throw error; } } async function assertPrivateRegularFile(path: string): Promise { 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 { 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); }