import { readdir } from 'node:fs/promises'; import { basename, join } from 'node:path'; import { loadFleetRoster } from '../commands/fleet.js'; import { loadProfiles } from '../commands/fleet-profiles.js'; import { provisionInteractionService, readInteractionServiceProfile, } from './interaction-service-profile.js'; export type FleetArtifactDisposition = | 'v1-fixture' | 'canonical-profile' | 'canonical-service-policy'; export interface ShippedFleetArtifactDisposition { readonly path: string; readonly disposition: FleetArtifactDisposition; } export interface ValidateShippedFleetArtifactDispositionsOptions { readonly frameworkFleet: string; readonly rolesDir?: string; readonly overrideDir?: string; } /** * The M0 inventory in executable form. Every shipped fleet YAML asset is either * a deliberately retained v1 fixture or validated through its canonical loader. */ export const SHIPPED_FLEET_ARTIFACT_DISPOSITIONS: readonly ShippedFleetArtifactDisposition[] = [ { path: 'examples/coding.yaml', disposition: 'v1-fixture' }, { path: 'examples/general.yaml', disposition: 'v1-fixture' }, { path: 'examples/hybrid.yaml', disposition: 'v1-fixture' }, { path: 'examples/local-canary.yaml', disposition: 'v1-fixture' }, { path: 'examples/minimal.yaml', disposition: 'v1-fixture' }, { path: 'examples/operator-interaction.yaml', disposition: 'v1-fixture' }, { path: 'examples/research.yaml', disposition: 'v1-fixture' }, { path: 'profiles/business.yaml', disposition: 'canonical-profile' }, { path: 'profiles/marketing.yaml', disposition: 'canonical-profile' }, { path: 'profiles/personal-assistant.yaml', disposition: 'canonical-profile' }, { path: 'profiles/research.yaml', disposition: 'canonical-profile' }, { path: 'profiles/software-delivery.yaml', disposition: 'canonical-profile' }, { path: 'services/operator-interaction.yaml', disposition: 'canonical-service-policy' }, ]; /** * Fail closed when a fleet YAML asset is added or removed without a disposition. * This keeps legacy v1 compatibility explicit instead of silently accepting new * unresolved classes outside the shared resolver. */ export async function validateShippedFleetArtifactDispositions( options: ValidateShippedFleetArtifactDispositionsOptions, ): Promise { await assertEveryShippedArtifactIsDeclared(options.frameworkFleet); const examples = SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.filter( ({ disposition }): boolean => disposition === 'v1-fixture', ); for (const artifact of examples) { const roster = await loadFleetRoster(join(options.frameworkFleet, artifact.path)); if (roster.version !== 1) { throw new Error(`v1 fixture ${artifact.path} must declare version: 1`); } } const profilesDir = join(options.frameworkFleet, 'profiles'); const profiles = await loadProfiles({ profilesDir, rolesDir: options.rolesDir ?? join(options.frameworkFleet, 'roles'), overrideDir: options.overrideDir ?? join(options.frameworkFleet, 'roles.local'), }); const declaredProfiles = SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.filter( ({ disposition }): boolean => disposition === 'canonical-profile', ).map(({ path }): string => basename(path, '.yaml')); const resolvedProfiles = new Set(profiles.map(({ id }): string => id)); for (const profileId of declaredProfiles) { if (!resolvedProfiles.has(profileId)) { throw new Error(`declared canonical profile ${profileId} did not resolve`); } } const servicePolicies = SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.filter( ({ disposition }): boolean => disposition === 'canonical-service-policy', ); for (const artifact of servicePolicies) { const serviceProfile = await readInteractionServiceProfile( join(options.frameworkFleet, artifact.path), ); provisionInteractionService(serviceProfile, { agentName: 'interaction-example' }); } return SHIPPED_FLEET_ARTIFACT_DISPOSITIONS; } async function assertEveryShippedArtifactIsDeclared(frameworkFleet: string): Promise { const declared = new Set(SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.map(({ path }): string => path)); const shipped = await listShippedFleetArtifactPaths(frameworkFleet); for (const path of shipped) { if (!declared.has(path)) { throw new Error(`undeclared shipped fleet artifact: ${path}`); } } for (const path of declared) { if (!shipped.has(path)) { throw new Error(`declared shipped fleet artifact is missing: ${path}`); } } } async function listShippedFleetArtifactPaths(frameworkFleet: string): Promise> { const directories = ['examples', 'profiles', 'services']; const paths = new Set(); for (const directory of directories) { const files = await readdir(join(frameworkFleet, directory)); for (const file of files) { if (file.endsWith('.yaml') || file.endsWith('.yml')) paths.add(`${directory}/${file}`); } } return paths; }