import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync, writeSync, type Stats, } from 'node:fs'; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; import type { Command } from 'commander'; import { harnessHome, launchFleetRuntime, type FleetHarnessContext, type RuntimeName, } from './launch.js'; import { defaultFleetDataHome } from '../fleet/fleet-agent-scaffold.js'; import { CREDENTIAL_DIR_ENV as CREDENTIAL_DIR_ENV_BY_HARNESS, CREDENTIAL_FILE_NAMES, } from '../fleet/credential-sharing.js'; export const FLEET_AGENT_PROFILE_SCHEMA = 1; const PROFILE_KEYS = [ 'schema', 'harness', 'bundle', 'model', 'overlay', 'plugins', 'skills', 'env', ]; const RUNTIMES: readonly RuntimeName[] = ['claude', 'codex', 'opencode', 'pi']; const AGENT_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/; const STORE_ENTRY = /^[A-Za-z0-9][A-Za-z0-9_.@-]*$/; const BUNDLE_NAME = /^[A-Za-z0-9][A-Za-z0-9_.@-]*$/; const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/; // Assignability here is what keeps CredentialHarness and RuntimeName from drifting apart. const CREDENTIAL_FILES: Record = CREDENTIAL_FILE_NAMES; const CREDENTIAL_DIR_ENV: Partial> = CREDENTIAL_DIR_ENV_BY_HARNESS; export type FleetLaunchErrorCode = | 'SCHEMA_TOO_NEW' | 'PROFILE_INVALID' | 'AGENT_NOT_SCAFFOLDED' | 'COMPOSITION_FAILED' | 'FIRST_AUTH_REFUSAL'; export class FleetLaunchError extends Error { constructor( readonly code: FleetLaunchErrorCode, message: string, ) { super(message); this.name = 'FleetLaunchError'; } } export interface FleetAgentLaunchProfile { readonly schema: 1; readonly harness: RuntimeName; readonly bundle: string; readonly model?: string; readonly overlay?: string; readonly plugins: readonly string[]; readonly skills: readonly string[]; readonly env: Readonly>; } export interface FleetLaunchRoots { readonly systemHome: string; readonly userHome: string; } interface SettingsLayer { readonly name: 'system' | 'user' | 'agent'; readonly path: string; readonly present: boolean; readonly value: Record; } interface PlannedLink { readonly kind: 'plugin' | 'skill'; readonly name: string; readonly link: string; readonly target: string; } interface ManagedLinkManifest { readonly links: Record; } interface ManagedLinkState { readonly path: string; readonly links: Map; readonly existed: boolean; } export interface FleetLaunchComposition { readonly name: string; readonly profilePath: string; readonly profile: FleetAgentLaunchProfile; readonly agentDir: string; readonly systemHome: string; readonly seatHome: string; readonly settings: { readonly layers: readonly SettingsLayer[]; readonly merged: Record; readonly output: string; readonly snapshot: string; }; readonly bundle: { readonly requested: string; readonly resolved: string; readonly email?: string; readonly display: string; }; readonly credential: { /** * The seat-local managed link to the bundle credential. Absent for harnesses * that reach the shared bundle by environment instead (see CREDENTIAL_DIR_ENV). */ readonly link?: string; readonly target: string; /** Resolved bundle directory holding the credential file. */ readonly dir: string; }; readonly managedLinks: ManagedLinkState; readonly installs: readonly PlannedLink[]; readonly prune: readonly string[]; readonly env: Readonly>; readonly argv: readonly string[]; } export interface FleetLaunchCommandDeps { readonly userHome?: string; readonly launcher?: ( runtime: RuntimeName, args: string[], declaredEnv: Readonly>, context: FleetHarnessContext, dangerous: boolean, ) => void; } function requiredObject(value: unknown, label: string): Record { if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw new FleetLaunchError('PROFILE_INVALID', `${label} must be a JSON object.`); } return value as Record; } function optionalString(value: unknown, label: string): string | undefined { if (value === undefined) return undefined; if (typeof value !== 'string' || value.trim() === '') { throw new FleetLaunchError('PROFILE_INVALID', `${label} must be a non-empty string.`); } return value.trim(); } function stringList(value: unknown, label: string): string[] { if (value === undefined) return []; if (!Array.isArray(value)) { throw new FleetLaunchError( 'PROFILE_INVALID', `${label} must be an array of store entry names.`, ); } return value.map((entry: unknown, index: number): string => { if (typeof entry !== 'string' || !STORE_ENTRY.test(entry)) { throw new FleetLaunchError( 'PROFILE_INVALID', `${label}[${index}] must be a safe store entry name.`, ); } return entry; }); } /** Parse and strictly validate the frozen, user-facing per-agent profile schema. */ export function parseFleetAgentProfile(source: string): FleetAgentLaunchProfile { let parsed: unknown; try { parsed = JSON.parse(source) as unknown; } catch (error: unknown) { const detail = error instanceof Error ? error.message : String(error); throw new FleetLaunchError('PROFILE_INVALID', `profile.json is not valid JSON: ${detail}`); } const raw = requiredObject(parsed, 'profile.json'); if (!Number.isSafeInteger(raw['schema'])) { throw new FleetLaunchError('PROFILE_INVALID', 'profile.json schema is required and must be 1.'); } if ((raw['schema'] as number) > FLEET_AGENT_PROFILE_SCHEMA) { throw new FleetLaunchError( 'SCHEMA_TOO_NEW', `SCHEMA_TOO_NEW: profile schema ${String(raw['schema'])} is newer than supported schema ${FLEET_AGENT_PROFILE_SCHEMA}; upgrade Mosaic before launching this agent.`, ); } if (raw['schema'] !== FLEET_AGENT_PROFILE_SCHEMA) { throw new FleetLaunchError( 'PROFILE_INVALID', `profile.json schema ${String(raw['schema'])} is unsupported; expected schema 1.`, ); } const unknown = Object.keys(raw).filter((key: string): boolean => !PROFILE_KEYS.includes(key)); if (unknown.length > 0) { throw new FleetLaunchError( 'PROFILE_INVALID', `unknown profile key "${unknown[0]}" (schema ${String(raw['schema'])})`, ); } if (typeof raw['harness'] !== 'string' || !RUNTIMES.includes(raw['harness'] as RuntimeName)) { throw new FleetLaunchError( 'PROFILE_INVALID', `profile.json harness is required and must be one of: ${RUNTIMES.join(', ')}.`, ); } const bundle = optionalString(raw['bundle'], 'profile.json bundle') ?? 'primary'; if (!BUNDLE_NAME.test(bundle)) { throw new FleetLaunchError( 'PROFILE_INVALID', 'profile.json bundle must be a safe bundle name.', ); } const overlay = optionalString(raw['overlay'], 'profile.json overlay'); if (overlay !== undefined && (isAbsolute(overlay) || overlay.split(/[\\/]/u).includes('..'))) { throw new FleetLaunchError( 'PROFILE_INVALID', 'profile.json overlay must remain inside the agent directory.', ); } const rawEnv = raw['env'] === undefined ? {} : requiredObject(raw['env'], 'profile.json env'); const env: Record = {}; for (const [key, value] of Object.entries(rawEnv)) { if (!ENV_NAME.test(key) || typeof value !== 'string') { throw new FleetLaunchError( 'PROFILE_INVALID', `profile.json env entry "${key}" must have a valid name and string value.`, ); } env[key] = value; } const model = optionalString(raw['model'], 'profile.json model'); return { schema: 1, harness: raw['harness'] as RuntimeName, bundle, ...(model === undefined ? {} : { model }), ...(overlay === undefined ? {} : { overlay }), plugins: stringList(raw['plugins'], 'profile.json plugins'), skills: stringList(raw['skills'], 'profile.json skills'), env, }; } function isPlainObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function cloneValue(value: unknown): unknown { if (Array.isArray(value)) return value.map(cloneValue); if (isPlainObject(value)) { return Object.fromEntries( Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]), ); } return value; } function mergeObject( lower: Record, higher: Record, ): Record { const result = cloneValue(lower) as Record; for (const [key, highValue] of Object.entries(higher)) { if (highValue === null) { delete result[key]; continue; } const lowValue = result[key]; result[key] = isPlainObject(lowValue) && isPlainObject(highValue) ? mergeObject(lowValue, highValue) : cloneValue(highValue); } return result; } /** * Deep object merge. Objects merge recursively; scalar and array conflicts * resolve last-layer-wins; null in a higher layer deletes the key. */ export function deepMergeSettings( ...layers: ReadonlyArray | undefined> ): Record { return layers.reduce>( (merged, layer) => (layer === undefined ? merged : mergeObject(merged, layer)), {}, ); } function lstatIfPresent(path: string): Stats | undefined { try { return lstatSync(path); } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; throw error; } } function assertRealDirectory(path: string, label: string, absentHint?: string): void { const info = lstatIfPresent(path); // Absent and wrong-shaped are different problems and want different words. A host that has // simply never enrolled a bundle was being told its auth directory "must be a real, // non-symlink directory", which reads as a tampering report rather than "log in first". if (!info) { throw new FleetLaunchError( 'COMPOSITION_FAILED', absentHint ? `${label} does not exist: ${path} — ${absentHint}` : `${label} does not exist: ${path}`, ); } if (!info.isDirectory() || info.isSymbolicLink()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `${label} must be a real, non-symlink directory: ${path}`, ); } } function assertContained(root: string, candidate: string, label: string): void { const rel = relative(resolve(root), resolve(candidate)); if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `${label} resolves outside ${root}: ${candidate}`, ); } } /** * Proves the framework is installed and knows this harness. This is the check the required * system settings layer used to stand in for, moved to the thing that is actually always * present: the runtime directory. A missing one means an uninstalled framework or a harness * the install does not carry, and both are worth failing on before a seat is composed. */ function assertHarnessRuntimeInstalled(systemHome: string, harness: string): void { const runtimeDir = join(systemHome, 'runtime', harness); if (!lstatIfPresent(runtimeDir)?.isDirectory()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `harness runtime is not installed: ${runtimeDir} — install the Mosaic framework, or check the harness name`, ); } } function readSettingsLayer( name: SettingsLayer['name'], path: string, required: boolean, ): SettingsLayer { const info = lstatIfPresent(path); if (!info) { if (required) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `required ${name} settings missing: ${path}`, ); } return { name, path, present: false, value: {} }; } if (!info.isFile() || info.isSymbolicLink()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `${name} settings must be a real, non-symlink JSON file: ${path}`, ); } let value: unknown; try { value = JSON.parse(readFileSync(path, 'utf8')) as unknown; } catch (error: unknown) { const detail = error instanceof Error ? error.message : String(error); throw new FleetLaunchError( 'COMPOSITION_FAILED', `${name} settings are invalid JSON: ${detail}`, ); } if (!isPlainObject(value)) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `${name} settings must contain a JSON object.`, ); } return { name, path, present: true, value }; } function accountEmail(bundleDir: string): string | undefined { const path = join(bundleDir, 'account.json'); const info = lstatIfPresent(path); if (!info?.isFile() || info.isSymbolicLink()) return undefined; try { const account = JSON.parse(readFileSync(path, 'utf8')) as Record; const oauth = isPlainObject(account['oauthAccount']) ? account['oauthAccount'] : undefined; for (const value of [oauth?.['emailAddress'], account['emailAddress'], account['email']]) { if (typeof value === 'string' && value.trim() !== '') return value.trim(); } } catch { return undefined; } return undefined; } function resolveCredential( profile: FleetAgentLaunchProfile, userHome: string, seatHome: string, _managedLinks: ManagedLinkState, ): Pick { assertRealDirectory(userHome, 'user Mosaic root'); const realUserHome = realpathSync(userHome); const authDirectory = join(userHome, 'auth'); const enrollHint = `no auth bundle has been enrolled yet — run: mosaic auth enroll --harness ${profile.harness} --bundle ${profile.bundle}`; assertRealDirectory(authDirectory, 'auth directory', enrollHint); const authRoot = join(authDirectory, profile.harness); assertRealDirectory(authRoot, `${profile.harness} auth root`, enrollHint); const resolvedAuthRoot = realpathSync(authRoot); assertContained(realUserHome, resolvedAuthRoot, `${profile.harness} auth root`); const bundlePath = join(authRoot, profile.bundle); const bundleInfo = lstatIfPresent(bundlePath); if (!bundleInfo) { throw new FleetLaunchError('COMPOSITION_FAILED', `credential bundle not found: ${bundlePath}`); } if (bundleInfo.isSymbolicLink() && profile.bundle !== 'primary') { throw new FleetLaunchError( 'COMPOSITION_FAILED', `only the primary bundle may be an alias symlink: ${bundlePath}`, ); } let resolvedBundleDir: string; try { resolvedBundleDir = realpathSync(bundlePath); } catch (error: unknown) { const detail = error instanceof Error ? error.message : String(error); throw new FleetLaunchError('COMPOSITION_FAILED', `credential bundle cannot resolve: ${detail}`); } assertContained(resolvedAuthRoot, resolvedBundleDir, 'credential bundle'); assertRealDirectory(resolvedBundleDir, 'resolved credential bundle'); const credentialTarget = join(resolvedBundleDir, CREDENTIAL_FILES[profile.harness]); const credentialInfo = lstatIfPresent(credentialTarget); if (!credentialInfo?.isFile() || credentialInfo.isSymbolicLink()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `bundle credential must be a real, non-symlink credential file: ${credentialTarget}`, ); } if ((credentialInfo.mode & 0o077) !== 0) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `bundle credential file must not grant group or other permissions: ${credentialTarget}`, ); } // Fleet launches and credential bundles share one operating-system user; ownership validation is deferred. const resolvedCredential = realpathSync(credentialTarget); assertContained(resolvedAuthRoot, resolvedCredential, 'bundle credential'); const credentialLink = join(seatHome, CREDENTIAL_FILES[profile.harness]); const seatInfo = lstatIfPresent(credentialLink); if (seatInfo && !seatInfo.isSymbolicLink()) { throw new FleetLaunchError( 'FIRST_AUTH_REFUSAL', `first-auth state detected at ${credentialLink}; refusing to delete or overwrite the real credential file. Enroll or promote it explicitly.`, ); } // Environment-shared harnesses never read the seat-local path, so no link is // planned for it. A leftover link from an earlier scaffold is inert: the harness // resolves its credential directory from the environment instead. const sharesByEnv = CREDENTIAL_DIR_ENV[profile.harness] !== undefined; const resolvedName = basename(resolvedBundleDir); const email = accountEmail(resolvedBundleDir); const display = profile.bundle === resolvedName ? `${resolvedName}${email ? ` (${email})` : ''}` : `${profile.bundle} -> ${resolvedName}${email ? ` (${email})` : ''}`; return { bundle: { requested: profile.bundle, resolved: resolvedName, ...(email === undefined ? {} : { email }), display, }, credential: { ...(sharesByEnv ? {} : { link: credentialLink }), target: resolvedCredential, dir: resolvedBundleDir, }, }; } function currentLinkTarget(link: string): string { return resolve(dirname(link), readlinkSync(link)); } function resolveManagedLinks( kind: PlannedLink['kind'], names: readonly string[], userHome: string, seatHome: string, managedLinks: ManagedLinkState, ): { installs: PlannedLink[]; prune: string[] } { const plural = kind === 'plugin' ? 'plugins' : 'skills'; const storeRoot = join(userHome, plural); const installRoot = join(seatHome, plural); if (names.length > 0) assertRealDirectory(storeRoot, `${kind} store root`); const installRootInfo = lstatIfPresent(installRoot); if (installRootInfo?.isSymbolicLink() || (installRootInfo && !installRootInfo.isDirectory())) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `${kind} install root must be a real directory (the historical whole-store symlink requires explicit migration): ${installRoot}`, ); } const installs: PlannedLink[] = names.map((name: string): PlannedLink => { const target = join(storeRoot, name); const targetInfo = lstatIfPresent(target); if (!targetInfo?.isDirectory() || targetInfo.isSymbolicLink()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `${kind} store entry must be a real, non-symlink directory: ${target}`, ); } assertContained(realpathSync(storeRoot), realpathSync(target), `${kind} store entry`); const link = join(installRoot, name); const linkInfo = lstatIfPresent(link); if (linkInfo && !linkInfo.isSymbolicLink()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `real ${kind} directory occupies managed symlink path ${link}; refusing to delete it.`, ); } return { kind, name, link, target: realpathSync(target) }; }); const desired = new Set(names); const prune: string[] = []; if (installRootInfo?.isDirectory()) { for (const entry of readdirSync(installRoot, { withFileTypes: true })) { const path = join(installRoot, entry.name); if (!entry.isSymbolicLink()) { // The harness writes its own metadata files (e.g. installed_plugins.json) // beside the managed links; only a real directory is an unmanaged entry // the pruner would orphan. if (entry.isDirectory()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `real ${kind} directory occupies managed install root ${path}; refusing to prune it.`, ); } continue; } const currentTarget = currentLinkTarget(path); const recordedTarget = managedLinks.links.get(path); if (recordedTarget === undefined) { if ( !managedLinks.existed && (() => { try { assertContained( realpathSync(storeRoot), realpathSync(currentTarget), `${kind} migration`, ); return true; } catch { return false; } })() ) { // A pre-manifest seat may adopt only links to the central Mosaic store; foreign links refuse. managedLinks.links.set(path, currentTarget); } } else if (recordedTarget !== currentTarget) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `managed ${kind} symlink target changed since composition: ${path}`, ); } if (!desired.has(entry.name)) prune.push(path); } } return { installs, prune }; } function readManagedLinkState( seatHome: string, profile: FleetAgentLaunchProfile, userHome: string, ): ManagedLinkState { const path = join(seatHome, '.mosaic-managed-links.json'); const info = lstatIfPresent(path); if (!info) return { path, links: new Map(), existed: false }; if (!info.isFile() || info.isSymbolicLink()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `managed link manifest must be a real, non-symlink JSON file: ${path}`, ); } let parsed: unknown; try { parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown; } catch (error: unknown) { const detail = error instanceof Error ? error.message : String(error); throw new FleetLaunchError( 'COMPOSITION_FAILED', `managed link manifest is invalid JSON: ${detail}`, ); } if (!isPlainObject(parsed) || !isPlainObject(parsed['links'])) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `managed link manifest has invalid shape: ${path}`, ); } const links = new Map(); for (const [link, target] of Object.entries(parsed['links'])) { if (typeof target !== 'string' || !isAbsolute(link) || !isAbsolute(target)) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `managed link manifest has invalid entry: ${path}`, ); } const credential = join(seatHome, CREDENTIAL_FILES[profile.harness]); const pluginRoot = join(seatHome, 'plugins'); const skillRoot = join(seatHome, 'skills'); const authRoot = join(userHome, 'auth', profile.harness); const inRoot = (root: string, candidate: string): boolean => { const rel = relative(resolve(root), resolve(candidate)); return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel); }; const valid = (link === credential && inRoot(authRoot, target)) || (inRoot(pluginRoot, link) && inRoot(join(userHome, 'plugins'), target)) || (inRoot(skillRoot, link) && inRoot(join(userHome, 'skills'), target)); if (!valid) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `managed link manifest entry escapes an approved seat/store root: ${path}`, ); } links.set(link, target); } return { path, links, existed: true }; } interface PreparedManagedLinkManifest { readonly path: string; readonly descriptor: number; } function prepareManagedLinkManifest(managedLinks: ManagedLinkState): PreparedManagedLinkManifest { const path = `${managedLinks.path}.tmp`; try { return { path, descriptor: openSync(path, 'wx', 0o600) }; } catch (error: unknown) { const detail = error instanceof Error ? error.message : String(error); throw new FleetLaunchError( 'COMPOSITION_FAILED', `managed link manifest temporary file cannot be created exclusively: ${detail}`, ); } } function writeManagedLinkState( managedLinks: ManagedLinkState, prepared: PreparedManagedLinkManifest, ): void { const entries = [...managedLinks.links.entries()].sort(([left], [right]) => left.localeCompare(right), ); const manifest: ManagedLinkManifest = { links: Object.fromEntries(entries) }; writeSync(prepared.descriptor, `${JSON.stringify(manifest, null, 2)}\n`); } function buildArgv( profile: FleetAgentLaunchProfile, seatHome: string, passthrough: string[], ): string[] { const argv: string[] = [profile.harness]; // A caller-supplied --model replaces the profile's rather than being appended after // it. The fleet roster carries a model per seat and is the surface operators edit, so // it has to win; emitting both flags would leave that to each harness's arg parser. if (profile.model && !passthrough.includes('--model')) argv.push('--model', profile.model); if (profile.harness === 'pi') { for (const skill of profile.skills) argv.push('--skill', join(seatHome, 'skills', skill)); } argv.push(...passthrough); return argv; } /** Resolve and validate the complete launch without changing the filesystem. */ export function resolveFleetLaunchComposition( name: string, roots: FleetLaunchRoots, passthrough: string[] = [], ): FleetLaunchComposition { if (!AGENT_NAME.test(name)) { throw new FleetLaunchError('PROFILE_INVALID', `invalid fleet agent name: ${name}`); } const agentDir = join(roots.userHome, 'fleet', 'agents', name); if (lstatIfPresent(agentDir) === undefined) { throw new FleetLaunchError( 'AGENT_NOT_SCAFFOLDED', `no such fleet agent '${name}' — run: mosaic fleet agent new ${name}`, ); } assertRealDirectory(agentDir, 'fleet agent directory'); const profilePath = join(agentDir, 'profile.json'); const profileInfo = lstatIfPresent(profilePath); if (!profileInfo?.isFile() || profileInfo.isSymbolicLink()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `agent profile must be a real, non-symlink file: ${profilePath}`, ); } const profile = parseFleetAgentProfile(readFileSync(profilePath, 'utf8')); const context: FleetHarnessContext = { agentDir }; const seatHome = harnessHome(profile.harness, context); const seatHomeInfo = lstatIfPresent(seatHome); if (seatHomeInfo && (!seatHomeInfo.isDirectory() || seatHomeInfo.isSymbolicLink())) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `agent harness home must be a real, non-symlink directory: ${seatHome}`, ); } const settingsOutput = join(seatHome, 'settings.json'); const settingsSnapshot = join(agentDir, 'settings.generated.json'); for (const [label, path] of [ ['generated settings', settingsOutput], ['generated settings snapshot', settingsSnapshot], ] as const) { const info = lstatIfPresent(path); if (info && (!info.isFile() || info.isSymbolicLink())) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `${label} path must be a real file or absent: ${path}`, ); } } const overlayPath = join(agentDir, profile.overlay ?? 'overlay.json'); assertContained(agentDir, overlayPath, 'agent overlay'); // The framework ships a runtime directory per harness but a settings.json only where it // has settings to state -- as of 0.0.49 that is claude alone, so requiring the file made // every pi, codex and opencode seat unlaunchable on a clean install. The install is what // has to be present; an absent base layer just means the harness has no system settings. assertHarnessRuntimeInstalled(roots.systemHome, profile.harness); const layers: SettingsLayer[] = [ readSettingsLayer( 'system', join(roots.systemHome, 'runtime', profile.harness, 'settings.json'), false, ), readSettingsLayer( 'user', join(roots.userHome, 'config', profile.harness, 'settings.json'), false, ), profile.overlay === undefined ? { name: 'agent', path: overlayPath, present: false, value: {} } : readSettingsLayer('agent', overlayPath, false), ]; const merged = deepMergeSettings(...layers.map((layer) => layer.value)); const managedLinks = readManagedLinkState(seatHome, profile, roots.userHome); const credential = resolveCredential(profile, roots.userHome, seatHome, managedLinks); const plugins = resolveManagedLinks( 'plugin', profile.plugins, roots.userHome, seatHome, managedLinks, ); const skills = resolveManagedLinks( 'skill', profile.skills, roots.userHome, seatHome, managedLinks, ); const homeEnvName: Record = { claude: 'CLAUDE_CONFIG_DIR', pi: 'PI_CODING_AGENT_DIR', codex: 'CODEX_HOME', opencode: 'XDG_CONFIG_HOME', }; const credentialDirEnvName = CREDENTIAL_DIR_ENV[profile.harness]; const env: Record = { ...profile.env, [homeEnvName[profile.harness]]: seatHome, // Only ever an absolute bundle path. Claude reads an empty value as ~/.claude, // which is the operator's own account, so an empty value is never exported. ...(credentialDirEnvName === undefined ? {} : { [credentialDirEnvName]: credential.credential.dir }), MOSAIC_AGENT_NAME: name, }; return { name, profilePath, systemHome: roots.systemHome, profile, agentDir, seatHome, settings: { layers, merged, output: settingsOutput, snapshot: settingsSnapshot, }, ...credential, managedLinks, installs: [...plugins.installs, ...skills.installs], prune: [...plugins.prune, ...skills.prune], env, argv: buildArgv(profile, seatHome, passthrough), }; } function ensureSymlink(link: string, target: string, managedLinks: ManagedLinkState): void { const info = lstatIfPresent(link); if (info?.isSymbolicLink()) { const current = currentLinkTarget(link); if (managedLinks.links.get(link) !== current) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `unrecorded or retargeted symlink occupies managed path: ${link}`, ); } rmSync(link); } else if (info) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `real object occupies managed symlink path ${link}; refusing to delete it.`, ); } mkdirSync(dirname(link), { recursive: true }); symlinkSync(target, link, 'file'); managedLinks.links.set(link, target); } function assertManagedLinkMutationAllowed( link: string, target: string | undefined, managedLinks: ManagedLinkState, ): void { const info = lstatIfPresent(link); if (!info) return; if (!info.isSymbolicLink()) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `real object occupies managed symlink path ${link}; refusing to delete it.`, ); } const current = currentLinkTarget(link); if (managedLinks.links.get(link) !== current) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `unrecorded or retargeted symlink occupies managed path: ${link}`, ); } } function canonicalJson(value: unknown): unknown { if (Array.isArray(value)) return value.map(canonicalJson); if (!isPlainObject(value)) return value; return Object.fromEntries( Object.keys(value) .sort((left, right) => left.localeCompare(right, 'en')) .map((key) => [key, canonicalJson(value[key])]), ); } /** Apply a previously resolved plan. No caller should apply a dry-run plan. */ export function applyFleetLaunchComposition(plan: FleetLaunchComposition): void { // All link-state checks must complete before the first filesystem mutation. // This makes a late foreign/retargeted link refusal leave the seat untouched. if (plan.credential.link !== undefined) { assertManagedLinkMutationAllowed( plan.credential.link, plan.credential.target, plan.managedLinks, ); } for (const path of plan.prune) assertManagedLinkMutationAllowed(path, undefined, plan.managedLinks); for (const install of plan.installs) { assertManagedLinkMutationAllowed(install.link, install.target, plan.managedLinks); } mkdirSync(plan.seatHome, { recursive: true }); const preparedManifest = prepareManagedLinkManifest(plan.managedLinks); let descriptorOpen = true; let committedManifest = false; try { const settings = `${JSON.stringify(canonicalJson(plan.settings.merged), null, 2)}\n`; writeFileSync(plan.settings.output, settings, { mode: 0o600 }); writeFileSync(plan.settings.snapshot, settings, { mode: 0o600 }); if (plan.credential.link !== undefined) { ensureSymlink(plan.credential.link, plan.credential.target, plan.managedLinks); } for (const path of plan.prune) { const info = lstatIfPresent(path); if (info?.isSymbolicLink()) { const current = currentLinkTarget(path); if (plan.managedLinks.links.get(path) !== current) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `unrecorded or retargeted symlink cannot be pruned: ${path}`, ); } rmSync(path); plan.managedLinks.links.delete(path); } else if (info) { throw new FleetLaunchError( 'COMPOSITION_FAILED', `real object replaced managed symlink before prune: ${path}`, ); } } for (const install of plan.installs) { ensureSymlink(install.link, install.target, plan.managedLinks); } writeManagedLinkState(plan.managedLinks, preparedManifest); closeSync(preparedManifest.descriptor); descriptorOpen = false; renameSync(preparedManifest.path, plan.managedLinks.path); committedManifest = true; } finally { if (!committedManifest) { if (descriptorOpen) closeSync(preparedManifest.descriptor); rmSync(preparedManifest.path, { force: true }); } } } /** Stable, auditable text representation used by --dry-run and snapshot tests. */ export function formatFleetLaunchDryRun(plan: FleetLaunchComposition): string { const lines = [ `mosaic fleet launch ${plan.name} --dry-run`, `profile: ${plan.profilePath} (schema ${plan.profile.schema})`, `harness: ${plan.profile.harness}`, `seat-home: ${plan.seatHome}`, 'settings sources:', ]; for (const layer of plan.settings.layers) { lines.push(` ${layer.name}: ${layer.path}${layer.present ? '' : ' (absent)'}`); } lines.push(` output: ${plan.settings.output}`); lines.push(` snapshot: ${plan.settings.snapshot}`); lines.push('merged settings:'); lines.push(JSON.stringify(canonicalJson(plan.settings.merged), null, 2)); lines.push(`bundle: ${plan.bundle.display}`); lines.push(`credential: ${plan.credential.target}`); lines.push('symlinks:'); // Environment-shared harnesses have no credential symlink; the exported // credential-directory variable below is what points them at the bundle. if (plan.credential.link !== undefined) { lines.push(` credentials: ${plan.credential.link} -> ${plan.credential.target}`); } for (const install of plan.installs) { lines.push(` ${install.kind} ${install.name}: ${install.link} -> ${install.target}`); } for (const path of plan.prune) lines.push(` prune: ${path}`); lines.push('declared env:'); for (const key of Object.keys(plan.env).sort()) lines.push(` ${key}=${plan.env[key]}`); lines.push(`argv: ${JSON.stringify(plan.argv)}`); return lines.join('\n'); } /** Register `mosaic fleet launch [--dry-run]` on the fleet control plane. */ export function registerFleetLaunchCommand( fleetCommand: Command, systemHomeFor: () => string, deps: FleetLaunchCommandDeps = {}, ): Command { return fleetCommand .command('launch ') .description('Compose and launch one per-agent harness home') .option('--dry-run', 'Print the fully resolved composition without writing or launching') .option('--dangerous', 'Launch the seat in dangerous-permissions mode, as `mosaic yolo` does') .allowUnknownOption(true) .allowExcessArguments(true) .action( (name: string, opts: { dryRun?: boolean; dangerous?: boolean }, command: Command): void => { try { const userHome = deps.userHome ?? defaultFleetDataHome(); const passthrough = command.args.slice(1); const plan = resolveFleetLaunchComposition( name, { systemHome: systemHomeFor(), userHome }, passthrough, ); if (opts.dryRun === true) { process.stdout.write(`${formatFleetLaunchDryRun(plan)}\n`); return; } applyFleetLaunchComposition(plan); console.log(`[mosaic] bundle: ${plan.bundle.display}`); const launcher = deps.launcher ?? launchFleetRuntime; // Dangerous mode is the caller's to ask for, not the seat's to assume. An // unattended tmux seat needs it -- a permission prompt with nobody at the pane // is a hung agent -- so the roster launcher passes the flag explicitly and it // stays visible in the process table rather than hiding in a profile default. launcher( plan.profile.harness, plan.argv.slice(1), plan.env, { agentDir: plan.agentDir, mosaicHome: plan.systemHome }, opts.dangerous === true, ); } catch (error: unknown) { process.exitCode = 1; const code = error instanceof FleetLaunchError ? `${error.code}: ` : ''; const message = error instanceof Error ? error.message : String(error); process.stderr.write(`mosaic fleet launch failed: ${code}${message}\n`); } }, ); }