feat(fleet): HARNESS-HOMES fleet MVP — profile-driven seat composition and launch (#1209) #1213
@@ -122,6 +122,24 @@ describe('profile-selected overlay', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('unscaffolded agent names', () => {
|
||||
it('points an unscaffolded name at mosaic fleet agent new', () => {
|
||||
const fx = fixture();
|
||||
try {
|
||||
resolveFleetLaunchComposition('ghost', {
|
||||
systemHome: fx.systemHome,
|
||||
userHome: fx.userHome,
|
||||
});
|
||||
throw new Error('expected resolution to fail');
|
||||
} catch (error: unknown) {
|
||||
const launchError = error as FleetLaunchError;
|
||||
expect(launchError.code).toBe('AGENT_NOT_SCAFFOLDED');
|
||||
expect(launchError.message).toContain("no such fleet agent 'ghost'");
|
||||
expect(launchError.message).toContain('mosaic fleet agent new ghost');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('A3 credential validation', () => {
|
||||
it('refuses a symlinked bundle credential file', () => {
|
||||
const fx = fixture();
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
writeFileSync,
|
||||
type Stats,
|
||||
} from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
import type { Command } from 'commander';
|
||||
import {
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
type FleetHarnessContext,
|
||||
type RuntimeName,
|
||||
} from './launch.js';
|
||||
import { defaultFleetDataHome } from '../fleet/fleet-agent-scaffold.js';
|
||||
|
||||
export const FLEET_AGENT_PROFILE_SCHEMA = 1;
|
||||
const PROFILE_KEYS = [
|
||||
@@ -47,6 +47,7 @@ const CREDENTIAL_FILES: Record<RuntimeName, string> = {
|
||||
export type FleetLaunchErrorCode =
|
||||
| 'SCHEMA_TOO_NEW'
|
||||
| 'PROFILE_INVALID'
|
||||
| 'AGENT_NOT_SCAFFOLDED'
|
||||
| 'COMPOSITION_FAILED'
|
||||
| 'FIRST_AUTH_REFUSAL';
|
||||
|
||||
@@ -513,6 +514,12 @@ export function resolveFleetLaunchComposition(
|
||||
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);
|
||||
@@ -687,8 +694,7 @@ export function registerFleetLaunchCommand(
|
||||
.allowExcessArguments(true)
|
||||
.action((name: string, opts: { dryRun?: boolean }, command: Command): void => {
|
||||
try {
|
||||
const userHome =
|
||||
deps.userHome ?? process.env['MOSAIC_USER_HOME'] ?? join(homedir(), '.mosaic');
|
||||
const userHome = deps.userHome ?? defaultFleetDataHome();
|
||||
const passthrough = command.args.slice(1);
|
||||
const plan = resolveFleetLaunchComposition(
|
||||
name,
|
||||
|
||||
Reference in New Issue
Block a user