Files
stack/packages/mosaic/src/fleet/fleet-roster-v1.ts
jason.woltje adad486b6f
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
fix(fleet): enforce exact comms authority (#787)
2026-07-16 00:32:23 +00:00

523 lines
16 KiB
TypeScript

import { lstatSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import YAML from 'yaml';
import { canonicalizeRoleClass } from '../commands/fleet-personas.js';
interface RawFleetRoster {
version?: unknown;
transport?: unknown;
tmux?: {
socket_name?: unknown;
socketName?: unknown;
holder_session?: unknown;
holderSession?: unknown;
};
defaults?: {
working_directory?: unknown;
workingDirectory?: unknown;
};
runtimes?: Record<string, { reset_command?: unknown; resetCommand?: unknown }>;
agents?: Array<{
name?: unknown;
alias?: unknown;
provider?: unknown;
runtime?: unknown;
class?: unknown;
host?: unknown;
ssh?: unknown;
socket?: unknown;
working_directory?: unknown;
workingDirectory?: unknown;
model_hint?: unknown;
modelHint?: unknown;
reasoning_level?: unknown;
reasoningLevel?: unknown;
tool_policy?: unknown;
toolPolicy?: unknown;
persistent_persona?: unknown;
persistentPersona?: unknown;
reset_between_tasks?: unknown;
resetBetweenTasks?: unknown;
kickstart_template?: unknown;
kickstartTemplate?: unknown;
}>;
connector?: {
kind?: unknown;
matrix?: {
homeserver_url?: unknown;
user_id?: unknown;
room_id?: unknown;
};
discord?: {
channel_id?: unknown;
};
};
}
export interface FleetAgent {
name: string;
alias?: string;
provider?: string;
runtime: string;
className: string;
/** Resolved host identity. Absent means the caller's authoritative local host. */
host?: string;
/** Explicit SSH destination for a cross-host inventory peer. */
ssh?: string;
/** Compatibility declaration; when set it must equal fleet-wide tmux.socketName. */
socket?: string;
workingDirectory?: string;
modelHint?: string;
reasoningLevel?: string;
toolPolicy?: string;
persistentPersona?: boolean | string;
resetBetweenTasks?: boolean;
kickstartTemplate?: string;
}
export type FleetConnector =
| { kind: 'tmux' }
| {
kind: 'discord';
discord: { channelId: string };
}
| {
kind: 'matrix';
matrix: { homeserverUrl: string; userId: string; roomId: string };
};
export interface FleetRoster {
version: 1;
transport: 'tmux';
tmux: {
socketName: string;
holderSession: string;
};
defaults: {
workingDirectory: string;
};
runtimes: Record<string, { resetCommand: string }>;
agents: FleetAgent[];
connector?: FleetConnector;
}
export type FleetRosterInputFormat = 'yaml' | 'json';
export function resolveInstalledFleetRosterPath(mosaicHome: string): string {
const yamlPath = join(mosaicHome, 'fleet', 'roster.yaml');
try {
lstatSync(yamlPath);
return yamlPath;
} catch (error) {
if (!isNodeErrorCode(error, 'ENOENT')) throw error;
return join(mosaicHome, 'fleet', 'roster.json');
}
}
const DEFAULT_HOLDER_SESSION = '_holder';
const DEFAULT_WORKING_DIRECTORY = '~/src';
const DEFAULT_RUNTIME_RESETS: Record<string, { resetCommand: string }> = {
claude: { resetCommand: '/clear' },
codex: { resetCommand: '/clear' },
opencode: { resetCommand: '/clear' },
pi: { resetCommand: '/new' },
};
/** One structural v1 resolver used by fleet commands and runtime comms composition. */
export function parseFleetRosterV1(
source: string,
format: FleetRosterInputFormat = 'yaml',
): FleetRoster {
const trimmed = source.trim();
const parsed =
format === 'json'
? (JSON.parse(trimmed) as RawFleetRoster)
: (YAML.parse(trimmed) as RawFleetRoster);
return normalizeFleetRosterV1(parsed);
}
export async function loadFleetRoster(path: string): Promise<FleetRoster> {
const source = await readFile(path, 'utf8');
return parseFleetRosterV1(source, path.endsWith('.json') ? 'json' : 'yaml');
}
export function getRosterAgent(roster: FleetRoster, name: string): FleetAgent {
const agent = roster.agents.find((candidate) => candidate.name === name);
if (!agent) throw new Error(`Agent "${name}" is not in the fleet roster.`);
return agent;
}
export function normalizeFleetRosterV1(raw: RawFleetRoster): FleetRoster {
assertObject(raw, 'Fleet roster');
assertKnownKeys(raw, 'Fleet roster', [
'version',
'transport',
'tmux',
'defaults',
'runtimes',
'agents',
'connector',
]);
if (raw.tmux !== undefined) {
assertObject(raw.tmux, 'Fleet roster tmux');
assertKnownKeys(raw.tmux, 'Fleet roster tmux', [
'socket_name',
'socketName',
'holder_session',
'holderSession',
]);
}
if (raw.defaults !== undefined) {
assertObject(raw.defaults, 'Fleet roster defaults');
assertKnownKeys(raw.defaults, 'Fleet roster defaults', [
'working_directory',
'workingDirectory',
]);
}
if (raw.runtimes !== undefined) {
assertObject(raw.runtimes, 'Fleet roster runtimes');
for (const [runtime, config] of Object.entries(raw.runtimes)) {
assertObject(config, `Fleet roster runtime "${runtime}"`);
assertKnownKeys(config, `Fleet roster runtime "${runtime}"`, [
'reset_command',
'resetCommand',
]);
}
}
if (raw.version !== 1) throw new Error('Fleet roster version must be 1.');
if (raw.transport !== 'tmux') throw new Error('Fleet roster transport must be "tmux".');
if (!Array.isArray(raw.agents) || raw.agents.length === 0) {
throw new Error('Fleet roster must define at least one agent.');
}
const socketName = targetingString(
aliasValue(raw.tmux, 'socket_name', 'socketName', 'Fleet roster tmux socket'),
'',
'Fleet roster tmux socket_name',
/^[A-Za-z0-9_.-]+$/,
);
const agents = raw.agents.map(normalizeAgent);
assertUniqueAgentNames(agents);
for (const agent of agents) {
if (agent.socket !== undefined && agent.socket !== socketName) {
throw new Error(
`Fleet agent "${agent.name}" socket must equal the fleet-wide tmux socket_name; independent per-agent sockets are not supported.`,
);
}
}
return {
version: 1,
transport: 'tmux',
tmux: {
socketName,
holderSession: stringValue(
aliasValue(raw.tmux, 'holder_session', 'holderSession', 'Fleet roster tmux holder'),
DEFAULT_HOLDER_SESSION,
'Fleet roster tmux holder_session',
),
},
defaults: {
workingDirectory: stringValue(
aliasValue(
raw.defaults,
'working_directory',
'workingDirectory',
'Fleet roster defaults working directory',
),
DEFAULT_WORKING_DIRECTORY,
'Fleet roster defaults working_directory',
),
},
runtimes: normalizeRuntimes(raw.runtimes as RawFleetRoster['runtimes']),
agents,
connector: normalizeConnector(raw.connector as RawFleetRoster['connector']),
};
}
function normalizeAgent(raw: NonNullable<RawFleetRoster['agents']>[number]): FleetAgent {
assertObject(raw, 'Fleet roster agent');
assertKnownKeys(raw, 'Fleet roster agent', [
'name',
'alias',
'provider',
'runtime',
'class',
'host',
'ssh',
'socket',
'working_directory',
'workingDirectory',
'model_hint',
'modelHint',
'reasoning_level',
'reasoningLevel',
'tool_policy',
'toolPolicy',
'persistent_persona',
'persistentPersona',
'reset_between_tasks',
'resetBetweenTasks',
'kickstart_template',
'kickstartTemplate',
]);
const name = stringValue(raw.name, '', 'Fleet roster agent name');
const runtime = stringValue(
raw.runtime,
'',
`Fleet roster agent "${name || '<unknown>'}" runtime`,
);
if (!name || !/^[A-Za-z0-9_.-]+$/.test(name)) {
throw new Error(`Invalid fleet agent name: ${name || '<empty>'}`);
}
if (!runtime) throw new Error(`Fleet agent "${name}" must define a runtime.`);
return {
name,
alias: optionalString(raw.alias, `Fleet roster agent "${name}" alias`),
provider: optionalString(raw.provider, `Fleet roster agent "${name}" provider`),
runtime,
className: canonicalizeRoleClass(
stringValue(raw.class, 'worker', `Fleet roster agent "${name}" class`),
).canonicalClass,
host: optionalTargetingString(
raw.host,
`Fleet roster agent "${name}" host`,
/^[A-Za-z0-9_.:[\]-]+$/,
),
ssh: optionalTargetingString(
raw.ssh,
`Fleet roster agent "${name}" ssh`,
/^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9_.:[\]-]+$/,
),
socket: optionalTargetingString(
raw.socket,
`Fleet roster agent "${name}" socket`,
/^[A-Za-z0-9_.-]+$/,
),
workingDirectory: optionalString(
aliasValue(
raw,
'working_directory',
'workingDirectory',
`Fleet roster agent "${name}" working directory`,
),
`Fleet roster agent "${name}" working_directory`,
),
modelHint: optionalString(
aliasValue(raw, 'model_hint', 'modelHint', `Fleet roster agent "${name}" model hint`),
`Fleet roster agent "${name}" model_hint`,
),
reasoningLevel: optionalString(
aliasValue(
raw,
'reasoning_level',
'reasoningLevel',
`Fleet roster agent "${name}" reasoning level`,
),
`Fleet roster agent "${name}" reasoning_level`,
),
toolPolicy: optionalString(
aliasValue(raw, 'tool_policy', 'toolPolicy', `Fleet roster agent "${name}" tool policy`),
`Fleet roster agent "${name}" tool_policy`,
),
persistentPersona: optionalBooleanOrString(
aliasValue(
raw,
'persistent_persona',
'persistentPersona',
`Fleet roster agent "${name}" persistent persona`,
),
`Fleet roster agent "${name}" persistent_persona`,
),
resetBetweenTasks: optionalBoolean(
aliasValue(
raw,
'reset_between_tasks',
'resetBetweenTasks',
`Fleet roster agent "${name}" reset between tasks`,
),
`Fleet roster agent "${name}" reset_between_tasks`,
),
kickstartTemplate: optionalString(
aliasValue(
raw,
'kickstart_template',
'kickstartTemplate',
`Fleet roster agent "${name}" kickstart template`,
),
`Fleet roster agent "${name}" kickstart_template`,
),
};
}
function normalizeRuntimes(
raw: RawFleetRoster['runtimes'] | undefined,
): Record<string, { resetCommand: string }> {
const result: Record<string, { resetCommand: string }> = { ...DEFAULT_RUNTIME_RESETS };
for (const [runtime, config] of Object.entries(raw ?? {})) {
result[runtime] = {
resetCommand: stringValue(
aliasValue(
config,
'reset_command',
'resetCommand',
`Fleet roster runtime "${runtime}" reset command`,
),
'/clear',
`Fleet roster runtime "${runtime}" reset_command`,
),
};
}
return result;
}
function normalizeConnector(raw: RawFleetRoster['connector']): FleetConnector | undefined {
if (raw === undefined) return undefined;
assertObject(raw, 'Fleet roster connector');
assertKnownKeys(raw, 'Fleet roster connector', ['kind', 'matrix', 'discord']);
const kind = stringValue(raw.kind, '', 'Fleet roster connector kind');
if (kind === 'tmux') {
if (raw.matrix !== undefined || raw.discord !== undefined) {
throw new Error('Fleet roster tmux connector must not define matrix or discord settings.');
}
return { kind };
}
if (kind === 'discord') {
if (raw.matrix !== undefined) {
throw new Error('Fleet roster discord connector must not define matrix settings.');
}
assertObject(raw.discord, 'Fleet roster connector discord');
assertKnownKeys(raw.discord, 'Fleet roster connector discord', ['channel_id']);
return {
kind,
discord: {
channelId: requiredString(
raw.discord.channel_id,
'Fleet roster connector discord channel_id',
),
},
};
}
if (kind === 'matrix') {
if (raw.discord !== undefined) {
throw new Error('Fleet roster matrix connector must not define discord settings.');
}
assertObject(raw.matrix, 'Fleet roster connector matrix');
assertKnownKeys(raw.matrix, 'Fleet roster connector matrix', [
'homeserver_url',
'user_id',
'room_id',
]);
return {
kind,
matrix: {
homeserverUrl: requiredString(
raw.matrix.homeserver_url,
'Fleet roster connector matrix homeserver_url',
),
userId: requiredString(raw.matrix.user_id, 'Fleet roster connector matrix user_id'),
roomId: requiredString(raw.matrix.room_id, 'Fleet roster connector matrix room_id'),
},
};
}
throw new Error('Fleet roster connector kind must be one of: tmux, discord, matrix.');
}
function aliasValue<T extends Record<string, unknown>>(
source: T | undefined,
snake: keyof T,
camel: keyof T,
label: string,
): unknown {
const snakeValue = source?.[snake];
const camelValue = source?.[camel];
if (snakeValue !== undefined && camelValue !== undefined && snakeValue !== camelValue) {
throw new Error(`${label} aliases ${String(snake)} and ${String(camel)} conflict.`);
}
return snakeValue ?? camelValue;
}
function isNodeErrorCode(error: unknown, code: string): boolean {
return error instanceof Error && 'code' in error && error.code === code;
}
function requiredString(value: unknown, label: string): string {
const resolved = stringValue(value, '', label).trim();
if (!resolved) throw new Error(`${label} is required.`);
return resolved;
}
function assertObject(value: unknown, label: string): asserts value is Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} must be an object.`);
}
}
function assertKnownKeys(
value: Record<string, unknown>,
label: string,
allowedKeys: readonly string[],
): void {
const allowed = new Set(allowedKeys);
const unknownKeys = Object.keys(value).filter((key) => !allowed.has(key));
if (unknownKeys.length > 0) {
throw new Error(`${label} has unknown field(s): ${unknownKeys.join(', ')}.`);
}
}
function assertUniqueAgentNames(agents: FleetAgent[]): void {
const seen = new Set<string>();
for (const agent of agents) {
if (seen.has(agent.name)) {
throw new Error(`Fleet roster has duplicate agent name: ${agent.name}.`);
}
seen.add(agent.name);
}
}
function stringValue(value: unknown, fallback = '', label = 'Value'): string {
if (value === undefined) return fallback;
if (typeof value !== 'string') throw new Error(`${label} must be a string.`);
return value;
}
function targetingString(value: unknown, fallback: string, label: string, pattern: RegExp): string {
const resolved = stringValue(value, fallback, label);
if (resolved && !pattern.test(resolved)) {
throw new Error(`${label} contains unsupported targeting characters.`);
}
return resolved;
}
function optionalTargetingString(
value: unknown,
label: string,
pattern: RegExp,
): string | undefined {
const resolved = optionalString(value, label);
if (resolved !== undefined && (!resolved || !pattern.test(resolved))) {
throw new Error(`${label} contains unsupported targeting characters.`);
}
return resolved;
}
function optionalString(value: unknown, label = 'Value'): string | undefined {
if (value === undefined) return undefined;
if (typeof value !== 'string') throw new Error(`${label} must be a string.`);
return value;
}
function optionalBoolean(value: unknown, label = 'Value'): boolean | undefined {
if (value === undefined) return undefined;
if (typeof value !== 'boolean') throw new Error(`${label} must be a boolean.`);
return value;
}
function optionalBooleanOrString(value: unknown, label = 'Value'): boolean | string | undefined {
if (value === undefined) return undefined;
if (typeof value !== 'boolean' && typeof value !== 'string') {
throw new Error(`${label} must be a boolean or string.`);
}
return value;
}