fix(fleet): enforce exact comms authority (#787)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

This commit was merged in pull request #787.
This commit is contained in:
2026-07-16 00:32:23 +00:00
parent c1aecfabe9
commit adad486b6f
22 changed files with 3534 additions and 859 deletions

View File

@@ -1,7 +1,20 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import {
accessSync,
chmodSync,
constants,
mkdtempSync,
mkdirSync,
writeFileSync,
rmSync,
readFileSync,
renameSync,
symlinkSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { FileConfigAdapter } from '../config/file-adapter.js';
import { composeContract } from './launch.js';
/**
@@ -22,7 +35,9 @@ import { composeContract } from './launch.js';
const CONSTITUTION = '# CONSTITUTION\n\nGATE-1: the non-negotiable law.\n';
const AGENTS = '# Mosaic Agent Dispatcher\n\nLoad order + guide router.\n';
const USER = '# operator\n\nName: Test Operator\n';
const TOOLS = '# tools index\n';
const TOOLS = '# tools index\n\n<!-- fleet-comms-contract: 1 -->\n';
const FRAMEWORK_SOURCE = fileURLToPath(new URL('../../framework', import.meta.url));
const SOURCE_TOOLS_PATH = join(FRAMEWORK_SOURCE, 'defaults', 'TOOLS.md');
function makeHome(): { home: string; root: string } {
const root = mkdtempSync(join(tmpdir(), 'mosaic-compose-'));
@@ -31,6 +46,12 @@ function makeHome(): { home: string; root: string } {
mkdirSync(join(home, 'runtime', h), { recursive: true });
writeFileSync(join(home, 'runtime', h, 'RUNTIME.md'), `# ${h} runtime contract\n`);
}
mkdirSync(join(home, 'defaults'), { recursive: true });
mkdirSync(join(home, 'tools', 'tmux'), { recursive: true });
writeFileSync(join(home, 'defaults', 'TOOLS.md'), TOOLS);
const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
writeFileSync(helper, '#!/bin/sh\n');
chmodSync(helper, 0o755);
writeFileSync(join(home, 'CONSTITUTION.md'), CONSTITUTION);
writeFileSync(join(home, 'AGENTS.md'), AGENTS);
writeFileSync(join(home, 'USER.md'), USER);
@@ -42,16 +63,31 @@ describe('composeContract — overlay composer', () => {
let fixture: ReturnType<typeof makeHome>;
let prevCwd: string;
let cwdDir: string;
let prevAgentName: string | undefined;
let prevAgentClass: string | undefined;
let prevAgentToolPolicy: string | undefined;
beforeEach(() => {
fixture = makeHome();
prevCwd = process.cwd();
prevAgentName = process.env['MOSAIC_AGENT_NAME'];
prevAgentClass = process.env['MOSAIC_AGENT_CLASS'];
prevAgentToolPolicy = process.env['MOSAIC_AGENT_TOOL_POLICY'];
delete process.env['MOSAIC_AGENT_NAME'];
delete process.env['MOSAIC_AGENT_CLASS'];
delete process.env['MOSAIC_AGENT_TOOL_POLICY'];
cwdDir = mkdtempSync(join(tmpdir(), 'mosaic-cwd-'));
process.chdir(cwdDir); // neutralize cwd-relative mission/PRD blocks
});
afterEach(() => {
process.chdir(prevCwd);
if (prevAgentName === undefined) delete process.env['MOSAIC_AGENT_NAME'];
else process.env['MOSAIC_AGENT_NAME'] = prevAgentName;
if (prevAgentClass === undefined) delete process.env['MOSAIC_AGENT_CLASS'];
else process.env['MOSAIC_AGENT_CLASS'] = prevAgentClass;
if (prevAgentToolPolicy === undefined) delete process.env['MOSAIC_AGENT_TOOL_POLICY'];
else process.env['MOSAIC_AGENT_TOOL_POLICY'] = prevAgentToolPolicy;
rmSync(fixture.root, { recursive: true, force: true });
rmSync(cwdDir, { recursive: true, force: true });
});
@@ -64,13 +100,17 @@ describe('composeContract — overlay composer', () => {
[
'version: 1',
'transport: tmux',
'tmux:',
' socket_name: mosaic-fleet',
'agents:',
' - name: orchestrator',
' runtime: claude',
' class: orchestrator',
' host: w-jarvis',
' - name: enhancer',
' runtime: claude',
' class: enhancer',
' host: w-jarvis',
' - name: coder0-0',
' runtime: claude',
' class: implementer',
@@ -82,19 +122,266 @@ describe('composeContract — overlay composer', () => {
const prev = process.env['MOSAIC_AGENT_NAME'];
try {
process.env['MOSAIC_AGENT_NAME'] = 'enhancer';
const out = composeContract('claude', fixture.home);
expect(out).toContain('# Fleet Comms');
expect(out).toMatch(/`\[[^\]]+:enhancer\]`/); // own [host:session] identity (host machine-dependent)
// local peer → no -H; cross-host peer → -H ssh
expect(out).toContain('-s orchestrator -m "…"');
expect(out).toContain('-H jwoltje@10.1.10.37 -s coder0-0 -m "…"');
expect(out).not.toContain('-H jwoltje@10.1.10.37 -s orchestrator'); // local stays local
const outputs = (['claude', 'codex', 'opencode', 'pi'] as const).map((runtime) =>
composeContract(runtime, fixture.home),
);
for (const out of outputs) {
expect(out).toContain('# Fleet Comms');
expect(out).toContain('Host: `w-jarvis`');
expect(out).toContain('Agent/session: `enhancer`');
expect(out).toContain('tmux socket: `mosaic-fleet`');
expect(out).toContain(
`Helper: \`${join(fixture.home, 'tools', 'tmux', 'agent-send.sh')}\``,
);
expect(out).toContain('-L mosaic-fleet -s orchestrator -m "…"');
expect(out).toContain('-L mosaic-fleet -H jwoltje@10.1.10.37 -s coder0-0 -m "…"');
expect(out).not.toContain('-H jwoltje@10.1.10.37 -s orchestrator');
}
const commsSection = (out: string): string => out.slice(out.indexOf('# Fleet Comms'));
const authoritative = commsSection(outputs[0]!);
expect(outputs.map(commsSection)).toEqual([
authoritative,
authoritative,
authoritative,
authoritative,
]);
} finally {
if (prev === undefined) delete process.env['MOSAIC_AGENT_NAME'];
else process.env['MOSAIC_AGENT_NAME'] = prev;
}
});
it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
'derives canonical class, persona, tool policy, and comms from one roster member for %s',
(runtime) => {
mkdirSync(join(fixture.home, 'fleet', 'roles'), { recursive: true });
writeFileSync(
join(fixture.home, 'fleet', 'roles', 'code.md'),
'# Code\n\n(`class: code`)\n\nCANONICAL-CODE-MANDATE.\n',
);
writeFileSync(
join(fixture.home, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'agents:',
' - name: exact-coder',
` runtime: ${runtime}`,
' class: implementer',
' tool_policy: operator-interaction',
'',
].join('\n'),
);
process.env['MOSAIC_AGENT_NAME'] = 'exact-coder';
process.env['MOSAIC_AGENT_CLASS'] = 'implementer';
process.env['MOSAIC_AGENT_TOOL_POLICY'] = 'ambient-policy-must-not-win';
const out = composeContract(runtime, fixture.home);
expect(out).toContain('# Persona Contract (code)');
expect(out).toContain('CANONICAL-CODE-MANDATE');
expect(out).toContain('Role/class: `code`');
expect(out).toContain('# Fleet Tool Policy (operator-interaction)');
expect(out).not.toContain('ambient-policy-must-not-win');
expect(out.indexOf('# Persona Contract')).toBeLessThan(out.indexOf('# Fleet Comms'));
},
);
it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
'does not inherit ambient tool policy when canonical member omits tool_policy for %s',
(runtime) => {
mkdirSync(join(fixture.home, 'fleet', 'roles'), { recursive: true });
writeFileSync(
join(fixture.home, 'fleet', 'roles', 'code.md'),
'# Code\n\n(`class: code`)\n\nCANONICAL-CODE-MANDATE.\n',
);
writeFileSync(
join(fixture.home, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'agents:',
' - name: exact-coder',
` runtime: ${runtime}`,
' class: implementer',
'',
].join('\n'),
);
process.env['MOSAIC_AGENT_NAME'] = 'exact-coder';
process.env['MOSAIC_AGENT_CLASS'] = 'implementer';
process.env['MOSAIC_AGENT_TOOL_POLICY'] = 'operator-interaction';
const out = composeContract(runtime, fixture.home);
expect(out).toContain('# Persona Contract (code)');
expect(out).toContain('Role/class: `code`');
expect(out).toContain('# Fleet Comms');
expect(out).not.toContain('# Fleet Tool Policy (operator-interaction)');
},
);
it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
'rejects an ambient class that mismatches the canonical roster member for %s',
(runtime) => {
mkdirSync(join(fixture.home, 'fleet'), { recursive: true });
writeFileSync(
join(fixture.home, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'agents:',
' - name: exact-coder',
` runtime: ${runtime}`,
' class: implementer',
'',
].join('\n'),
);
process.env['MOSAIC_AGENT_NAME'] = 'exact-coder';
process.env['MOSAIC_AGENT_CLASS'] = 'reviewer';
expect(() => composeContract(runtime, fixture.home)).toThrow(
/ambient MOSAIC_AGENT_CLASS.*review.*canonical roster.*code/i,
);
},
);
it('composes solo role mandate and boundaries before explicit no-peer authority', () => {
mkdirSync(join(fixture.home, 'fleet', 'roles'), { recursive: true });
writeFileSync(
join(fixture.home, 'fleet', 'roles', 'orchestrator.md'),
'# Orchestrator\n\n## Mandate\n\nCoordinate exact work.\n\n## Boundaries\n\nDo not infer authority.\n',
);
writeFileSync(
join(fixture.home, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'agents:',
' - name: solo',
' runtime: claude',
' class: orchestrator',
'',
].join('\n'),
);
process.env['MOSAIC_AGENT_NAME'] = 'solo';
process.env['MOSAIC_AGENT_CLASS'] = 'orchestrator';
const out = composeContract('claude', fixture.home);
expect(out).toContain('## Mandate');
expect(out).toContain('## Boundaries');
expect(out).toContain('Role/class: `orchestrator`');
expect(out).toContain('## Solo authority boundaries');
expect(out.indexOf('## Mandate')).toBeLessThan(out.indexOf('# Fleet Comms'));
expect(out.indexOf('## Boundaries')).toBeLessThan(out.indexOf('# Fleet Comms'));
expect(out).toContain('no peer, orchestrator, or remote communication authority');
});
it('proves real source TOOLS.md through fresh install, executable helper, and final composition', async () => {
const installRoot = mkdtempSync(join(tmpdir(), 'mosaic-real-contract-'));
const installedHome = join(installRoot, 'mosaic-home');
mkdirSync(installedHome, { recursive: true });
const previous = process.env['MOSAIC_AGENT_NAME'];
try {
const adapter = new FileConfigAdapter(installedHome, FRAMEWORK_SOURCE);
await adapter.syncFramework('fresh');
const sourceTools = readFileSync(SOURCE_TOOLS_PATH, 'utf8');
const installedToolsPath = join(installedHome, 'TOOLS.md');
expect(readFileSync(installedToolsPath, 'utf8')).toBe(sourceTools);
expect(sourceTools).toContain('fleet-comms-contract: 1');
expect(sourceTools).not.toMatch(
/<(?:user@host|src_host|src_session|dst_host|dst_session|target-session)>/,
);
const helper = join(installedHome, 'tools', 'tmux', 'agent-send.sh');
expect(() => accessSync(helper, constants.X_OK)).not.toThrow();
mkdirSync(join(installedHome, 'fleet'), { recursive: true });
writeFileSync(
join(installedHome, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'tmux:',
' socket_name: exact-socket',
'agents:',
' - name: exact-self',
' runtime: pi',
' class: orchestrator',
' host: local-host',
' - name: exact-peer',
' runtime: claude',
' class: implementer',
' host: local-host',
'',
].join('\n'),
);
process.env['MOSAIC_AGENT_NAME'] = 'exact-self';
const composed = composeContract('pi', installedHome);
expect(composed).toContain(sourceTools);
expect(composed).toContain(`Helper: \`${helper}\``);
expect(composed).toContain(`${helper} -L exact-socket -s exact-peer -m "…"`);
expect(composed).not.toContain('# Fleet Comms Installation Status');
} finally {
if (previous === undefined) delete process.env['MOSAIC_AGENT_NAME'];
else process.env['MOSAIC_AGENT_NAME'] = previous;
rmSync(installRoot, { recursive: true, force: true });
}
});
it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
'never injects installed TOOLS.md through a target symlink for %s',
(runtime) => {
const toolsPath = join(fixture.home, 'TOOLS.md');
const external = join(fixture.root, 'attacker-tools.md');
writeFileSync(external, 'UNSAFE-TARGET-SYMLINK-CONTENT\n');
rmSync(toolsPath);
symlinkSync(external, toolsPath);
const out = composeContract(runtime, fixture.home);
expect(out).not.toContain('UNSAFE-TARGET-SYMLINK-CONTENT');
expect(out).toContain('# Fleet Comms Installation Status');
expect(out).toContain('unavailable');
expect(readFileSync(external, 'utf8')).toBe('UNSAFE-TARGET-SYMLINK-CONTENT\n');
},
);
it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
'never injects installed TOOLS.md through an ancestor symlink for %s',
(runtime) => {
const realHome = join(fixture.root, 'real-mosaic-home');
renameSync(fixture.home, realHome);
symlinkSync(realHome, fixture.home, 'dir');
writeFileSync(join(realHome, 'TOOLS.md'), 'UNSAFE-ANCESTOR-SYMLINK-CONTENT\n');
const out = composeContract(runtime, fixture.home);
expect(out).not.toContain('UNSAFE-ANCESTOR-SYMLINK-CONTENT');
expect(out).toContain('# Fleet Comms Installation Status');
expect(out).toContain('unavailable');
expect(readFileSync(join(realHome, 'TOOLS.md'), 'utf8')).toBe(
'UNSAFE-ANCESTOR-SYMLINK-CONTENT\n',
);
},
);
it('reports stale preserved TOOLS.md without rewriting it', () => {
const toolsPath = join(fixture.home, 'TOOLS.md');
const stale = '# user-customized tools without fleet contract marker\n';
writeFileSync(toolsPath, stale);
const out = composeContract('pi', fixture.home);
expect(out).toContain('# Fleet Comms Installation Status');
expect(out).toContain('does not byte-match');
expect(out).toContain('active context was not rewritten');
expect(readFileSync(toolsPath, 'utf8')).toBe(stale);
});
it('does NOT inject fleet comms when MOSAIC_AGENT_NAME is unset (non-fleet launch)', () => {
const prev = process.env['MOSAIC_AGENT_NAME'];
try {
@@ -105,6 +392,24 @@ describe('composeContract — overlay composer', () => {
}
});
it('fails closed when an explicitly requested fleet identity is unknown', () => {
mkdirSync(join(fixture.home, 'fleet'), { recursive: true });
writeFileSync(
join(fixture.home, 'fleet', 'roster.yaml'),
['version: 1', 'transport: tmux', 'agents:', ' - name: exact-agent', ' runtime: pi'].join(
'\n',
),
);
const previous = process.env['MOSAIC_AGENT_NAME'];
try {
process.env['MOSAIC_AGENT_NAME'] = 'invented-agent';
expect(() => composeContract('pi', fixture.home)).toThrow(/known exact names: exact-agent/i);
} finally {
if (previous === undefined) delete process.env['MOSAIC_AGENT_NAME'];
else process.env['MOSAIC_AGENT_NAME'] = previous;
}
});
it('includes the per-tier anchors and the selected harness runtime', () => {
const out = composeContract('claude', fixture.home);
expect(out).toContain('GATE-1: the non-negotiable law.'); // L0
@@ -240,10 +545,14 @@ describe('composeContract — overlay composer', () => {
writeFileSync(
join(fixture.home, 'fleet', 'roster.yaml'),
[
'version: 1',
'transport: tmux',
'agents:',
' - name: orchestrator',
' runtime: claude',
' class: orchestrator',
' - name: enhancer',
' runtime: claude',
' class: enhancer',
'',
].join('\n'),

View File

@@ -288,8 +288,8 @@ describe('fleet roster parsing', () => {
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
[
'MOSAIC_AGENT_NAME=coder0',
// Reflects the roster's non-default `class: implementer` (A3a).
'MOSAIC_AGENT_CLASS=implementer',
// Reflects the roster's canonicalized compatibility class (A3a).
'MOSAIC_AGENT_CLASS=code',
'MOSAIC_AGENT_RUNTIME=codex',
'MOSAIC_AGENT_MODEL=',
'MOSAIC_AGENT_REASONING=',
@@ -3505,7 +3505,66 @@ describe('fleet add/remove — pure helpers', () => {
}
});
it('serializeRosterToYaml round-trips optional fields (modelHint, workingDirectory)', async () => {
it.each([
['tmux', { kind: 'tmux' }],
['discord', { kind: 'discord', discord: { channelId: '1234567890' } }],
[
'matrix',
{
kind: 'matrix',
matrix: {
homeserverUrl: 'https://matrix.example.test',
userId: '@mosaic:example.test',
roomId: '!fleet:example.test',
},
},
],
] as const)('round-trips the supported %s connector through YAML', async (_kind, connector) => {
const yaml = serializeRosterToYaml({ ...baseRoster, connector });
const dir = await mkdtemp(join(tmpdir(), 'mosaic-fleet-connector-'));
const rosterPath = join(dir, 'roster.yaml');
try {
await writeFile(rosterPath, yaml);
expect((await loadFleetRoster(rosterPath)).connector).toEqual(connector);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it.each([
['tmux', { kind: 'tmux' }],
['discord', { kind: 'discord', discord: { channel_id: '1234567890' } }],
[
'matrix',
{
kind: 'matrix',
matrix: {
homeserver_url: 'https://matrix.example.test',
user_id: '@mosaic:example.test',
room_id: '!fleet:example.test',
},
},
],
] as const)('parses the supported %s connector from JSON', async (_kind, connector) => {
const dir = await mkdtemp(join(tmpdir(), 'mosaic-fleet-connector-'));
const rosterPath = join(dir, 'roster.json');
try {
await writeFile(
rosterPath,
JSON.stringify({
version: 1,
transport: 'tmux',
agents: [{ name: 'orchestrator', runtime: 'claude', class: 'orchestrator' }],
connector,
}),
);
expect((await loadFleetRoster(rosterPath)).connector?.kind).toBe(_kind);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('serializeRosterToYaml round-trips optional fields and exact comms targets', async () => {
const rosterWithOptionals: FleetRoster = {
...baseRoster,
agents: [
@@ -3517,6 +3576,9 @@ describe('fleet add/remove — pure helpers', () => {
workingDirectory: '/tmp/work',
persistentPersona: true,
resetBetweenTasks: false,
host: '10.1.10.37',
ssh: 'jwoltje@10.1.10.37',
socket: 'mosaic-fleet',
},
],
};
@@ -3524,6 +3586,9 @@ describe('fleet add/remove — pure helpers', () => {
expect(yaml).toContain('model_hint: claude-3-5-sonnet');
expect(yaml).toContain('working_directory: /tmp/work');
expect(yaml).toContain('persistent_persona: true');
expect(yaml).toContain('host: 10.1.10.37');
expect(yaml).toContain('ssh: jwoltje@10.1.10.37');
expect(yaml).toContain('socket: mosaic-fleet');
const dir = await mkdtemp(join(tmpdir(), 'mosaic-fleet-'));
const rosterPath = join(dir, 'roster.yaml');
@@ -3533,6 +3598,9 @@ describe('fleet add/remove — pure helpers', () => {
expect(loaded.agents[0]!.modelHint).toBe('claude-3-5-sonnet');
expect(loaded.agents[0]!.workingDirectory).toBe('/tmp/work');
expect(loaded.agents[0]!.persistentPersona).toBe(true);
expect(loaded.agents[0]!.host).toBe('10.1.10.37');
expect(loaded.agents[0]!.ssh).toBe('jwoltje@10.1.10.37');
expect(loaded.agents[0]!.socket).toBe('mosaic-fleet');
} finally {
await rm(dir, { recursive: true, force: true });
}

View File

@@ -18,6 +18,19 @@ import { spawn } from 'node:child_process';
import * as readline from 'node:readline';
import type { Command } from 'commander';
import YAML from 'yaml';
import {
getRosterAgent,
loadFleetRoster,
resolveInstalledFleetRosterPath,
type FleetAgent,
type FleetRoster,
} from '../fleet/fleet-roster-v1.js';
export {
getRosterAgent,
loadFleetRoster,
resolveInstalledFleetRosterPath,
} from '../fleet/fleet-roster-v1.js';
export type { FleetAgent, FleetRoster } from '../fleet/fleet-roster-v1.js';
import {
registerFleetAgentCrudCommands,
type FleetAgentCrudCommandDeps,
@@ -86,72 +99,6 @@ export interface FleetCommandDeps {
reconcileDeps?: FleetReconcilerCommandDeps['reconcileDeps'];
}
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;
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;
}>;
}
export interface FleetAgent {
name: string;
alias?: string;
provider?: string;
runtime: string;
className: string;
workingDirectory?: string;
modelHint?: string;
reasoningLevel?: string;
toolPolicy?: string;
persistentPersona?: boolean | string;
resetBetweenTasks?: boolean;
kickstartTemplate?: string;
}
export interface FleetRoster {
version: 1;
transport: 'tmux';
tmux: {
socketName: string;
holderSession: string;
};
defaults: {
workingDirectory: string;
};
runtimes: Record<string, { resetCommand: string }>;
agents: FleetAgent[];
}
export interface FleetPaths {
mosaicHome: string;
rosterPath: string;
@@ -170,8 +117,6 @@ type FleetServiceAction = 'start' | 'stop' | 'restart' | 'status';
* fallback for a socket-less roster (that now resolves to the default socket).
*/
export const DEFAULT_SOCKET_NAME = 'mosaic-fleet';
const DEFAULT_HOLDER_SESSION = '_holder';
const DEFAULT_WORKING_DIRECTORY = '~/src';
/**
* tmux `-L` args for a socket name. An empty/absent socket ⇒ the LITERAL default
@@ -195,13 +140,6 @@ export const VERIFY_POLL_INTERVAL_MS = 400;
* Configurable via `--verify-timeout <ms>` on `agent send`.
*/
export const VERIFY_DEFAULT_TIMEOUT_MS = 6_000;
const DEFAULT_RUNTIME_RESETS: Record<string, { resetCommand: string }> = {
claude: { resetCommand: '/clear' },
codex: { resetCommand: '/clear' },
opencode: { resetCommand: '/clear' },
pi: { resetCommand: '/new' },
};
export function resolveFleetPaths(mosaicHome = defaultMosaicHome()): FleetPaths {
return {
mosaicHome,
@@ -229,20 +167,6 @@ function assertDefaultMosaicHomeForSystemd(mosaicHome: string): void {
}
}
export async function loadFleetRoster(path: string): Promise<FleetRoster> {
const rawText = await readFile(path, 'utf8');
const parsed = parseRosterText(rawText, path);
return normalizeRoster(parsed);
}
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;
}
// ---------------------------------------------------------------------------
// NORTH_STAR — machine-readable fleet planning source + Markdown projection
//
@@ -557,7 +481,7 @@ function generateAgentEnvValues(
MOSAIC_AGENT_REASONING: agent.reasoningLevel ?? '',
MOSAIC_AGENT_TOOL_POLICY: agent.toolPolicy ?? '',
MOSAIC_AGENT_WORKDIR: expandHome(workingDirectory),
MOSAIC_TMUX_SOCKET: roster.tmux.socketName,
MOSAIC_TMUX_SOCKET: agent.socket ?? roster.tmux.socketName,
};
}
@@ -2157,14 +2081,11 @@ export function registerFleetAgentCommands(
});
agentCommand
.command('comms-block <role>')
.description(
"Print the Fleet Comms cheat-sheet for a roster role (preview a peer's peer-reach view)",
)
.option('--host <host>', 'Override the fleet host (preview a cross-host peer view)')
.action((role: string, opts: { host?: string }) => {
.command('comms-block <exact-member>')
.description('Print the Fleet Comms contract for one exact roster member')
.action((exactMember: string) => {
const mosaicHome = resolveMosaicHomeFromCommand(agentCommand, deps.mosaicHome);
const res = resolveCommsBlock(mosaicHome, role, opts.host);
const res = resolveCommsBlock(mosaicHome, exactMember);
if (!res.ok) {
console.error(`[mosaic] comms-block: ${res.error}`);
process.exitCode = 1;
@@ -2490,249 +2411,6 @@ function resolveMosaicHomeFromCommand(command: Command, override?: string): stri
return opts.mosaicHome ?? override ?? defaultMosaicHome();
}
function parseRosterText(text: string, path: string): RawFleetRoster {
const trimmed = text.trim();
if (path.endsWith('.json')) {
return JSON.parse(trimmed) as RawFleetRoster;
}
return YAML.parse(trimmed) as RawFleetRoster;
}
function normalizeRoster(raw: RawFleetRoster): FleetRoster {
assertObject(raw, 'Fleet roster');
assertKnownKeys(raw, 'Fleet roster', [
'version',
'transport',
'tmux',
'defaults',
'runtimes',
'agents',
]);
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 agents = raw.agents.map(normalizeAgent);
assertUniqueAgentNames(agents);
return {
version: 1,
transport: 'tmux',
tmux: {
// Absent socket_name ⇒ '' (the literal default tmux socket, no -L) — NOT
// mosaic-fleet. Shipped presets set socket_name explicitly, so they are
// unaffected; only socket-less rosters get default-socket behavior.
socketName: stringValue(
raw.tmux?.socket_name ?? raw.tmux?.socketName,
'',
'Fleet roster tmux socket_name',
),
holderSession: stringValue(
raw.tmux?.holder_session ?? raw.tmux?.holderSession,
DEFAULT_HOLDER_SESSION,
'Fleet roster tmux holder_session',
),
},
defaults: {
workingDirectory: stringValue(
raw.defaults?.working_directory ?? raw.defaults?.workingDirectory,
DEFAULT_WORKING_DIRECTORY,
'Fleet roster defaults working_directory',
),
},
runtimes: normalizeRuntimes(raw.runtimes as RawFleetRoster['runtimes']),
agents,
};
}
function normalizeAgent(raw: NonNullable<RawFleetRoster['agents']>[number]): FleetAgent {
assertObject(raw, 'Fleet roster agent');
assertKnownKeys(raw, 'Fleet roster agent', [
'name',
'alias',
'provider',
'runtime',
'class',
'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: stringValue(raw.class, 'worker', `Fleet roster agent "${name}" class`),
workingDirectory: optionalString(
raw.working_directory ?? raw.workingDirectory,
`Fleet roster agent "${name}" working_directory`,
),
modelHint: optionalString(
raw.model_hint ?? raw.modelHint,
`Fleet roster agent "${name}" model_hint`,
),
reasoningLevel: optionalString(
raw.reasoning_level ?? raw.reasoningLevel,
`Fleet roster agent "${name}" reasoning_level`,
),
toolPolicy: optionalString(
raw.tool_policy ?? raw.toolPolicy,
`Fleet roster agent "${name}" tool_policy`,
),
persistentPersona: optionalBooleanOrString(
raw.persistent_persona ?? raw.persistentPersona,
`Fleet roster agent "${name}" persistent_persona`,
),
resetBetweenTasks: optionalBoolean(
raw.reset_between_tasks ?? raw.resetBetweenTasks,
`Fleet roster agent "${name}" reset_between_tasks`,
),
kickstartTemplate: optionalString(
raw.kickstart_template ?? raw.kickstartTemplate,
`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(
config.reset_command ?? config.resetCommand,
'/clear',
`Fleet roster runtime "${runtime}" reset_command`,
),
};
}
return result;
}
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 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;
}
function expandHome(path: string): string {
return path === '~' || path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
}
@@ -2889,6 +2567,23 @@ export function removeAgentFromRoster(roster: FleetRoster, name: string): FleetR
};
}
function serializeConnector(
connector: NonNullable<FleetRoster['connector']>,
): Record<string, unknown> {
if (connector.kind === 'tmux') return { kind: 'tmux' };
if (connector.kind === 'discord') {
return { kind: 'discord', discord: { channel_id: connector.discord.channelId } };
}
return {
kind: 'matrix',
matrix: {
homeserver_url: connector.matrix.homeserverUrl,
user_id: connector.matrix.userId,
room_id: connector.matrix.roomId,
},
};
}
/**
* Serialize a FleetRoster to YAML text (snake_case keys).
* The output is parseable by loadFleetRoster.
@@ -2906,6 +2601,15 @@ export function serializeRosterToYaml(roster: FleetRoster): string {
if (agent.provider !== undefined) {
raw['provider'] = agent.provider;
}
if (agent.host !== undefined) {
raw['host'] = agent.host;
}
if (agent.ssh !== undefined) {
raw['ssh'] = agent.ssh;
}
if (agent.socket !== undefined) {
raw['socket'] = agent.socket;
}
if (agent.workingDirectory !== undefined) {
raw['working_directory'] = agent.workingDirectory;
}
@@ -2947,6 +2651,7 @@ export function serializeRosterToYaml(roster: FleetRoster): string {
},
runtimes,
agents,
...(roster.connector ? { connector: serializeConnector(roster.connector) } : {}),
};
return YAML.stringify(raw);
@@ -3093,6 +2798,5 @@ export async function resolveRosterPath(
if (await canRead(yamlPath)) {
return yamlPath;
}
const jsonPath = join(mosaicHome, 'fleet', 'roster.json');
return jsonPath;
return resolveInstalledFleetRosterPath(mosaicHome);
}

View File

@@ -19,10 +19,17 @@ import { createRequire } from 'node:module';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import type { Command } from 'commander';
import { readFleetCommsBlock } from '../fleet/comms-onboarding.js';
import {
buildResolvedFleetCommsBlock,
renderToolsContractStatus,
resolveFleetIdentity,
} from '../fleet/comms-onboarding.js';
import { readRegularFileSecure } from '../fleet/secure-file.js';
import { readPersonaContractBlock } from '../fleet/persona-contract.js';
import { canonicalizeRoleClass } from './fleet-personas.js';
const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
const MAX_INSTALLED_TOOLS_BYTES = 256 * 1024;
type RuntimeName = 'claude' | 'codex' | 'opencode' | 'pi';
@@ -185,6 +192,17 @@ function readOptional(path: string): string {
}
}
function readInstalledToolsSecure(mosaicHome: string): string {
try {
return readRegularFileSecure(join(mosaicHome, 'TOOLS.md'), {
root: mosaicHome,
maxBytes: MAX_INSTALLED_TOOLS_BYTES,
}).content.toString('utf8');
} catch {
return '';
}
}
function readJson(path: string): Record<string, unknown> | null {
try {
return JSON.parse(readFileSync(path, 'utf-8')) as Record<string, unknown>;
@@ -361,9 +379,25 @@ For required push/merge/issue-close/release actions, execute without routine con
parts.push('\n\n## Operator Overlay (USER.local.md)\n\n' + userLocal);
}
const fleetIdentity = resolveFleetIdentity(mosaicHome, process.env['MOSAIC_AGENT_NAME']);
if (!fleetIdentity.ok) {
throw new Error(`Fleet communications contract unavailable: ${fleetIdentity.error}`);
}
const canonicalMember = fleetIdentity.identity?.member;
if (canonicalMember && process.env['MOSAIC_AGENT_CLASS']?.trim()) {
const ambientClass = canonicalizeRoleClass(process.env['MOSAIC_AGENT_CLASS']).canonicalClass;
if (ambientClass !== canonicalMember.className) {
throw new Error(
`Ambient MOSAIC_AGENT_CLASS resolves to "${ambientClass}" but canonical roster member "${canonicalMember.name}" resolves to "${canonicalMember.className}". Refusing split identity authority.`,
);
}
}
// TOOLS.md
const tools = readOptional(join(mosaicHome, 'TOOLS.md'));
const tools = readInstalledToolsSecure(mosaicHome);
if (tools) parts.push('\n\n# Machine Tools\n\n' + tools);
const toolsContractStatus = renderToolsContractStatus(mosaicHome);
if (toolsContractStatus) parts.push('\n\n' + toolsContractStatus);
// Operator overlays whose base layers are load-on-demand (SOUL, STANDARDS):
// inject only the small `.local` delta by value so the customization reaches
@@ -385,24 +419,23 @@ For required push/merge/issue-close/release actions, execute without routine con
// Runtime-specific contract
parts.push('\n\n# Runtime-Specific Contract\n\n' + readFileSync(runtimeFile, 'utf-8'));
// Persona contract (A3b): when this agent was spawned with a class
// (MOSAIC_AGENT_CLASS, exported into the pane env by A3a), inject its resolved
// role contract so its identity (mandate + boundaries) is resident from the
// first turn. Override-aware via the persona resolver: a user-customized
// persona in fleet/roles.local/ wins over the baseline (AC-NS-7 launch proof).
// Placed BEFORE fleet comms: identity first, then how-to-reach-peers. No-ops
// silently when the class is unset/unknown (mirrors the comms block).
const persona = readPersonaContractBlock(mosaicHome, process.env['MOSAIC_AGENT_CLASS']);
// Fleet launches derive every identity projection from the one canonical roster
// member resolved above. Non-fleet launches retain the legacy ambient persona
// and tool-policy behavior.
const personaClass = canonicalMember?.className ?? process.env['MOSAIC_AGENT_CLASS'];
const persona = readPersonaContractBlock(mosaicHome, personaClass);
if (persona) parts.push('\n\n' + persona);
const toolPolicy = readFleetToolPolicyBlock(process.env['MOSAIC_AGENT_TOOL_POLICY']);
const toolPolicyName = canonicalMember
? canonicalMember.toolPolicy
: process.env['MOSAIC_AGENT_TOOL_POLICY'];
const toolPolicy = readFleetToolPolicyBlock(toolPolicyName);
if (toolPolicy) parts.push('\n\n' + toolPolicy);
// Fleet onboarding: when this is a spawned fleet agent (MOSAIC_AGENT_NAME set
// and present in the roster), inject a comms cheat-sheet + peer roster so it
// knows how to reach the orchestrator and its peers from its first turn.
const fleetComms = readFleetCommsBlock(mosaicHome, process.env['MOSAIC_AGENT_NAME']);
if (fleetComms) parts.push('\n\n' + fleetComms);
if (fleetIdentity.identity) {
const fleetComms = buildResolvedFleetCommsBlock(fleetIdentity.identity);
if (fleetComms) parts.push('\n\n' + fleetComms);
}
return parts.join('\n');
}