chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
@@ -0,0 +1,114 @@
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
brainHomeIsActive,
fleetAgentEnvDir,
fleetProfilesDir,
fleetRolesLocalDir,
fleetStateDir,
resolveBrainHome,
type BrainHomeOptions,
} from './brain-home.js';
describe('fleet brain-home resolution', (): void => {
let cleanup: string | undefined;
const savedBrainEnv = process.env['MOSAIC_BRAIN_HOME'];
beforeEach((): void => {
delete process.env['MOSAIC_BRAIN_HOME'];
});
afterEach(async (): Promise<void> => {
if (savedBrainEnv === undefined) {
delete process.env['MOSAIC_BRAIN_HOME'];
} else {
process.env['MOSAIC_BRAIN_HOME'] = savedBrainEnv;
}
if (cleanup !== undefined) {
await rm(cleanup, { recursive: true, force: true });
cleanup = undefined;
}
});
async function makeTmp(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'mosaic-brain-home-'));
cleanup = root;
return root;
}
it('MOSAIC_BRAIN_HOME env wins over every other signal', (): void => {
process.env['MOSAIC_BRAIN_HOME'] = '/explicit/brain';
expect(resolveBrainHome('/any/mosaic-home')).toBe('/explicit/brain');
expect(fleetAgentEnvDir('/any/mosaic-home')).toBe('/explicit/brain/fleet/agents');
expect(brainHomeIsActive('/any/mosaic-home')).toBe(true);
});
it('injected envBrainHome wins identically (test seam)', (): void => {
const opts: BrainHomeOptions = { envBrainHome: '/injected/brain' };
expect(resolveBrainHome('/any/mosaic-home', opts)).toBe('/injected/brain');
expect(fleetAgentEnvDir('/any/mosaic-home', opts)).toBe('/injected/brain/fleet/agents');
});
it('a non-default mosaicHome never adopts the canonical brain (hermetic legacy)', (): void => {
const mosaicHome = '/tmp/not-the-default-config-home';
expect(resolveBrainHome(mosaicHome)).toBe(mosaicHome);
expect(brainHomeIsActive(mosaicHome)).toBe(false);
expect(fleetAgentEnvDir(mosaicHome)).toBe(join(mosaicHome, 'fleet', 'agents'));
});
it('the default config home adopts the brain when it carries fleet/agents', async (): Promise<void> => {
const root = await makeTmp();
const brain = join(root, 'brain');
await mkdir(join(brain, 'fleet', 'agents'), { recursive: true });
const configHome = join(root, 'config', 'mosaic');
const opts: BrainHomeOptions = { homes: { brain, configDefault: configHome } };
expect(resolveBrainHome(configHome, opts)).toBe(brain);
expect(fleetAgentEnvDir(configHome, opts)).toBe(join(brain, 'fleet', 'agents'));
expect(fleetRolesLocalDir(configHome, opts)).toBe(join(brain, 'fleet', 'roles.local'));
expect(fleetProfilesDir(configHome, opts)).toBe(join(brain, 'fleet', 'profiles'));
expect(fleetStateDir(configHome, opts)).toBe(join(brain, 'fleet'));
expect(brainHomeIsActive(configHome, opts)).toBe(true);
});
it('the default config home stays legacy when no brain exists', async (): Promise<void> => {
const root = await makeTmp();
const configHome = join(root, 'config', 'mosaic');
const opts: BrainHomeOptions = {
homes: { brain: join(root, 'brain'), configDefault: configHome },
};
expect(resolveBrainHome(configHome, opts)).toBe(configHome);
expect(brainHomeIsActive(configHome, opts)).toBe(false);
});
it('an empty MOSAIC_BRAIN_HOME is ignored, not treated as set', (): void => {
process.env['MOSAIC_BRAIN_HOME'] = ' ';
expect(resolveBrainHome('/tmp/legacy-home')).toBe('/tmp/legacy-home');
});
it('adoption requires fleet/agents specifically, not any brain content', async (): Promise<void> => {
const root = await makeTmp();
const brain = join(root, 'brain');
await mkdir(join(brain, 'fleet'), { recursive: true }); // fleet without agents
const configHome = join(root, 'config', 'mosaic');
const opts: BrainHomeOptions = { homes: { brain, configDefault: configHome } };
expect(resolveBrainHome(configHome, opts)).toBe(configHome);
});
it('real-home control: a host brain is adopted only through the default home', (): void => {
// Control on the un-injected path: this host carries ~/.mosaic/fleet/agents,
// so the default config home resolves to the brain or legacy — both valid
// canonical endpoints — while a non-default home never adopts.
const defaultHome = join(homedir(), '.config', 'mosaic');
const resolved = resolveBrainHome(defaultHome);
expect([defaultHome, join(homedir(), '.mosaic')]).toContain(resolved);
expect(resolveBrainHome(join(homedir(), 'elsewhere', 'mosaic'))).toBe(
join(homedir(), 'elsewhere', 'mosaic'),
);
});
});
@@ -0,0 +1,76 @@
import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
/**
* Overridable resolution inputs (tests inject tmp homes; production reads
* the environment and the real home directory).
*/
export interface BrainHomeOptions {
/** Explicit brain home; defaults to `MOSAIC_BRAIN_HOME`. */
readonly envBrainHome?: string;
/**
* Canonical homes used for adoption. Defaults derive from the real
* `homedir()`: `{ brain: ~/.mosaic, configDefault: ~/.config/mosaic }`.
*/
readonly homes?: { readonly brain: string; readonly configDefault: string };
}
/**
* Brain-home resolution — the three-tree fleet split (stack canon
* `docs/STRUCTURE-CANON.md` §2, first carried by the USC estate brain):
*
* config home (~/.config/mosaic) framework templates + dispatch state:
* fleet/roles (baseline), fleet/roster.yaml,
* fleet/run (heartbeats), fleet/services
* brain home (~/.mosaic) user-owned fleet state, committed:
* fleet/agents/<seat>.env.*, fleet/roles.local,
* fleet/profiles working copies
*
* Resolution order:
* 1. `MOSAIC_BRAIN_HOME` env (explicit, always wins)
* 2. canonical `~/.mosaic` — adopted ONLY when mosaicHome is the real
* default config home AND `~/.mosaic/fleet/agents` exists. Custom
* `--mosaic-home` values (tests, sandboxes, canaries) never trigger
* adoption, keeping them hermetic and deterministic.
* 3. mosaicHome itself (legacy single-tree behavior).
*/
export function resolveBrainHome(mosaicHome: string, options: BrainHomeOptions = {}): string {
const explicit = options.envBrainHome ?? process.env['MOSAIC_BRAIN_HOME'];
if (explicit !== undefined && explicit.trim() !== '') {
return explicit;
}
const homes = options.homes ?? {
brain: join(homedir(), '.mosaic'),
configDefault: join(homedir(), '.config', 'mosaic'),
};
if (resolve(mosaicHome) !== resolve(homes.configDefault)) {
return mosaicHome;
}
return existsSync(join(homes.brain, 'fleet', 'agents')) ? homes.brain : mosaicHome;
}
/** True when fleet state resolves somewhere other than the config home. */
export function brainHomeIsActive(mosaicHome: string, options: BrainHomeOptions = {}): boolean {
return resolve(resolveBrainHome(mosaicHome, options)) !== resolve(mosaicHome);
}
/** Fleet state root (brain home when active, else the config home). */
export function fleetStateDir(mosaicHome: string, options: BrainHomeOptions = {}): string {
return join(resolveBrainHome(mosaicHome, options), 'fleet');
}
/** Seat launch envs — `<brainHome>/fleet/agents` when a brain is active. */
export function fleetAgentEnvDir(mosaicHome: string, options: BrainHomeOptions = {}): string {
return join(fleetStateDir(mosaicHome, options), 'agents');
}
/** PRESERVE-protected persona override layer — `<brainHome>/fleet/roles.local`. */
export function fleetRolesLocalDir(mosaicHome: string, options: BrainHomeOptions = {}): string {
return join(fleetStateDir(mosaicHome, options), 'roles.local');
}
/** System-type profiles (user working copies) — `<brainHome>/fleet/profiles`. */
export function fleetProfilesDir(mosaicHome: string, options: BrainHomeOptions = {}): string {
return join(fleetStateDir(mosaicHome, options), 'profiles');
}
@@ -0,0 +1,894 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
chmodSync,
mkdtempSync,
mkdirSync,
writeFileSync,
rmSync,
readFileSync,
symlinkSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parseFleetRosterV1, type FleetRoster, type FleetAgent } from './fleet-roster-v1.js';
import {
buildFleetCommsBlock,
renderPeerReach,
readFleetCommsBlock,
resolveCommsBlock,
resolveFleetIdentity,
resolvePeerCommand,
renderToolsContractStatus,
} from './comms-onboarding.js';
const ROSTER = [
'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',
' runtime: pi',
' class: implementer',
' host: w-jarvis',
' - name: coder0-0',
' runtime: claude',
' class: implementer',
' host: 10.1.10.37',
' ssh: [email protected]',
'',
].join('\n');
function roster(source = ROSTER): FleetRoster {
return parseFleetRosterV1(source, 'yaml');
}
describe('shared fleet roster v1 resolver', () => {
it('resolves comms fields and the global socket through the canonical roster contract', () => {
const resolved = roster();
expect(resolved.tmux.socketName).toBe('mosaic-fleet');
expect(resolved.agents.find((agent) => agent.name === 'coder0-0')).toMatchObject({
className: 'code',
host: '10.1.10.37',
ssh: '[email protected]',
});
});
// stack#1380 verification unblock: the fleet's own roster-v2 tooling writes
// the v1 body plus a generation fence and seat lifecycle/launch envelopes.
// The parser tolerates exactly that envelope (validated, opaque to comms).
const V2_ROSTER = [
'version: 2',
'generation: 8',
'transport: tmux',
'tmux:',
' socket_name: mosaic-fleet',
'defaults:',
' working_directory: ~/.mosaic',
' runtime: claude',
'agents:',
' - name: orch-01',
' runtime: claude',
' class: orchestrator',
' model: opus',
' reasoning: high',
' lifecycle:',
' enabled: true',
' desired_state: running',
' launch:',
' yolo: false',
'',
].join('\n');
it('accepts the roster-v2 envelope (generation + lifecycle/launch) on the v1 body', () => {
const resolved = parseFleetRosterV1(V2_ROSTER, 'yaml');
expect(resolved.tmux.socketName).toBe('mosaic-fleet');
expect(resolved.agents[0]?.name).toBe('orch-01');
});
it('rejects a non-integer generation', () => {
expect(() =>
parseFleetRosterV1(V2_ROSTER.replace('generation: 8', 'generation: eight'), 'yaml'),
).toThrow(/generation must be a non-negative integer/);
});
it('rejects an invalid lifecycle desired_state', () => {
expect(() =>
parseFleetRosterV1(
V2_ROSTER.replace('desired_state: running', 'desired_state: paused'),
'yaml',
),
).toThrow(/desired_state must be running\|stopped/);
});
it('rejects unknown fields inside the lifecycle envelope', () => {
expect(() =>
parseFleetRosterV1(
V2_ROSTER.replace(' enabled: true', ' enabled: true\n surprise: 1'),
'yaml',
),
).toThrow(/lifecycle has unknown field/);
});
it('rejects a non-boolean launch.yolo', () => {
expect(() =>
parseFleetRosterV1(V2_ROSTER.replace('yolo: false', 'yolo: sometimes'), 'yaml'),
).toThrow(/launch\.yolo must be a boolean/);
});
it('rejects unknown fields instead of leniently constructing a second roster view', () => {
expect(() => parseFleetRosterV1(`${ROSTER}\nunknown: value\n`, 'yaml')).toThrow(
/unknown field/i,
);
});
it('rejects an unsupported independent per-agent socket instead of targeting a nonexistent session', () => {
expect(() =>
roster(
ROSTER.replace(
' host: w-jarvis\n - name: coder0',
' host: w-jarvis\n socket: other-socket\n - name: coder0',
),
),
).toThrow(/independent per-agent sockets are not supported/i);
});
it('rejects unsafe operational targeting values', () => {
expect(() =>
roster(ROSTER.replace(' ssh: [email protected]', ' ssh: host;touch-owned')),
).toThrow(/unsupported targeting characters/i);
});
it('normalizes matching connector settings for YAML and JSON rosters', () => {
const yamlSource = `${ROSTER}connector:\n kind: discord\n discord:\n channel_id: "123"\n`;
const jsonSource = JSON.stringify({
version: 1,
transport: 'tmux',
agents: [{ name: 'a', runtime: 'claude', class: 'worker' }],
connector: {
kind: 'matrix',
matrix: {
homeserver_url: 'https://matrix.example',
user_id: '@a:example',
room_id: '!room:example',
},
},
});
expect(parseFleetRosterV1(yamlSource, 'yaml').connector).toEqual({
kind: 'discord',
discord: { channelId: '123' },
});
expect(parseFleetRosterV1(jsonSource, 'json').connector).toEqual({
kind: 'matrix',
matrix: {
homeserverUrl: 'https://matrix.example',
userId: '@a:example',
roomId: '!room:example',
},
});
});
it.each([
['discord channel_id', { kind: 'discord', discord: { channel_id: '' } }],
['discord channel_id whitespace', { kind: 'discord', discord: { channel_id: ' ' } }],
[
'matrix homeserver_url',
{
kind: 'matrix',
matrix: { homeserver_url: '', user_id: '@a:example', room_id: '!room:example' },
},
],
[
'matrix homeserver_url whitespace',
{
kind: 'matrix',
matrix: { homeserver_url: '\t', user_id: '@a:example', room_id: '!room:example' },
},
],
[
'matrix user_id',
{
kind: 'matrix',
matrix: {
homeserver_url: 'https://matrix.example',
user_id: '',
room_id: '!room:example',
},
},
],
[
'matrix user_id whitespace',
{
kind: 'matrix',
matrix: {
homeserver_url: 'https://matrix.example',
user_id: ' ',
room_id: '!room:example',
},
},
],
[
'matrix room_id',
{
kind: 'matrix',
matrix: {
homeserver_url: 'https://matrix.example',
user_id: '@a:example',
room_id: '',
},
},
],
[
'matrix room_id whitespace',
{
kind: 'matrix',
matrix: {
homeserver_url: 'https://matrix.example',
user_id: '@a:example',
room_id: '\n',
},
},
],
])(
'rejects empty or whitespace-only parser-required connector string: %s',
(_label, connector) => {
expect(() =>
parseFleetRosterV1(
JSON.stringify({
version: 1,
transport: 'tmux',
agents: [{ name: 'a', runtime: 'claude', class: 'worker' }],
connector,
}),
'json',
),
).toThrow(/required/i);
},
);
it.each([
['tmux with discord settings', { kind: 'tmux', discord: { channel_id: '123' } }],
['discord without discord settings', { kind: 'discord' }],
[
'discord with matrix settings',
{ kind: 'discord', discord: { channel_id: '123' }, matrix: {} },
],
['matrix without matrix settings', { kind: 'matrix' }],
[
'matrix with discord settings',
{
kind: 'matrix',
matrix: {
homeserver_url: 'https://matrix.example',
user_id: '@a:example',
room_id: '!room:example',
},
discord: { channel_id: '123' },
},
],
])('rejects connector kind/settings mismatch: %s', (_label, connector) => {
expect(() =>
parseFleetRosterV1(
JSON.stringify({
version: 1,
transport: 'tmux',
agents: [{ name: 'a', runtime: 'claude', class: 'worker' }],
connector,
}),
'json',
),
).toThrow();
});
it.each([
['tmux socket', ['tmux', 'socket_name'], ['tmux', 'socketName'], 'same', 'different'],
['tmux holder', ['tmux', 'holder_session'], ['tmux', 'holderSession'], 'same', 'different'],
[
'defaults working directory',
['defaults', 'working_directory'],
['defaults', 'workingDirectory'],
'same',
'different',
],
[
'runtime reset command',
['runtimes', 'claude', 'reset_command'],
['runtimes', 'claude', 'resetCommand'],
'same',
'different',
],
[
'agent working directory',
['agents', 0, 'working_directory'],
['agents', 0, 'workingDirectory'],
'same',
'different',
],
[
'agent model hint',
['agents', 0, 'model_hint'],
['agents', 0, 'modelHint'],
'same',
'different',
],
[
'agent reasoning level',
['agents', 0, 'reasoning_level'],
['agents', 0, 'reasoningLevel'],
'same',
'different',
],
[
'agent tool policy',
['agents', 0, 'tool_policy'],
['agents', 0, 'toolPolicy'],
'same',
'different',
],
[
'agent persistent persona',
['agents', 0, 'persistent_persona'],
['agents', 0, 'persistentPersona'],
true,
false,
],
[
'agent reset between tasks',
['agents', 0, 'reset_between_tasks'],
['agents', 0, 'resetBetweenTasks'],
true,
false,
],
[
'agent kickstart template',
['agents', 0, 'kickstart_template'],
['agents', 0, 'kickstartTemplate'],
'same',
'different',
],
] as const)(
'rejects conflicting %s aliases and accepts identical aliases',
(_label, snake, camel, same, different) => {
const base: Record<string, unknown> = {
version: 1,
transport: 'tmux',
tmux: {},
defaults: {},
runtimes: { claude: {} },
agents: [{ name: 'a', runtime: 'claude', class: 'worker' }],
};
const assign = (
root: Record<string, unknown>,
path: readonly (string | number)[],
value: unknown,
) => {
let cursor: unknown = root;
for (const segment of path.slice(0, -1)) {
cursor = (cursor as Record<string | number, unknown>)[segment];
}
(cursor as Record<string | number, unknown>)[path.at(-1)!] = value;
};
assign(base, snake, same);
assign(base, camel, different);
expect(() => parseFleetRosterV1(JSON.stringify(base), 'json')).toThrow(
/aliases .* conflict/i,
);
assign(base, camel, same);
expect(() => parseFleetRosterV1(JSON.stringify(base), 'json')).not.toThrow();
},
);
});
describe('renderPeerReach — exact same-host/cross-host/socket targeting', () => {
const send = '/home/u/.config/mosaic/tools/tmux/agent-send.sh';
const base: FleetAgent = {
name: 'peer',
runtime: 'claude',
className: 'worker',
};
it('renders the global named socket and omits -H for a same-host peer', () => {
expect(renderPeerReach(base, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', send)).toBe(
`${send} -L mosaic-fleet -s peer -m "…"`,
);
});
it('uses only the explicit roster ssh target for a cross-host peer', () => {
const peer: FleetAgent = {
...base,
name: 'coder0-0',
host: '10.1.10.37',
ssh: '[email protected]',
};
expect(renderPeerReach(peer, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', send)).toBe(
`${send} -L mosaic-fleet -H [email protected] -s coder0-0 -m "…"`,
);
});
it('fails closed when a cross-host peer has no explicit roster ssh target', () => {
const peer: FleetAgent = { ...base, name: 'x', host: '10.0.0.9' };
expect(() => renderPeerReach(peer, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', send)).toThrow(
/explicit roster ssh target/i,
);
});
it('renders only the fleet-wide supported socket', () => {
const peer: FleetAgent = { ...base, socket: 'mosaic-fleet' };
expect(renderPeerReach(peer, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', send)).toBe(
`${send} -L mosaic-fleet -s peer -m "…"`,
);
});
it('resolves hostless peers against the stable fleet-host baseline, not the viewer host', () => {
const peer: FleetAgent = { ...base, ssh: 'fleet-user@w-jarvis' };
expect(renderPeerReach(peer, 'remote-host', 'w-jarvis', 'mosaic-fleet', send)).toBe(
`${send} -L mosaic-fleet -H fleet-user@w-jarvis -s peer -m "…"`,
);
});
it('shell-quotes an exact helper path that contains spaces', () => {
expect(
renderPeerReach(base, 'w-jarvis', 'w-jarvis', 'mosaic-fleet', '/home/test user/send.sh'),
).toBe(`'/home/test user/send.sh' -L mosaic-fleet -s peer -m "…"`);
});
it('omits -L only for the literal default socket', () => {
expect(renderPeerReach(base, 'w-jarvis', 'w-jarvis', '', send)).toBe(`${send} -s peer -m "…"`);
});
});
describe('buildFleetCommsBlock', () => {
const send = '/h/.config/mosaic/tools/tmux/agent-send.sh';
it('renders authoritative identity, exact rows, generation, and no operational metavariables', () => {
const block = buildFleetCommsBlock({
selfName: 'enhancer',
roster: roster(),
localHost: 'ignored-process-host',
agentSendPath: send,
});
expect(block).toContain('# Fleet Comms');
expect(block).toContain('Host: `w-jarvis`');
expect(block).toContain('Agent/session: `enhancer`');
expect(block).toContain('tmux socket: `mosaic-fleet`');
expect(block).toContain(`Helper: \`${send}\``);
expect(block).toMatch(/Comms generation: `[a-f0-9]{64}`/);
expect(block).not.toMatch(/\|\s*enhancer\s*\|/);
expect(block).toContain(`${send} -L mosaic-fleet -s orchestrator -m "…"`);
expect(block).toContain(`${send} -L mosaic-fleet -H [email protected] -s coder0-0 -m "…"`);
expect(block).toContain(`mosaic agent comms-block enhancer`);
expect(block).toMatch(/Never invent, substitute, or fuzzy-match/i);
expect(block).not.toMatch(
/<(?:user@host|src_host|src_session|dst_host|dst_session|target-session)>/,
);
expect(block).not.toContain('FLIP the preamble');
});
it('changes the generation when a rendered peer role changes', () => {
const generation = (block: string) => block.match(/Comms generation: `([a-f0-9]{64})`/)?.[1];
const before = buildFleetCommsBlock({
selfName: 'enhancer',
roster: roster(),
localHost: 'w-jarvis',
agentSendPath: send,
});
const changedRoster = roster(ROSTER.replace('class: implementer', 'class: reviewer'));
const after = buildFleetCommsBlock({
selfName: 'enhancer',
roster: changedRoster,
localHost: 'w-jarvis',
agentSendPath: send,
});
expect(generation(before)).toMatch(/^[a-f0-9]{64}$/);
expect(generation(after)).not.toBe(generation(before));
});
it('fails closed when any rendered cross-host row lacks ssh', () => {
const bad = roster(ROSTER.replace(' ssh: [email protected]\n', ''));
expect(() =>
buildFleetCommsBlock({
selfName: 'enhancer',
roster: bad,
localHost: 'w-jarvis',
agentSendPath: send,
}),
).toThrow(/explicit roster ssh target/i);
});
it('still renders authoritative local identity when the agent has no peers', () => {
const solo = roster(
[
'version: 1',
'transport: tmux',
'agents:',
' - name: solo',
' runtime: claude',
' class: orchestrator',
].join('\n'),
);
const block = buildFleetCommsBlock({
selfName: 'solo',
roster: solo,
localHost: 'h',
agentSendPath: send,
});
expect(block).toContain('Host: `h`');
expect(block).toContain('Agent/session: `solo`');
expect(block).toContain('Role/class: `orchestrator`');
expect(block).toMatch(/Comms generation: `[a-f0-9]{64}`/);
expect(block).toContain('This roster has no peers');
expect(block).toContain('## Solo authority boundaries');
expect(block).toContain('no peer, orchestrator, or remote communication authority');
expect(block).toContain('Do not send, infer a target, or claim fleet coordination');
});
});
describe('resolvePeerCommand', () => {
const send = '/h/.config/mosaic/tools/tmux/agent-send.sh';
it('returns one exact known-peer row', () => {
const result = resolvePeerCommand(roster(), 'enhancer', 'coder0-0', 'w-jarvis', send);
expect(result.ok).toBe(true);
expect(result.command).toContain('-H [email protected] -s coder0-0');
});
it('fails closed for an unknown peer with exact-name discovery guidance', () => {
const result = resolvePeerCommand(roster(), 'enhancer', 'invented-host', 'w-jarvis', send);
expect(result.ok).toBe(false);
expect(result.command).toBe('');
expect(result.error).toContain('invented-host');
expect(result.error).toContain('orchestrator, coder0, coder0-0');
expect(result.error).toContain('mosaic agent comms-block enhancer');
expect(result.error).not.toContain('tmux ls');
});
});
describe('readFleetCommsBlock — spawned-agent context', () => {
let home: string;
beforeEach(() => {
// Hermetic helper fallback (stack#1380): the resolver probes
// $HOME/.config/mosaic when mosaicHome itself carries no helper — point
// HOME at a sandbox parent so tests never see the real host install.
vi.stubEnv('HOME', mkdtempSync(join(tmpdir(), 'mosaic-homeless-')));
vi.stubEnv('MOSAIC_HOME', '');
home = mkdtempSync(join(tmpdir(), 'mosaic-comms-'));
mkdirSync(join(home, 'fleet'), { recursive: true });
mkdirSync(join(home, 'tools', 'tmux'), { recursive: true });
writeFileSync(join(home, 'fleet', 'roster.yaml'), ROSTER);
const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
writeFileSync(helper, '#!/bin/sh\n');
chmodSync(helper, 0o755);
});
afterEach(() => {
vi.unstubAllEnvs();
rmSync(home, { recursive: true, force: true });
});
it('uses the authoritative self host and global socket from the shared roster resolver', () => {
const result = readFleetCommsBlock(home, 'enhancer', 'process-host-must-not-win');
expect(result.ok).toBe(true);
expect(result.output).toContain('Host: `w-jarvis`');
expect(result.output).toContain('tmux socket: `mosaic-fleet`');
expect(result.output).toContain('-L mosaic-fleet -s orchestrator');
});
it('fails closed for a requested fleet identity that is absent', () => {
const result = readFleetCommsBlock(home, 'stranger', 'w-jarvis');
expect(result.ok).toBe(false);
expect(result.output).toBe('');
expect(result.error).toContain('Known exact names');
});
it('resolves a supported JSON-only installed roster', () => {
rmSync(join(home, 'fleet', 'roster.yaml'));
writeFileSync(
join(home, 'fleet', 'roster.json'),
JSON.stringify({
version: 1,
transport: 'tmux',
tmux: { socket_name: 'mosaic-fleet' },
agents: [
{ name: 'enhancer', runtime: 'claude', class: 'enhancer', host: 'w-jarvis' },
{ name: 'orchestrator', runtime: 'claude', class: 'orchestrator', host: 'w-jarvis' },
],
}),
);
const result = readFleetCommsBlock(home, 'enhancer', 'process-host-must-not-win');
expect(result.ok).toBe(true);
expect(result.output).toContain('-L mosaic-fleet -s orchestrator');
});
it('fails closed on a YAML I/O error instead of falling back to JSON', () => {
rmSync(join(home, 'fleet', 'roster.yaml'));
mkdirSync(join(home, 'fleet', 'roster.yaml'));
writeFileSync(
join(home, 'fleet', 'roster.json'),
JSON.stringify({
version: 1,
transport: 'tmux',
agents: [{ name: 'enhancer', runtime: 'claude', class: 'enhancer' }],
}),
);
const result = readFleetCommsBlock(home, 'enhancer', 'w-jarvis');
expect(result.ok).toBe(false);
expect(result.error).toContain('invalid fleet roster at');
expect(result.error).toContain('roster.yaml');
});
it.each([
['missing', () => rmSync(join(home, 'tools', 'tmux', 'agent-send.sh'))],
[
'directory',
() => {
rmSync(join(home, 'tools', 'tmux', 'agent-send.sh'));
mkdirSync(join(home, 'tools', 'tmux', 'agent-send.sh'));
},
],
[
'symlink escaping the install home',
() => {
const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
rmSync(helper);
const outside = mkdtempSync(join(tmpdir(), 'mosaic-helper-outside-'));
writeFileSync(join(outside, 'real-send.sh'), '#!/bin/sh\n', { mode: 0o755 });
symlinkSync(join(outside, 'real-send.sh'), helper);
},
],
['non-executable', () => chmodSync(join(home, 'tools', 'tmux', 'agent-send.sh'), 0o644)],
])(
'fails closed for a %s helper with deterministic guidance (no forbidden remedy)',
(_case, mutate) => {
mutate();
const result = readFleetCommsBlock(home, 'enhancer', 'w-jarvis');
expect(result.ok).toBe(false);
expect(result.output).toBe('');
expect(result.error).not.toContain('--repair-tools'); // stack#1380 M5a
expect(result.error).toContain('no active context or session was rewritten');
},
);
it('does not rewrite the roster while resolving context', () => {
const path = join(home, 'fleet', 'roster.yaml');
const before = readFileSync(path, 'utf8');
readFleetCommsBlock(home, 'enhancer', 'w-jarvis');
expect(readFileSync(path, 'utf8')).toBe(before);
});
});
describe('renderToolsContractStatus — non-mutating install drift', () => {
let home: string;
beforeEach(() => {
home = mkdtempSync(join(tmpdir(), 'mosaic-tools-status-'));
mkdirSync(join(home, 'defaults'), { recursive: true });
writeFileSync(
join(home, 'defaults', 'TOOLS.md'),
'# authoritative tools\n<!-- fleet-comms-contract: 1 -->\n',
);
});
afterEach(() => rmSync(home, { recursive: true, force: true }));
it('names operator-verified recovery instead of a forbidden remedy when installed TOOLS.md is missing', () => {
const status = renderToolsContractStatus(home);
expect(status).toContain('authorized operator');
expect(status).not.toContain('--repair-tools'); // stack#1380 M5a
expect(status).not.toContain('--reseed');
});
it('reports stale preserved content without rewriting it', () => {
const path = join(home, 'TOOLS.md');
const stale = '# customized tools\n';
writeFileSync(path, stale);
const status = renderToolsContractStatus(home);
expect(status).toContain('fleet-comms-contract: 1');
expect(status).toContain('authorized operator');
expect(status).not.toContain('--repair-tools'); // stack#1380 M5a
expect(status).toContain('active context was not rewritten');
expect(readFileSync(path, 'utf8')).toBe(stale);
});
it('does not accept marker-only customized content as current', () => {
const path = join(home, 'TOOLS.md');
writeFileSync(path, '<!-- fleet-comms-contract: 1 -->\ncorrupt\n');
expect(renderToolsContractStatus(home)).toContain('does not byte-match');
});
it('rejects markerless byte-equal source and installed content', () => {
const content = '# markerless but equal\n';
writeFileSync(join(home, 'defaults', 'TOOLS.md'), content);
writeFileSync(join(home, 'TOOLS.md'), content);
const status = renderToolsContractStatus(home);
expect(status).toContain('source contract');
expect(status).toContain('does not declare the expected');
});
it.each([
['source', join('defaults', 'TOOLS.md')],
['installed', 'TOOLS.md'],
])('rejects a wrong contract version in %s content', (_case, relativePath) => {
const current = '# authoritative tools\n<!-- fleet-comms-contract: 1 -->\n';
writeFileSync(join(home, 'TOOLS.md'), current);
writeFileSync(join(home, 'defaults', 'TOOLS.md'), current);
writeFileSync(join(home, relativePath), current.replace('contract: 1', 'contract: 2'));
expect(renderToolsContractStatus(home)).not.toBe('');
});
it('reads an installed TOOLS.md symlink whose validated target diverges (stack#1380 resolve-then-validate)', () => {
const external = join(home, 'external-tools.md');
const externalContent = '# external\n<!-- fleet-comms-contract: 1 -->\n';
writeFileSync(external, externalContent);
symlinkSync(external, join(home, 'TOOLS.md'));
const status = renderToolsContractStatus(home);
expect(status).toContain('does not byte-match');
expect(readFileSync(external, 'utf8')).toBe(externalContent);
});
it('treats a source TOOLS.md symlink escaping the install home as unavailable', () => {
const outside = mkdtempSync(join(tmpdir(), 'mosaic-source-outside-'));
const content = '# authoritative tools\n<!-- fleet-comms-contract: 1 -->\n';
writeFileSync(join(outside, 'external-source.md'), content);
rmSync(join(home, 'defaults', 'TOOLS.md'));
symlinkSync(join(outside, 'external-source.md'), join(home, 'defaults', 'TOOLS.md'));
writeFileSync(join(home, 'TOOLS.md'), content);
const status = renderToolsContractStatus(home);
expect(status).toContain('source contract');
expect(status).toContain('unavailable');
expect(readFileSync(join(outside, 'external-source.md'), 'utf8')).toBe(content);
});
it('accepts byte-equal bounded source and installed contracts', () => {
const source = readFileSync(join(home, 'defaults', 'TOOLS.md'), 'utf8');
writeFileSync(join(home, 'TOOLS.md'), source);
expect(renderToolsContractStatus(home)).toBe('');
});
});
describe('resolveCommsBlock — mosaic agent comms-block', () => {
let home: string;
beforeEach(() => {
home = mkdtempSync(join(tmpdir(), 'mosaic-commsblk-'));
mkdirSync(join(home, 'fleet'), { recursive: true });
mkdirSync(join(home, 'tools', 'tmux'), { recursive: true });
writeFileSync(join(home, 'fleet', 'roster.yaml'), ROSTER);
const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
writeFileSync(helper, '#!/bin/sh\n');
chmodSync(helper, 0o755);
});
afterEach(() => rmSync(home, { recursive: true, force: true }));
it('returns the exact contract for a roster member', () => {
const result = resolveCommsBlock(home, 'enhancer');
expect(result.ok).toBe(true);
expect(result.output).toContain('Host: `w-jarvis`');
expect(result.error).toBeUndefined();
});
it('fails loud and lists known exact names for a non-member', () => {
const result = resolveCommsBlock(home, 'stranger');
expect(result.ok).toBe(false);
expect(result.output).toBe('');
expect(result.error).toContain('stranger');
expect(result.error).toContain('orchestrator');
expect(result.error).toContain('enhancer');
});
it('fails loud when no roster exists', () => {
const noRoster = mkdtempSync(join(tmpdir(), 'mosaic-noroster-'));
mkdirSync(join(noRoster, 'tools', 'tmux'), { recursive: true });
const helper = join(noRoster, 'tools', 'tmux', 'agent-send.sh');
writeFileSync(helper, '#!/bin/sh\n');
chmodSync(helper, 0o755);
const result = resolveCommsBlock(noRoster, 'orchestrator');
expect(result.ok).toBe(false);
expect(result.error).toContain('no fleet roster');
rmSync(noRoster, { recursive: true, force: true });
});
it('fails loud for a missing role argument', () => {
const result = resolveCommsBlock(home, undefined);
expect(result.ok).toBe(false);
expect(result.error).toContain('requires');
});
});
describe('resolveFleetIdentity — stack#1380 split-home layouts', () => {
// Brain-shaped mosaicHome (fleet state, NO tools/tmux) + framework config
// home carrying the helper, roster unified by the framework-created symlink
// <configHome>/fleet/roster.yaml -> <brain>/fleet/roster.yaml. This is the
// host layout that was down; all probes are POSITIONAL per the #1380
// verification protocol (an object arg proves nothing — M5b). HOME is
// stubbed so the config-default fallback stays inside the sandbox.
let brain: string;
let configHome: string;
beforeEach(() => {
const parent = mkdtempSync(join(tmpdir(), 'mosaic-i1380-parent-'));
brain = join(parent, 'brain');
// Framework home at the stubbed DEFAULT location so the fallback derives
// exactly as in production ($HOME/.config/mosaic), not by coincidence.
configHome = join(parent, 'home', '.config', 'mosaic');
vi.stubEnv('HOME', join(parent, 'home'));
vi.stubEnv('MOSAIC_HOME', '');
mkdirSync(join(brain, 'fleet'), { recursive: true });
writeFileSync(join(brain, 'fleet', 'roster.yaml'), ROSTER, { mode: 0o600 });
mkdirSync(join(configHome, 'fleet'), { recursive: true });
symlinkSync(join(brain, 'fleet', 'roster.yaml'), join(configHome, 'fleet', 'roster.yaml'));
mkdirSync(join(configHome, 'tools', 'tmux'), { recursive: true });
writeFileSync(join(configHome, 'tools', 'tmux', 'agent-send.sh'), '#!/bin/sh\n', {
mode: 0o755,
});
process.env['MOSAIC_BRAIN_HOME'] = brain;
});
afterEach(() => {
delete process.env['MOSAIC_BRAIN_HOME'];
vi.unstubAllEnvs();
rmSync(join(brain, '..'), { recursive: true, force: true });
});
it('resolves a member through the roster symlink under the config home', () => {
const result = resolveFleetIdentity(configHome, 'orchestrator', 'w-jarvis');
expect(result.ok).toBe(true);
expect(result.identity?.member.name).toBe('orchestrator');
expect(result.identity?.agentSendPath).toBe(join(configHome, 'tools', 'tmux', 'agent-send.sh'));
});
it('resolves a member when mosaicHome is the brain (helper found under the framework home)', () => {
const result = resolveFleetIdentity(brain, 'enhancer', 'w-jarvis');
expect(result.ok).toBe(true);
expect(result.identity?.member.name).toBe('enhancer');
expect(result.identity?.agentSendPath).toBe(join(configHome, 'tools', 'tmux', 'agent-send.sh'));
});
it('no-name control stays a quiet no-op', () => {
expect(resolveFleetIdentity(configHome, undefined, 'w-jarvis')).toEqual({ ok: true });
expect(resolveFleetIdentity(brain, undefined, 'w-jarvis')).toEqual({ ok: true });
});
it('a nonce name fails naming membership, not the symlink or the helper', () => {
const result = resolveFleetIdentity(configHome, 'nonce-' + Date.now(), 'w-jarvis');
expect(result.ok).toBe(false);
expect(result.error).not.toContain('symbolic link');
expect(result.error).not.toContain('helper');
expect(result.error).toContain('nonce-');
});
it('a non-member failure names membership, not the symlink (protocol control)', () => {
const result = resolveFleetIdentity(configHome, 'jarvis', 'w-jarvis');
expect(result.ok).toBe(false);
expect(result.error).not.toContain('symbolic link');
expect(result.error).not.toContain('helper is unavailable');
expect(result.error).toContain('orchestrator'); // known-member listing
});
it('names every searched framework home when the helper is missing everywhere', () => {
rmSync(join(configHome, 'tools'), { recursive: true, force: true });
const result = resolveFleetIdentity(brain, 'orchestrator', 'w-jarvis');
expect(result.ok).toBe(false);
expect(result.error).toContain('agent-send.sh');
expect(result.error).toContain(join(brain, 'tools', 'tmux', 'agent-send.sh'));
expect(result.error).not.toContain('--repair-tools');
});
it('readFleetCommsBlock composes the full contract on the split-home layout', () => {
const result = readFleetCommsBlock(configHome, 'orchestrator', 'w-jarvis');
expect(result.ok).toBe(true);
expect(result.output).toContain(
'Helper: `' + join(configHome, 'tools', 'tmux', 'agent-send.sh'),
);
});
});
@@ -0,0 +1,481 @@
/**
* Exact roster-resolved fleet communications contract (#766).
*
* The runtime composer and `mosaic fleet` command surface share the canonical
* v1 roster resolver. This module never probes tmux, guesses an SSH target, or
* mutates an active session.
*/
import { createHash } from 'node:crypto';
import { existsSync } from 'node:fs';
import { homedir, hostname } from 'node:os';
import { join, resolve } from 'node:path';
import { readRegularFileSecure } from './secure-file.js';
import { resolveBrainHome } from './brain-home.js';
import {
parseFleetRosterV1,
resolveInstalledFleetRosterPath,
getRosterAgent,
type FleetAgent,
type FleetRoster,
} from './fleet-roster-v1.js';
export interface FleetCommsOptions {
/** Exact current roster member. */
selfName: string;
/** Canonically resolved roster. */
roster: FleetRoster;
/** Stable fleet-host baseline for members whose roster host is absent. */
localHost: string;
/** Absolute helper path in this installation. */
agentSendPath: string;
}
export interface CommsBlockResult {
ok: boolean;
output: string;
error?: string;
}
export interface ResolvedFleetIdentity {
readonly roster: FleetRoster;
readonly member: FleetAgent;
readonly requestedName: string;
readonly agentSendPath: string;
readonly localHost: string;
}
export interface FleetIdentityResult {
ok: boolean;
identity?: ResolvedFleetIdentity;
error?: string;
}
export interface PeerCommandResult {
ok: boolean;
command: string;
error?: string;
}
export const FLEET_COMMS_TOOLS_CONTRACT = 'fleet-comms-contract: 1';
const MAX_TOOLS_CONTRACT_BYTES = 256 * 1024;
function shortHostname(): string {
return hostname().split('.')[0] || 'localhost';
}
function resolvedHost(agent: FleetAgent, fleetHost: string): string {
return agent.host ?? fleetHost;
}
function displaySocket(socket: string): string {
return socket || '(default)';
}
function knownNames(roster: FleetRoster, except?: string): string {
return roster.agents
.filter((agent) => agent.name !== except)
.map((agent) => agent.name)
.join(', ');
}
function missingMemberError(roster: FleetRoster, selfName: string): string {
return `Agent "${selfName}" is not in the fleet roster. Known exact names: ${knownNames(roster)}. Select an exact roster name; do not infer or fuzzy-match a tmux session.`;
}
/** Render one shell argument without changing already-safe exact values. */
function shellArg(value: string): string {
if (/^[A-Za-z0-9_./:@=+-]+$/.test(value)) return value;
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
/** Render the exact command for one peer. Throws rather than guessing. */
export function renderPeerReach(
peer: FleetAgent,
selfHost: string,
fleetHost: string,
rosterSocket: string,
agentSendPath: string,
): string {
const parts = [shellArg(agentSendPath)];
if (rosterSocket) parts.push('-L', shellArg(rosterSocket));
const peerHost = resolvedHost(peer, fleetHost);
if (peerHost !== selfHost) {
if (!peer.ssh) {
throw new Error(
`Cross-host peer "${peer.name}" (${peerHost}) requires an explicit roster ssh target; refusing to substitute its host value.`,
);
}
parts.push('-H', shellArg(peer.ssh));
}
parts.push('-s', shellArg(peer.name), '-m', '"…"');
return parts.join(' ');
}
/** Resolve one requested peer without fuzzy lookup. */
export function resolvePeerCommand(
roster: FleetRoster,
selfName: string,
peerName: string,
localHost: string,
agentSendPath: string,
): PeerCommandResult {
const self = roster.agents.find((agent) => agent.name === selfName);
if (!self) return { ok: false, command: '', error: missingMemberError(roster, selfName) };
const peer = roster.agents.find((agent) => agent.name === peerName && agent.name !== selfName);
if (!peer) {
return {
ok: false,
command: '',
error:
`Peer "${peerName}" is absent from the fleet roster. Known exact peer names: ${knownNames(roster, selfName)}. ` +
`Run \`mosaic agent comms-block ${selfName}\` to rediscover exact rendered rows; do not infer or fuzzy-match a tmux session.`,
};
}
try {
return {
ok: true,
command: renderPeerReach(
peer,
resolvedHost(self, localHost),
localHost,
roster.tmux.socketName,
agentSendPath,
),
};
} catch (error) {
return {
ok: false,
command: '',
error: error instanceof Error ? error.message : String(error),
};
}
}
interface ResolvedRow {
readonly peer: FleetAgent;
readonly host: string;
readonly socket: string;
readonly command: string;
}
function resolveRows(opts: FleetCommsOptions, self: FleetAgent): readonly ResolvedRow[] {
const selfHost = resolvedHost(self, opts.localHost);
return opts.roster.agents
.filter((agent) => agent.name !== opts.selfName)
.map(
(peer): ResolvedRow => ({
peer,
host: resolvedHost(peer, opts.localHost),
socket: opts.roster.tmux.socketName,
command: renderPeerReach(
peer,
selfHost,
opts.localHost,
opts.roster.tmux.socketName,
opts.agentSendPath,
),
}),
);
}
function commsGeneration(
self: FleetAgent,
selfHost: string,
selfSocket: string,
helper: string,
rows: readonly ResolvedRow[],
): string {
const canonical = JSON.stringify({
self: { ...self, resolvedHost: selfHost, resolvedSocket: selfSocket, helper },
peers: rows.map((row) => ({
...row.peer,
resolvedHost: row.host,
resolvedSocket: row.socket,
exactCommand: row.command,
})),
});
return createHash('sha256').update(canonical).digest('hex');
}
/** Build the authoritative Markdown contract for one exact roster member. */
export function buildFleetCommsBlock(opts: FleetCommsOptions): string {
const self = opts.roster.agents.find((agent) => agent.name === opts.selfName);
if (!self) throw new Error(missingMemberError(opts.roster, opts.selfName));
const rows = resolveRows(opts, self);
const selfHost = resolvedHost(self, opts.localHost);
const selfSocket = opts.roster.tmux.socketName;
const generation = commsGeneration(self, selfHost, selfSocket, opts.agentSendPath, rows);
const orchestrator = rows.find((row) => row.peer.className === 'orchestrator');
const peerSection =
rows.length === 0
? 'This roster has no peers. Do not invent a target.'
: `| Agent | Role | Host | Socket | Exact command |
| ----- | ---- | ---- | ------ | ------------- |
${rows
.map((row) => {
const pointOfContact = row.peer.className === 'orchestrator' ? ' ← point of contact' : '';
return `| ${row.peer.name} | ${row.peer.className}${pointOfContact} | ${row.host} | ${displaySocket(row.socket)} | \`${row.command}\` |`;
})
.join('\n')}`;
const contact = orchestrator
? `Your point of contact is **${orchestrator.peer.name}**. Select that exact peer row for status, questions, and decisions.`
: rows.length === 0
? 'No peer coordination target exists in this roster.'
: 'This roster has no orchestrator. Select an exact peer row for coordination.';
const soloAuthority =
rows.length === 0
? `\n## Solo authority boundaries\n\nThis member is normalized as role/class **${self.className}**. The roster grants no peer, orchestrator, or remote communication authority. Do not send, infer a target, or claim fleet coordination until an exact peer is added to the canonical roster and this block is recomposed.\n`
: '';
return `# Fleet Comms — authoritative exact targets
## Local identity
- Host: \`${selfHost}\`
- Agent/session: \`${self.name}\`
- Role/class: \`${self.className}\`
- tmux socket: \`${displaySocket(selfSocket)}\`
- Helper: \`${opts.agentSendPath}\`
- Comms generation: \`${generation}\`
The roster-resolved rows below are the only valid operational targets. Select the row whose Agent value
exactly matches the requested peer. Never invent, substitute, or fuzzy-match host, session, socket, SSH,
or helper-path values. If the peer is absent, stop and run \`mosaic agent comms-block ${self.name}\` to
rediscover this exact member's rows; if it is still absent, report the unknown peer.
## Peers
${peerSection}
${contact}
${soloAuthority}
## Context freshness
This block is a snapshot; Mosaic does not rewrite an active agent's context. Compare its Comms generation
with fresh output from \`mosaic agent comms-block ${self.name}\`. If they differ, report stale composed
context and have an authorized operator relaunch only this exact roster member with
\`mosaic fleet restart ${self.name}\`. Do not restart or mutate a session automatically.`;
}
function validateAgentSendHelper(path: string, mosaicHome: string): string | undefined {
try {
readRegularFileSecure(path, {
root: mosaicHome,
executable: true,
// The helper tree may live under the framework config home while this
// caller's mosaicHome is the brain; both are framework-owned roots.
symlinkTargetRoots: [resolveBrainHome(mosaicHome)],
});
return undefined;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
return `helper is unavailable or unsafe: ${path} (${reason})`;
}
}
/**
* Framework install homes probed for tools/tmux/agent-send.sh (stack#1380 M2).
* The helper ships with the FRAMEWORK install, which on split-home layouts is
* the config home — not the brain (~/.mosaic carries fleet state, no tools).
*/
function frameworkHelperHomes(mosaicHome: string): string[] {
const homes = [resolve(mosaicHome)];
const envHome = process.env['MOSAIC_HOME'];
if (envHome && envHome.trim() !== '' && resolve(envHome) !== resolve(mosaicHome)) {
homes.push(resolve(envHome));
}
const configDefault = join(homedir(), '.config', 'mosaic');
if (resolve(configDefault) !== resolve(mosaicHome)) homes.push(configDefault);
return homes;
}
function resolveAgentSendHelper(mosaicHome: string): { path: string; error?: string } {
const homes = frameworkHelperHomes(mosaicHome);
for (const home of homes) {
const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
if (!existsSync(helper)) continue;
const error = validateAgentSendHelper(helper, home);
if (!error) return { path: helper };
// Present but unsafe: surface that verdict instead of silently probing on.
return { path: helper, error };
}
return {
path: join(resolve(mosaicHome), 'tools', 'tmux', 'agent-send.sh'),
error:
`fleet helper agent-send.sh was not found under any framework install home ` +
`(${homes.map((h) => join(h, 'tools', 'tmux', 'agent-send.sh')).join('; ')}). ` +
`Verify the framework install for this host (the helper ships with the framework ` +
`config home; the brain home carries fleet state, not tools) and have an authorized ` +
`operator restore it if missing.`,
};
}
function helperFailureGuidance(reason: string): string {
// stack#1380 M5a: `mosaic update --repair-tools` is a forbidden remedy on the
// affected estate (and wrong for a layout/missing-helper failure). Name the
// actual recovery shape instead.
return `${reason}. Verify the framework install provides tools/tmux/agent-send.sh under the framework config home and that the roster resolves (split-home layouts symlink the roster into the brain); contact the operator if it persists; no active context or session was rewritten.`;
}
export function resolveFleetIdentity(
mosaicHome: string,
requestedName: string | undefined,
localHost: string = shortHostname(),
): FleetIdentityResult {
if (!requestedName) return { ok: true };
const helper = resolveAgentSendHelper(mosaicHome);
if (helper.error) return { ok: false, error: helperFailureGuidance(helper.error) };
const agentSendPath = helper.path;
// Split-home layouts unify the roster by symlinking
// <configHome>/fleet/roster.yaml -> <brain>/fleet/roster.yaml. The secure
// read resolves that framework-created symlink when the brain is a
// sanctioned target root (stack#1380 M1).
const rosterSymlinkRoots = [resolveBrainHome(mosaicHome)];
let rosterPath: string;
try {
rosterPath = resolveInstalledFleetRosterPath(mosaicHome);
} catch (error) {
return {
ok: false,
error: `cannot inspect fleet roster.yaml: ${error instanceof Error ? error.message : String(error)}; refusing JSON fallback because fallback is allowed only when YAML is absent`,
};
}
if (!existsSync(rosterPath)) {
return {
ok: false,
error: `no fleet roster at ${join(mosaicHome, 'fleet', 'roster.yaml')} or ${join(mosaicHome, 'fleet', 'roster.json')}`,
};
}
let roster: FleetRoster;
try {
roster = parseFleetRosterV1(
readRegularFileSecure(rosterPath, {
root: mosaicHome,
symlinkTargetRoots: rosterSymlinkRoots,
}).content.toString('utf8'),
rosterPath.endsWith('.json') ? 'json' : 'yaml',
);
} catch (error) {
return {
ok: false,
error: `invalid fleet roster at ${rosterPath}: ${error instanceof Error ? error.message : String(error)}`,
};
}
try {
return {
ok: true,
identity: {
roster,
member: getRosterAgent(roster, requestedName),
requestedName,
agentSendPath,
localHost,
},
};
} catch {
return { ok: false, error: missingMemberError(roster, requestedName) };
}
}
/** Render Fleet Comms from one already-resolved canonical member identity. */
export function buildResolvedFleetCommsBlock(identity: ResolvedFleetIdentity): string {
return buildFleetCommsBlock({
selfName: identity.member.name,
roster: identity.roster,
localHost: identity.localHost,
agentSendPath: identity.agentSendPath,
});
}
/**
* Read and resolve the installed roster for runtime composition. A requested
* fleet identity fails closed; only a genuinely non-fleet launch (no selfName)
* is a quiet no-op.
*/
export function readFleetCommsBlock(
mosaicHome: string,
selfName: string | undefined,
localHost: string = shortHostname(),
): CommsBlockResult {
const resolved = resolveFleetIdentity(mosaicHome, selfName, localHost);
if (!resolved.ok) return { ok: false, output: '', error: resolved.error };
if (!resolved.identity) return { ok: true, output: '' };
try {
return { ok: true, output: buildResolvedFleetCommsBlock(resolved.identity) };
} catch (error) {
return {
ok: false,
output: '',
error: error instanceof Error ? error.message : String(error),
};
}
}
/** Backing resolver for `mosaic agent comms-block <exact-member>`. */
export function resolveCommsBlock(
mosaicHome: string,
exactMember: string | undefined,
): CommsBlockResult {
if (!exactMember) {
return {
ok: false,
output: '',
error: 'comms-block requires an exact <exact-member> argument',
};
}
return readFleetCommsBlock(mosaicHome, exactMember);
}
function expectedContractVersion(content: Buffer | string): boolean {
return content.toString().includes(`<!-- ${FLEET_COMMS_TOOLS_CONTRACT} -->`);
}
function boundedContractDigest(
path: string,
mosaicHome: string,
): { digest?: string; versionOk: boolean } {
try {
const content = readRegularFileSecure(path, {
root: mosaicHome,
maxBytes: MAX_TOOLS_CONTRACT_BYTES,
}).content;
return {
digest: createHash('sha256').update(content).digest('hex'),
versionOk: expectedContractVersion(content),
};
} catch {
return { versionOk: false };
}
}
function replacementGuidance(): string {
// stack#1380 M5a: never recommend the forbidden --repair-tools remedy from
// error text; name the operator-verified recovery shape instead.
return `Verify the installed TOOLS contract against the framework source with an authorized operator (the installed file must byte-match the supported current version) and have the operator explicitly relaunch the exact roster member. The active context was not rewritten.`;
}
/** Detect preserved installed TOOLS.md drift without changing it. */
export function renderToolsContractStatus(mosaicHome: string): string {
const installedPath = join(mosaicHome, 'TOOLS.md');
const sourcePath = join(mosaicHome, 'defaults', 'TOOLS.md');
if (!existsSync(installedPath)) {
return `# Fleet Comms Installation Status\n\nInstalled TOOLS.md is missing at \`${installedPath}\`. ${replacementGuidance()}`;
}
const installed = boundedContractDigest(installedPath, mosaicHome);
const source = boundedContractDigest(sourcePath, mosaicHome);
if (!source.digest || !source.versionOk) {
return `# Fleet Comms Installation Status\n\nThe bounded framework source contract at \`${sourcePath}\` is unavailable or does not declare the expected \`${FLEET_COMMS_TOOLS_CONTRACT}\` version. Run \`mosaic update\` to restore framework source data, verify again, then have an authorized operator explicitly relaunch the exact roster member. The installed file and active context were not rewritten.`;
}
if (installed.versionOk && installed.digest === source.digest) return '';
return `# Fleet Comms Installation Status\n\nInstalled TOOLS.md is unavailable, has the wrong contract version, or does not byte-match the bounded framework source contract \`${FLEET_COMMS_TOOLS_CONTRACT}\`. ${replacementGuidance()}`;
}
export const DEFAULT_MOSAIC_HOME_FOR_COMMS = join(homedir(), '.config', 'mosaic');
@@ -0,0 +1,184 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
MatrixConnector,
buildMessageBody,
parseSyncResponse,
registerMatrixConnector,
type FetchLike,
} from './matrix.js';
import { createConnector, _resetConnectorRegistry } from './registry.js';
import type { MatrixConnectorConfig } from './types.js';
const CONFIG: MatrixConnectorConfig = {
homeserverUrl: 'https://matrix.internal/',
userId: '@mos:internal',
roomId: '!room:internal',
};
/** A fetch mock that returns queued responses and records calls. */
function mockFetch(responses: Array<{ ok?: boolean; status?: number; body?: unknown }>): {
fetchImpl: FetchLike;
calls: Array<{ url: string; method?: string; body?: string }>;
} {
const calls: Array<{ url: string; method?: string; body?: string }> = [];
let i = 0;
const fetchImpl: FetchLike = async (url, init) => {
calls.push({ url, method: init?.method, body: init?.body });
const r = responses[Math.min(i, responses.length - 1)] ?? {};
i += 1;
return {
ok: r.ok ?? true,
status: r.status ?? 200,
json: async () => r.body ?? {},
text: async () => JSON.stringify(r.body ?? {}),
};
};
return { fetchImpl, calls };
}
describe('buildMessageBody', () => {
it('builds an m.text event', () => {
expect(buildMessageBody({ text: 'hi' })).toEqual({ msgtype: 'm.text', body: 'hi' });
});
it('adds an m.thread relation when threadId is set', () => {
expect(buildMessageBody({ text: 'hi', threadId: '$evt' })).toEqual({
msgtype: 'm.text',
body: 'hi',
'm.relates_to': { rel_type: 'm.thread', event_id: '$evt' },
});
});
});
describe('parseSyncResponse', () => {
it('extracts operator messages and skips the orchestrators own echoes', () => {
const data = {
next_batch: 's2',
rooms: {
join: {
'!room:internal': {
timeline: {
events: [
{
type: 'm.room.message',
sender: '@jason:internal',
origin_server_ts: 1_700_000_000_000,
content: { body: 'status?' },
},
{
type: 'm.room.message',
sender: '@mos:internal', // self — skipped
origin_server_ts: 1_700_000_001_000,
content: { body: 'working on it' },
},
{ type: 'm.reaction', sender: '@jason:internal', content: {} }, // non-message
],
},
},
},
},
};
const msgs = parseSyncResponse(data, '!room:internal', '@mos:internal');
expect(msgs).toHaveLength(1);
expect(msgs[0]).toMatchObject({ text: 'status?', sender: '@jason:internal' });
expect(msgs[0]!.ts).toBe(new Date(1_700_000_000_000).toISOString());
});
it('carries threadId through thread-relments', () => {
const data = {
rooms: {
join: {
'!room:internal': {
timeline: {
events: [
{
type: 'm.room.message',
sender: '@jason:internal',
origin_server_ts: 1,
content: {
body: 'in thread',
'm.relates_to': { rel_type: 'm.thread', event_id: '$root' },
},
},
],
},
},
},
},
};
expect(parseSyncResponse(data, '!room:internal', '@mos:internal')[0]!.threadId).toBe('$root');
});
it('returns [] for an empty/foreign sync', () => {
expect(parseSyncResponse({}, '!room:internal', '@mos:internal')).toEqual([]);
});
});
describe('MatrixConnector', () => {
it('throws without an access token', () => {
expect(() => new MatrixConnector(CONFIG, { accessToken: '' })).toThrow(/access token/i);
});
it('send PUTs an m.text event and returns the event id', async () => {
const { fetchImpl, calls } = mockFetch([{ body: { event_id: '$abc' } }]);
const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl });
const res = await c.send({ text: 'pong' }, 1234);
expect(res).toEqual({ delivered: true, messageId: '$abc' });
expect(calls[0]!.method).toBe('PUT');
expect(calls[0]!.url).toContain(
'/_matrix/client/v3/rooms/!room%3Ainternal/send/m.room.message/mosaic-1234-1',
);
expect(JSON.parse(calls[0]!.body!)).toEqual({ msgtype: 'm.text', body: 'pong' });
});
it('send reports not-delivered on a non-2xx', async () => {
const { fetchImpl } = mockFetch([{ ok: false, status: 403 }]);
const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl });
const res = await c.send({ text: 'x' });
expect(res.delivered).toBe(false);
expect(res.error).toContain('403');
});
it('health reports reachable + authenticated when whoami matches', async () => {
const { fetchImpl } = mockFetch([
{ body: { versions: ['v1.11'] } }, // /versions
{ body: { user_id: '@mos:internal' } }, // /whoami
]);
const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl });
const h = await c.health();
expect(h.reachable).toBe(true);
expect(h.authenticated).toBe(true);
});
it('health flags auth mismatch', async () => {
const { fetchImpl } = mockFetch([
{ body: {} },
{ body: { user_id: '@someone-else:internal' } },
]);
const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl });
const h = await c.health();
expect(h.reachable).toBe(true);
expect(h.authenticated).toBe(false);
});
it('health reports unreachable when /versions fails', async () => {
const { fetchImpl } = mockFetch([{ ok: false, status: 502 }]);
const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl });
const h = await c.health();
expect(h.reachable).toBe(false);
});
});
describe('registerMatrixConnector', () => {
beforeEach(() => _resetConnectorRegistry());
it('registers a matrix factory createConnector can build', () => {
registerMatrixConnector({ MATRIX_ACCESS_TOKEN: 'tok' } as NodeJS.ProcessEnv);
const c = createConnector({ kind: 'matrix', matrix: CONFIG });
expect(c.kind).toBe('matrix');
});
it('the factory rejects config missing the matrix block', () => {
registerMatrixConnector({ MATRIX_ACCESS_TOKEN: 'tok' } as NodeJS.ProcessEnv);
expect(() => createConnector({ kind: 'matrix' })).toThrow(/missing the .matrix. block/i);
});
});
@@ -0,0 +1,246 @@
/**
* Matrix connector (F4 Phase 2) — speaks the Matrix client-server API directly
* over HTTPS so it is homeserver-agnostic (Conduit default, Synapse alt). No
* SDK: a small injectable fetch keeps it dependency-light and unit-testable.
*
* The access token is supplied by the caller (from the environment —
* MATRIX_ACCESS_TOKEN — per the gateway secret pattern), never the roster.
*/
import {
type OrchestratorConnector,
type OutboundMessage,
type InboundMessage,
type SendResult,
type ConnectorHealth,
type MatrixConnectorConfig,
type Unsubscribe,
} from './types.js';
import { registerConnector } from './registry.js';
/** Minimal fetch surface — avoids a lib.dom dependency and is trivial to mock. */
export interface FetchLike {
(
url: string,
init?: { method?: string; headers?: Record<string, string>; body?: string },
): Promise<{
ok: boolean;
status: number;
json: () => Promise<unknown>;
text: () => Promise<string>;
}>;
}
export interface MatrixConnectorOptions {
accessToken: string;
/** Injectable fetch (defaults to global fetch). */
fetchImpl?: FetchLike;
/** Long-poll timeout for /sync, ms. */
syncTimeoutMs?: number;
}
/** Build the `m.room.message` event content, threading when a threadId is set. */
export function buildMessageBody(message: OutboundMessage): Record<string, unknown> {
const content: Record<string, unknown> = {
msgtype: 'm.text',
body: message.text,
};
if (message.threadId) {
content['m.relates_to'] = { rel_type: 'm.thread', event_id: message.threadId };
}
return content;
}
/** Shape of the bits of a /sync response we consume. */
interface SyncResponse {
next_batch?: string;
rooms?: {
join?: Record<
string,
{
timeline?: {
events?: Array<{
type?: string;
sender?: string;
origin_server_ts?: number;
content?: {
body?: string;
['m.relates_to']?: { rel_type?: string; event_id?: string };
};
}>;
};
}
>;
};
}
/**
* Extract inbound operator messages from a /sync response for one room,
* skipping the orchestrator's own echoes. Pure — the testable core of receive.
*/
export function parseSyncResponse(
data: unknown,
roomId: string,
selfUserId: string,
): InboundMessage[] {
const sync = data as SyncResponse;
const events = sync.rooms?.join?.[roomId]?.timeline?.events ?? [];
const out: InboundMessage[] = [];
for (const ev of events) {
if (ev.type !== 'm.room.message') continue;
if (!ev.sender || ev.sender === selfUserId) continue; // skip our own messages
const body = ev.content?.body;
if (typeof body !== 'string') continue;
const rel = ev.content?.['m.relates_to'];
out.push({
text: body,
sender: ev.sender,
ts: new Date(ev.origin_server_ts ?? 0).toISOString(),
...(rel?.rel_type === 'm.thread' && rel.event_id ? { threadId: rel.event_id } : {}),
});
}
return out;
}
export class MatrixConnector implements OrchestratorConnector {
readonly kind = 'matrix' as const;
private readonly fetchImpl: FetchLike;
private readonly token: string;
private readonly syncTimeoutMs: number;
private txnCounter = 0;
private stopped = false;
constructor(
private readonly config: MatrixConnectorConfig,
opts: MatrixConnectorOptions,
) {
this.token = opts.accessToken;
this.fetchImpl = opts.fetchImpl ?? (globalThis.fetch as unknown as FetchLike);
this.syncTimeoutMs = opts.syncTimeoutMs ?? 30_000;
if (!this.token) {
throw new Error('MatrixConnector requires an access token (set MATRIX_ACCESS_TOKEN).');
}
}
private url(path: string): string {
return `${this.config.homeserverUrl.replace(/\/$/, '')}${path}`;
}
private authHeaders(): Record<string, string> {
return { Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json' };
}
/** Monotonic, unique-per-instance transaction id for idempotent sends. */
private nextTxnId(nowMs: number): string {
this.txnCounter += 1;
return `mosaic-${nowMs}-${this.txnCounter}`;
}
async send(message: OutboundMessage, nowMs = Date.now()): Promise<SendResult> {
const txnId = this.nextTxnId(nowMs);
const path = `/_matrix/client/v3/rooms/${encodeURIComponent(
this.config.roomId,
)}/send/m.room.message/${encodeURIComponent(txnId)}`;
try {
const res = await this.fetchImpl(this.url(path), {
method: 'PUT',
headers: this.authHeaders(),
body: JSON.stringify(buildMessageBody(message)),
});
if (!res.ok) {
return { delivered: false, error: `Matrix send failed: HTTP ${res.status}` };
}
const json = (await res.json()) as { event_id?: string };
return { delivered: true, ...(json.event_id ? { messageId: json.event_id } : {}) };
} catch (err) {
return { delivered: false, error: err instanceof Error ? err.message : String(err) };
}
}
subscribe(handler: (message: InboundMessage) => void): Unsubscribe {
this.stopped = false;
let since: string | undefined;
const loop = async (): Promise<void> => {
while (!this.stopped) {
try {
const q = new URLSearchParams({ timeout: String(this.syncTimeoutMs) });
if (since) q.set('since', since);
const res = await this.fetchImpl(this.url(`/_matrix/client/v3/sync?${q.toString()}`), {
method: 'GET',
headers: this.authHeaders(),
});
if (!res.ok) {
await this.backoff();
continue;
}
const data = await res.json();
since = (data as SyncResponse).next_batch ?? since;
for (const msg of parseSyncResponse(data, this.config.roomId, this.config.userId)) {
handler(msg);
}
} catch {
await this.backoff();
}
}
};
void loop();
return () => {
this.stopped = true;
};
}
private backoff(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 2_000));
}
async health(): Promise<ConnectorHealth> {
try {
const versions = await this.fetchImpl(this.url('/_matrix/client/versions'), {
method: 'GET',
});
if (!versions.ok) {
return {
reachable: false,
authenticated: false,
detail: `versions HTTP ${versions.status}`,
};
}
const who = await this.fetchImpl(this.url('/_matrix/client/v3/account/whoami'), {
method: 'GET',
headers: this.authHeaders(),
});
if (!who.ok) {
return { reachable: true, authenticated: false, detail: `whoami HTTP ${who.status}` };
}
const json = (await who.json()) as { user_id?: string };
const authenticated = json.user_id === this.config.userId;
return {
reachable: true,
authenticated,
lastSeen: new Date().toISOString(),
...(authenticated
? {}
: { detail: `whoami user ${json.user_id} != ${this.config.userId}` }),
};
} catch (err) {
return {
reachable: false,
authenticated: false,
detail: err instanceof Error ? err.message : String(err),
};
}
}
}
/**
* Register the Matrix connector factory. The token is read from the environment
* (MATRIX_ACCESS_TOKEN) at build time, never the roster.
*/
export function registerMatrixConnector(env: NodeJS.ProcessEnv = process.env): void {
registerConnector('matrix', (config) => {
if (!config.matrix) {
throw new Error('Matrix connector config missing the `matrix` block (homeserver/user/room).');
}
return new MatrixConnector(config.matrix, { accessToken: env['MATRIX_ACCESS_TOKEN'] ?? '' });
});
}
@@ -0,0 +1,85 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
KNOWN_CONNECTOR_KINDS,
isKnownConnectorKind,
resolveConnectorKind,
registerConnector,
hasConnector,
createConnector,
ConnectorNotImplementedError,
_resetConnectorRegistry,
} from './registry.js';
import type { ConnectorConfig, OrchestratorConnector } from './types.js';
function fakeConnector(kind: 'tmux' | 'discord' | 'matrix'): OrchestratorConnector {
return {
kind,
send: async () => ({ delivered: true, messageId: 'x' }),
subscribe: () => () => {},
health: async () => ({ reachable: true, authenticated: true }),
};
}
describe('connector registry (F4 Phase 1)', () => {
beforeEach(() => {
_resetConnectorRegistry();
});
it('knows the three peer connector kinds', () => {
expect(KNOWN_CONNECTOR_KINDS).toEqual(['tmux', 'discord', 'matrix']);
});
it('isKnownConnectorKind guards correctly', () => {
expect(isKnownConnectorKind('matrix')).toBe(true);
expect(isKnownConnectorKind('irc')).toBe(false);
expect(isKnownConnectorKind(42)).toBe(false);
});
it('resolveConnectorKind defaults to tmux when config is absent (back-compat)', () => {
expect(resolveConnectorKind(undefined)).toBe('tmux');
expect(resolveConnectorKind({ kind: 'matrix' })).toBe('matrix');
});
it('createConnector throws ConnectorNotImplementedError for an unregistered kind', () => {
const cfg: ConnectorConfig = { kind: 'matrix' };
expect(() => createConnector(cfg)).toThrow(ConnectorNotImplementedError);
expect(() => createConnector(cfg)).toThrow(/not implemented yet/i);
});
it('createConnector with no config resolves the default kind (tmux) and reports it unimplemented in Phase 1', () => {
try {
createConnector();
throw new Error('expected throw');
} catch (err) {
expect(err).toBeInstanceOf(ConnectorNotImplementedError);
expect((err as ConnectorNotImplementedError).kind).toBe('tmux');
}
});
it('register → has → create resolves a registered factory', () => {
expect(hasConnector('matrix')).toBe(false);
registerConnector('matrix', (cfg) => fakeConnector(cfg.kind));
expect(hasConnector('matrix')).toBe(true);
const connector = createConnector({ kind: 'matrix' });
expect(connector.kind).toBe('matrix');
});
it('passes the config through to the factory', () => {
let received: ConnectorConfig | null = null;
registerConnector('matrix', (cfg) => {
received = cfg;
return fakeConnector(cfg.kind);
});
const cfg: ConnectorConfig = {
kind: 'matrix',
matrix: {
homeserverUrl: 'https://matrix.internal',
userId: '@mos:internal',
roomId: '!room:internal',
},
};
createConnector(cfg);
expect(received).toEqual(cfg);
});
});
@@ -0,0 +1,76 @@
/**
* Connector registry (F4 Phase 1).
*
* A tiny extensible registry so connector implementations (Phase 2: tmux,
* Discord, Matrix) register a factory by kind and fleet core resolves one from
* roster config without branching on kind. Phase 1 ships the registry + the
* config→kind resolution; the connector factories land in Phase 2.
*/
import {
type ConnectorConfig,
type ConnectorKind,
type OrchestratorConnector,
DEFAULT_CONNECTOR_KIND,
} from './types.js';
/** The set of connector kinds the framework recognizes. */
export const KNOWN_CONNECTOR_KINDS: readonly ConnectorKind[] = ['tmux', 'discord', 'matrix'];
/** Type guard: is `value` a known connector kind? */
export function isKnownConnectorKind(value: unknown): value is ConnectorKind {
return typeof value === 'string' && (KNOWN_CONNECTOR_KINDS as readonly string[]).includes(value);
}
/**
* Resolve the connector kind from roster config. Absent config ⇒ the default
* (tmux) so existing rosters keep working unchanged (back-compat).
*/
export function resolveConnectorKind(config?: ConnectorConfig): ConnectorKind {
return config?.kind ?? DEFAULT_CONNECTOR_KIND;
}
/** A factory builds a live connector from its validated config. */
export type ConnectorFactory = (config: ConnectorConfig) => OrchestratorConnector;
/** Thrown when no factory is registered for a requested kind. */
export class ConnectorNotImplementedError extends Error {
constructor(public readonly kind: ConnectorKind) {
super(
`Connector "${kind}" is not implemented yet. ` +
`Register a factory via registerConnector('${kind}', …) (F4 Phase 2).`,
);
this.name = 'ConnectorNotImplementedError';
}
}
const registry = new Map<ConnectorKind, ConnectorFactory>();
/** Register a connector factory for a kind (idempotent — last registration wins). */
export function registerConnector(kind: ConnectorKind, factory: ConnectorFactory): void {
registry.set(kind, factory);
}
/** True when a factory is registered for `kind`. */
export function hasConnector(kind: ConnectorKind): boolean {
return registry.has(kind);
}
/**
* Build a connector from roster config. Throws `ConnectorNotImplementedError`
* when no factory is registered for the resolved kind (the Phase-1 default for
* every kind until Phase 2 registers them).
*/
export function createConnector(config?: ConnectorConfig): OrchestratorConnector {
const kind = resolveConnectorKind(config);
const factory = registry.get(kind);
if (!factory) {
throw new ConnectorNotImplementedError(kind);
}
return factory(config ?? { kind });
}
/** Test/runtime helper: drop all registrations. */
export function _resetConnectorRegistry(): void {
registry.clear();
}
@@ -0,0 +1,111 @@
/**
* Orchestrator chat connectors (F4).
*
* A connector mediates the chat channel between the fleet **orchestrator** and
* its human operator. Connectors are PEERS — tmux (default), Discord, Matrix,
* and future first-party plugins — selected per fleet, never hardwired. Fleet
* core depends only on the small uniform interface below, so a new connector
* drops in without touching the fleet.
*
* The interface is deliberately minimal: send (orchestrator → human),
* subscribe (human → orchestrator), health (reachable/authed liveness). Thread
* support is optional metadata (`threadId`) so thread-capable connectors
* (Matrix rooms/threads, the future Mosaic Discord plugin) fit without an
* interface change.
*/
/** The connector kinds shipped/known to the framework. */
export type ConnectorKind = 'tmux' | 'discord' | 'matrix';
/** A message the orchestrator sends out to the human operator. */
export interface OutboundMessage {
/** Message body (markdown where the connector supports it). */
text: string;
/** Optional thread/topic id for thread-capable connectors. */
threadId?: string;
/** Optional attachment references (paths or URLs); connector-dependent. */
attachments?: string[];
}
/** A message received from the human operator. */
export interface InboundMessage {
/** Message body. */
text: string;
/** Thread/topic id if the connector carries one. */
threadId?: string;
/** Opaque sender identifier (connector-scoped). */
sender: string;
/** ISO-8601 timestamp the connector assigns/observes. */
ts: string;
}
/** Result of a send — the "ack" half of ack/health. */
export interface SendResult {
/** True when the connector accepted/delivered the message. */
delivered: boolean;
/** Connector-assigned message id when available. */
messageId?: string;
/** Reason when `delivered` is false. */
error?: string;
}
/** Liveness of a connector — the "health" half of ack/health. */
export interface ConnectorHealth {
/** The transport endpoint is reachable. */
reachable: boolean;
/** Credentials are valid / the connector is authenticated. */
authenticated: boolean;
/** ISO-8601 of the last successful interaction, if any. */
lastSeen?: string;
/** Human-readable detail (e.g. failure reason). */
detail?: string;
}
/** Unsubscribe handle returned by `subscribe`. */
export type Unsubscribe = () => void;
/**
* The uniform contract every orchestrator chat connector implements. Small by
* design — send / subscribe / health — so connectors are interchangeable and
* fleet core never branches on connector kind.
*/
export interface OrchestratorConnector {
/** Which kind of connector this is. */
readonly kind: ConnectorKind;
/** Send a message from the orchestrator to the operator. */
send(message: OutboundMessage): Promise<SendResult>;
/** Subscribe to inbound operator messages; returns an unsubscribe handle. */
subscribe(handler: (message: InboundMessage) => void): Unsubscribe;
/** Report connector liveness (reachable + authenticated). */
health(): Promise<ConnectorHealth>;
}
/**
* Connector configuration carried by the roster (the `connector` block).
* Secrets (access tokens, bot tokens) are NEVER stored here — they come from
* the environment (the gateway env-config pattern). Absent config ⇒ tmux.
*/
export interface ConnectorConfig {
kind: ConnectorKind;
/** Matrix connector settings (homeserver + room); token via env. */
matrix?: MatrixConnectorConfig;
/** Discord connector settings (channel); token via env. */
discord?: DiscordConnectorConfig;
}
export interface MatrixConnectorConfig {
/** Local homeserver base URL, e.g. https://matrix.example.internal */
homeserverUrl: string;
/** Full Matrix user id of the orchestrator, e.g. @mos:example.internal */
userId: string;
/** Room id/alias the orchestrator converses in. */
roomId: string;
}
export interface DiscordConnectorConfig {
/** Channel id the orchestrator converses in. */
channelId: string;
}
/** The default connector when a roster declares none (back-compat). */
export const DEFAULT_CONNECTOR_KIND: ConnectorKind = 'tmux';
@@ -0,0 +1,18 @@
/** Locale-independent Unicode code-point ordering for canonical fleet evidence. */
export function compareCodePoints(left: string, right: string): number {
const leftPoints = Array.from(left, (character): number => character.codePointAt(0) ?? 0);
const rightPoints = Array.from(right, (character): number => character.codePointAt(0) ?? 0);
const sharedLength = Math.min(leftPoints.length, rightPoints.length);
for (let index = 0; index < sharedLength; index += 1) {
const leftPoint = leftPoints[index];
const rightPoint = rightPoints[index];
if (leftPoint === undefined || rightPoint === undefined) continue;
if (leftPoint !== rightPoint) return leftPoint < rightPoint ? -1 : 1;
}
return leftPoints.length < rightPoints.length
? -1
: leftPoints.length > rightPoints.length
? 1
: 0;
}
@@ -0,0 +1,90 @@
import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
import {
SHIPPED_FLEET_ARTIFACT_DISPOSITIONS,
validateShippedFleetArtifactDispositions,
} from './example-profile-dispositions.js';
const frameworkFleet = resolve(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'framework',
'fleet',
);
const EXPECTED_DISPOSITIONS = [
'examples/coding.yaml:v1-fixture',
'examples/general.yaml:v1-fixture',
'examples/hybrid.yaml:v1-fixture',
'examples/local-canary.yaml:v1-fixture',
'examples/minimal.yaml:v1-fixture',
'examples/operator-interaction.yaml:v1-fixture',
'examples/research.yaml:v1-fixture',
'profiles/business.yaml:canonical-profile',
'profiles/marketing.yaml:canonical-profile',
'profiles/personal-assistant.yaml:canonical-profile',
'profiles/research.yaml:canonical-profile',
'profiles/software-delivery.yaml:canonical-profile',
'services/operator-interaction.yaml:canonical-service-policy',
];
const declared = SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.map(
({ path, disposition }): string => `${path}:${disposition}`,
);
describe('shipped fleet example/profile/service disposition validation', (): void => {
it('enumerates every inventory artifact with an explicit executable disposition', (): void => {
expect(declared).toEqual(EXPECTED_DISPOSITIONS);
});
it('validates every shipped artifact through its declared v1 or canonical path', async (): Promise<void> => {
const results = await validateShippedFleetArtifactDispositions({ frameworkFleet });
expect(results.map(({ path, disposition }): string => `${path}:${disposition}`)).toEqual(
EXPECTED_DISPOSITIONS,
);
expect(results.filter(({ disposition }): boolean => disposition === 'v1-fixture')).toHaveLength(
7,
);
expect(
results.filter(({ disposition }): boolean => disposition === 'canonical-profile'),
).toHaveLength(5);
expect(
results.find(({ path }): boolean => path === 'services/operator-interaction.yaml'),
).toMatchObject({
disposition: 'canonical-service-policy',
});
});
let temporaryFleet: string | undefined;
afterEach(async (): Promise<void> => {
if (temporaryFleet) await rm(temporaryFleet, { recursive: true, force: true });
temporaryFleet = undefined;
});
it('fails closed when a shipped artifact lacks a declared disposition', async (): Promise<void> => {
temporaryFleet = await mkdtemp(join(tmpdir(), 'mosaic-dispositions-'));
await cp(frameworkFleet, temporaryFleet, { recursive: true });
await writeFile(join(temporaryFleet, 'examples', 'undeclared.yaml'), 'version: 1\n');
await expect(
validateShippedFleetArtifactDispositions({ frameworkFleet: temporaryFleet }),
).rejects.toThrow(/undeclared shipped fleet artifact.*examples\/undeclared.yaml/i);
});
it('rejects a v1 fixture when its explicit version declaration is removed', async (): Promise<void> => {
temporaryFleet = await mkdtemp(join(tmpdir(), 'mosaic-dispositions-'));
await cp(frameworkFleet, temporaryFleet, { recursive: true });
const fixturePath = join(temporaryFleet, 'examples', 'coding.yaml');
const fixture = await readFile(fixturePath, 'utf8');
await writeFile(fixturePath, fixture.replace(/^version: 1\n/, ''));
await expect(
validateShippedFleetArtifactDispositions({ frameworkFleet: temporaryFleet }),
).rejects.toThrow(/Fleet roster version must be 1/);
});
});
@@ -0,0 +1,121 @@
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<readonly ShippedFleetArtifactDisposition[]> {
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<void> {
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<Set<string>> {
const directories = ['examples', 'profiles', 'services'];
const paths = new Set<string>();
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;
}
@@ -0,0 +1,220 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
FleetAgentMutationError,
executeFleetAgentMutation,
planFleetAgentMutation,
type FleetAgentMutationRequest,
} from './fleet-agent-crud.js';
import { parseRosterV2, renderRosterV2Yaml } from './roster-v2.js';
const roster = parseRosterV2(`
version: 2
generation: 7
transport: tmux
tmux:
socket_name: mosaic-fleet
holder_session: _holder
defaults:
working_directory: /srv/mosaic
runtime: pi
runtimes:
pi:
reset_command: /new
agents:
- name: orchestrator
alias: Orchestrator
class: orchestrator
runtime: pi
provider: openai
model: gpt-5.6-sol
reasoning: high
tool_policy: orchestrator
working_directory: /srv/mosaic
persistent_persona: true
reset_between_tasks: false
lifecycle:
enabled: true
desired_state: stopped
launch:
yolo: true
`);
function createRequest(): FleetAgentMutationRequest {
return {
operation: 'create',
expectedGeneration: 7,
agent: {
name: 'coder0',
alias: 'Coder 0',
className: 'code',
runtime: 'pi',
provider: 'openai',
model: 'gpt-5.6-sol',
reasoning: 'high',
toolPolicy: 'code',
workingDirectory: '/srv/mosaic',
persistentPersona: false,
resetBetweenTasks: true,
launch: { yolo: true },
},
};
}
let cleanup: string | undefined;
afterEach(async (): Promise<void> => {
if (cleanup) await rm(cleanup, { recursive: true, force: true });
cleanup = undefined;
});
async function semanticDirs(): Promise<{ rolesDir: string; overrideDir: string }> {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-fleet-crud-'));
const rolesDir = join(cleanup, 'roles');
const overrideDir = join(cleanup, 'roles.local');
await mkdir(rolesDir, { recursive: true });
await mkdir(overrideDir, { recursive: true });
for (const klass of ['orchestrator', 'code']) {
await writeFile(join(rolesDir, `${klass}.md`), `# ${klass}\n\n(\`class: ${klass}\`)\n`);
}
return { rolesDir, overrideDir };
}
describe('fleet agent CRUD plan', (): void => {
it('creates stopped-by-default desired state and a deterministic generation plan', (): void => {
const plan = planFleetAgentMutation(roster, createRequest());
expect(plan).toMatchObject({
operation: 'create',
currentGeneration: 7,
nextGeneration: 8,
changed: true,
agent: { name: 'coder0', lifecycle: { enabled: true, desiredState: 'stopped' } },
});
});
it('rejects a stale generation before proposing effects', (): void => {
expect((): void => {
planFleetAgentMutation(roster, { ...createRequest(), expectedGeneration: 6 });
}).toThrow(FleetAgentMutationError);
});
it('treats an equivalent retry as an idempotent no-op', (): void => {
const created = planFleetAgentMutation(roster, createRequest()).roster;
const retried = planFleetAgentMutation(created, {
...createRequest(),
expectedGeneration: created.generation,
});
expect(retried).toMatchObject({ changed: false, currentGeneration: 8, nextGeneration: 8 });
});
});
describe('fleet agent CRUD execution', (): void => {
it('keeps roster and projections unchanged for dry-run', async (): Promise<void> => {
const dirs = await semanticDirs();
const home = join(cleanup!, 'mosaic');
const rosterPath = join(home, 'fleet', 'roster.yaml');
await mkdir(join(home, 'fleet', 'agents'), { recursive: true, mode: 0o700 });
await writeFile(rosterPath, 'before\n', { mode: 0o600 });
const result = await executeFleetAgentMutation({
roster,
request: createRequest(),
mosaicHome: home,
rosterPath,
agentEnvDir: join(home, 'fleet', 'agents'),
rolesDir: dirs.rolesDir,
overrideDir: dirs.overrideDir,
dryRun: true,
});
expect(result.applied).toBe(false);
expect(await readFile(rosterPath, 'utf8')).toBe('before\n');
await expect(
readFile(join(home, 'fleet', 'agents', 'coder0.env.generated'), 'utf8'),
).rejects.toThrow();
});
it('fails closed when another writer owns the mutation lock', async (): Promise<void> => {
const dirs = await semanticDirs();
const home = join(cleanup!, 'mosaic');
const rosterPath = join(home, 'fleet', 'roster.yaml');
await mkdir(join(home, 'fleet', 'agents'), { recursive: true, mode: 0o700 });
await writeFile(rosterPath, renderRosterV2Yaml(roster), { mode: 0o600 });
await writeFile(`${rosterPath}.mutation.lock`, 'owned\n', { mode: 0o600 });
await expect(
executeFleetAgentMutation({
roster,
request: createRequest(),
mosaicHome: home,
rosterPath,
agentEnvDir: join(home, 'fleet', 'agents'),
rolesDir: dirs.rolesDir,
overrideDir: dirs.overrideDir,
}),
).rejects.toMatchObject({ code: 'concurrent-mutation' });
expect(await readFile(rosterPath, 'utf8')).toBe(renderRosterV2Yaml(roster));
});
it('deletes only the removed agent generated projection when it is stale or absent', async (): Promise<void> => {
const dirs = await semanticDirs();
const home = join(cleanup!, 'mosaic');
const rosterPath = join(home, 'fleet', 'roster.yaml');
const agentEnvDir = join(home, 'fleet', 'agents');
const created = planFleetAgentMutation(roster, createRequest()).roster;
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await writeFile(rosterPath, renderRosterV2Yaml(created), { mode: 0o600 });
await writeFile(join(agentEnvDir, 'coder0.env.local'), 'MOSAIC_RUNTIME_BIN=/usr/bin/pi\n', {
mode: 0o600,
});
const result = await executeFleetAgentMutation({
roster: created,
request: { operation: 'delete', expectedGeneration: 8, name: 'coder0' },
mosaicHome: home,
rosterPath,
agentEnvDir,
rolesDir: dirs.rolesDir,
overrideDir: dirs.overrideDir,
});
expect(result).toMatchObject({ applied: true, plan: { nextGeneration: 9 } });
await expect(readFile(join(agentEnvDir, 'coder0.env.generated'), 'utf8')).rejects.toThrow();
expect(await readFile(join(agentEnvDir, 'coder0.env.local'), 'utf8')).toBe(
'MOSAIC_RUNTIME_BIN=/usr/bin/pi\n',
);
});
it('returns redacted projection recovery after the authoritative roster write', async (): Promise<void> => {
const dirs = await semanticDirs();
const home = join(cleanup!, 'mosaic');
const rosterPath = join(home, 'fleet', 'roster.yaml');
await mkdir(join(home, 'fleet', 'agents'), { recursive: true, mode: 0o700 });
await writeFile(rosterPath, renderRosterV2Yaml(roster), { mode: 0o600 });
const result = await executeFleetAgentMutation({
roster,
request: createRequest(),
mosaicHome: home,
rosterPath,
agentEnvDir: join(home, 'fleet', 'agents'),
rolesDir: dirs.rolesDir,
overrideDir: dirs.overrideDir,
projectionApplier: async (): Promise<void> => {
throw new Error('MOSAIC_AGENT_COMMAND=must-not-leak');
},
});
expect(result).toMatchObject({
applied: false,
authoritativeRoster: 'committed',
projections: 'incomplete',
recovery: { code: 'projection-apply-failed', action: 'regenerate-projections-from-roster' },
});
expect(JSON.stringify(result)).not.toContain('must-not-leak');
expect(parseRosterV2(await readFile(rosterPath, 'utf8'), 'yaml').generation).toBe(8);
});
});
@@ -0,0 +1,414 @@
import { open, readFile, unlink } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import {
applyPreparedAgentEnvironmentProjection,
prepareAgentGeneratedProjectionDeletion,
prepareAgentEnvironmentProjection,
type PreparedAgentEnvironmentProjection,
writeManagedFleetRoster,
} from './generated-env-boundary.js';
import {
parseRosterV2,
renderRosterV2Yaml,
validateRosterV2Semantics,
type FleetRosterV2,
type FleetRosterV2Agent,
type FleetRosterV2Launch,
type FleetRosterV2Lifecycle,
type RosterV2ReasoningLevel,
type RosterV2RuntimeName,
} from './roster-v2.js';
export type FleetAgentMutationOperation = 'create' | 'update' | 'delete';
export interface FleetAgentMutationAgent {
readonly name: string;
readonly alias: string;
readonly className: string;
readonly runtime: RosterV2RuntimeName;
readonly provider: string;
readonly model: string;
readonly reasoning: RosterV2ReasoningLevel;
readonly toolPolicy: string;
readonly workingDirectory: string;
readonly persistentPersona: boolean;
readonly resetBetweenTasks: boolean;
readonly launch: FleetRosterV2Launch;
}
export interface FleetAgentMutationRequest {
readonly operation: FleetAgentMutationOperation;
readonly expectedGeneration: number;
readonly name?: string;
readonly agent?: FleetAgentMutationAgent;
readonly persistedStart?: boolean;
}
export interface FleetAgentMutationPlan {
readonly operation: FleetAgentMutationOperation;
readonly currentGeneration: number;
readonly nextGeneration: number;
readonly changed: boolean;
readonly roster: FleetRosterV2;
readonly agent?: FleetRosterV2Agent;
}
export type FleetAgentMutationAuthoritativeRosterState = 'unchanged' | 'committed';
export type FleetAgentMutationProjectionState = 'not-applied' | 'complete' | 'incomplete';
export interface FleetAgentMutationResult {
/** True only when every requested roster and projection write completed. */
readonly applied: boolean;
/** Whether the authoritative roster changed on disk. */
readonly authoritativeRoster: FleetAgentMutationAuthoritativeRosterState;
/** Whether derived projections were skipped, complete, or require recovery. */
readonly projections: FleetAgentMutationProjectionState;
readonly plan: FleetAgentMutationPlan;
readonly recovery?: FleetAgentMutationRecovery;
}
export interface FleetAgentMutationRecovery {
readonly code: 'projection-apply-failed';
readonly rosterPath: string;
readonly action: 'regenerate-projections-from-roster';
}
export interface FleetAgentMutationOptions {
readonly roster: FleetRosterV2;
readonly request: FleetAgentMutationRequest;
readonly mosaicHome: string;
readonly rosterPath: string;
readonly agentEnvDir: string;
readonly rolesDir: string;
readonly overrideDir: string;
readonly dryRun?: boolean;
readonly projectionApplier?: (prepared: PreparedAgentEnvironmentProjection) => Promise<unknown>;
}
export class FleetAgentMutationError extends Error {
constructor(
readonly code:
| 'stale-generation'
| 'agent-conflict'
| 'agent-not-found'
| 'invalid-request'
| 'concurrent-mutation'
| 'projection-apply-failed',
message: string,
) {
super(message);
this.name = FleetAgentMutationError.name;
}
}
/** Plans a generation-guarded desired-state mutation without writing any file. */
export function planFleetAgentMutation(
roster: FleetRosterV2,
request: FleetAgentMutationRequest,
): FleetAgentMutationPlan {
if (request.expectedGeneration !== roster.generation) {
throw new FleetAgentMutationError(
'stale-generation',
`Expected roster generation ${request.expectedGeneration}; current generation is ${roster.generation}.`,
);
}
if (request.operation === 'create') return planCreate(roster, request);
if (request.operation === 'update') return planUpdate(roster, request);
return planDelete(roster, request);
}
/**
* Validates the complete proposed roster and its deterministic projections before
* changing the roster authority. The only post-roster write is a derived projection.
*/
export async function executeFleetAgentMutation(
options: FleetAgentMutationOptions,
): Promise<FleetAgentMutationResult> {
const plan = planFleetAgentMutation(options.roster, options.request);
await validateRosterV2Semantics(plan.roster, {
rolesDir: options.rolesDir,
overrideDir: options.overrideDir,
});
const prepared = await prepareProjections(plan.roster, options.mosaicHome, options.agentEnvDir);
if (plan.operation === 'delete' && plan.agent) {
// Delete validates only the exact generated projection. Local overrides,
// legacy input, and quarantine records are retained operator state.
await prepareAgentGeneratedProjectionDeletion({
mosaicHome: options.mosaicHome,
agentEnvDir: options.agentEnvDir,
agentName: plan.agent.name,
});
}
if (options.dryRun === true || !plan.changed) {
return { applied: false, authoritativeRoster: 'unchanged', projections: 'not-applied', plan };
}
const lockPath = `${options.rosterPath}.mutation.lock`;
const release = await acquireMutationLock(lockPath);
try {
const persisted = parseRosterV2(await readFile(options.rosterPath, 'utf8'), 'yaml');
if (persisted.generation !== options.roster.generation) {
throw new FleetAgentMutationError(
'stale-generation',
`Expected roster generation ${options.roster.generation}; current generation is ${persisted.generation}.`,
);
}
await writeManagedFleetRoster(
options.mosaicHome,
options.rosterPath,
renderRosterV2Yaml(plan.roster),
);
const apply = options.projectionApplier ?? applyPreparedAgentEnvironmentProjection;
try {
for (const projection of prepared) await apply(projection);
if (plan.operation === 'delete' && plan.agent) {
await prepareAgentGeneratedProjectionDeletion({
mosaicHome: options.mosaicHome,
agentEnvDir: options.agentEnvDir,
agentName: plan.agent.name,
});
await removeGeneratedProjection(options.agentEnvDir, plan.agent.name);
}
} catch {
throw new FleetAgentMutationError(
'projection-apply-failed',
`Projection application failed after roster write; regenerate projections from ${options.rosterPath}.`,
);
}
return { applied: true, authoritativeRoster: 'committed', projections: 'complete', plan };
} catch (error: unknown) {
if (error instanceof FleetAgentMutationError && error.code === 'projection-apply-failed') {
return {
applied: false,
authoritativeRoster: 'committed',
projections: 'incomplete',
plan,
recovery: {
code: 'projection-apply-failed',
rosterPath: options.rosterPath,
action: 'regenerate-projections-from-roster',
},
};
}
throw error;
} finally {
await release();
}
}
function planCreate(
roster: FleetRosterV2,
request: FleetAgentMutationRequest,
): FleetAgentMutationPlan {
if (!request.agent)
throw new FleetAgentMutationError('invalid-request', 'Create requires an agent.');
const existing = roster.agents.find(
(agent: FleetRosterV2Agent): boolean => agent.name === request.agent?.name,
);
const created = toRosterAgent(request.agent, request.persistedStart === true);
if (existing) {
if (sameAgent(existing, created)) return unchangedPlan(roster, request.operation, existing);
throw new FleetAgentMutationError('agent-conflict', `Agent "${created.name}" already exists.`);
}
return changedPlan(roster, request.operation, [...roster.agents, created], created);
}
function planUpdate(
roster: FleetRosterV2,
request: FleetAgentMutationRequest,
): FleetAgentMutationPlan {
if (!request.name || !request.agent) {
throw new FleetAgentMutationError('invalid-request', 'Update requires name and agent.');
}
const existing = roster.agents.find(
(agent: FleetRosterV2Agent): boolean => agent.name === request.name,
);
if (!existing)
throw new FleetAgentMutationError(
'agent-not-found',
`Agent "${request.name}" is not in roster.`,
);
if (request.agent.name !== request.name) {
throw new FleetAgentMutationError(
'invalid-request',
'Agent names are immutable during update.',
);
}
const updated = {
...toRosterAgent(request.agent, existing.lifecycle.desiredState === 'running'),
lifecycle: existing.lifecycle,
};
if (sameAgent(existing, updated)) return unchangedPlan(roster, request.operation, existing);
return changedPlan(
roster,
request.operation,
roster.agents.map(
(agent: FleetRosterV2Agent): FleetRosterV2Agent =>
agent.name === request.name ? updated : agent,
),
updated,
);
}
function planDelete(
roster: FleetRosterV2,
request: FleetAgentMutationRequest,
): FleetAgentMutationPlan {
if (!request.name) throw new FleetAgentMutationError('invalid-request', 'Delete requires name.');
const existing = roster.agents.find(
(agent: FleetRosterV2Agent): boolean => agent.name === request.name,
);
if (!existing) return unchangedPlan(roster, request.operation);
return changedPlan(
roster,
request.operation,
roster.agents.filter((agent: FleetRosterV2Agent): boolean => agent.name !== request.name),
existing,
);
}
function toRosterAgent(
input: FleetAgentMutationAgent,
persistedStart: boolean,
): FleetRosterV2Agent {
const lifecycle: FleetRosterV2Lifecycle = {
enabled: true,
desiredState: persistedStart ? 'running' : 'stopped',
};
return { ...input, lifecycle };
}
function changedPlan(
roster: FleetRosterV2,
operation: FleetAgentMutationOperation,
agents: readonly FleetRosterV2Agent[],
agent?: FleetRosterV2Agent,
): FleetAgentMutationPlan {
const proposed = { ...roster, generation: roster.generation + 1, agents };
const normalized = parseRosterV2(renderRosterV2Yaml(proposed), 'yaml');
return {
operation,
currentGeneration: roster.generation,
nextGeneration: normalized.generation,
changed: true,
roster: normalized,
...(agent ? { agent } : {}),
};
}
function unchangedPlan(
roster: FleetRosterV2,
operation: FleetAgentMutationOperation,
agent?: FleetRosterV2Agent,
): FleetAgentMutationPlan {
return {
operation,
currentGeneration: roster.generation,
nextGeneration: roster.generation,
changed: false,
roster,
...(agent ? { agent } : {}),
};
}
function sameAgent(left: FleetRosterV2Agent, right: FleetRosterV2Agent): boolean {
return (
left.name === right.name &&
left.alias === right.alias &&
left.className === right.className &&
left.runtime === right.runtime &&
left.provider === right.provider &&
left.model === right.model &&
left.reasoning === right.reasoning &&
left.toolPolicy === right.toolPolicy &&
left.workingDirectory === right.workingDirectory &&
left.persistentPersona === right.persistentPersona &&
left.resetBetweenTasks === right.resetBetweenTasks &&
left.launch.yolo === right.launch.yolo &&
left.lifecycle.enabled === right.lifecycle.enabled &&
left.lifecycle.desiredState === right.lifecycle.desiredState
);
}
async function prepareProjections(
roster: FleetRosterV2,
mosaicHome: string,
agentEnvDir: string,
): Promise<readonly PreparedAgentEnvironmentProjection[]> {
const projections: PreparedAgentEnvironmentProjection[] = [];
for (const agent of roster.agents) {
projections.push(
await prepareAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: agent.name,
generated: generatedValues(roster, agent),
}),
);
}
return projections;
}
function generatedValues(
roster: FleetRosterV2,
agent: FleetRosterV2Agent,
): Readonly<Record<string, string>> {
return {
MOSAIC_AGENT_NAME: agent.name,
MOSAIC_GIT_IDENTITY: agent.name,
MOSAIC_AGENT_CLASS: agent.className,
MOSAIC_AGENT_RUNTIME: agent.runtime,
MOSAIC_AGENT_MODEL: agent.model,
MOSAIC_AGENT_REASONING: agent.reasoning,
MOSAIC_AGENT_TOOL_POLICY: agent.toolPolicy,
MOSAIC_AGENT_WORKDIR: agent.workingDirectory,
MOSAIC_TMUX_SOCKET: roster.tmux.socketName,
};
}
async function removeGeneratedProjection(agentEnvDir: string, agentName: string): Promise<void> {
try {
await unlink(join(agentEnvDir, `${agentName}.env.generated`));
} catch (error: unknown) {
if (isMissingFile(error)) return;
throw error;
}
}
function isMissingFile(error: unknown): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
}
/**
* Non-blocking acquire of the roster mutation lock: an exclusive `wx` create that
* throws `concurrent-mutation` (never waits) if the lock is already held. The
* on-disk format is an empty private file. CRUD releases in a plain `finally` with
* no result-preservation logic, so a release fault here must not override the
* mutation result — the release swallows unlink failures by design.
*
* The recovery-framed `fleet regen` contends on this exact same lock file, but it
* does so through the hardened, ownership-proving acquirer in the reconciler
* (`acquirePrivateRosterMutationLock`), not this one: whoever wins the `wx` create
* owns the file (the loser always gets `concurrent-mutation`), so the reconciler's
* ownership token is only ever written and read back by the same regen invocation,
* never by this empty-file writer. The two acquirers stay mutually compatible at
* the `wx`-contention layer while regen additionally proves ownership before it
* unlinks — a guarantee CRUD does not need because it releases unconditionally.
*/
async function acquireMutationLock(lockPath: string): Promise<() => Promise<void>> {
try {
const handle = await open(lockPath, 'wx', 0o600);
await handle.close();
} catch {
throw new FleetAgentMutationError(
'concurrent-mutation',
`Another roster mutation is in progress for ${dirname(lockPath)}.`,
);
}
return async (): Promise<void> => {
await unlink(lockPath).catch((): void => {});
};
}
@@ -0,0 +1,939 @@
import { readFile, readdir } from 'node:fs/promises';
import { dirname, extname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { parseRosterV2, validateRosterV2Semantics } from './roster-v2.js';
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
const repositoryRoot = resolve(packageRoot, '..', '..');
const fleetDocs = join(repositoryRoot, 'docs', 'fleet');
const frameworkFleet = join(packageRoot, 'framework', 'fleet');
const REQUIRED_FLEET_PAGES = [
'README.md',
'concepts/desired-vs-observed-state.md',
'concepts/identity-class-runtime.md',
'concepts/role-authority-and-leases.md',
'concepts/generated-env-launch-chain.md',
'reference/roster-v2.schema.json',
'reference/roster-v2-fields.md',
'reference/cli.md',
'reference/role-classes.md',
'reference/lifecycle-transitions.md',
'reference/status-and-drift.md',
'how-to/create-update-delete-agent.md',
'how-to/start-stop-restart.md',
'how-to/configure-tess-interaction.md',
'how-to/configure-ultron-validator.md',
'how-to/customize-roles.md',
'operations/reconcile-and-recover.md',
'operations/env-quarantine.md',
'operations/systemd-tmux-troubleshooting.md',
'operations/backup-restore.md',
'operations/upgrade-assets.md',
'migration/v1-to-v2.md',
'migration/example-profile-disposition.md',
'migration/legacy-class-aliases.md',
] as const;
async function markdownFiles(root: string): Promise<string[]> {
const entries = await readdir(root, { withFileTypes: true });
const paths = await Promise.all(
entries.map(async (entry): Promise<string[]> => {
const path = join(root, entry.name);
if (entry.isDirectory()) return markdownFiles(path);
return extname(entry.name) === '.md' ? [path] : [];
}),
);
return paths.flat().sort();
}
function localMarkdownTargets(source: string): string[] {
const link =
/\[[^\]]*\]\(\s*(?:<([^>]+)>|((?:\\.|[^()\s]|\([^()]*\))+))(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)/g;
return [...source.matchAll(link)]
.map((match): string => match[1] ?? match[2] ?? '')
.filter(
(target): boolean =>
target !== '' &&
!target.startsWith('http://') &&
!target.startsWith('https://') &&
!target.startsWith('mailto:'),
);
}
function markdownHeadingAnchors(source: string): Set<string> {
const anchors = new Set<string>();
let fence: { readonly marker: string; readonly length: number } | undefined;
for (const line of source.split('\n')) {
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
if (fence === undefined && fenceMatch !== null) {
const run = fenceMatch[1] ?? '';
fence = { marker: run[0] ?? '', length: run.length };
continue;
}
if (fence !== undefined) {
const closingRun = line.match(/^\s{0,3}(`{3,}|~{3,})\s*$/)?.[1];
if (
closingRun !== undefined &&
closingRun[0] === fence.marker &&
closingRun.length >= fence.length
) {
fence = undefined;
}
continue;
}
const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/)?.[1];
if (heading === undefined) continue;
const base = heading
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/<[^>]*>/g, '')
.replace(/[`*_~]/g, '')
.toLowerCase()
.trim()
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.replace(/\s+/g, '-');
let anchor = base;
let duplicate = 0;
while (anchors.has(anchor)) {
duplicate += 1;
anchor = `${base}-${duplicate}`;
}
anchors.add(anchor);
}
return anchors;
}
function markdownLinkViolations(
sourcePath: string,
source: string,
documents: Readonly<Record<string, string>>,
): string[] {
const violations: string[] = [];
for (const target of localMarkdownTargets(source)) {
const [encodedPath = '', encodedFragment] = target.split('#', 2);
const targetPath = decodeURIComponent(encodedPath);
const normalizedTarget = resolve('/', dirname(sourcePath), targetPath).slice(1);
const targetSource = documents[normalizedTarget];
if (targetSource === undefined) {
violations.push(`${sourcePath} -> ${target}: missing file`);
continue;
}
if (encodedFragment !== undefined) {
const fragment = decodeURIComponent(encodedFragment);
if (fragment === '' || !markdownHeadingAnchors(targetSource).has(fragment)) {
violations.push(`${sourcePath} -> ${target}: missing heading`);
}
}
}
return violations;
}
type CodeSurfaceCategory = 'ConcreteCommand' | 'Synopsis' | 'DataProfile' | 'InlineLiteral';
interface CodeSurface {
readonly category: CodeSurfaceCategory;
readonly path: string;
readonly line: number;
readonly block?: number;
readonly info?: string;
readonly source: string;
}
interface SurfaceDiagnostic {
readonly path: string;
readonly line: number;
readonly block?: number;
readonly category?: CodeSurfaceCategory;
readonly code: string;
}
interface FenceProfile {
readonly path: string;
readonly block: number;
readonly info: string;
readonly category: Exclude<CodeSurfaceCategory, 'InlineLiteral'>;
readonly recordSchemas?: readonly string[];
}
const FENCE_PROFILES = [
{
path: 'FLEET-LAUNCH.md',
block: 1,
info: 'dotenv',
category: 'DataProfile',
recordSchemas: ['DATA.DOTENV.FLEET_LAUNCH'],
},
{
path: 'TASKS.md',
block: 1,
info: 'text-table',
category: 'DataProfile',
recordSchemas: ['DATA.TEXT_TABLE.FLEET_TASKS'],
},
{
path: 'backlog-conventions.md',
block: 1,
info: 'text-diagram',
category: 'DataProfile',
recordSchemas: ['DATA.TEXT_DIAGRAM.BACKLOG_FLOW'],
},
{
path: 'backlog-conventions.md',
block: 2,
info: 'fleet-command',
category: 'ConcreteCommand',
recordSchemas: [
'CMD.BACKLOG.CREATE.1',
'CMD.BACKLOG.CREATE.2',
'CMD.BACKLOG.CLAIM',
'CMD.BACKLOG.COMPLETE',
'CMD.BACKLOG.LIST_READY',
'CMD.BACKLOG.RECLAIM',
],
},
{
path: 'f4-matrix-connector.md',
block: 1,
info: 'typescript',
category: 'DataProfile',
recordSchemas: ['DATA.TYPESCRIPT.MATRIX_CONNECTOR_TYPE'],
},
{
path: 'f4-matrix-connector.md',
block: 2,
info: 'yaml',
category: 'DataProfile',
recordSchemas: ['DATA.YAML.MATRIX_CONNECTOR_CONFIG'],
},
{
path: 'how-to/configure-tess-interaction.md',
block: 1,
info: 'yaml',
category: 'DataProfile',
recordSchemas: ['DATA.YAML.INTERACTION_AGENT'],
},
{
path: 'how-to/configure-ultron-validator.md',
block: 1,
info: 'yaml',
category: 'DataProfile',
recordSchemas: ['DATA.YAML.VALIDATOR_AGENT'],
},
{
path: 'how-to/create-update-delete-agent.md',
block: 1,
info: 'fleet-synopsis',
category: 'Synopsis',
recordSchemas: ['SYN.AGENT.GET', 'SYN.PLAN.CREATE', 'SYN.PLAN.UPDATE', 'SYN.PLAN.DELETE'],
},
{
path: 'how-to/create-update-delete-agent.md',
block: 2,
info: 'fleet-command',
category: 'ConcreteCommand',
recordSchemas: ['CMD.AGENT.CREATE_JSON'],
},
{
path: 'how-to/create-update-delete-agent.md',
block: 3,
info: 'fleet-synopsis',
category: 'Synopsis',
recordSchemas: ['SYN.AGENT.UPDATE_COMPLETE', 'SYN.AGENT.DELETE'],
},
{
path: 'how-to/create-update-delete-agent.md',
block: 4,
info: 'json',
category: 'DataProfile',
recordSchemas: ['DATA.JSON.PARTIAL_FAILURE'],
},
{
path: 'how-to/customize-roles.md',
block: 1,
info: 'markdown',
category: 'DataProfile',
recordSchemas: ['DATA.MARKDOWN.ROLE_TEMPLATE'],
},
{
path: 'how-to/customize-roles.md',
block: 2,
info: 'markdown',
category: 'DataProfile',
recordSchemas: ['DATA.MARKDOWN.ROLE_EXAMPLE'],
},
{
path: 'how-to/start-stop-restart.md',
block: 1,
info: 'fleet-synopsis',
category: 'Synopsis',
recordSchemas: [
'SYN.APPLY.DRY',
'SYN.APPLY',
'SYN.RECONCILE',
'SYN.START.REQUIRED',
'SYN.STOP.REQUIRED',
'SYN.RESTART.REQUIRED',
'SYN.STATUS.OPTIONAL',
'SYN.VERIFY',
'SYN.DOCTOR',
],
},
{
path: 'migration/example-profile-disposition.md',
block: 1,
info: 'fleet-command',
category: 'ConcreteCommand',
recordSchemas: ['CMD.PNPM.MIGRATION_TEST'],
},
{
path: 'migration/v1-to-v2.md',
block: 1,
info: 'fleet-command',
category: 'ConcreteCommand',
recordSchemas: ['CMD.MIGRATE.PREVIEW'],
},
{
path: 'migration/v1-to-v2.md',
block: 2,
info: 'json',
category: 'DataProfile',
recordSchemas: ['DATA.JSON.LIFECYCLE_OBSERVATIONS'],
},
{
path: 'operations/reconcile-and-recover.md',
block: 1,
info: 'json',
category: 'DataProfile',
recordSchemas: ['DATA.JSON.RECOVERY_RESULT'],
},
{
path: 'reference/agent-mutations.md',
block: 1,
info: 'fleet-synopsis',
category: 'Synopsis',
recordSchemas: [
'SYN.AGENT.GET',
'SYN.PLAN.GENERIC',
'SYN.AGENT.CREATE',
'SYN.AGENT.UPDATE',
'SYN.AGENT.DELETE_DRY',
],
},
{
path: 'reference/agent-mutations.md',
block: 2,
info: 'json',
category: 'DataProfile',
recordSchemas: ['DATA.JSON.MUTATION_RESULT'],
},
{
path: 'reference/cli.md',
block: 1,
info: 'fleet-synopsis',
category: 'Synopsis',
recordSchemas: [
'SYN.APPLY',
'SYN.RECONCILE',
'SYN.START.OPTIONAL',
'SYN.STOP.OPTIONAL',
'SYN.RESTART.OPTIONAL',
'SYN.STATUS.OPTIONAL',
'SYN.VERIFY',
'SYN.DOCTOR',
'SYN.MIGRATE.PREVIEW',
],
},
{
path: 'reference/generated-env-boundary.md',
block: 1,
info: 'dotenv',
category: 'DataProfile',
recordSchemas: ['DATA.DOTENV.GENERATED_ENV'],
},
{
path: 'reference/roster-v2-fields.md',
block: 1,
info: 'yaml',
category: 'DataProfile',
recordSchemas: ['DATA.YAML.ROSTER_FIELDS'],
},
] as const satisfies readonly FenceProfile[];
const COMMAND_RECORDS: Readonly<Record<string, RegExp>> = {
'CMD.BACKLOG.CREATE.1': /^mosaic fleet backlog create --id A1 --title "schema" --priority 5$/,
'CMD.BACKLOG.CREATE.2':
/^mosaic fleet backlog create --id A2 --title "service" --depends-on A1 --priority 9$/,
'CMD.BACKLOG.CLAIM': /^mosaic fleet backlog claim --owner worker-1 --ttl 600 --json$/,
'CMD.BACKLOG.COMPLETE': /^mosaic fleet backlog complete --id A1$/,
'CMD.BACKLOG.LIST_READY': /^mosaic fleet backlog list --ready-only --json$/,
'CMD.BACKLOG.RECLAIM': /^mosaic fleet backlog reclaim --json$/,
'CMD.AGENT.CREATE_JSON':
/^mosaic fleet create --expected-generation [1-9][0-9]* --agent '\{\n(?:[ -~]*\n)*\}'$/,
'CMD.MIGRATE.PREVIEW':
/^mosaic fleet migrate-v1 preview \\\n --source [A-Za-z0-9_./@:+,=-]+ \\\n --decisions [A-Za-z0-9_./@:+,=-]+ \\\n --observations [A-Za-z0-9_./@:+,=-]+$/,
'CMD.PNPM.MIGRATION_TEST':
/^pnpm --filter @mosaicstack\/mosaic test -- v1-v2-migration\.spec\.ts \\\n -t "[A-Za-z0-9 _./@:+,=-]+"$/,
};
const DATA_PROFILE_BODIES: Readonly<Record<string, string>> = {
'DATA.DOTENV.FLEET_LAUNCH':
'MOSAIC_AGENT_NAME=<roster name>\nMOSAIC_GIT_IDENTITY=<roster name>\nMOSAIC_AGENT_CLASS=<roster class>\nMOSAIC_AGENT_RUNTIME=<roster runtime>\nMOSAIC_AGENT_MODEL=<roster model hint>\nMOSAIC_AGENT_REASONING=<roster reasoning>\nMOSAIC_AGENT_TOOL_POLICY=<roster tool policy>\nMOSAIC_AGENT_WORKDIR=<absolute roster work directory>\nMOSAIC_TMUX_SOCKET=<roster socket or empty>',
'DATA.TEXT_TABLE.FLEET_TASKS':
'| W-FLEET | in-progress | Fleet (agent-session execution layer) | Phase 2/5 | docs/fleet/TASKS.md | observability dogfooded on live stub fleet; control plane rides federation (W1) |',
'DATA.TEXT_DIAGRAM.BACKLOG_FLOW':
' create\n \u2502\n \u25bc\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25ba ready \u2500\u2500\u2500\u2500\u2500 claim \u2500\u2500\u2500\u2500\u2500\u25ba claimed \u2500\u2500\u2500\u2500\u2500 complete \u2500\u2500\u2500\u2500\u2500\u25ba done\n \u2502 \u2502 \u2502\n \u2502 block reclaim (TTL expiry or --id)\n \u2502 \u25bc \u2502\n \u2502 blocked \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 (back to ready)\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 (reclaim / re-create can return a card to ready)',
'DATA.TYPESCRIPT.MATRIX_CONNECTOR_TYPE':
"interface OrchestratorConnector {\n readonly kind: 'tmux' | 'discord' | 'matrix';\n send(message: OutboundMessage): Promise<SendResult>; // orchestrator \u2192 human\n subscribe(handler: (m: InboundMessage) => void): Unsubscribe; // human \u2192 orchestrator\n health(): Promise<ConnectorHealth>; // reachable + authenticated\n}",
'DATA.YAML.MATRIX_CONNECTOR_CONFIG':
"connector:\n kind: matrix\n matrix:\n homeserver_url: https://matrix.example.internal\n user_id: '@mos:example.internal'\n room_id: '!abc:example.internal'",
'DATA.YAML.INTERACTION_AGENT':
'name: interaction-example\nalias: Interaction Example\nclass: interaction\ntool_policy: interaction\nlifecycle:\n enabled: true\n desired_state: stopped',
'DATA.YAML.VALIDATOR_AGENT':
'name: validator-example\nalias: Validator Example\nclass: validator\ntool_policy: validator\nlifecycle:\n enabled: true\n desired_state: stopped',
'DATA.JSON.PARTIAL_FAILURE':
'{\n "applied": false,\n "authoritativeRoster": "committed",\n "projections": "incomplete",\n "recovery": {\n "code": "projection-apply-failed",\n "action": "regenerate-projections-from-roster"\n }\n}',
'DATA.MARKDOWN.ROLE_TEMPLATE':
"# Code \u2014 local role definition\n\nThe local code role (`class: code`) follows the operator's repository conventions.",
'DATA.MARKDOWN.ROLE_EXAMPLE':
'# Release notes \u2014 local role definition\n\nThe release-notes role (`class: release-notes`) prepares operator-reviewed release copy.',
'DATA.JSON.LIFECYCLE_OBSERVATIONS':
'{\n "coder0": { "systemd": "inactive", "tmux": "missing" }\n}',
'DATA.JSON.RECOVERY_RESULT':
'{\n "applied": false,\n "authoritativeRoster": "unchanged",\n "projections": "incomplete",\n "lifecycle": "not-applied",\n "recovery": { "code": "projection-apply-failed", "action": "regenerate-projections-from-roster" }\n}',
'DATA.JSON.MUTATION_RESULT':
'{\n "applied": false,\n "authoritativeRoster": "committed",\n "projections": "incomplete",\n "recovery": {\n "code": "projection-apply-failed",\n "action": "regenerate-projections-from-roster"\n }\n}',
'DATA.DOTENV.GENERATED_ENV':
'MOSAIC_AGENT_NAME=<roster name>\nMOSAIC_GIT_IDENTITY=<roster name>\nMOSAIC_AGENT_CLASS=<roster class>\nMOSAIC_AGENT_RUNTIME=<roster runtime>\nMOSAIC_AGENT_MODEL=<roster model hint>\nMOSAIC_AGENT_REASONING=<roster reasoning>\nMOSAIC_AGENT_TOOL_POLICY=<roster tool policy>\nMOSAIC_AGENT_WORKDIR=<absolute roster work directory>\nMOSAIC_TMUX_SOCKET=<roster socket or empty>',
'DATA.YAML.ROSTER_FIELDS':
'version: 2\ngeneration: 1\ntransport: tmux\ntmux:\n socket_name: mosaic-fleet\n holder_session: _holder\ndefaults:\n working_directory: ~/src\n runtime: pi\nruntimes:\n pi:\n reset_command: /new\nagents:\n - name: coder0\n alias: Coder 0\n class: code\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: high\n tool_policy: code\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true',
};
const SYNOPSIS_RECORDS: Readonly<Record<string, string>> = {
'SYN.AGENT.GET': 'mosaic fleet get <name>',
'SYN.PLAN.CREATE': "mosaic fleet plan create --expected-generation <n> --agent '<json>'",
'SYN.PLAN.UPDATE': "mosaic fleet plan update <name> --expected-generation <n> --agent '<json>'",
'SYN.PLAN.DELETE': 'mosaic fleet plan delete <name> --expected-generation <n>',
'SYN.AGENT.UPDATE_COMPLETE':
"mosaic fleet update <name> --expected-generation <n> --agent '<complete JSON agent payload>'",
'SYN.AGENT.DELETE': 'mosaic fleet delete <name> --expected-generation <n>',
'SYN.PLAN.GENERIC':
"mosaic fleet plan <create|update|delete> [<name>] --expected-generation <n> [--agent '<json>'] [--persisted-start]",
'SYN.AGENT.CREATE':
"mosaic fleet create --expected-generation <n> --agent '<json>' [--dry-run] [--persisted-start]",
'SYN.AGENT.UPDATE':
"mosaic fleet update <name> --expected-generation <n> --agent '<json>' [--dry-run]",
'SYN.AGENT.DELETE_DRY': 'mosaic fleet delete <name> --expected-generation <n> [--dry-run]',
'SYN.APPLY.DRY': 'mosaic fleet apply --expected-generation <n> --dry-run',
'SYN.APPLY': 'mosaic fleet apply --expected-generation <n>',
'SYN.RECONCILE': 'mosaic fleet reconcile --expected-generation <n>',
'SYN.START.REQUIRED': 'mosaic fleet start <name> --expected-generation <n>',
'SYN.STOP.REQUIRED': 'mosaic fleet stop <name> --expected-generation <n>',
'SYN.RESTART.REQUIRED': 'mosaic fleet restart <name> --expected-generation <n>',
'SYN.START.OPTIONAL': 'mosaic fleet start [<name>] --expected-generation <n> [--dry-run]',
'SYN.STOP.OPTIONAL': 'mosaic fleet stop [<name>] --expected-generation <n> [--dry-run]',
'SYN.RESTART.OPTIONAL': 'mosaic fleet restart [<name>] --expected-generation <n> [--dry-run]',
'SYN.STATUS.OPTIONAL': 'mosaic fleet status [<name>]',
'SYN.VERIFY': 'mosaic fleet verify',
'SYN.DOCTOR': 'mosaic fleet doctor',
'SYN.MIGRATE.PREVIEW':
'mosaic fleet migrate-v1 preview --source <path> --decisions <path> --observations <path>',
};
function profileFor(path: string, block: number): FenceProfile | undefined {
return FENCE_PROFILES.find(
(profile): boolean => profile.path === path && profile.block === block,
);
}
function publicDiagnostic(
surface: Pick<CodeSurface, 'path' | 'line' | 'block' | 'category'>,
code: string,
): SurfaceDiagnostic {
return {
path: surface.path,
line: surface.line,
...(surface.block === undefined ? {} : { block: surface.block }),
...(surface.category === undefined ? {} : { category: surface.category }),
code,
};
}
function codeSurfaceDiagnostics(path: string, source: string): SurfaceDiagnostic[] {
const diagnostics: SurfaceDiagnostic[] = [];
let inFence = false;
let block = 0;
for (const [index, line] of source.split('\n').entries()) {
const lineNumber = index + 1;
if (inFence) {
if (line === '```') inFence = false;
else if (/^(?:`{3,}|~{3,})/.test(line)) {
diagnostics.push({ path, line: lineNumber, block, code: 'fence-conflict' });
}
continue;
}
if (/^(?: {0,3}(?:(?:> ?)|(?:(?:[-+*]|[0-9]{1,9}[.)]) +)))+(`{3,}|~{3,})/.test(line)) {
diagnostics.push({ path, line: lineNumber, code: 'fence-context' });
continue;
}
if (/^(?: {4,}|\t).*\S/.test(line)) {
diagnostics.push({ path, line: lineNumber, code: 'indented-code' });
continue;
}
const marker = line.match(/^(`{3,}|~{3,})(.*)$/);
if (marker !== null) {
block += 1;
const info = marker[2] ?? '';
if (marker[1] !== '```' || /[`~]/.test(info)) {
diagnostics.push({ path, line: lineNumber, block, code: 'fence-marker' });
continue;
}
if (info === '' || !FENCE_PROFILES.some((profile): boolean => profile.info === info)) {
diagnostics.push({ path, line: lineNumber, block, code: 'fence-info' });
continue;
}
const profile = profileFor(path, block);
if (profile === undefined || profile.info !== info) {
diagnostics.push({ path, line: lineNumber, block, code: 'profile-unknown' });
continue;
}
inFence = true;
continue;
}
if (/`{2,}/.test(line)) {
diagnostics.push({ path, line: lineNumber, code: 'inline-delimiter' });
continue;
}
const withoutClosedInlineSpans = line.replace(/`[^`\n]+`/g, '');
if (withoutClosedInlineSpans.includes('`')) {
diagnostics.push({ path, line: lineNumber, code: 'inline-delimiter' });
continue;
}
if (/<\/?(?:pre|code|script|style|xmp|listing)(?=$|\s|[>/])/i.test(line)) {
diagnostics.push({ path, line: lineNumber, code: 'raw-code-container' });
}
for (const match of line.matchAll(/(?<!`)`([^`\n]+)`(?!`)/g)) {
if (!/^[A-Za-z0-9_./@:+,=-]{1,256}$/.test(match[1] ?? '')) {
diagnostics.push({
path,
line: lineNumber,
category: 'InlineLiteral',
code: 'inline-literal',
});
}
}
}
if (inFence)
diagnostics.push({ path, line: source.split('\n').length, block, code: 'fence-unclosed' });
return diagnostics;
}
function codeSurfaces(path: string, source: string): CodeSurface[] {
const diagnostics = codeSurfaceDiagnostics(path, source);
if (diagnostics.length > 0) return [];
const surfaces: CodeSurface[] = [];
const lines = source.split('\n');
let fence:
| {
readonly block: number;
readonly line: number;
readonly info: string;
readonly body: string[];
}
| undefined;
let block = 0;
for (const [index, line] of lines.entries()) {
const lineNumber = index + 1;
if (fence !== undefined) {
if (line === '```') {
const profile = profileFor(path, fence.block);
if (profile !== undefined && profile.info === fence.info) {
surfaces.push({
category: profile.category,
path,
line: fence.line,
block: fence.block,
info: fence.info,
source: fence.body.join('\n'),
});
}
fence = undefined;
} else fence.body.push(line);
continue;
}
const opener = line.match(/^```([a-z-]+)$/);
if (opener !== null) {
block += 1;
fence = { block, line: lineNumber, info: opener[1] ?? '', body: [] };
continue;
}
for (const match of line.matchAll(/(?<!`)`([^`\n]+)`(?!`)/g)) {
surfaces.push({
category: 'InlineLiteral',
path,
line: lineNumber,
source: match[1] ?? '',
});
}
}
return surfaces;
}
function closedGrammarViolationKinds(surface: CodeSurface): string[] {
const kinds = new Set<string>();
const credentialFormat =
/(?:\bAKIA[0-9A-Z]{16}\b|\bAIza[0-9A-Za-z_-]{35}\b|\bgh[pousr]_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b|\bglpat-[A-Za-z0-9_-]{20,}\b|\bnpm_[A-Za-z0-9]{20,}\b|\bsk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{20,}\b|\bsk-proj-[A-Za-z0-9_-]{20,}\b|\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b|\bxox[baprs]-[A-Za-z0-9-]{10,}\b|\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b|-----BEGIN [A-Z ]*PRIVATE KEY-----|\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:]+:[^\s/@]+@)/;
if (credentialFormat.test(surface.source)) kinds.add('credential-format');
if (/\b(?:Tess|Ultron)\b/.test(surface.source)) kinds.add('identity');
if (surface.category === 'InlineLiteral') {
if (!/^[A-Za-z0-9_./@:+,=-]{1,256}$/.test(surface.source)) kinds.add('inline-literal');
return [...kinds].sort();
}
const profile = profileFor(surface.path, surface.block ?? 0);
if (
profile === undefined ||
profile.info !== surface.info ||
profile.category !== surface.category
) {
kinds.add('profile-unknown');
return [...kinds].sort();
}
if (surface.category === 'DataProfile') {
const schema = profile.recordSchemas?.[0] ?? '';
if (profile.recordSchemas?.length !== 1 || surface.source !== DATA_PROFILE_BODIES[schema]) {
kinds.add('data-profile');
}
return [...kinds].sort();
}
if (/^(?:# |\$ |> )/m.test(surface.source)) kinds.add('comment-or-prompt');
if (/(?:^|\s)[A-Za-z_][A-Za-z0-9_]*\+?=[^\s]*/.test(surface.source)) kinds.add('assignment');
const schemas = profile.recordSchemas ?? [];
if (surface.category === 'Synopsis') {
const records = surface.source.split('\n').filter((record): boolean => record !== '');
if (
records.length !== schemas.length ||
records.some((record, index): boolean => record !== SYNOPSIS_RECORDS[schemas[index] ?? ''])
) {
kinds.add('synopsis-schema');
}
} else if (schemas.length === 1) {
if (!COMMAND_RECORDS[schemas[0] ?? '']?.test(surface.source)) kinds.add('command-schema');
} else {
const records = surface.source.split('\n').filter((record): boolean => record !== '');
if (
records.length !== schemas.length ||
records.some((record, index): boolean => !COMMAND_RECORDS[schemas[index] ?? '']?.test(record))
) {
kinds.add('command-schema');
}
}
return [...kinds].sort();
}
function surfaceDiagnostics(surfaces: readonly CodeSurface[]): SurfaceDiagnostic[] {
return surfaces.flatMap((surface): SurfaceDiagnostic[] =>
closedGrammarViolationKinds(surface).map(
(kind): SurfaceDiagnostic => publicDiagnostic(surface, kind),
),
);
}
const CLOSED_GRAMMAR_REJECTION_FIXTURES = [
'sudo systemctl restart example',
'env -S apt-get install example',
"sh -c 'apt-get --version'",
'su root',
'command -- apt-get install example',
'mosaic fleet verify; reboot',
'# mosaic fleet verify',
'$ mosaic fleet verify',
'FOO=value mosaic fleet verify',
] as const;
describe('closed documentation publication grammar', (): void => {
it('classifies only the four approved code-surface categories', (): void => {
const categories: readonly CodeSurfaceCategory[] = [
'ConcreteCommand',
'Synopsis',
'DataProfile',
'InlineLiteral',
];
expect(new Set(categories)).toEqual(
new Set(['ConcreteCommand', 'Synopsis', 'DataProfile', 'InlineLiteral']),
);
});
it.each(CLOSED_GRAMMAR_REJECTION_FIXTURES)(
'rejects shell-shaped input without parsing shell grammar',
(source): void => {
const surface: CodeSurface = {
category: 'ConcreteCommand',
path: 'migration/v1-to-v2.md',
line: 1,
block: 1,
info: 'fleet-command',
source,
};
expect(closedGrammarViolationKinds(surface)).toContain('command-schema');
},
);
it('rejects DSL comments, prompt prefixes, assignments, and root-prompt ambiguity', (): void => {
for (const source of [
'# mosaic fleet verify',
'$ mosaic fleet verify',
'> mosaic fleet verify',
'ROOT=1 mosaic fleet verify',
]) {
const surface: CodeSurface = {
category: 'Synopsis',
path: 'reference/cli.md',
line: 1,
block: 1,
info: 'fleet-synopsis',
source,
};
expect(closedGrammarViolationKinds(surface)).not.toEqual([]);
}
});
it('rejects unmatched single-backtick delimiters on either side', (): void => {
for (const source of ['`literal', 'literal`', 'text `literal', 'literal` text']) {
expect(codeSurfaceDiagnostics('fixture.md', source)).toContainEqual({
path: 'fixture.md',
line: 1,
code: 'inline-delimiter',
});
}
});
it('counts every invalid column-one fence candidate before later profile selection', (): void => {
for (const firstCandidate of [
'````fleet-synopsis',
'```fleet-synopsis```',
'~~~fleet-synopsis~~~',
]) {
expect(
codeSurfaceDiagnostics(
'reference/cli.md',
`${firstCandidate}\n\`\`\`unknown\n\`\`\`fleet-synopsis\nmosaic fleet verify\n\`\`\``,
),
).toEqual([
{ path: 'reference/cli.md', line: 1, block: 1, code: 'fence-marker' },
{ path: 'reference/cli.md', line: 2, block: 2, code: 'fence-info' },
{ path: 'reference/cli.md', line: 3, block: 3, code: 'profile-unknown' },
{ path: 'reference/cli.md', line: 5, block: 4, code: 'fence-info' },
]);
}
});
it('rejects inline delimiter runs instead of silently omitting them', (): void => {
for (const source of ['``literal``', 'text ```literal```', 'text ``literal`` text']) {
expect(codeSurfaceDiagnostics('fixture.md', source)).toContainEqual({
path: 'fixture.md',
line: 1,
code: 'inline-delimiter',
});
}
});
it('rejects every nonblank line with four leading spaces or a leading tab', (): void => {
for (const source of [
' mosaic fleet verify',
' mosaic fleet verify',
' \tmosaic fleet verify',
'\t mosaic fleet verify',
]) {
expect(codeSurfaceDiagnostics('fixture.md', source)).toEqual([
{ path: 'fixture.md', line: 1, code: 'indented-code' },
]);
}
});
it('rejects raw HTML code-container tag prefixes at every boundary', (): void => {
for (const source of [
'<pre',
'<pre ',
'<pre>',
'<pre/',
'<code',
'<code ',
'<code>',
'<code/',
]) {
expect(codeSurfaceDiagnostics('fixture.md', source)).toEqual([
{ path: 'fixture.md', line: 1, code: 'raw-code-container' },
]);
}
expect(codeSurfaceDiagnostics('fixture.md', '<prelude>prose</prelude>')).toEqual([]);
});
it('rejects nested blockquote and list fence contexts', (): void => {
for (const source of [
'>> ```fleet-command',
'> > ```fleet-command',
'> - ```fleet-command',
'- > ```fleet-command',
]) {
expect(codeSurfaceDiagnostics('fixture.md', source)).toEqual([
{ path: 'fixture.md', line: 1, code: 'fence-context' },
]);
}
});
it('requires optional metavariables to use bracketed angle notation', (): void => {
const accepted: CodeSurface = {
category: 'Synopsis',
path: 'reference/cli.md',
line: 1,
block: 1,
info: 'fleet-synopsis',
source: Object.values(SYNOPSIS_RECORDS)
.filter((record): boolean =>
[
'SYN.APPLY',
'SYN.RECONCILE',
'SYN.START.OPTIONAL',
'SYN.STOP.OPTIONAL',
'SYN.RESTART.OPTIONAL',
'SYN.STATUS.OPTIONAL',
'SYN.VERIFY',
'SYN.DOCTOR',
'SYN.MIGRATE.PREVIEW',
].includes(
Object.entries(SYNOPSIS_RECORDS).find(([, value]): boolean => value === record)?.[0] ??
'',
),
)
.join('\n'),
};
expect(closedGrammarViolationKinds(accepted)).toEqual([]);
expect(
closedGrammarViolationKinds({
...accepted,
source: accepted.source.replace('[<name>]', '[name]'),
}),
).toContain('synopsis-schema');
});
it('emits only location, category, and closed diagnostic codes', (): void => {
const sensitiveFixture = ['sk-ant-api03-', 'a'.repeat(80)].join('');
const diagnostic = surfaceDiagnostics([
{
category: 'InlineLiteral',
path: 'fixture.md',
line: 7,
source: sensitiveFixture,
},
]);
expect(diagnostic).toEqual([
{
path: 'fixture.md',
line: 7,
category: 'InlineLiteral',
code: 'credential-format',
},
]);
expect(JSON.stringify(diagnostic)).not.toContain(sensitiveFixture);
});
});
describe('fleet operator documentation', (): void => {
it('ships every accepted information-architecture page', async (): Promise<void> => {
await expect(
Promise.all(REQUIRED_FLEET_PAGES.map((path) => readFile(join(fleetDocs, path), 'utf8'))),
).resolves.toHaveLength(REQUIRED_FLEET_PAGES.length);
});
it('resolves every local Markdown link and heading fragment in the fleet book and sitemap', async (): Promise<void> => {
const files = [...(await markdownFiles(fleetDocs)), join(repositoryRoot, 'docs', 'SITEMAP.md')];
const documents: Record<string, string> = {};
for (const file of files) {
const relative = file.slice(repositoryRoot.length + 1);
documents[relative] = await readFile(file, 'utf8');
}
const violations: string[] = [];
for (const [sourcePath, source] of Object.entries(documents)) {
for (const target of localMarkdownTargets(source)) {
const encodedPath = target.split('#', 1)[0] ?? '';
const targetPath = resolve(
dirname(join(repositoryRoot, sourcePath)),
decodeURIComponent(encodedPath),
);
const relativeTarget = targetPath.slice(repositoryRoot.length + 1);
if (documents[relativeTarget] === undefined) {
try {
documents[relativeTarget] = await readFile(targetPath, 'utf8');
} catch {
// The deterministic validator below records the missing target without exposing content.
}
}
}
violations.push(...markdownLinkViolations(sourcePath, source, documents));
}
expect(violations).toEqual([]);
});
it('validates the canonical documentation example through the production compiler and resolver', async (): Promise<void> => {
const source = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
const roster = parseRosterV2(source, 'yaml');
const validated = await validateRosterV2Semantics(roster, {
rolesDir: join(frameworkFleet, 'roles'),
overrideDir: join(fleetDocs, 'examples', 'roles.local'),
});
expect(validated.generation).toBe(1);
expect(validated.agents.map((agent) => agent.canonicalClass)).toEqual([
'code',
'interaction',
'validator',
]);
});
it('keeps every rendered fleet code surface in one closed category without exposing sensitive values', async (): Promise<void> => {
const diagnostics: SurfaceDiagnostic[] = [];
const surfaces: CodeSurface[] = [];
for (const file of await markdownFiles(fleetDocs)) {
const source = await readFile(file, 'utf8');
const relative = file.slice(fleetDocs.length + 1);
diagnostics.push(...codeSurfaceDiagnostics(relative, source));
surfaces.push(...codeSurfaces(relative, source));
}
diagnostics.push(...surfaceDiagnostics(surfaces));
expect(diagnostics).toEqual([]);
expect(
surfaces.filter((surface): boolean => surface.category === 'ConcreteCommand'),
).toHaveLength(4);
expect(surfaces.filter((surface): boolean => surface.category === 'Synopsis')).toHaveLength(5);
expect(surfaces.filter((surface): boolean => surface.category === 'DataProfile')).toHaveLength(
15,
);
expect(
surfaces.filter((surface): boolean => surface.category === 'InlineLiteral'),
).toHaveLength(882);
expect(surfaces).toHaveLength(906);
const rosterSource = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
const auxiliary: CodeSurface = {
category: 'DataProfile',
path: 'examples/roster-v2.yaml',
line: 1,
source: rosterSource,
};
expect(
closedGrammarViolationKinds(auxiliary).filter((kind): boolean => kind !== 'profile-unknown'),
).toEqual([]);
});
});
@@ -0,0 +1,495 @@
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Command } from 'commander';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { registerFleetCommand, type CommandResult, type CommandRunner } from '../commands/fleet.js';
import {
executeFleetReconcile,
type FleetReconcileCommandResult,
type FleetReconcileDeps,
type FleetReconcileResult,
} from './fleet-reconciler.js';
import {
parseRosterV2,
renderRosterV2Yaml,
type FleetRosterV2,
type FleetRosterV2Agent,
} from './roster-v2.js';
import { FleetTmuxRuntimeTransport } from './tmux-runtime-transport.js';
const holderIdentity = '11111111-1111-4111-8111-111111111111';
const stoppedAgent: FleetRosterV2Agent = {
name: 'coder0',
alias: 'Coder 0',
className: 'code',
runtime: 'pi',
provider: 'openai',
model: 'gpt-5.6-sol',
reasoning: 'high',
toolPolicy: 'code',
workingDirectory: '/srv/mosaic',
persistentPersona: false,
resetBetweenTasks: true,
lifecycle: { enabled: true, desiredState: 'stopped' },
launch: { yolo: true },
};
const baseRoster: FleetRosterV2 = {
version: 2,
generation: 7,
transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' },
defaults: { workingDirectory: '/srv/mosaic', runtime: 'pi' },
runtimes: { pi: { resetCommand: '/new' } },
agents: [stoppedAgent],
};
interface InjectedLifecycleFailure {
readonly action: 'start' | 'stop' | 'restart';
readonly service: string;
readonly diagnostic: string;
}
class FakeLifecycleHost {
readonly calls: string[][] = [];
readonly sessions = new Set<string>();
readonly activeServices = new Set<string>();
private failure: InjectedLifecycleFailure | undefined;
constructor(readonly roster: FleetRosterV2) {
this.sessions.add(roster.tmux.holderSession);
}
injectFailure(failure: InjectedLifecycleFailure): void {
this.failure = failure;
}
readonly run = async (
command: string,
args: readonly string[],
): Promise<FleetReconcileCommandResult> => {
this.calls.push([command, ...args]);
if (command === 'tmux') return this.runTmux(args);
if (command === 'systemctl') return this.runSystemctl(args);
return { stdout: '', stderr: 'unsupported fake command', exitCode: 127 };
};
private runTmux(args: readonly string[]): FleetReconcileCommandResult {
if (args.includes('list-sessions')) {
return { stdout: `${[...this.sessions].join('\n')}\n`, stderr: '', exitCode: 0 };
}
if (args.includes('has-session')) {
const targetArgument = args[args.indexOf('-t') + 1];
const sessionName = targetArgument?.replace(/^=/, '').split(':')[0];
return {
stdout: '',
stderr: '',
exitCode: sessionName !== undefined && this.sessions.has(sessionName) ? 0 : 1,
};
}
if (args.includes('show-environment')) {
return {
stdout: [
'HOME=/home/mosaic',
`MOSAIC_FLEET_OWNER=${holderIdentity}`,
`MOSAIC_TMUX_HOLDER=${this.roster.tmux.holderSession}`,
`MOSAIC_TMUX_SOCKET=${this.roster.tmux.socketName}`,
'PATH=/usr/bin:/bin',
'PWD=/home/mosaic',
'',
].join('\n'),
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: 'destructive tmux action rejected by fake', exitCode: 125 };
}
private runSystemctl(args: readonly string[]): FleetReconcileCommandResult {
const action = args[1];
const service = args[2];
if (action === 'show' && service !== undefined) {
return {
stdout: `ActiveState=${this.activeServices.has(service) ? 'active' : 'inactive'}\n`,
stderr: '',
exitCode: 0,
};
}
if (
(action === 'start' || action === 'stop' || action === 'restart') &&
service !== undefined
) {
this.applyLifecycleEffect(action, service);
if (this.failure?.action === action && this.failure.service === service) {
const diagnostic = this.failure.diagnostic;
this.failure = undefined;
return { stdout: '', stderr: diagnostic, exitCode: 1 };
}
return { stdout: '', stderr: '', exitCode: 0 };
}
return { stdout: '', stderr: 'unsupported fake systemctl action', exitCode: 125 };
}
private applyLifecycleEffect(action: 'start' | 'stop' | 'restart', service: string): void {
if (service === 'mosaic-tmux-holder.service') return;
const match = /^mosaic-agent@(.+)\.service$/.exec(service);
if (!match) return;
const agentName = match[1];
if (agentName === undefined) return;
if (action === 'stop') {
this.activeServices.delete(service);
this.sessions.delete(agentName);
return;
}
this.activeServices.add(service);
this.sessions.add(agentName);
}
}
const cleanupDirectories: string[] = [];
afterEach(async (): Promise<void> => {
vi.restoreAllMocks();
process.exitCode = undefined;
await Promise.all(
cleanupDirectories.splice(0).map(async (directory: string): Promise<void> => {
await rm(directory, { recursive: true, force: true });
}),
);
});
function reconcileDeps(host: FakeLifecycleHost): FleetReconcileDeps {
return {
runner: host.run,
homeDirectory: '/home/mosaic',
readHolderIdentity: async () => holderIdentity,
validateRoster: async () => undefined,
prepareProjections: async () => [{ agentName: 'coder0' }],
applyProjection: async () => undefined,
readRoster: async () => host.roster,
acquireMutationLock: async () => async () => undefined,
// Hermetic broker observation (#1297 F3): without this, the plan probes
// the REAL host filesystem, so the "stable JSON" fixtures answered true
// on any machine with a live lease broker and false elsewhere. Pointing
// both paths at fixtures that do not exist pins socketPresent:false and
// unitInstalled:false on every host, which is what these fixtures assert.
brokerSocketEnv: {
MOSAIC_LEASE_BROKER_SOCKET: '/nonexistent/mosaic-lease/broker.sock',
XDG_CONFIG_HOME: '/nonexistent/mosaic-config',
XDG_RUNTIME_DIR: '/nonexistent/run',
},
};
}
async function execute(
host: FakeLifecycleHost,
command: 'apply' | 'reconcile' | 'restart' | 'status' | 'stop',
agentName?: string,
): Promise<FleetReconcileResult> {
return executeFleetReconcile({
roster: host.roster,
command,
...(agentName === undefined ? {} : { agentName }),
...(command === 'status' ? {} : { expectedGeneration: host.roster.generation }),
deps: reconcileDeps(host),
});
}
async function fixtureHome(roster: FleetRosterV2): Promise<string> {
const home = await mkdtemp(join(tmpdir(), 'mosaic-reconciler-acceptance-'));
cleanupDirectories.push(home);
const fleetDirectory = join(home, 'fleet');
await mkdir(fleetDirectory, { mode: 0o700 });
await chmod(home, 0o700);
await chmod(fleetDirectory, 0o700);
await writeFile(join(fleetDirectory, 'roster.yaml'), renderRosterV2Yaml(roster), { mode: 0o600 });
return home;
}
async function fixtureLegacyRoster(): Promise<string> {
const home = await mkdtemp(join(tmpdir(), 'mosaic-reconciler-acceptance-v1-'));
cleanupDirectories.push(home);
const fleetDirectory = join(home, 'fleet');
await mkdir(fleetDirectory, { mode: 0o700 });
await chmod(home, 0o700);
await chmod(fleetDirectory, 0o700);
const rosterPath = join(fleetDirectory, 'roster.yaml');
await writeFile(
rosterPath,
[
'version: 1',
'transport: tmux',
'tmux:',
' holder_session: _holder',
'agents:',
' - name: coder0',
' runtime: pi',
' class: code',
'',
].join('\n'),
{ mode: 0o600 },
);
return rosterPath;
}
function cliProgram(home: string, host: FakeLifecycleHost): Command {
const program = new Command();
program.exitOverride();
registerFleetCommand(program, {
mosaicHome: home,
runner: async (command: string, args: string[]): Promise<CommandResult> =>
host.run(command, args),
reconcileDeps: reconcileDeps(host),
});
return program;
}
function captureJson(): string[] {
const output: string[] = [];
vi.spyOn(console, 'log').mockImplementation((line: string): void => {
output.push(line);
});
return output;
}
describe('FCM-M3-002 reconciler lifecycle acceptance', (): void => {
it('observes named-socket drift through canonical roster-v2 parsing without runtime mutation', async (): Promise<void> => {
const roster: FleetRosterV2 = {
...baseRoster,
agents: [
stoppedAgent,
{
...stoppedAgent,
name: 'reviewer0',
alias: 'Reviewer 0',
lifecycle: { enabled: true, desiredState: 'running' },
},
{
...stoppedAgent,
name: 'validator0',
alias: 'Validator 0',
lifecycle: { enabled: false, desiredState: 'stopped' },
},
],
};
const home = await fixtureHome(roster);
const host = new FakeLifecycleHost(roster);
host.sessions.add('coder0');
host.sessions.add('validator0');
host.sessions.add('coder0-shadow');
const output = captureJson();
await cliProgram(home, host).parseAsync(['node', 'mosaic', 'fleet', 'status']);
expect(output).toHaveLength(1);
expect(JSON.parse(output[0] ?? '{}')).toMatchObject({
plan: {
agents: [
{ name: 'coder0', drift: ['unexpected-session'] },
{ name: 'reviewer0', drift: ['missing-session'] },
{ name: 'validator0', drift: ['unexpected-session', 'disabled-running'] },
],
unmanagedSessions: ['coder0-shadow'],
},
});
expect(host.calls[0]).toEqual([
'tmux',
'-L',
'mosaic-fleet',
'list-sessions',
'-F',
'#{session_name}',
]);
expect(
host.calls.every(
(call: string[]): boolean =>
call[0] !== 'tmux' || (call[1] === '-L' && call[2] === 'mosaic-fleet'),
),
).toBe(true);
expect(
host.calls.every((call: string[]): boolean => call[0] !== 'systemctl' || call[2] === 'show'),
).toBe(true);
expect(process.exitCode).toBe(0);
});
it('rejects a missing canonical roster-v2 tmux socket through parseRosterV2', (): void => {
const source = renderRosterV2Yaml(baseRoster).replace(/^ socket_name:.*\n/m, '');
expect(() => parseRosterV2(source, 'yaml')).toThrow(
'Roster v2 tmux socket_name is required and must be a string.',
);
});
it('accepts an explicit empty canonical roster-v2 socket as the literal default server', (): void => {
const source = renderRosterV2Yaml(baseRoster).replace(
/^ socket_name:.*$/m,
' socket_name: ""',
);
expect(parseRosterV2(source, 'yaml').tmux.socketName).toBe('');
});
it('omits -L for the literal default tmux server at the runtime transport boundary', async (): Promise<void> => {
const rosterPath = await fixtureLegacyRoster();
const runner = vi.fn<CommandRunner>(
async (): Promise<CommandResult> => ({
stdout: '111 pi 0 0 0 0\n',
stderr: '',
exitCode: 0,
}),
);
const transport = new FleetTmuxRuntimeTransport({
mosaicHome: '/unused',
rosterPath,
runner,
});
await expect(transport.verifySession('coder0')).resolves.toEqual({
id: 'coder0',
runtimeId: 'pi',
socketName: '',
});
expect(runner).toHaveBeenCalledTimes(1);
expect(runner).toHaveBeenCalledWith('tmux', [
'list-panes',
'-t',
'=coder0:0.0',
'-F',
'#{pane_pid} #{pane_current_command} #{pane_dead} #{pane_activity} #{window_activity} #{session_activity}',
]);
expect(runner.mock.calls[0]?.[1]).not.toContain('-L');
});
it('classifies unmanaged near-collisions and stops only the exact roster-owned service', async (): Promise<void> => {
const host = new FakeLifecycleHost(baseRoster);
host.sessions.add('coder0');
host.sessions.add('coder0-shadow');
host.sessions.add('unmanaged');
host.activeServices.add('[email protected]');
host.activeServices.add('[email protected]');
const observed = await execute(host, 'status');
const stopped = await execute(host, 'stop', 'coder0');
expect(observed.plan.unmanagedSessions).toEqual(['coder0-shadow', 'unmanaged']);
expect(stopped).toMatchObject({ applied: true, lifecycle: 'complete' });
expect(host.activeServices.has('[email protected]')).toBe(false);
expect(host.activeServices.has('[email protected]')).toBe(true);
expect(host.sessions.has('coder0-shadow')).toBe(true);
expect(host.sessions.has('unmanaged')).toBe(true);
expect(host.calls).toContainEqual([
'systemctl',
'--user',
'stop',
'[email protected]',
]);
expect(
host.calls.some(
(call: string[]): boolean =>
call.includes('kill-session') ||
call.includes('coder0-shadow.service') ||
call.includes('unmanaged.service'),
),
).toBe(false);
});
it('preserves persisted stopped state through apply, reconcile, restart failure, and recovery reconcile', async (): Promise<void> => {
const host = new FakeLifecycleHost(baseRoster);
host.sessions.add('coder0');
host.activeServices.add('[email protected]');
const applied = await execute(host, 'apply');
const reconciled = await execute(host, 'reconcile');
host.injectFailure({
action: 'restart',
service: '[email protected]',
diagnostic: 'crash after effect: TOKEN=acceptance-secret',
});
const partialRestart = await execute(host, 'restart', 'coder0');
expect(applied).toMatchObject({ applied: true, lifecycle: 'complete' });
expect(reconciled).toMatchObject({ applied: true, lifecycle: 'complete' });
expect(host.roster.agents[0]?.lifecycle.desiredState).toBe('stopped');
expect(partialRestart).toMatchObject({
applied: false,
authoritativeRoster: 'unchanged',
projections: 'not-applied',
lifecycle: 'incomplete',
recovery: {
code: 'lifecycle-apply-failed',
action: 'rerun-after-inspecting-owned-resources',
},
});
expect(host.activeServices.has('[email protected]')).toBe(true);
const recovered = await execute(host, 'reconcile');
expect(recovered).toMatchObject({ applied: true, lifecycle: 'complete' });
expect(host.roster.agents[0]?.lifecycle.desiredState).toBe('stopped');
expect(host.activeServices.has('[email protected]')).toBe(false);
expect(host.sessions.has('coder0')).toBe(false);
const destructiveCalls = host.calls.filter(
(call: string[]): boolean =>
call[0] === 'systemctl' && ['start', 'stop', 'restart'].includes(call[2] ?? ''),
);
expect(
destructiveCalls.every(
(call: string[]): boolean => call[3] === '[email protected]',
),
).toBe(true);
expect(destructiveCalls.some((call: string[]): boolean => call[2] === 'start')).toBe(false);
});
it('emits stable non-zero redacted JSON for a partial lifecycle effect', async (): Promise<void> => {
const home = await fixtureHome(baseRoster);
const host = new FakeLifecycleHost(baseRoster);
host.injectFailure({
action: 'restart',
service: '[email protected]',
diagnostic: 'simulated runner stderr with PASSWORD=acceptance-secret',
});
const output = captureJson();
await cliProgram(home, host).parseAsync([
'node',
'mosaic',
'fleet',
'restart',
'coder0',
'--expected-generation',
'7',
]);
const line = output.at(-1) ?? '';
expect(output).toHaveLength(1);
expect(JSON.parse(line)).toEqual({
applied: false,
authoritativeRoster: 'unchanged',
projections: 'not-applied',
lifecycle: 'incomplete',
plan: {
generation: 7,
holder: 'owned',
broker: { unitInstalled: false, socketPresent: false },
agents: [
{
name: 'coder0',
desiredState: 'stopped',
enabled: true,
systemd: 'inactive',
tmux: 'missing',
drift: [],
},
],
unmanagedSessions: [],
},
recovery: {
code: 'lifecycle-apply-failed',
action: 'rerun-after-inspecting-owned-resources',
},
});
expect(process.exitCode).toBe(1);
expect(line).not.toContain('PASSWORD');
expect(line).not.toContain('acceptance-secret');
expect(line).not.toContain('simulated runner stderr');
});
});
@@ -0,0 +1,705 @@
import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createServer } from 'node:net';
import { afterEach, describe, expect, it } from 'vitest';
import {
acquirePrivateReconcileLock,
FleetReconcileError,
executeFleetReconcile,
type FleetReconcileCommand,
type FleetReconcileDeps,
} from './fleet-reconciler.js';
import type { FleetRosterV2 } from './roster-v2.js';
let cleanup: string | undefined;
afterEach(async (): Promise<void> => {
if (cleanup) await rm(cleanup, { recursive: true, force: true });
cleanup = undefined;
});
async function lockHome(): Promise<string> {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-reconcile-lock-'));
const fleet = join(cleanup, 'fleet');
await mkdir(fleet, { mode: 0o700 });
await chmod(cleanup, 0o700);
await chmod(fleet, 0o700);
return cleanup;
}
const roster: FleetRosterV2 = {
version: 2,
generation: 7,
transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' },
defaults: { workingDirectory: '/srv/mosaic', runtime: 'pi' },
runtimes: { pi: { resetCommand: '/new' } },
agents: [
{
name: 'coder0',
alias: 'Coder 0',
className: 'code',
runtime: 'pi',
provider: 'openai',
model: 'gpt-5.6-sol',
reasoning: 'high',
toolPolicy: 'code',
workingDirectory: '/srv/mosaic',
persistentPersona: false,
resetBetweenTasks: true,
lifecycle: { enabled: true, desiredState: 'stopped' },
launch: { yolo: true },
},
],
};
function deps(overrides: Partial<FleetReconcileDeps> = {}): FleetReconcileDeps {
return {
runner: async (command, args) => {
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\ncoder0\n', stderr: '', exitCode: 0 };
}
if (command === 'tmux' && args.includes('show-environment')) {
return {
stdout:
'HOME=/home/mosaic\nMOSAIC_FLEET_OWNER=11111111-1111-4111-8111-111111111111\nMOSAIC_TMUX_HOLDER=_holder\nMOSAIC_TMUX_SOCKET=mosaic-fleet\nPATH=/usr/bin:/bin\nPWD=/home/mosaic\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: '', exitCode: 0 };
},
homeDirectory: '/home/mosaic',
readHolderIdentity: async () => '11111111-1111-4111-8111-111111111111',
validateRoster: async () => undefined,
prepareProjections: async () => [{ agentName: 'coder0' }],
applyProjection: async () => undefined,
readRoster: async () => roster,
acquireMutationLock: async () => async () => undefined,
...overrides,
};
}
async function run(command: FleetReconcileCommand, overrides: Partial<FleetReconcileDeps> = {}) {
return executeFleetReconcile({
roster,
command,
...(command === 'status' || command === 'verify' || command === 'doctor'
? {}
: { expectedGeneration: 7 }),
deps: deps({ readRoster: async () => roster, ...overrides }),
});
}
describe('fleet roster-owned reconciler', (): void => {
// ── #1292: broker as first-class plan member + broker-first start ordering ──
it('reports broker unit and socket state in the plan (socket is the signal, not unit state)', async (): Promise<void> => {
const result = await run('status', {
statPath: async () => true,
checkBrokerSocket: async () => true,
});
expect(result.plan.broker).toEqual({ unitInstalled: true, socketPresent: true });
});
it('reports a dead broker as socketPresent=false even when the unit is installed (enabled-but-dead is the #1292 shape)', async (): Promise<void> => {
const result = await run('status', {
statPath: async () => true,
checkBrokerSocket: async () => false,
});
expect(result.plan.broker).toEqual({ unitInstalled: true, socketPresent: false });
});
it('probes the REAL filesystem when no seam is injected — live socket and unit report healthy, absent paths report absent (#1297 F3)', async (): Promise<void> => {
const dir = await mkdtemp(join(tmpdir(), 'mosaic-broker-probe-'));
cleanup = dir;
const configHome = join(dir, 'config');
const unitDir = join(configHome, 'systemd', 'user');
await mkdir(unitDir, { recursive: true });
await writeFile(join(unitDir, 'mosaic-lease-broker.service'), '[Unit]\n');
const sockPath = join(dir, 'broker.sock');
const server = createServer();
await new Promise<void>((resolve) => {
server.listen(sockPath, resolve);
});
try {
const result = await run('status', {
brokerSocketEnv: {
MOSAIC_LEASE_BROKER_SOCKET: sockPath,
XDG_CONFIG_HOME: configHome,
XDG_RUNTIME_DIR: dir,
},
});
expect(result.plan.broker).toEqual({ unitInstalled: true, socketPresent: true });
// Absent paths through the SAME seam-less path answer false — this is
// the half the old default got right; healthy is the half it got wrong.
const absent = await run('status', {
brokerSocketEnv: {
MOSAIC_LEASE_BROKER_SOCKET: join(dir, 'gone.sock'),
XDG_CONFIG_HOME: join(dir, 'gone-config'),
},
});
expect(absent.plan.broker).toEqual({ unitInstalled: false, socketPresent: false });
} finally {
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});
it('command start refuses with a named error when the broker socket does not appear after enable+start (#1297 F3)', async (): Promise<void> => {
const calls: string[][] = [];
await expect(
run('start', {
checkBrokerSocket: async () => false,
runner: async (command, args) => {
calls.push([command, ...args]);
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\ncoder0\n', stderr: '', exitCode: 0 };
}
if (command === 'tmux' && args.includes('show-environment')) {
return {
stdout:
'HOME=/home/mosaic\nMOSAIC_FLEET_OWNER=11111111-1111-4111-8111-111111111111\nMOSAIC_TMUX_HOLDER=_holder\nMOSAIC_TMUX_SOCKET=mosaic-fleet\nPATH=/usr/bin:/bin\nPWD=/home/mosaic\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: '', exitCode: 0 };
},
}),
).rejects.toThrow(/broker-absent/);
// Refused: broker enable+start attempted, no holder/agent unit touched.
const agentStarts = calls.filter(
(c) => c.join(' ') === 'systemctl --user start [email protected]',
);
expect(agentStarts).toHaveLength(0);
});
it('command start enables and starts the broker BEFORE the holder and any agent unit', async (): Promise<void> => {
const calls: string[][] = [];
const result = await run('start', {
// Deterministic broker presence: without the seam this test answers the
// HOST's broker state (passes on a machine with a live broker, refuses
// on CI), not the ordering property it exists for (#1297 follow-up).
checkBrokerSocket: async () => true,
runner: async (command, args) => {
calls.push([command, ...args]);
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\ncoder0\n', stderr: '', exitCode: 0 };
}
if (command === 'tmux' && args.includes('show-environment')) {
return {
stdout:
'HOME=/home/mosaic\nMOSAIC_FLEET_OWNER=11111111-1111-4111-8111-111111111111\nMOSAIC_TMUX_HOLDER=_holder\nMOSAIC_TMUX_SOCKET=mosaic-fleet\nPATH=/usr/bin:/bin\nPWD=/home/mosaic\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: '', exitCode: 0 };
},
});
expect(result.lifecycle).toBe('complete');
const brokerEnable = calls.findIndex(
(c) => c.join(' ') === 'systemctl --user enable mosaic-lease-broker.service',
);
const brokerStart = calls.findIndex(
(c) => c.join(' ') === 'systemctl --user start mosaic-lease-broker.service',
);
const holderStart = calls.findIndex(
(c) => c.join(' ') === 'systemctl --user start mosaic-tmux-holder.service',
);
const agentStart = calls.findIndex(
(c) => c.join(' ') === 'systemctl --user start [email protected]',
);
expect(brokerEnable).toBeGreaterThanOrEqual(0);
expect(brokerStart).toBeGreaterThan(brokerEnable);
// Holder start may be absent (holder 'owned' in this fixture); if present it must follow the broker.
if (holderStart >= 0) expect(holderStart).toBeGreaterThan(brokerStart);
expect(agentStart).toBeGreaterThan(brokerStart);
});
it('apply with a running desired agent also enables and starts the broker first', async (): Promise<void> => {
const calls: string[][] = [];
const runningRoster: FleetRosterV2 = {
...roster,
agents: roster.agents.map((agent) =>
agent.name === 'coder0'
? { ...agent, lifecycle: { enabled: true, desiredState: 'running' as const } }
: agent,
),
};
const result = await executeFleetReconcile({
roster: runningRoster,
command: 'apply',
expectedGeneration: 7,
deps: deps({
readRoster: async () => runningRoster,
// Deterministic broker presence (see start-ordering test note).
checkBrokerSocket: async () => true,
runner: async (command, args) => {
calls.push([command, ...args]);
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\n', stderr: '', exitCode: 0 };
}
if (command === 'tmux' && args.includes('show-environment')) {
return {
stdout:
'HOME=/home/mosaic\nMOSAIC_FLEET_OWNER=11111111-1111-4111-8111-111111111111\nMOSAIC_TMUX_HOLDER=_holder\nMOSAIC_TMUX_SOCKET=mosaic-fleet\nPATH=/usr/bin:/bin\nPWD=/home/mosaic\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: '', exitCode: 0 };
},
}),
});
expect(result.applied).toBe(true);
const brokerStart = calls.findIndex(
(c) => c.join(' ') === 'systemctl --user start mosaic-lease-broker.service',
);
const agentStart = calls.findIndex(
(c) => c.join(' ') === 'systemctl --user start [email protected]',
);
expect(brokerStart).toBeGreaterThanOrEqual(0);
expect(agentStart).toBeGreaterThan(brokerStart);
});
it('fails closed on a symlinked fleet ancestor without touching its target', async (): Promise<void> => {
const home = await lockHome();
const fleet = join(home, 'fleet');
const attacker = await mkdtemp(join(tmpdir(), 'mosaic-reconcile-attacker-'));
await writeFile(join(attacker, 'sentinel'), 'unchanged\n', { mode: 0o600 });
try {
await rm(fleet, { recursive: true });
await symlink(attacker, fleet, 'dir');
await expect(acquirePrivateReconcileLock(home)()).rejects.toMatchObject({
code: 'unsafe-managed-path',
});
expect(await readFile(join(attacker, 'sentinel'), 'utf8')).toBe('unchanged\n');
} finally {
await rm(attacker, { recursive: true, force: true });
}
});
it('rejects unsafe ancestors, symlink leaves, EEXIST, and non-EEXIST lock creation failures', async (): Promise<void> => {
const home = await lockHome();
const fleet = join(home, 'fleet');
const lockPath = join(fleet, 'roster.yaml.reconcile.lock');
await chmod(fleet, 0o770);
await expect(acquirePrivateReconcileLock(home)()).rejects.toMatchObject({
code: 'unsafe-managed-path',
});
await chmod(fleet, 0o700);
const target = join(home, 'target');
await writeFile(target, 'target\n', { mode: 0o600 });
await symlink(target, lockPath);
await expect(acquirePrivateReconcileLock(home)()).rejects.toMatchObject({
code: 'unsafe-lock',
});
await rm(lockPath);
await writeFile(lockPath, 'other\n', { mode: 0o600 });
await expect(acquirePrivateReconcileLock(home)()).rejects.toMatchObject({
code: 'concurrent-mutation',
});
await rm(lockPath);
const ioFailure = Object.assign(new Error('injected I/O failure'), { code: 'EIO' });
await expect(
acquirePrivateReconcileLock(home, async () => Promise.reject(ioFailure))(),
).rejects.toMatchObject({ code: 'lock-io-failed' });
});
it('does not unlink a replacement lock and normally releases its own lock', async (): Promise<void> => {
const home = await lockHome();
const lockPath = join(home, 'fleet', 'roster.yaml.reconcile.lock');
const release = await acquirePrivateReconcileLock(home)();
await rm(lockPath);
await writeFile(lockPath, 'replacement\n', { mode: 0o600 });
await expect(release()).rejects.toMatchObject({ code: 'lock-cleanup-failed' });
expect(await readFile(lockPath, 'utf8')).toBe('replacement\n');
await rm(lockPath);
const normalRelease = await acquirePrivateReconcileLock(home)();
await normalRelease();
await expect(readFile(lockPath, 'utf8')).rejects.toThrow();
});
it('requires a generation before any mutating preflight or effect', async (): Promise<void> => {
let effects = 0;
await expect(
executeFleetReconcile({
roster,
command: 'apply',
deps: deps({
validateRoster: async () => {
effects += 1;
},
}),
}),
).rejects.toMatchObject({ code: 'missing-generation' });
expect(effects).toBe(0);
});
it('fences mutation against the canonical roster reread under lock', async (): Promise<void> => {
let effects = 0;
await expect(
executeFleetReconcile({
roster,
command: 'apply',
expectedGeneration: 7,
deps: deps({
readRoster: async () => ({ ...roster, generation: 8 }),
runner: async () => {
effects += 1;
return { stdout: '', stderr: '', exitCode: 0 };
},
applyProjection: async () => {
effects += 1;
},
}),
}),
).rejects.toMatchObject({ code: 'stale-generation' });
expect(effects).toBe(0);
});
it('rejects stale generation before projection or lifecycle effects', async (): Promise<void> => {
let effects = 0;
await expect(
executeFleetReconcile({
roster,
command: 'apply',
expectedGeneration: 6,
deps: deps({
validateRoster: async () => {
effects += 1;
},
}),
}),
).rejects.toMatchObject({ code: 'stale-generation' });
expect(effects).toBe(0);
});
it('denies a concurrent mutation lock without effects and leaves observations lock-free', async (): Promise<void> => {
let effects = 0;
const busy = async (): Promise<() => Promise<void>> => {
throw new FleetReconcileError('concurrent-mutation', 'busy');
};
await expect(
run('apply', {
acquireMutationLock: busy,
applyProjection: async () => {
effects += 1;
},
}),
).rejects.toMatchObject({ code: 'concurrent-mutation' });
expect(effects).toBe(0);
await expect(run('doctor', { acquireMutationLock: busy })).resolves.toMatchObject({
applied: false,
lifecycle: 'not-applied',
});
});
it('always releases the mutation lock after success and partial failure', async (): Promise<void> => {
let releases = 0;
const lock = async (): Promise<() => Promise<void>> => async (): Promise<void> => {
releases += 1;
};
await run('apply', { acquireMutationLock: lock });
await run('apply', {
acquireMutationLock: lock,
applyProjection: async () => {
throw new Error('injected failure');
},
});
expect(releases).toBe(2);
});
it('preserves stopped desired state during apply', async (): Promise<void> => {
const calls: string[][] = [];
const result = await run('apply', {
runner: async (command, args) => {
calls.push([command, ...args]);
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\ncoder0\n', stderr: '', exitCode: 0 };
}
if (command === 'tmux' && args.includes('show-environment')) {
return {
stdout:
'HOME=/home/mosaic\nMOSAIC_FLEET_OWNER=11111111-1111-4111-8111-111111111111\nMOSAIC_TMUX_HOLDER=_holder\nMOSAIC_TMUX_SOCKET=mosaic-fleet\nPATH=/usr/bin:/bin\nPWD=/home/mosaic\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: '', exitCode: 0 };
},
});
expect(result.lifecycle).toBe('complete');
expect(calls).not.toContainEqual([
'systemctl',
'--user',
'start',
'[email protected]',
]);
});
it.each(['plan', 'status', 'doctor', 'verify'] as const)(
'keeps default-server %s observational and free of lifecycle effects',
async (command) => {
const calls: string[][] = [];
const defaultServerRoster: FleetRosterV2 = {
...roster,
tmux: { ...roster.tmux, socketName: '' },
};
await expect(
executeFleetReconcile({
roster: defaultServerRoster,
command,
deps: deps({
runner: async (executable, args) => {
calls.push([executable, ...args]);
if (executable === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\n', stderr: '', exitCode: 0 };
}
if (executable === 'tmux' && args.includes('show-environment')) {
return {
stdout:
'HOME=/home/mosaic\nMOSAIC_FLEET_OWNER=11111111-1111-4111-8111-111111111111\nMOSAIC_TMUX_HOLDER=_holder\nMOSAIC_TMUX_SOCKET=\nPATH=/usr/bin:/bin\nPWD=/home/mosaic\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: 'ActiveState=inactive\n', stderr: '', exitCode: 0 };
},
}),
}),
).resolves.toMatchObject({ applied: false, lifecycle: 'not-applied' });
expect(
calls.some(
([executable, , action]): boolean =>
executable === 'systemctl' &&
(action === 'start' || action === 'stop' || action === 'restart'),
),
).toBe(false);
},
);
it.each(['start', 'stop', 'restart', 'apply', 'reconcile'] as const)(
'fails closed before %s can route fixed named-socket services for a default-server roster',
async (command) => {
const calls: string[][] = [];
let projectionPrepares = 0;
let projectionApplies = 0;
const defaultServerRoster: FleetRosterV2 = {
...roster,
tmux: { ...roster.tmux, socketName: '' },
agents: [
{
...roster.agents[0]!,
lifecycle: {
enabled: true,
desiredState: command === 'start' || command === 'restart' ? 'running' : 'stopped',
},
},
],
};
await expect(
executeFleetReconcile({
roster: defaultServerRoster,
command,
expectedGeneration: 7,
deps: deps({
readRoster: async () => defaultServerRoster,
prepareProjections: async () => {
projectionPrepares += 1;
return [{ agentName: 'coder0' }];
},
applyProjection: async () => {
projectionApplies += 1;
},
runner: async (executable, args) => {
calls.push([executable, ...args]);
return { stdout: '', stderr: '', exitCode: 0 };
},
}),
}),
).rejects.toMatchObject({ code: 'lifecycle-precondition-failed' });
expect(projectionPrepares).toBe(0);
expect(projectionApplies).toBe(0);
expect(calls).toEqual([]);
},
);
it('starts only an explicitly running roster agent with exact systemd targets', async (): Promise<void> => {
const calls: string[][] = [];
const runningRoster: FleetRosterV2 = {
...roster,
agents: [{ ...roster.agents[0]!, lifecycle: { enabled: true, desiredState: 'running' } }],
};
const result = await executeFleetReconcile({
roster: runningRoster,
command: 'apply',
expectedGeneration: 7,
deps: deps({
readRoster: async () => runningRoster,
// Deterministic broker presence (see start-ordering test note).
checkBrokerSocket: async () => true,
runner: async (command, args) => {
calls.push([command, ...args]);
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '', stderr: '', exitCode: 1 };
}
return { stdout: '', stderr: '', exitCode: 0 };
},
}),
});
expect(result.lifecycle).toBe('complete');
expect(calls).toContainEqual(['systemctl', '--user', 'start', 'mosaic-tmux-holder.service']);
expect(calls).toContainEqual(['systemctl', '--user', 'start', '[email protected]']);
});
it('rejects a fake holder with contaminated global state before projection application', async (): Promise<void> => {
let projectionApplied = false;
await expect(
run('apply', {
runner: async (command, args) => {
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\ncoder0\n', stderr: '', exitCode: 0 };
}
if (command === 'tmux' && args.includes('show-environment')) {
return { stdout: 'MOSAIC_FLEET_OWNER=forged\n', stderr: '', exitCode: 0 };
}
return { stdout: '', stderr: '', exitCode: 0 };
},
applyProjection: async () => {
projectionApplied = true;
},
}),
).rejects.toMatchObject({ code: 'ownership-mismatch' });
expect(projectionApplied).toBe(false);
});
it('reports but never adopts unmanaged sessions', async (): Promise<void> => {
await expect(
run('apply', {
runner: async (command, args) => {
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\ncoder0\nother\n', stderr: '', exitCode: 0 };
}
if (command === 'tmux' && args.includes('show-environment')) {
return {
stdout:
'HOME=/home/mosaic\nMOSAIC_FLEET_OWNER=11111111-1111-4111-8111-111111111111\nMOSAIC_TMUX_HOLDER=_holder\nMOSAIC_TMUX_SOCKET=mosaic-fleet\nPATH=/usr/bin:/bin\nPWD=/home/mosaic\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: '', stderr: '', exitCode: 0 };
},
}),
).rejects.toMatchObject({ code: 'unmanaged-session' } satisfies Partial<FleetReconcileError>);
});
it('rejects remote inventory before a local lifecycle command is constructed', async (): Promise<void> => {
const calls: string[][] = [];
const remoteRoster = {
...roster,
agents: [{ ...roster.agents[0]!, remote: { host: 'inventory-only' } }],
} as unknown as FleetRosterV2;
await expect(
executeFleetReconcile({
roster: remoteRoster,
command: 'apply',
expectedGeneration: 7,
deps: deps({
readRoster: async () => remoteRoster,
runner: async (command, args) => {
calls.push([command, ...args]);
return { stdout: '', stderr: '', exitCode: 0 };
},
}),
}),
).rejects.toMatchObject({ code: 'lifecycle-precondition-failed' });
expect(calls).toEqual([]);
});
it('plans without applying projections or lifecycle effects', async (): Promise<void> => {
let applied = false;
const result = await run('plan', {
applyProjection: async () => {
applied = true;
},
});
expect(result.applied).toBe(false);
expect(result.lifecycle).toBe('not-applied');
expect(applied).toBe(false);
});
it('reports a truthful partial result if projection application fails', async (): Promise<void> => {
const result = await run('apply', {
applyProjection: async () => {
throw new Error('injected failure');
},
});
expect(result).toMatchObject({
applied: false,
projections: 'incomplete',
lifecycle: 'not-applied',
recovery: { code: 'projection-apply-failed', action: 'regenerate-projections-from-roster' },
});
});
it('adds cleanup diagnostics without masking projection or lifecycle partial truth', async (): Promise<void> => {
const failingRelease = async (): Promise<never> => {
throw new FleetReconcileError('lock-cleanup-failed', 'injected');
};
const projectionPartial = await run('apply', {
applyProjection: async () => {
throw new Error('injected projection failure');
},
acquireMutationLock: async () => failingRelease,
});
expect(projectionPartial).toMatchObject({
projections: 'incomplete',
lifecycle: 'not-applied',
recovery: { code: 'projection-apply-failed' },
cleanup: { code: 'lock-cleanup-failed', action: 'inspect-lock-before-retry' },
});
const lifecyclePartial = await run('apply', {
runner: async () => ({ stdout: '', stderr: '', exitCode: 1 }),
acquireMutationLock: async () => failingRelease,
});
expect(lifecyclePartial).toMatchObject({
projections: 'complete',
lifecycle: 'incomplete',
recovery: { code: 'lifecycle-apply-failed' },
cleanup: { code: 'lock-cleanup-failed', action: 'inspect-lock-before-retry' },
});
});
it('adds cleanup diagnostics after successful effects without changing effect completion', async (): Promise<void> => {
const result = await run('apply', {
acquireMutationLock: async () => async () => {
throw new FleetReconcileError('lock-cleanup-failed', 'injected');
},
});
expect(result).toMatchObject({
applied: true,
projections: 'complete',
lifecycle: 'complete',
cleanup: { code: 'lock-cleanup-failed', action: 'inspect-lock-before-retry' },
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,644 @@
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;
generation?: 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;
model?: unknown;
reasoning?: unknown;
lifecycle?: { enabled?: unknown; desired_state?: unknown };
launch?: { yolo?: 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 class FleetRosterConfigurationError extends Error {
override name = 'FleetRosterConfigurationError';
}
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 readFleetRosterText(path);
try {
return parseFleetRosterV1(source, path.endsWith('.json') ? 'json' : 'yaml');
} catch (error) {
if (isRosterParserError(error)) throw invalidFleetRosterError(path);
throw error;
}
}
/** Read an operator-owned roster with errors that say how to recover. */
export async function readFleetRosterText(path: string): Promise<string> {
try {
return await readFile(path, 'utf8');
} catch (error) {
if (isNodeErrorCode(error, 'ENOENT')) {
throw new FleetRosterConfigurationError(
`No fleet roster found at ${path}. Run \`mosaic fleet init\` to create one.`,
);
}
throw new FleetRosterConfigurationError(
`Could not read fleet roster at ${path}. Check the file exists and is readable.`,
);
}
}
/** Parse a roster document needed only to select the v1/v2 command path. */
export function parseFleetRosterDocument(source: string, path: string): unknown {
try {
return YAML.parse(source);
} catch (error) {
if (isRosterParserError(error)) throw invalidFleetRosterError(path);
throw error;
}
}
function invalidFleetRosterError(path: string): FleetRosterConfigurationError {
return new FleetRosterConfigurationError(
`Fleet roster at ${path} is invalid. Fix the file or run \`mosaic fleet init --force\`.`,
);
}
function isRosterParserError(error: unknown): boolean {
return (
error instanceof SyntaxError ||
(error instanceof Error && (error.name === 'YAMLParseError' || error.name === 'YAMLWarning'))
);
}
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 {
try {
return normalizeFleetRosterV1Unchecked(raw);
} catch (error) {
if (error instanceof FleetRosterConfigurationError) throw error;
if (error instanceof Error) throw new FleetRosterConfigurationError(error.message);
throw error;
}
}
function normalizeFleetRosterV1Unchecked(raw: RawFleetRoster): FleetRoster {
assertObject(raw, 'Fleet roster');
assertKnownKeys(raw, 'Fleet roster', [
'version',
'transport',
'tmux',
'defaults',
'runtimes',
'agents',
'connector',
// stack#1380 verification unblock: the fleet's own roster-v2 mutation
// tooling writes a `generation` fence on the same v1 body. Tolerated here
// as an opaque non-negative integer; comms semantics are unchanged.
'generation',
]);
if (
raw.generation !== undefined &&
(typeof raw.generation !== 'number' || !Number.isInteger(raw.generation) || raw.generation < 0)
) {
throw new Error('Fleet roster generation must be a non-negative integer.');
}
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',
// stack#1380 verification unblock: roster-v2 default runtime hint.
'runtime',
]);
}
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 && raw.version !== 2) {
throw new Error('Fleet roster version must be 1 or 2.');
}
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',
// stack#1380 verification unblock: roster-v2 envelope fields written by
// the fleet's own mutation tooling. Validated, then opaque to comms.
'model',
'reasoning',
'lifecycle',
'launch',
]);
if (raw.model !== undefined && typeof raw.model !== 'string') {
throw new Error('Fleet roster agent model must be a string.');
}
if (raw.reasoning !== undefined && typeof raw.reasoning !== 'string') {
throw new Error('Fleet roster agent reasoning must be a string.');
}
const lifecycle = raw.lifecycle as { enabled?: unknown; desired_state?: unknown } | undefined;
if (lifecycle !== undefined) {
if (typeof lifecycle !== 'object' || lifecycle === null) {
throw new Error('Fleet roster agent lifecycle must be an object.');
}
const lifecycleKeys = Object.keys(lifecycle);
if (!lifecycleKeys.every((key) => key === 'enabled' || key === 'desired_state')) {
throw new Error('Fleet roster agent lifecycle has unknown field(s).');
}
if (lifecycle.enabled !== undefined && typeof lifecycle.enabled !== 'boolean') {
throw new Error('Fleet roster agent lifecycle.enabled must be a boolean.');
}
if (
lifecycle.desired_state !== undefined &&
(typeof lifecycle.desired_state !== 'string' ||
!['running', 'stopped'].includes(lifecycle.desired_state))
) {
throw new Error('Fleet roster agent lifecycle.desired_state must be running|stopped.');
}
}
const launch = raw.launch as { yolo?: unknown } | undefined;
if (launch !== undefined) {
if (typeof launch !== 'object' || launch === null) {
throw new Error('Fleet roster agent launch must be an object.');
}
const launchKeys = Object.keys(launch);
if (!launchKeys.every((key) => key === 'yolo')) {
throw new Error('Fleet roster agent launch has unknown field(s).');
}
if (launch.yolo !== undefined && typeof launch.yolo !== 'boolean') {
throw new Error('Fleet roster agent launch.yolo must be a boolean.');
}
}
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;
}
@@ -0,0 +1,415 @@
import {
chmod,
mkdir,
mkdtemp,
readFile,
rename,
rm,
stat,
symlink,
writeFile,
} from 'node:fs/promises';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
AgentEnvBoundaryError,
parseAgentEnvironment,
previewAgentEnvironmentProjection,
renderGeneratedAgentEnvironment,
writeAgentEnvironmentProjection,
} from './generated-env-boundary.js';
const generatedValues = {
MOSAIC_AGENT_NAME: 'coder0',
MOSAIC_GIT_IDENTITY: 'coder0',
MOSAIC_AGENT_CLASS: 'code',
MOSAIC_AGENT_RUNTIME: 'pi',
MOSAIC_AGENT_MODEL: 'openai-codex/gpt-5.6-sol',
MOSAIC_AGENT_REASONING: 'high',
MOSAIC_AGENT_TOOL_POLICY: 'code',
MOSAIC_AGENT_WORKDIR: '/srv/mosaic',
MOSAIC_TMUX_SOCKET: 'mosaic-fleet',
};
describe('generated fleet agent environment boundary', (): void => {
let cleanup: string | undefined;
afterEach(async (): Promise<void> => {
if (cleanup) {
await rm(cleanup, { recursive: true, force: true });
cleanup = undefined;
}
});
it('renders a deterministic complete generated projection', (): void => {
expect(renderGeneratedAgentEnvironment(generatedValues)).toBe(
[
'MOSAIC_AGENT_NAME=coder0',
'MOSAIC_GIT_IDENTITY=coder0',
'MOSAIC_AGENT_CLASS=code',
'MOSAIC_AGENT_RUNTIME=pi',
'MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol',
'MOSAIC_AGENT_REASONING=high',
'MOSAIC_AGENT_TOOL_POLICY=code',
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
'MOSAIC_TMUX_SOCKET=mosaic-fleet',
'',
].join('\n'),
);
});
it.each([
['generated-key shadowing', 'MOSAIC_AGENT_RUNTIME=codex\n'],
['unknown key', 'UNRELATED_SETTING=value\n'],
['arbitrary command', 'MOSAIC_AGENT_COMMAND=mosaic yolo codex\n'],
['sensitive key', 'MOSAIC_AGENT_TOKEN=do-not-log-me\n'],
['malformed syntax', 'export MOSAIC_RUNTIME_BIN=/opt/bin\n'],
['duplicate keys', 'MOSAIC_RUNTIME_BIN=/opt/bin\nMOSAIC_RUNTIME_BIN=/other/bin\n'],
])('rejects local %s without echoing values', (_name: string, source: string): void => {
let error: unknown;
try {
parseAgentEnvironment(source, 'local');
} catch (caught: unknown) {
error = caught;
}
expect(error).toBeInstanceOf(AgentEnvBoundaryError);
expect(String(error)).not.toContain('mosaic yolo codex');
expect(String(error)).not.toContain('do-not-log-me');
expect(String(error)).toMatch(/key=.*sha256=/);
});
it.each([
['unsafe-git-identity', 'other/identity'],
['git-identity-mismatch', 'reviewer0'],
])('rejects %s before any launch consumer can use it', (code: string, identity: string): void => {
expect((): void => {
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_GIT_IDENTITY: identity,
});
}).toThrow(
expect.objectContaining({
diagnostic: expect.objectContaining({ code, key: 'MOSAIC_GIT_IDENTITY' }),
}),
);
});
it('rejects unsafe generated paths before any launch consumer can use them', (): void => {
expect((): void => {
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_AGENT_WORKDIR: '../outside',
});
}).toThrow(AgentEnvBoundaryError);
});
it('rejects traversal in home-relative workdirs before expansion', (): void => {
for (const workingDirectory of ['~/../escape', '~/src/../../escape']) {
expect((): void => {
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_AGENT_WORKDIR: workingDirectory,
});
}).toThrow(
expect.objectContaining({
diagnostic: expect.objectContaining({
code: 'unsafe-path',
key: 'MOSAIC_AGENT_WORKDIR',
}),
}),
);
}
});
it('expands home-relative workdirs before preserving absolute-path validation', (): void => {
expect(
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_AGENT_WORKDIR: '~/src',
}),
).toContain(`MOSAIC_AGENT_WORKDIR=${join(homedir(), 'src')}\n`);
for (const workingDirectory of ['relative/path', '../outside']) {
expect((): void => {
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_AGENT_WORKDIR: workingDirectory,
});
}).toThrow(AgentEnvBoundaryError);
}
});
it('previews legacy relocation and quarantine without exposing content or mutating files', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
const legacyPath = join(agentEnvDir, 'coder0.env');
const legacy = 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\nMOSAIC_AGENT_COMMAND=never-print-command\n';
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await writeFile(legacyPath, legacy, { mode: 0o600 });
const preview = await previewAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
});
expect(preview).toMatchObject({
agentName: 'coder0',
generated: 'rebuild',
legacy: 'quarantine',
relocatedKeys: ['MOSAIC_RUNTIME_BIN'],
diagnostics: [
expect.objectContaining({
code: 'unknown-key',
key: 'MOSAIC_AGENT_COMMAND',
sha256: expect.stringMatching(/^[a-f0-9]{64}$/),
}),
],
});
expect(JSON.stringify(preview)).not.toContain('never-print-command');
await expect(readFile(legacyPath, 'utf8')).resolves.toBe(legacy);
await expect(readFile(join(agentEnvDir, 'coder0.env.generated'), 'utf8')).rejects.toThrow();
await expect(readFile(join(agentEnvDir, 'coder0.env.quarantine'), 'utf8')).rejects.toThrow();
});
it('creates missing managed directories privately before writing a projection', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
const result = await writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
});
for (const directory of [mosaicHome, join(mosaicHome, 'fleet'), agentEnvDir]) {
expect((await stat(directory)).mode & 0o777).toBe(0o700);
}
expect((await stat(result.generatedPath)).mode & 0o777).toBe(0o600);
});
it('brain home: accepts and writes projections under MOSAIC_BRAIN_HOME/fleet/agents', async (): Promise<void> => {
const savedBrainHome = process.env['MOSAIC_BRAIN_HOME'];
try {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'config-home');
const brainHome = join(cleanup, 'brain');
const agentEnvDir = join(brainHome, 'fleet', 'agents');
process.env['MOSAIC_BRAIN_HOME'] = brainHome;
const result = await writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
});
// Projection landed in the brain tree, not under the config home.
expect(result.generatedPath).toBe(join(agentEnvDir, 'coder0.env.generated'));
expect((await stat(join(brainHome, 'fleet'))).mode & 0o777).toBe(0o700);
expect((await stat(agentEnvDir)).mode & 0o777).toBe(0o700);
expect((await stat(result.generatedPath)).mode & 0o777).toBe(0o600);
await expect(stat(join(mosaicHome, 'fleet'))).rejects.toThrow();
// A config-home agentEnvDir is now REJECTED while the brain is active —
// the boundary must not silently split state across two trees.
let rejected: unknown;
try {
await writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir: join(mosaicHome, 'fleet', 'agents'),
agentName: 'coder1',
generated: { ...generatedValues, MOSAIC_AGENT_NAME: 'coder1' },
});
} catch (caught: unknown) {
rejected = caught;
}
expect(rejected).toBeInstanceOf(AgentEnvBoundaryError);
} finally {
if (savedBrainHome === undefined) {
delete process.env['MOSAIC_BRAIN_HOME'];
} else {
process.env['MOSAIC_BRAIN_HOME'] = savedBrainHome;
}
}
});
it('regenerates desired keys, relocates safe legacy local data, and quarantines forbidden legacy input', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await writeFile(
join(agentEnvDir, 'coder0.env'),
[
'MOSAIC_AGENT_NAME=stale-name',
'MOSAIC_AGENT_RUNTIME=codex',
'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin',
'MOSAIC_AGENT_COMMAND=mosaic yolo codex --dangerous',
'',
].join('\n'),
{ mode: 0o600 },
);
const result = await writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
});
expect(await readFile(result.generatedPath, 'utf8')).toBe(
renderGeneratedAgentEnvironment(generatedValues),
);
expect(await readFile(result.localPath, 'utf8')).toBe('MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n');
const quarantine = await readFile(result.quarantinePath!, 'utf8');
expect(quarantine).toContain('MOSAIC_AGENT_COMMAND=mosaic yolo codex --dangerous');
await expect(readFile(join(agentEnvDir, 'coder0.env'), 'utf8')).rejects.toThrow();
expect((await stat(result.generatedPath)).mode & 0o077).toBe(0);
expect((await stat(result.localPath)).mode & 0o077).toBe(0);
expect(result.diagnostics).toEqual([expect.objectContaining({ key: 'MOSAIC_AGENT_COMMAND' })]);
expect(JSON.stringify(result.diagnostics)).not.toContain('mosaic yolo codex --dangerous');
});
it('fails closed on an existing local override with unsafe permissions', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
const localPath = join(agentEnvDir, 'coder0.env.local');
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n', { mode: 0o600 });
await chmod(localPath, 0o644);
await expect(
writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
}),
).rejects.toThrow(/permissions/i);
});
it('rejects an existing group/world-accessible projection directory before reading it', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
const localPath = join(agentEnvDir, 'coder0.env.local');
const generatedPath = join(agentEnvDir, 'coder0.env.generated');
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n', { mode: 0o600 });
await chmod(agentEnvDir, 0o777);
await expect(
writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
}),
).rejects.toThrow(/unsafe-permissions/i);
await expect(readFile(localPath, 'utf8')).resolves.toBe('MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n');
await expect(readFile(generatedPath, 'utf8')).rejects.toThrow();
});
it.each([
['MOSAIC_HOME', 'symlink'],
['MOSAIC_HOME/fleet', 'symlink'],
['MOSAIC_HOME/fleet/agents', 'symlink'],
['MOSAIC_HOME', 'group/world-writable'],
['MOSAIC_HOME/fleet', 'group/world-writable'],
['MOSAIC_HOME/fleet/agents', 'group/world-writable'],
])(
'rejects a %s %s managed ancestor before any projection mutation',
async (ancestor: string, hazard: string): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const fleetDir = join(mosaicHome, 'fleet');
const agentEnvDir = join(fleetDir, 'agents');
const generatedPath = join(agentEnvDir, 'coder0.env.generated');
const localPath = join(agentEnvDir, 'coder0.env.local');
const legacyPath = join(agentEnvDir, 'coder0.env');
const quarantinePath = join(agentEnvDir, 'coder0.env.quarantine');
const managedPaths: Record<string, string> = {
MOSAIC_HOME: mosaicHome,
'MOSAIC_HOME/fleet': fleetDir,
'MOSAIC_HOME/fleet/agents': agentEnvDir,
};
const unsafePath = managedPaths[ancestor];
if (unsafePath === undefined) throw new Error(`Unknown managed ancestor: ${ancestor}`);
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await chmod(mosaicHome, 0o700);
await chmod(fleetDir, 0o700);
await chmod(agentEnvDir, 0o700);
await writeFile(generatedPath, 'generated-before\n', { mode: 0o600 });
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/safe/runtime\n', { mode: 0o600 });
await writeFile(legacyPath, 'MOSAIC_AGENT_COMMAND=legacy-command\n', { mode: 0o600 });
if (hazard === 'symlink') {
const targetPath = `${unsafePath}-target`;
await rename(unsafePath, targetPath);
await symlink(targetPath, unsafePath, 'dir');
} else {
await chmod(unsafePath, 0o777);
}
await expect(
writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir,
agentName: 'coder0',
generated: generatedValues,
}),
).rejects.toThrow(/unsafe-(directory|permissions)/i);
await expect(readFile(generatedPath, 'utf8')).resolves.toBe('generated-before\n');
await expect(readFile(localPath, 'utf8')).resolves.toBe('MOSAIC_RUNTIME_BIN=/safe/runtime\n');
await expect(readFile(legacyPath, 'utf8')).resolves.toBe(
'MOSAIC_AGENT_COMMAND=legacy-command\n',
);
await expect(readFile(quarantinePath, 'utf8')).rejects.toThrow();
},
);
it('rejects a symlinked projection directory without mutating its target', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');
const targetDirectory = join(cleanup, 'target');
const linkedDirectory = join(mosaicHome, 'fleet', 'agents');
const legacyPath = join(targetDirectory, 'coder0.env');
const generatedPath = join(targetDirectory, 'coder0.env.generated');
const localPath = join(targetDirectory, 'coder0.env.local');
const quarantinePath = join(targetDirectory, 'coder0.env.quarantine');
await mkdir(join(mosaicHome, 'fleet'), { recursive: true, mode: 0o700 });
await mkdir(targetDirectory, { mode: 0o700 });
await writeFile(legacyPath, 'MOSAIC_AGENT_COMMAND=legacy-command\n', { mode: 0o600 });
await writeFile(generatedPath, 'generated-before\n', { mode: 0o600 });
await writeFile(localPath, 'local-before\n', { mode: 0o600 });
await writeFile(quarantinePath, 'quarantine-before\n', { mode: 0o600 });
await symlink(targetDirectory, linkedDirectory, 'dir');
await expect(
writeAgentEnvironmentProjection({
mosaicHome,
agentEnvDir: linkedDirectory,
agentName: 'coder0',
generated: generatedValues,
}),
).rejects.toThrow(/unsafe-directory/i);
await expect(readFile(legacyPath, 'utf8')).resolves.toBe(
'MOSAIC_AGENT_COMMAND=legacy-command\n',
);
await expect(readFile(generatedPath, 'utf8')).resolves.toBe('generated-before\n');
await expect(readFile(localPath, 'utf8')).resolves.toBe('local-before\n');
await expect(readFile(quarantinePath, 'utf8')).resolves.toBe('quarantine-before\n');
});
});
@@ -0,0 +1,638 @@
import { createHash, randomUUID } from 'node:crypto';
import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fleetAgentEnvDir, resolveBrainHome } from './brain-home.js';
import { compareCodePoints } from './deterministic-order.js';
export type AgentEnvironmentKind = 'generated' | 'local';
export interface AgentEnvironmentDiagnostic {
readonly code: string;
readonly key: string;
readonly sha256: string;
}
export interface AgentEnvironmentProjectionOptions {
readonly mosaicHome: string;
readonly agentEnvDir: string;
readonly agentName: string;
readonly generated: Readonly<Record<string, string>>;
}
export interface AgentGeneratedProjectionDeletionOptions {
readonly mosaicHome: string;
readonly agentEnvDir: string;
readonly agentName: string;
}
export interface AgentEnvironmentProjectionResult {
readonly generatedPath: string;
readonly localPath: string;
readonly quarantinePath?: string;
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
/** Sanitized, non-mutating projection evidence safe for migration output. */
export interface AgentEnvironmentProjectionPreview {
readonly agentName: string;
readonly generated: 'rebuild';
readonly legacy: 'absent' | 'regenerate-only' | 'relocate-local' | 'quarantine';
readonly relocatedKeys: readonly string[];
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
/** A projection fully validated without changing managed files. */
export interface PreparedAgentEnvironmentProjection {
readonly mosaicHome: string;
readonly agentEnvDir: string;
readonly generatedPath: string;
readonly localPath: string;
readonly legacyPath: string;
readonly generated: string;
readonly local: string;
readonly legacy?: string;
readonly legacyRelocatedKeys: readonly string[];
readonly quarantinePath?: string;
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
export class AgentEnvBoundaryError extends Error {
readonly diagnostic: AgentEnvironmentDiagnostic;
constructor(code: string, key: string, value: string) {
const diagnostic: AgentEnvironmentDiagnostic = {
code,
key,
sha256: hashValue(value),
};
super(`Agent environment rejected: code=${code} key=${key} sha256=${diagnostic.sha256}`);
this.name = 'AgentEnvBoundaryError';
this.diagnostic = diagnostic;
}
}
export const GENERATED_AGENT_ENV_KEYS = [
'MOSAIC_AGENT_NAME',
'MOSAIC_GIT_IDENTITY',
'MOSAIC_AGENT_CLASS',
'MOSAIC_AGENT_RUNTIME',
'MOSAIC_AGENT_MODEL',
'MOSAIC_AGENT_REASONING',
'MOSAIC_AGENT_TOOL_POLICY',
'MOSAIC_AGENT_WORKDIR',
'MOSAIC_TMUX_SOCKET',
] as const;
export const LOCAL_AGENT_ENV_KEYS = [
'MOSAIC_RUNTIME_BIN',
'MOSAIC_HEARTBEAT_RUN_DIR',
'MOSAIC_HEARTBEAT_INTERVAL',
'MOSAIC_CLAUDE_JSON',
'CLAUDE_CONFIG_DIR',
] as const;
const GENERATED_KEY_SET = new Set<string>(GENERATED_AGENT_ENV_KEYS);
const LOCAL_KEY_SET = new Set<string>(LOCAL_AGENT_ENV_KEYS);
const SENSITIVE_KEY = /(?:API[_-]?KEY|AUTH|CREDENTIAL|PASSWORD|PRIVATE|SECRET|TOKEN)/i;
const AGENT_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const POLICY_NAME = /^[a-z][a-z0-9-]*$/;
const TMUX_SOCKET = /^[A-Za-z0-9_.-]*$/;
const MODEL = /^[A-Za-z0-9._/:+-]*$/;
/** Runtime launchers supported by the generated environment contract. */
export const GENERATED_AGENT_ENV_SUPPORTED_RUNTIMES = [
'claude',
'codex',
'opencode',
'pi',
] as const;
const SUPPORTED_RUNTIMES = new Set<string>(GENERATED_AGENT_ENV_SUPPORTED_RUNTIMES);
const REASONING = new Set(['', 'low', 'medium', 'high']);
/** Parses a strict data-only generated or local environment file without shell evaluation. */
export function parseAgentEnvironment(
source: string,
kind: AgentEnvironmentKind,
): Readonly<Record<string, string>> {
const values: Record<string, string> = {};
const seen = new Set<string>();
for (const line of source.split('\n')) {
if (line === '') continue;
const match = /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
if (!match) throw new AgentEnvBoundaryError('malformed-line', '(malformed)', line);
const [, key, value] = match;
if (key === undefined || value === undefined) {
throw new AgentEnvBoundaryError('malformed-line', '(malformed)', line);
}
if (seen.has(key)) throw new AgentEnvBoundaryError('duplicate-key', key, value);
seen.add(key);
if (SENSITIVE_KEY.test(key)) throw new AgentEnvBoundaryError('sensitive-key', key, value);
if (containsShellSyntax(value)) throw new AgentEnvBoundaryError('unsafe-value', key, value);
if (kind === 'generated') {
if (!GENERATED_KEY_SET.has(key)) throw new AgentEnvBoundaryError('unknown-key', key, value);
} else {
if (GENERATED_KEY_SET.has(key))
throw new AgentEnvBoundaryError('generated-key-shadow', key, value);
if (!LOCAL_KEY_SET.has(key)) throw new AgentEnvBoundaryError('unknown-key', key, value);
}
values[key] = value;
}
if (kind === 'generated') assertGeneratedValues(values);
else assertLocalValues(values);
return Object.freeze(values);
}
/** Renders the roster-derived generated projection in a stable, complete key order. */
export function renderGeneratedAgentEnvironment(values: Readonly<Record<string, string>>): string {
const normalized = normalizeGeneratedValues(values);
return `${GENERATED_AGENT_ENV_KEYS.map((key): string => `${key}=${normalized[key]}`).join('\n')}\n`;
}
/** Validates all deterministic projection inputs and target state without mutation. */
export async function prepareAgentEnvironmentProjection(
options: AgentEnvironmentProjectionOptions,
): Promise<PreparedAgentEnvironmentProjection> {
if (!AGENT_NAME.test(options.agentName)) {
throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', options.agentName);
}
await validatePrivateProjectionDirectory(options.mosaicHome, options.agentEnvDir);
const generatedPath = join(options.agentEnvDir, `${options.agentName}.env.generated`);
const localPath = join(options.agentEnvDir, `${options.agentName}.env.local`);
const legacyPath = join(options.agentEnvDir, `${options.agentName}.env`);
const generated = renderGeneratedAgentEnvironment(options.generated);
await assertPrivateRegularFileIfPresent(generatedPath);
const currentLocal = await readOptionalPrivateFile(localPath);
const parsedLocal =
currentLocal === undefined ? {} : parseAgentEnvironment(currentLocal, 'local');
const legacy = await readOptionalPrivateFile(legacyPath);
const legacyDisposition =
legacy === undefined ? emptyLegacyDisposition() : classifyLegacyEnvironment(legacy);
for (const [key, value] of Object.entries(legacyDisposition.localValues)) {
if (parsedLocal[key] !== undefined && parsedLocal[key] !== value) {
throw new AgentEnvBoundaryError('legacy-local-conflict', key, value);
}
}
const local = renderLocalAgentEnvironment({ ...legacyDisposition.localValues, ...parsedLocal });
const quarantinePath = legacyDisposition.diagnostics.length
? join(options.agentEnvDir, `${options.agentName}.env.quarantine`)
: undefined;
if (quarantinePath !== undefined) {
const existingQuarantine = await readOptionalPrivateFile(quarantinePath);
if (existingQuarantine !== undefined) {
throw new AgentEnvBoundaryError('quarantine-exists', '(quarantine)', existingQuarantine);
}
}
return {
mosaicHome: options.mosaicHome,
agentEnvDir: options.agentEnvDir,
generatedPath,
localPath,
legacyPath,
generated,
local,
...(legacy === undefined ? {} : { legacy }),
legacyRelocatedKeys: Object.keys(legacyDisposition.localValues).sort(compareCodePoints),
...(quarantinePath === undefined ? {} : { quarantinePath }),
diagnostics: legacyDisposition.diagnostics,
};
}
/** A generated-only projection validated without touching any legacy/local/quarantine file. */
export interface PreparedGeneratedAgentEnvironmentProjection {
readonly mosaicHome: string;
readonly agentEnvDir: string;
readonly generatedPath: string;
readonly generated: string;
}
/**
* Validates ONLY the roster-derived generated projection for a recovery rebuild.
* Unlike {@link prepareAgentEnvironmentProjection}, this never reads, classifies,
* relocates, or quarantines the legacy `.env` / `.env.local` operator surface — it
* exists so `mosaic fleet regen` has no code path that can mutate anything except
* `<name>.env.generated`. The existing generated file, if present, must already be
* a private regular file.
*/
export async function prepareGeneratedAgentEnvironmentProjection(
options: AgentEnvironmentProjectionOptions,
): Promise<PreparedGeneratedAgentEnvironmentProjection> {
if (!AGENT_NAME.test(options.agentName)) {
throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', options.agentName);
}
await validatePrivateProjectionDirectory(options.mosaicHome, options.agentEnvDir);
const generatedPath = join(options.agentEnvDir, `${options.agentName}.env.generated`);
const generated = renderGeneratedAgentEnvironment(options.generated);
await assertPrivateRegularFileIfPresent(generatedPath);
return {
mosaicHome: options.mosaicHome,
agentEnvDir: options.agentEnvDir,
generatedPath,
generated,
};
}
/**
* Applies a generated-only projection: writes ONLY `<name>.env.generated` atomically
* and touches nothing else. It never writes `.env.local`/`.env.quarantine` and never
* unlinks the legacy `.env` — the projection-only recovery guarantee is structural.
*/
export async function applyPreparedGeneratedAgentEnvironmentProjection(
prepared: PreparedGeneratedAgentEnvironmentProjection,
): Promise<string> {
await ensurePrivateProjectionDirectory(prepared.mosaicHome, prepared.agentEnvDir);
await writePrivateAtomically(prepared.generatedPath, prepared.generated);
return prepared.generatedPath;
}
/**
* Validates only the exact generated projection eligible for a roster delete.
* Local overrides, legacy input, and quarantine records are operator-retained and
* intentionally excluded from delete validation and cleanup.
*/
export async function prepareAgentGeneratedProjectionDeletion(
options: AgentGeneratedProjectionDeletionOptions,
): Promise<string> {
if (!AGENT_NAME.test(options.agentName)) {
throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', options.agentName);
}
await validatePrivateProjectionDirectory(options.mosaicHome, options.agentEnvDir);
const generatedPath = join(options.agentEnvDir, `${options.agentName}.env.generated`);
await assertPrivateRegularFileIfPresent(generatedPath);
return generatedPath;
}
export async function previewAgentEnvironmentProjection(
options: AgentEnvironmentProjectionOptions,
): Promise<AgentEnvironmentProjectionPreview> {
const prepared = await prepareAgentEnvironmentProjection(options);
const relocatedKeys = prepared.legacyRelocatedKeys;
const legacy =
prepared.legacy === undefined
? 'absent'
: prepared.diagnostics.length > 0
? 'quarantine'
: relocatedKeys.length > 0
? 'relocate-local'
: 'regenerate-only';
return {
agentName: options.agentName,
generated: 'rebuild',
legacy,
relocatedKeys,
diagnostics: [...prepared.diagnostics].sort((left, right): number =>
compareCodePoints(
`${left.key}:${left.code}:${left.sha256}`,
`${right.key}:${right.code}:${right.sha256}`,
),
),
};
}
/** Applies a previously prepared deterministic projection. */
export async function applyPreparedAgentEnvironmentProjection(
prepared: PreparedAgentEnvironmentProjection,
): Promise<AgentEnvironmentProjectionResult> {
await ensurePrivateProjectionDirectory(prepared.mosaicHome, prepared.agentEnvDir);
await writePrivateAtomically(prepared.generatedPath, prepared.generated);
if (prepared.local !== '') await writePrivateAtomically(prepared.localPath, prepared.local);
if (prepared.quarantinePath !== undefined && prepared.legacy !== undefined) {
await writePrivateAtomically(prepared.quarantinePath, prepared.legacy);
}
if (prepared.legacy !== undefined) await unlink(prepared.legacyPath);
return {
generatedPath: prepared.generatedPath,
localPath: prepared.localPath,
...(prepared.quarantinePath === undefined ? {} : { quarantinePath: prepared.quarantinePath }),
diagnostics: prepared.diagnostics,
};
}
/** Writes deterministic projection files and explicitly removes legacy `.env` authority. */
export async function writeAgentEnvironmentProjection(
options: AgentEnvironmentProjectionOptions,
): Promise<AgentEnvironmentProjectionResult> {
return applyPreparedAgentEnvironmentProjection(await prepareAgentEnvironmentProjection(options));
}
/** Creates the canonical managed roster path with private fresh-chain permissions. */
export async function writeManagedFleetRoster(
mosaicHome: string,
rosterPath: string,
content: string,
): Promise<void> {
const fleetDir = join(mosaicHome, 'fleet');
const expectedRosterPath = join(fleetDir, 'roster.yaml');
if (resolve(rosterPath) !== resolve(expectedRosterPath)) {
throw new AgentEnvBoundaryError('unsafe-file', '(roster)', rosterPath);
}
await ensureManagedDirectory(mosaicHome, false);
await ensureManagedDirectory(fleetDir, false);
await writePrivateAtomically(rosterPath, content);
}
/** Ensures the private install-derived identity used to own a named tmux server. */
export async function ensureFleetHolderIdentity(mosaicHome: string): Promise<string> {
const fleetDir = join(mosaicHome, 'fleet');
const runDir = join(fleetDir, 'run');
const identityPath = join(runDir, 'holder-owner');
await ensureManagedDirectory(mosaicHome, false);
await ensureManagedDirectory(fleetDir, false);
await ensureManagedDirectory(runDir, true);
const existing = await readOptionalPrivateFile(identityPath);
if (existing !== undefined) {
const identity = existing.trim();
if (!/^[a-f0-9-]{36}$/.test(identity)) {
throw new AgentEnvBoundaryError('unsafe-owner-identity', '(holder-owner)', existing);
}
return identity;
}
const identity = randomUUID();
await writePrivateAtomically(identityPath, `${identity}\n`);
return identity;
}
function normalizeGeneratedValues(
values: Readonly<Record<string, string>>,
): Readonly<Record<string, string>> {
const normalized: Record<string, string> = {};
for (const key of GENERATED_AGENT_ENV_KEYS) {
const value = values[key];
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
normalized[key] = key === 'MOSAIC_AGENT_WORKDIR' ? expandHomeDirectory(value) : value;
}
for (const [key, value] of Object.entries(values)) {
if (!GENERATED_KEY_SET.has(key)) throw new AgentEnvBoundaryError('unknown-key', key, value);
}
assertGeneratedValues(normalized);
return Object.freeze(normalized);
}
function expandHomeDirectory(path: string): string {
if (path === '~') return homedir();
if (!path.startsWith('~/') || path.split('/').includes('..')) return path;
return join(homedir(), path.slice(2));
}
function renderLocalAgentEnvironment(values: Readonly<Record<string, string>>): string {
if (Object.keys(values).length === 0) return '';
const parsed = parseAgentEnvironment(
Object.entries(values)
.sort(([left], [right]): number => compareCodePoints(left, right))
.map(([key, value]): string => `${key}=${value}`)
.join('\n'),
'local',
);
return `${Object.entries(parsed)
.sort(([left], [right]): number => compareCodePoints(left, right))
.map(([key, value]): string => `${key}=${value}`)
.join('\n')}\n`;
}
function assertGeneratedValues(values: Readonly<Record<string, string>>): void {
for (const key of GENERATED_AGENT_ENV_KEYS) {
const value = values[key];
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
}
const name = requiredGeneratedValue(values, 'MOSAIC_AGENT_NAME');
const gitIdentity = requiredGeneratedValue(values, 'MOSAIC_GIT_IDENTITY');
const className = requiredGeneratedValue(values, 'MOSAIC_AGENT_CLASS');
const runtime = requiredGeneratedValue(values, 'MOSAIC_AGENT_RUNTIME');
const model = requiredGeneratedValue(values, 'MOSAIC_AGENT_MODEL');
const reasoning = requiredGeneratedValue(values, 'MOSAIC_AGENT_REASONING');
const toolPolicy = requiredGeneratedValue(values, 'MOSAIC_AGENT_TOOL_POLICY');
const workingDirectory = requiredGeneratedValue(values, 'MOSAIC_AGENT_WORKDIR');
const socket = requiredGeneratedValue(values, 'MOSAIC_TMUX_SOCKET');
if (!AGENT_NAME.test(name))
throw new AgentEnvBoundaryError('unsafe-agent-name', 'MOSAIC_AGENT_NAME', name);
if (!AGENT_NAME.test(gitIdentity)) {
throw new AgentEnvBoundaryError('unsafe-git-identity', 'MOSAIC_GIT_IDENTITY', gitIdentity);
}
if (gitIdentity !== name) {
throw new AgentEnvBoundaryError('git-identity-mismatch', 'MOSAIC_GIT_IDENTITY', gitIdentity);
}
if (!POLICY_NAME.test(className)) {
throw new AgentEnvBoundaryError('unsafe-class', 'MOSAIC_AGENT_CLASS', className);
}
if (!SUPPORTED_RUNTIMES.has(runtime)) {
throw new AgentEnvBoundaryError('unsupported-runtime', 'MOSAIC_AGENT_RUNTIME', runtime);
}
if (!MODEL.test(model))
throw new AgentEnvBoundaryError('unsafe-model', 'MOSAIC_AGENT_MODEL', model);
if (!REASONING.has(reasoning)) {
throw new AgentEnvBoundaryError('unsupported-reasoning', 'MOSAIC_AGENT_REASONING', reasoning);
}
if (toolPolicy !== '' && !POLICY_NAME.test(toolPolicy)) {
throw new AgentEnvBoundaryError('unsafe-tool-policy', 'MOSAIC_AGENT_TOOL_POLICY', toolPolicy);
}
if (!isSafeAbsolutePath(workingDirectory)) {
throw new AgentEnvBoundaryError('unsafe-path', 'MOSAIC_AGENT_WORKDIR', workingDirectory);
}
if (!TMUX_SOCKET.test(socket)) {
throw new AgentEnvBoundaryError('unsafe-socket', 'MOSAIC_TMUX_SOCKET', socket);
}
}
function requiredGeneratedValue(values: Readonly<Record<string, string>>, key: string): string {
const value = values[key];
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
return value;
}
function assertLocalValues(values: Readonly<Record<string, string>>): void {
for (const [key, value] of Object.entries(values)) {
if (key === 'MOSAIC_HEARTBEAT_INTERVAL') {
if (!/^[1-9][0-9]*$/.test(value)) {
throw new AgentEnvBoundaryError('invalid-interval', key, value);
}
continue;
}
if (!isSafeAbsolutePath(value)) throw new AgentEnvBoundaryError('unsafe-path', key, value);
}
}
function isSafeAbsolutePath(value: string): boolean {
return (
value.startsWith('/') && !value.split('/').includes('..') && !/[\s"'`$\\;&|<>(){}]/.test(value)
);
}
function containsShellSyntax(value: string): boolean {
return /["'`$\\;&|<>(){}]/.test(value);
}
interface LegacyDisposition {
readonly localValues: Readonly<Record<string, string>>;
readonly diagnostics: readonly AgentEnvironmentDiagnostic[];
}
function emptyLegacyDisposition(): LegacyDisposition {
return { localValues: {}, diagnostics: [] };
}
function classifyLegacyEnvironment(source: string): LegacyDisposition {
const localValues: Record<string, string> = {};
const diagnostics: AgentEnvironmentDiagnostic[] = [];
const seen = new Set<string>();
for (const line of source.split('\n')) {
if (line === '') continue;
const match = /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
if (!match || match[1] === undefined || match[2] === undefined) {
diagnostics.push(diagnostic('malformed-line', '(malformed)', line));
continue;
}
const [, key, value] = match;
if (seen.has(key)) {
diagnostics.push(diagnostic('duplicate-key', key, value));
continue;
}
seen.add(key);
if (GENERATED_KEY_SET.has(key)) continue;
try {
parseAgentEnvironment(`${key}=${value}\n`, 'local');
localValues[key] = value;
} catch (error: unknown) {
if (error instanceof AgentEnvBoundaryError) diagnostics.push(error.diagnostic);
else throw error;
}
}
return { localValues: Object.freeze(localValues), diagnostics: Object.freeze(diagnostics) };
}
function diagnostic(code: string, key: string, value: string): AgentEnvironmentDiagnostic {
return { code, key, sha256: hashValue(value) };
}
function hashValue(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
async function readOptionalPrivateFile(path: string): Promise<string | undefined> {
try {
await assertPrivateRegularFile(path);
return await readFile(path, 'utf8');
} catch (error: unknown) {
if (isMissingFile(error)) return undefined;
throw error;
}
}
function isMissingFile(error: unknown): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
}
async function validatePrivateProjectionDirectory(
mosaicHome: string,
agentEnvDir: string,
): Promise<void> {
// Brain-home split (canon §2): seat envs live under the brain home's
// fleet/agents when a brain is active; roster + templates stay config-home.
const expectedAgentEnvDir = fleetAgentEnvDir(mosaicHome);
if (resolve(agentEnvDir) !== resolve(expectedAgentEnvDir)) {
throw new AgentEnvBoundaryError('unsafe-directory', '(directory)', agentEnvDir);
}
const stateHome = resolveBrainHome(mosaicHome);
const fleetDir = join(stateHome, 'fleet');
await assertManagedDirectoryIfPresent(stateHome, false);
await assertManagedDirectoryIfPresent(fleetDir, false);
await assertManagedDirectoryIfPresent(agentEnvDir, true);
}
async function ensurePrivateProjectionDirectory(
mosaicHome: string,
agentEnvDir: string,
): Promise<void> {
await validatePrivateProjectionDirectory(mosaicHome, agentEnvDir);
const stateHome = resolveBrainHome(mosaicHome);
const fleetDir = join(stateHome, 'fleet');
await ensureManagedDirectory(stateHome, false);
await ensureManagedDirectory(fleetDir, false);
await ensureManagedDirectory(agentEnvDir, true);
}
async function ensureManagedDirectory(path: string, privateDirectory: boolean): Promise<void> {
let created = false;
try {
await lstat(path);
} catch (error: unknown) {
if (!isMissingFile(error)) throw error;
try {
await mkdir(path, { recursive: true, mode: 0o700 });
created = true;
} catch (mkdirError: unknown) {
if (!isAlreadyExists(mkdirError)) throw mkdirError;
}
}
await assertManagedDirectory(path, privateDirectory);
if (created) {
await chmod(path, 0o700);
await assertManagedDirectory(path, privateDirectory);
}
}
function isAlreadyExists(error: unknown): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST';
}
async function assertManagedDirectoryIfPresent(
path: string,
privateDirectory: boolean,
): Promise<void> {
try {
await assertManagedDirectory(path, privateDirectory);
} catch (error: unknown) {
if (isMissingFile(error)) return;
throw error;
}
}
async function assertManagedDirectory(path: string, privateDirectory: boolean): Promise<void> {
const metadata = await lstat(path);
if (!metadata.isDirectory()) {
throw new AgentEnvBoundaryError('unsafe-directory', '(directory)', path);
}
if ((metadata.mode & 0o022) !== 0 || (privateDirectory && (metadata.mode & 0o077) !== 0)) {
throw new AgentEnvBoundaryError('unsafe-permissions', '(directory)', path);
}
}
async function assertPrivateRegularFileIfPresent(path: string): Promise<void> {
try {
await assertPrivateRegularFile(path);
} catch (error: unknown) {
if (isMissingFile(error)) return;
throw error;
}
}
async function assertPrivateRegularFile(path: string): Promise<void> {
const metadata = await lstat(path);
if (!metadata.isFile()) throw new AgentEnvBoundaryError('unsafe-file', '(file)', path);
if ((metadata.mode & 0o077) !== 0) {
throw new AgentEnvBoundaryError('unsafe-permissions', '(file)', path);
}
}
async function writePrivateAtomically(path: string, content: string): Promise<void> {
try {
await assertPrivateRegularFile(path);
} catch (error: unknown) {
if (!isMissingFile(error)) throw error;
}
const temporaryPath = join(dirname(path), `.${randomUUID()}.tmp`);
await writeFile(temporaryPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
await rename(temporaryPath, path);
}
@@ -0,0 +1,110 @@
import { readFile } from 'node:fs/promises';
import YAML from 'yaml';
import type { FleetAgent } from '../commands/fleet.js';
const REQUIRED_RUNTIME = 'pi';
const REQUIRED_MODEL = 'openai/gpt-5.6-sol';
const REQUIRED_REASONING = 'high';
const REQUIRED_TOOL_POLICY = 'operator-interaction';
const AGENT_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
export interface InteractionServiceProfile {
runtime: string;
model: string;
reasoning: string;
toolPolicy: string;
}
export interface EffectiveInteractionPolicy {
agentName: string;
runtime: string;
model: string;
reasoning: string;
toolPolicy: string;
}
export class InteractionServiceProfileError extends Error {
constructor(
readonly code: 'invalid_profile' | 'invalid_request',
message: string,
) {
super(message);
this.name = InteractionServiceProfileError.name;
}
}
export async function readInteractionServiceProfile(
path: string,
overrides: Partial<InteractionServiceProfile> = {},
): Promise<InteractionServiceProfile> {
const parsed: unknown = YAML.parse(await readFile(path, 'utf8'));
if (!isRecord(parsed)) {
throw new InteractionServiceProfileError(
'invalid_profile',
'Service profile must be an object',
);
}
const profile: InteractionServiceProfile = {
runtime: valueOrUndefined(overrides.runtime, parsed['runtime']),
model: valueOrUndefined(overrides.model, parsed['model']),
reasoning: valueOrUndefined(overrides.reasoning, parsed['reasoning']),
toolPolicy: valueOrUndefined(overrides.toolPolicy, parsed['tool_policy']),
};
assertPinnedPolicy(profile);
return profile;
}
export function provisionInteractionService(
profile: InteractionServiceProfile,
input: { agentName: string },
): { rosterAgent: FleetAgent; effectivePolicy: EffectiveInteractionPolicy } {
assertPinnedPolicy(profile);
if (!AGENT_NAME_PATTERN.test(input.agentName)) {
throw new InteractionServiceProfileError(
'invalid_request',
'Agent name must contain only letters, numbers, dots, underscores, or hyphens',
);
}
const effectivePolicy: EffectiveInteractionPolicy = {
agentName: input.agentName,
runtime: profile.runtime,
model: profile.model,
reasoning: profile.reasoning,
toolPolicy: profile.toolPolicy,
};
return {
rosterAgent: {
name: input.agentName,
runtime: profile.runtime,
className: profile.toolPolicy,
modelHint: profile.model,
reasoningLevel: profile.reasoning,
toolPolicy: profile.toolPolicy,
persistentPersona: true,
},
effectivePolicy,
};
}
function assertPinnedPolicy(profile: InteractionServiceProfile): void {
const invalid =
profile.runtime !== REQUIRED_RUNTIME ||
profile.model !== REQUIRED_MODEL ||
profile.reasoning !== REQUIRED_REASONING ||
profile.toolPolicy !== REQUIRED_TOOL_POLICY;
if (invalid) {
throw new InteractionServiceProfileError(
'invalid_profile',
`Service profile must pin ${REQUIRED_RUNTIME}, ${REQUIRED_MODEL}, ${REQUIRED_REASONING}, and ${REQUIRED_TOOL_POLICY}`,
);
}
}
function valueOrUndefined(override: string | undefined, value: unknown): string {
if (override !== undefined) return override;
return typeof value === 'string' ? value : '';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -0,0 +1,182 @@
import { describe, expect, it, vi } from 'vitest';
import type { RuntimeScope } from '@mosaicstack/types';
import {
MatrixNativeRuntimeTransport,
type MatrixFetchLike,
} from './matrix-native-runtime-transport.js';
const scope: RuntimeScope = {
actorId: 'operator-1',
tenantId: 'tenant-a',
channelId: 'cli',
correlationId: 'corr-1',
};
const bindings = [
{
id: 'native-1',
runtimeId: '@native-worker:example.test',
roomId: '!room:example.test',
remoteUserId: '@native-worker:example.test',
state: 'active' as const,
createdAt: '2026-07-13T00:00:00.000Z',
updatedAt: '2026-07-13T00:00:00.000Z',
},
];
function response(
status: number,
body: unknown = {},
): ReturnType<MatrixFetchLike> extends Promise<infer T> ? T : never {
return {
ok: status >= 200 && status < 300,
status,
json: async () => body,
text: async () => JSON.stringify(body),
} as never;
}
function transport(fetchImpl: MatrixFetchLike): MatrixNativeRuntimeTransport {
return new MatrixNativeRuntimeTransport({
homeserverUrl: 'https://matrix.example.test/base',
accessToken: 'test-token',
userId: '@mosaic:example.test',
sessions: bindings,
fetchImpl,
});
}
describe('MatrixNativeRuntimeTransport', (): void => {
it('uses only configured session-to-room bindings and idempotency-derived Matrix transactions', async (): Promise<void> => {
const fetchImpl = vi
.fn<MatrixFetchLike>()
.mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
.mockResolvedValueOnce(response(200, { event_id: '$sent' }));
const matrix = transport(fetchImpl);
await matrix.send('native-1', { content: 'hello', idempotencyKey: 'message-1' }, scope);
expect(fetchImpl).toHaveBeenNthCalledWith(
1,
'https://matrix.example.test/base/_matrix/client/v3/account/whoami',
expect.objectContaining({
headers: expect.objectContaining({ Authorization: 'Bearer test-token' }),
}),
);
const [url, init] = fetchImpl.mock.calls[1]!;
expect(url).toMatch(
/^https:\/\/matrix\.example\.test\/base\/_matrix\/client\/v3\/rooms\/!room%3Aexample\.test\/send\/m\.room\.message\/mosaic-send-[A-Za-z0-9_-]+$/,
);
expect(init?.method).toBe('PUT');
expect(JSON.parse(init?.body ?? '')).toEqual({
msgtype: 'm.text',
body: 'hello',
'mosaic.runtime.v1': {
session_id: 'native-1',
runtime_id: '@native-worker:example.test',
actor_id: 'operator-1',
tenant_id: 'tenant-a',
channel_id: 'cli',
correlation_id: 'corr-1',
idempotency_key: 'message-1',
},
});
});
it('sends a termination only to the bound room with the exact approval reference', async (): Promise<void> => {
const fetchImpl = vi
.fn<MatrixFetchLike>()
.mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
.mockResolvedValueOnce(response(200, { event_id: '$terminated' }));
const matrix = transport(fetchImpl);
await matrix.terminate('native-1', 'approval-1', scope);
const [url, init] = fetchImpl.mock.calls[1]!;
expect(url).toMatch(
/^https:\/\/matrix\.example\.test\/base\/_matrix\/client\/v3\/rooms\/!room%3Aexample\.test\/send\/mosaic\.runtime\.terminate\/mosaic-terminate-[A-Za-z0-9_-]+$/,
);
expect(JSON.parse(init?.body ?? '')).toEqual({
session_id: 'native-1',
runtime_id: '@native-worker:example.test',
actor_id: 'operator-1',
tenant_id: 'tenant-a',
channel_id: 'cli',
correlation_id: 'corr-1',
approval_ref: 'approval-1',
});
});
it('fails closed when Matrix whoami does not match the configured native identity', async (): Promise<void> => {
const fetchImpl = vi
.fn<MatrixFetchLike>()
.mockResolvedValue(response(200, { user_id: '@other:example.test' }));
const matrix = transport(fetchImpl);
await expect(matrix.listSessions(scope)).rejects.toMatchObject({ code: 'forbidden' });
});
it('rejects unbound session IDs without using caller-supplied Matrix room data', async (): Promise<void> => {
const fetchImpl = vi
.fn<MatrixFetchLike>()
.mockResolvedValue(response(200, { user_id: '@mosaic:example.test' }));
const matrix = transport(fetchImpl);
await expect(matrix.verifySession('!attacker-room:example.test', scope)).rejects.toMatchObject({
code: 'not_found',
});
});
it('maps replay-cursor events from only the configured remote identity', async (): Promise<void> => {
const fetchImpl = vi
.fn<MatrixFetchLike>()
.mockResolvedValueOnce(response(200, { user_id: '@mosaic:example.test' }))
.mockResolvedValueOnce(
response(200, {
next_batch: 'next-cursor',
rooms: {
join: {
'!room:example.test': {
timeline: {
events: [
{
type: 'mosaic.runtime.event',
sender: '@intruder:example.test',
content: { 'mosaic.runtime.v1': { type: 'message.delta', content: 'nope' } },
},
{
type: 'mosaic.runtime.event',
sender: '@native-worker:example.test',
origin_server_ts: 1_784_246_400_000,
content: {
'mosaic.runtime.v1': {
session_id: 'native-1',
type: 'message.delta',
content: 'accepted',
},
},
},
],
},
},
},
},
}),
);
const matrix = transport(fetchImpl);
const events = [];
for await (const event of matrix.stream('native-1', 'prior-cursor', scope)) events.push(event);
expect(events).toEqual([
{
type: 'message.delta',
sessionId: 'native-1',
cursor: 'next-cursor',
occurredAt: '2026-07-17T00:00:00.000Z',
content: 'accepted',
},
]);
expect(fetchImpl.mock.calls[1]?.[0]).toContain('since=prior-cursor');
});
});
@@ -0,0 +1,367 @@
import { createHash } from 'node:crypto';
import type {
RuntimeHealth,
RuntimeMessage,
RuntimeScope,
RuntimeSessionState,
RuntimeStreamEvent,
} from '@mosaicstack/types';
export type MatrixRuntimeTransportErrorCode =
| 'forbidden'
| 'invalid_request'
| 'not_found'
| 'unavailable';
export class MatrixRuntimeTransportError extends Error {
constructor(
readonly code: MatrixRuntimeTransportErrorCode,
message: string,
) {
super(message);
this.name = MatrixRuntimeTransportError.name;
}
}
/** Minimal injectable Matrix fetch surface; it avoids coupling this transport to an SDK. */
export interface MatrixFetchLike {
(
url: string,
init?: { method?: string; headers?: Record<string, string>; body?: string },
): Promise<{
ok: boolean;
status: number;
json(): Promise<unknown>;
text(): Promise<string>;
}>;
}
/** A server-configured runtime session binding. Callers never select a Matrix room. */
export interface MatrixNativeRuntimeSessionBinding {
id: string;
runtimeId: string;
roomId: string;
remoteUserId: string;
parentSessionId?: string;
state: RuntimeSessionState;
createdAt: string;
updatedAt: string;
}
export interface MatrixNativeRuntimeTransportOptions {
homeserverUrl: string;
accessToken: string;
/** Matrix user authenticated by the service token. */
userId: string;
sessions: readonly MatrixNativeRuntimeSessionBinding[];
fetchImpl?: MatrixFetchLike;
now?: () => Date;
}
interface MatrixSyncEvent {
type?: string;
sender?: string;
origin_server_ts?: number;
content?: Record<string, unknown>;
}
interface MatrixSyncResponse {
next_batch?: string;
rooms?: { join?: Record<string, { timeline?: { events?: MatrixSyncEvent[] } }> };
}
/**
* Concrete Matrix CS-API transport for the native provider. It authenticates
* the configured sender before each operation and resolves rooms exclusively
* from configured bindings, preventing client-selected room/identity routing.
*/
export class MatrixNativeRuntimeTransport {
private readonly baseUrl: string;
private readonly fetchImpl: MatrixFetchLike;
private readonly now: () => Date;
private readonly bindings: ReadonlyMap<string, MatrixNativeRuntimeSessionBinding>;
constructor(private readonly options: MatrixNativeRuntimeTransportOptions) {
this.baseUrl = validatedHomeserverUrl(options.homeserverUrl);
if (!options.accessToken.trim()) {
throw new MatrixRuntimeTransportError('invalid_request', 'Matrix access token is required');
}
if (!options.userId.trim()) {
throw new MatrixRuntimeTransportError('invalid_request', 'Matrix user identity is required');
}
this.bindings = new Map(
options.sessions.map((binding) => [binding.id, Object.freeze({ ...binding })]),
);
if (this.bindings.size !== options.sessions.length) {
throw new MatrixRuntimeTransportError(
'invalid_request',
'Matrix runtime session IDs must be unique',
);
}
this.fetchImpl = options.fetchImpl ?? (globalThis.fetch as unknown as MatrixFetchLike);
this.now = options.now ?? (() => new Date());
}
async health(_scope: RuntimeScope): Promise<RuntimeHealth> {
try {
const versions = await this.request('/_matrix/client/versions', { method: 'GET' });
if (!versions.ok) {
return this.healthResult('down', `versions HTTP ${versions.status}`);
}
await this.assertIdentity();
return this.healthResult('healthy');
} catch (error: unknown) {
return this.healthResult('down', message(error));
}
}
async listSessions(scope: RuntimeScope): Promise<MatrixNativeRuntimeSessionBinding[]> {
await this.assertIdentity();
void scope;
return [...this.bindings.values()].map((binding) => ({ ...binding }));
}
async verifySession(
sessionId: string,
scope: RuntimeScope,
): Promise<MatrixNativeRuntimeSessionBinding> {
await this.assertIdentity();
void scope;
const binding = this.bindings.get(sessionId);
if (!binding) {
throw new MatrixRuntimeTransportError(
'not_found',
'Matrix runtime session is not configured',
);
}
return { ...binding };
}
async *stream(
sessionId: string,
cursor: string | undefined,
scope: RuntimeScope,
): AsyncIterable<RuntimeStreamEvent> {
const binding = await this.verifySession(sessionId, scope);
const query = new URLSearchParams({ timeout: '0' });
if (cursor?.trim()) query.set('since', cursor);
const response = await this.request(`/_matrix/client/v3/sync?${query.toString()}`, {
method: 'GET',
headers: this.authHeaders(),
});
if (!response.ok) {
throw new MatrixRuntimeTransportError(
'unavailable',
`Matrix sync failed: HTTP ${response.status}`,
);
}
const payload = (await response.json()) as MatrixSyncResponse;
const nextCursor = payload.next_batch?.trim() || cursor?.trim() || 'initial';
const events = payload.rooms?.join?.[binding.roomId]?.timeline?.events ?? [];
for (const event of events) {
const normalized = normalizeEvent(event, binding, nextCursor);
if (normalized) yield normalized;
}
}
async send(sessionId: string, message: RuntimeMessage, scope: RuntimeScope): Promise<void> {
const binding = await this.verifySession(sessionId, scope);
const txnId = transactionId('send', binding.id, message.idempotencyKey);
const response = await this.request(
`/_matrix/client/v3/rooms/${encodeURIComponent(binding.roomId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
{
method: 'PUT',
headers: this.authHeaders(),
body: JSON.stringify({
msgtype: 'm.text',
body: message.content,
'mosaic.runtime.v1': metadata(binding, scope, message.idempotencyKey),
}),
},
);
if (!response.ok) {
throw new MatrixRuntimeTransportError(
'unavailable',
`Matrix message delivery failed: HTTP ${response.status}`,
);
}
}
async terminate(sessionId: string, approvalRef: string, scope: RuntimeScope): Promise<void> {
const binding = await this.verifySession(sessionId, scope);
const txnId = transactionId('terminate', binding.id, `${approvalRef}:${scope.correlationId}`);
const response = await this.request(
`/_matrix/client/v3/rooms/${encodeURIComponent(binding.roomId)}/send/mosaic.runtime.terminate/${encodeURIComponent(txnId)}`,
{
method: 'PUT',
headers: this.authHeaders(),
body: JSON.stringify({
...metadata(binding, scope),
approval_ref: approvalRef,
}),
},
);
if (!response.ok) {
throw new MatrixRuntimeTransportError(
'unavailable',
`Matrix termination delivery failed: HTTP ${response.status}`,
);
}
}
private async assertIdentity(): Promise<void> {
let response: Awaited<ReturnType<MatrixFetchLike>>;
try {
response = await this.request('/_matrix/client/v3/account/whoami', {
method: 'GET',
headers: this.authHeaders(),
});
} catch (error: unknown) {
throw new MatrixRuntimeTransportError('unavailable', message(error));
}
if (!response.ok) {
throw new MatrixRuntimeTransportError(
'forbidden',
`Matrix whoami failed: HTTP ${response.status}`,
);
}
const body = (await response.json()) as { user_id?: unknown };
if (body.user_id !== this.options.userId) {
throw new MatrixRuntimeTransportError(
'forbidden',
'Matrix whoami identity does not match configuration',
);
}
}
private request(
path: string,
init: { method?: string; headers?: Record<string, string>; body?: string },
) {
return this.fetchImpl(`${this.baseUrl}${path}`, init);
}
private authHeaders(): Record<string, string> {
return {
Authorization: `Bearer ${this.options.accessToken}`,
'Content-Type': 'application/json',
};
}
private healthResult(status: RuntimeHealth['status'], detail?: string): RuntimeHealth {
return { status, checkedAt: this.now().toISOString(), ...(detail ? { detail } : {}) };
}
}
function metadata(
binding: MatrixNativeRuntimeSessionBinding,
scope: RuntimeScope,
idempotencyKey?: string,
): Record<string, string> {
return {
session_id: binding.id,
runtime_id: binding.runtimeId,
actor_id: scope.actorId,
tenant_id: scope.tenantId,
channel_id: scope.channelId,
correlation_id: scope.correlationId,
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
};
}
function transactionId(kind: 'send' | 'terminate', sessionId: string, identity: string): string {
return `mosaic-${kind}-${createHash('sha256').update(`${sessionId}\u0000${identity}`).digest('base64url')}`;
}
function normalizeEvent(
event: MatrixSyncEvent,
binding: MatrixNativeRuntimeSessionBinding,
cursor: string,
): RuntimeStreamEvent | undefined {
if (event.type !== 'mosaic.runtime.event' || event.sender !== binding.remoteUserId)
return undefined;
const payload = event.content?.['mosaic.runtime.v1'];
if (
!isRecord(payload) ||
payload['session_id'] !== binding.id ||
typeof payload['type'] !== 'string'
) {
return undefined;
}
const occurredAt =
typeof payload['occurred_at'] === 'string'
? payload['occurred_at']
: new Date(event.origin_server_ts ?? 0).toISOString();
const eventCursor = typeof payload['cursor'] === 'string' ? payload['cursor'] : cursor;
switch (payload['type']) {
case 'session.state':
return isState(payload['state'])
? {
type: 'session.state',
sessionId: binding.id,
cursor: eventCursor,
occurredAt,
state: payload['state'],
}
: undefined;
case 'message.delta':
return typeof payload['content'] === 'string'
? {
type: 'message.delta',
sessionId: binding.id,
cursor: eventCursor,
occurredAt,
content: payload['content'],
}
: undefined;
case 'message.complete':
return typeof payload['message_id'] === 'string'
? {
type: 'message.complete',
sessionId: binding.id,
cursor: eventCursor,
occurredAt,
messageId: payload['message_id'],
}
: undefined;
default:
return undefined;
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isState(value: unknown): value is RuntimeSessionState {
return (
value === 'starting' ||
value === 'active' ||
value === 'idle' ||
value === 'stopped' ||
value === 'failed'
);
}
function validatedHomeserverUrl(value: string): string {
let url: URL;
try {
url = new URL(value);
} catch {
throw new MatrixRuntimeTransportError(
'invalid_request',
'Matrix homeserver URL must be absolute',
);
}
if (url.protocol !== 'https:') {
throw new MatrixRuntimeTransportError(
'invalid_request',
'Matrix homeserver URL must use HTTPS',
);
}
return url.toString().replace(/\/$/, '');
}
function message(error: unknown): string {
return error instanceof Error ? error.message : 'Matrix transport request failed';
}
@@ -0,0 +1,119 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { readPersonaContractBlock } from './persona-contract.js';
/**
* Persona-contract launch injection (A3b). Asserts the override-aware resolver
* is wired so a customized persona in roles.local/ wins at launch (AC-NS-7), and
* that any miss (unset/empty/unknown class, missing file) no-ops silently —
* never throws — mirroring readFleetCommsBlock's tolerant contract.
*/
const BASELINE_CODER = `# Coder — fleet role definition
The **coder** persona (\`class: coder\`, \`domain: engineering\`).
## Mandate
BASELINE-MANDATE: implement the assigned lane.
`;
const OVERRIDE_CODER = `# Coder — fleet role definition (override)
The **coder** persona (\`class: coder\`).
## Mandate
OVERRIDE-MANDATE: implement the assigned lane, the user's way.
`;
function makeHome(): string {
const root = mkdtempSync(join(tmpdir(), 'mosaic-persona-'));
return join(root, 'mosaic-home');
}
function seedBaseline(home: string, klass: string, body: string): void {
const dir = join(home, 'fleet', 'roles');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, `${klass}.md`), body);
}
function seedOverride(home: string, klass: string, body: string): void {
const dir = join(home, 'fleet', 'roles.local');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, `${klass}.md`), body);
}
describe('readPersonaContractBlock — launch-time persona injection (A3b)', () => {
let home: string;
beforeEach(() => {
home = makeHome();
});
afterEach(() => {
// root is the parent of mosaic-home
rmSync(join(home, '..'), { recursive: true, force: true });
});
it('injects the baseline persona when the class has a fleet/roles/<class>.md', () => {
seedBaseline(home, 'coder', BASELINE_CODER);
const block = readPersonaContractBlock(home, 'coder');
expect(block).toContain('# Persona Contract (coder)');
expect(block).toContain('BASELINE-MANDATE');
expect(block).toContain('baseline `fleet/roles/` layer');
});
it('OVERRIDE WINS: roles.local/<class>.md content is injected over the baseline (AC-NS-7)', () => {
seedBaseline(home, 'coder', BASELINE_CODER);
seedOverride(home, 'coder', OVERRIDE_CODER);
const block = readPersonaContractBlock(home, 'coder');
expect(block).toContain('# Persona Contract (coder)');
expect(block).toContain('OVERRIDE-MANDATE'); // override body present
expect(block).not.toContain('BASELINE-MANDATE'); // baseline NOT used
expect(block).toContain('roles.local'); // layer note names the override layer
});
it('injects an override-only (user-added) persona with no baseline at all', () => {
seedOverride(home, 'mascot', '# Mascot\n\n(`class: mascot`)\n\nCUSTOM-ROLE.\n');
const block = readPersonaContractBlock(home, 'mascot');
expect(block).toContain('# Persona Contract (mascot)');
expect(block).toContain('CUSTOM-ROLE');
});
it('canonicalizes an approved alias before launch-time override lookup', () => {
seedBaseline(home, 'code', '# Code\n\n(`class: code`)\n\nCANONICAL-CODE.\n');
seedOverride(
home,
'implementer',
'# Legacy implementer\n\n(`class: implementer`)\n\nLEGACY-OVERRIDE.\n',
);
const block = readPersonaContractBlock(home, 'implementer');
expect(block).toContain('# Persona Contract (code)');
expect(block).toContain('CANONICAL-CODE');
expect(block).not.toContain('LEGACY-OVERRIDE');
});
it('no-ops (empty string) when the class is undefined', () => {
seedBaseline(home, 'coder', BASELINE_CODER);
expect(readPersonaContractBlock(home, undefined)).toBe('');
});
it('no-ops (empty string) when the class is empty/whitespace', () => {
seedBaseline(home, 'coder', BASELINE_CODER);
expect(readPersonaContractBlock(home, '')).toBe('');
expect(readPersonaContractBlock(home, ' ')).toBe('');
});
it('no-ops (empty string) for an unknown class with no role file', () => {
seedBaseline(home, 'coder', BASELINE_CODER);
expect(readPersonaContractBlock(home, 'nonexistent')).toBe('');
});
it('no-ops (empty string, no throw) when no roles directories exist at all', () => {
expect(() => readPersonaContractBlock(home, 'coder')).not.toThrow();
expect(readPersonaContractBlock(home, 'coder')).toBe('');
});
});
@@ -0,0 +1,63 @@
/**
* Persona-contract injection at launch (North Star A3b).
*
* A spawned fleet agent should boot already knowing WHO it is: its class's role
* contract (mandate + boundaries). The companion goal A3a exports the agent's
* resolved class into the pane env as `MOSAIC_AGENT_CLASS`; here we read that
* class at launch (composeContract → system prompt) and inject the resolved
* persona contract so the identity is resident from the agent's first turn.
*
* OVERRIDE-AWARE: resolution goes through fleet-personas' resolver, so a
* user-customized persona in the PRESERVE-protected `fleet/roles.local/` layer
* WINS over the baseline `fleet/roles/` of the same class. That is the
* launch-time proof of AC-NS-7 — a customized persona actually reaches the model
* when the agent boots, not just in `mosaic fleet persona show`.
*
* Tolerant by contract (mirrors readFleetCommsBlock): an empty/missing class, an
* unknown class, or a missing role file all yield '' so the launcher no-ops
* silently. This MUST never throw during launch.
*
* Standalone module (no fleet.ts import) to keep launch.ts's prompt path free of
* the heavy fleet command module; it depends only on the lightweight persona
* resolver.
*/
import {
resolvePersonaSync,
defaultRolesDir,
defaultOverrideDir,
} from '../commands/fleet-personas.js';
/**
* Resolve `klass`'s persona contract (override-aware) and render it as a
* clearly-delimited launch block. Returns '' on any miss (falsy class, unknown
* class, missing/unreadable file) so composeContract can push it unconditionally
* and have it no-op silently. Never throws.
*/
export function readPersonaContractBlock(mosaicHome: string, klass: string | undefined): string {
if (!klass || !klass.trim()) return '';
let resolved: ReturnType<typeof resolvePersonaSync>;
try {
resolved = resolvePersonaSync(klass.trim(), {
rolesDir: defaultRolesDir(mosaicHome),
overrideDir: defaultOverrideDir(mosaicHome),
});
} catch {
// Best-effort onboarding: a resolver hiccup must not abort the launch.
return '';
}
if (!resolved) return '';
const layerNote =
resolved.layer === 'override'
? '_(resolved from the `fleet/roles.local/` override layer — wins over baseline)_'
: '_(resolved from the baseline `fleet/roles/` layer)_';
return `# Persona Contract (${resolved.klass})
${layerNote}
You are operating as the **${resolved.klass}** persona. The role contract below is your identity — its mandate and boundaries govern what you own and what you must not do for this assignment.
${resolved.content.trim()}`;
}
@@ -0,0 +1,104 @@
import { describe, expect, it, vi } from 'vitest';
import type { CommandResult, CommandRunner, FleetRoster } from '../commands/fleet.js';
import { TmuxPromotionTransport } from './promotion-transport.js';
const sessionId = 'a'.repeat(64);
const roster: FleetRoster = {
agents: [{ className: 'worker', name: 'claude-seat', runtime: 'claude' }],
defaults: { workingDirectory: '~/src' },
runtimes: {},
tmux: { holderSession: '_holder', socketName: 'mosaic-fleet' },
transport: 'tmux',
version: 1,
};
function result(stdout = '', exitCode = 0, stderr = ''): CommandResult {
return { exitCode, stderr, stdout };
}
describe('TmuxPromotionTransport', () => {
it('resolves the exact roster seat and sends the registered command literally', async () => {
const runner = vi
.fn<CommandRunner>()
.mockResolvedValueOnce(result('1234 claude 0 0 0 0\n'))
.mockResolvedValueOnce(result())
.mockResolvedValueOnce(result());
const environmentReader = vi.fn(async () => `MOSAIC_LEASE_SESSION_ID=${sessionId}\0`);
const transport = new TmuxPromotionTransport({
environmentReader,
mosaicHome: '/mosaic',
rosterLoader: async () => roster,
runner,
});
const target = await transport.resolve('claude-seat');
await transport.sendPromotion(target);
expect(target).toEqual({
bundle: 'mosaic-fleet',
seat: 'claude-seat',
sessionId,
});
expect(environmentReader).toHaveBeenCalledWith(1234);
expect(runner).toHaveBeenNthCalledWith(2, 'tmux', [
'-L',
'mosaic-fleet',
'send-keys',
'-t',
'=claude-seat:0.0',
'-l',
'/mosaic-promote',
]);
expect(runner).toHaveBeenNthCalledWith(3, 'tmux', [
'-L',
'mosaic-fleet',
'send-keys',
'-t',
'=claude-seat:0.0',
'Enter',
]);
});
// Regression for #1124: the launcher runs claude as a spawnSync CHILD of
// node(mosaic), so the lease env is on the child, not the tmux pane pid. The
// transport must WALK the subtree. This test exercises the real walk (no
// full mock of the resolution) — the seam the original unit test hid.
it('walks the pane subtree to the claude child that carries the lease id', async () => {
const runner = vi.fn<CommandRunner>().mockResolvedValueOnce(result('1234 node 0 0 0 0\n'));
// pane pid 1234 = node(mosaic): NO lease env. child 5678 = claude: carries it.
const environmentReader = vi.fn(async (pid: number) =>
pid === 5678 ? `FOO=bar\0MOSAIC_LEASE_SESSION_ID=${sessionId}\0` : `FOO=bar\0`,
);
const childrenReader = vi.fn(async (pid: number) => (pid === 1234 ? [5678] : []));
const transport = new TmuxPromotionTransport({
environmentReader,
childrenReader,
mosaicHome: '/mosaic',
rosterLoader: async () => roster,
runner,
});
const target = await transport.resolve('claude-seat');
expect(target.sessionId).toBe(sessionId);
expect(environmentReader).toHaveBeenCalledWith(1234); // pane pid: no lease
expect(environmentReader).toHaveBeenCalledWith(5678); // walked to the child
expect(childrenReader).toHaveBeenCalledWith(1234); // walk actually ran
});
it('fails closed when no process in the pane subtree carries a lease id', async () => {
const runner = vi.fn<CommandRunner>().mockResolvedValueOnce(result('1234 node 0 0 0 0\n'));
const environmentReader = vi.fn(async () => `FOO=bar\0`);
const childrenReader = vi.fn(async (pid: number) => (pid === 1234 ? [5678] : []));
const transport = new TmuxPromotionTransport({
environmentReader,
childrenReader,
mosaicHome: '/mosaic',
rosterLoader: async () => roster,
runner,
});
await expect(transport.resolve('claude-seat')).rejects.toThrow('no readable lease session');
expect(childrenReader).toHaveBeenCalledWith(1234);
});
});
@@ -0,0 +1,208 @@
import { readFile } from 'node:fs/promises';
import {
buildTmuxListPanesCommand,
getRosterAgent,
parseTmuxListPanes,
resolveFleetPaths,
type CommandResult,
type CommandRunner,
type FleetRoster,
RUNTIME_ACCEPTABLE_COMMANDS,
socketArgs,
} from '../commands/fleet.js';
import { loadFleetRoster } from './fleet-roster-v1.js';
const PROMOTION_COMMAND = '/mosaic-promote';
const SESSION_ID_PATTERN = /^[a-f0-9]{64}$/;
const TRANSPORT_COMMAND_TIMEOUT_MS = 5_000;
export interface PromotionTarget {
bundle: string;
seat: string;
sessionId: string;
}
export interface PromotionTransport {
resolve(seat: string): Promise<PromotionTarget>;
sendPromotion(target: PromotionTarget): Promise<void>;
}
export interface TmuxPromotionTransportOptions {
environmentReader?: (pid: number) => Promise<string>;
childrenReader?: (pid: number) => Promise<number[]>;
mosaicHome: string;
rosterLoader?: () => Promise<FleetRoster>;
runner: CommandRunner;
}
// The launcher runs the runtime as a spawnSync CHILD of node(mosaic) (see
// launch.ts:99 — deliberate, so the parent survives to propagate signals), so
// MOSAIC_LEASE_SESSION_ID lives on the claude child, NOT on the tmux pane's root
// pid. Bound the descendant search so a hung/large process tree can't stall it.
const MAX_SUBTREE_PIDS = 128;
/** Local, roster-bound transport for the in-seat promotion command. */
export class TmuxPromotionTransport implements PromotionTransport {
private readonly environmentReader: (pid: number) => Promise<string>;
private readonly childrenReader: (pid: number) => Promise<number[]>;
private readonly rosterLoader: () => Promise<FleetRoster>;
constructor(private readonly options: TmuxPromotionTransportOptions) {
this.environmentReader = options.environmentReader ?? readPaneEnvironment;
this.childrenReader = options.childrenReader ?? readChildPids;
this.rosterLoader =
options.rosterLoader ??
(() => loadFleetRoster(resolveFleetPaths(options.mosaicHome).rosterPath));
}
async resolve(seat: string): Promise<PromotionTarget> {
const roster = await this.rosterLoader();
const agent = getRosterAgent(roster, seat);
if (agent.runtime !== 'claude') {
throw new Error(`Lease promotion is currently available only for Claude seats: ${seat}.`);
}
const paneResult = await this.run(
buildTmuxListPanesCommand(agent.name, roster.tmux.socketName),
);
if (paneResult.exitCode !== 0) {
throw new Error(`Promotion seat is unavailable: ${seat}.`);
}
const pane = parseTmuxListPanes(paneResult.stdout);
const allowedCommands = RUNTIME_ACCEPTABLE_COMMANDS.claude;
if (
pane.dead ||
pane.pid === null ||
pane.command === null ||
allowedCommands === undefined ||
!allowedCommands.includes(pane.command)
) {
throw new Error(`Promotion seat runtime identity mismatch: ${seat}.`);
}
const sessionId = await this.resolveLeaseSessionId(pane.pid);
if (sessionId === null) {
throw new Error(`Promotion seat has no readable lease session: ${seat}.`);
}
return {
bundle: roster.tmux.socketName || 'default',
seat: agent.name,
sessionId,
};
}
/**
* Find the lease session id in the pane's process subtree. The pane's root pid
* is node(mosaic), which has no lease env; the id lives on the claude child.
* BFS from the root, bounded, returning the first descendant that carries a
* valid MOSAIC_LEASE_SESSION_ID. Fail-closed (null) if none is found.
*/
private async resolveLeaseSessionId(rootPid: number): Promise<string | null> {
const queue: number[] = [rootPid];
const seen = new Set<number>();
while (queue.length > 0 && seen.size < MAX_SUBTREE_PIDS) {
const pid = queue.shift()!;
if (seen.has(pid)) continue;
seen.add(pid);
let sessionId: string | null = null;
try {
sessionId = parseLeaseSessionId(await this.environmentReader(pid));
} catch {
sessionId = null;
}
if (sessionId !== null) return sessionId;
let children: number[] = [];
try {
children = await this.childrenReader(pid);
} catch {
children = [];
}
for (const child of children) {
if (!seen.has(child)) queue.push(child);
}
}
return null;
}
async sendPromotion(target: PromotionTarget): Promise<void> {
const targetPane = `=${target.seat}:0.0`;
const socketName = target.bundle === 'default' ? '' : target.bundle;
// Registered Claude commands must arrive as their exact literal text; the
// fleet agent sender prepends an identity envelope, so it cannot carry this
// command without preventing the UserPromptSubmit matcher from recognizing it.
await this.runPromotionCommand([
'tmux',
...socketArgs(socketName),
'send-keys',
'-t',
targetPane,
'-l',
PROMOTION_COMMAND,
]);
await this.runPromotionCommand([
'tmux',
...socketArgs(socketName),
'send-keys',
'-t',
targetPane,
'Enter',
]);
}
private async runPromotionCommand(command: string[]): Promise<void> {
const result = await this.run(command);
if (result.exitCode !== 0) {
throw new Error('Promotion command delivery failed.');
}
}
private async run(command: string[]): Promise<CommandResult> {
const [executable, ...args] = command;
if (executable === undefined) {
throw new Error('Promotion transport command is empty.');
}
return await withTimeout(this.options.runner(executable, args), TRANSPORT_COMMAND_TIMEOUT_MS);
}
}
function withTimeout<T>(operation: Promise<T>, timeoutMs: number): Promise<T> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Promotion transport command timed out after ${timeoutMs}ms.`));
}, timeoutMs);
void operation.then(
(value) => {
clearTimeout(timeout);
resolve(value);
},
(error: unknown) => {
clearTimeout(timeout);
reject(error);
},
);
});
}
async function readPaneEnvironment(pid: number): Promise<string> {
return readFile(`/proc/${pid}/environ`, 'utf8');
}
async function readChildPids(pid: number): Promise<number[]> {
// Linux exposes direct children of the main thread here (CONFIG_PROC_CHILDREN).
try {
const raw = await readFile(`/proc/${pid}/task/${pid}/children`, 'utf8');
return raw
.split(/\s+/)
.filter(Boolean)
.map((value) => Number.parseInt(value, 10))
.filter((value) => Number.isInteger(value) && value > 0);
} catch {
return [];
}
}
function parseLeaseSessionId(environment: string): string | null {
const value = environment
.split('\0')
.find((entry) => entry.startsWith('MOSAIC_LEASE_SESSION_ID='))
?.slice('MOSAIC_LEASE_SESSION_ID='.length);
return value !== undefined && SESSION_ID_PATTERN.test(value) ? value : null;
}
@@ -0,0 +1,372 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
import {
ROSTER_V2_JSON_SCHEMA,
RosterV2ValidationError,
parseRosterV2,
renderRosterV2Yaml,
validateRosterV2Semantics,
} from './roster-v2.js';
const validRoster = `
version: 2
generation: 7
transport: tmux
tmux:
socket_name: mosaic-fleet
holder_session: _holder
defaults:
working_directory: ~/src
runtime: pi
runtimes:
pi:
reset_command: /new
agents:
- name: coder0
alias: Coder 0
class: code
runtime: pi
provider: openai
model: gpt-5.6-sol
reasoning: high
tool_policy: code
working_directory: ~/src
persistent_persona: false
reset_between_tasks: true
lifecycle:
enabled: true
desired_state: stopped
launch:
yolo: true
`;
let semanticTmp: string | undefined;
it('preserves an explicit empty socket as the literal default tmux server', () => {
const roster = parseRosterV2(validRoster.replace('socket_name: mosaic-fleet', "socket_name: ''"));
expect(roster.tmux.socketName).toBe('');
expect(renderRosterV2Yaml(roster)).toContain('socket_name: ""');
});
afterEach(async (): Promise<void> => {
if (semanticTmp) await rm(semanticTmp, { recursive: true, force: true });
semanticTmp = undefined;
});
async function semanticDirs(): Promise<{ rolesDir: string; overrideDir: string }> {
semanticTmp = await mkdtemp(join(tmpdir(), 'roster-v2-semantics-'));
const rolesDir = join(semanticTmp, 'roles');
const overrideDir = join(semanticTmp, 'roles.local');
await mkdir(rolesDir, { recursive: true });
await mkdir(overrideDir, { recursive: true });
for (const klass of [
'code',
'review',
'interaction',
'orchestrator',
'merge-gate',
'validator',
'team-leader',
]) {
await writeFile(join(rolesDir, `${klass}.md`), `# ${klass}\n\n(\`class: ${klass}\`)\n`, 'utf8');
}
return { rolesDir, overrideDir };
}
function rosterWithClass(klass: string, toolPolicy = klass): string {
return validRoster
.replace('class: code', `class: ${klass}`)
.replace('tool_policy: code', `tool_policy: ${toolPolicy}`);
}
describe('roster v2 semantic validation', (): void => {
it.each([
['implementer', 'code'],
['reviewer', 'review'],
['operator-interaction', 'interaction'],
])(
'canonicalizes requested alias %s while retaining requested and canonical class',
async (requested: string, canonical: string) => {
const dirs = await semanticDirs();
const roster = parseRosterV2(rosterWithClass(requested, requested), 'yaml');
const validated = await validateRosterV2Semantics(roster, dirs);
expect(validated.agents[0]).toMatchObject({
requestedClass: requested,
canonicalClass: canonical,
canonicalToolPolicy: canonical,
});
},
);
it.each(['worker', 'analyst', 'canary'])(
'rejects %s when no genuine custom role exists',
async (klass: string) => {
const dirs = await semanticDirs();
await expect(
validateRosterV2Semantics(parseRosterV2(rosterWithClass(klass), 'yaml'), dirs),
).rejects.toThrow(/unresolved|readable persona/i);
},
);
it('accepts a genuine custom roles.local class without protected authority', async () => {
const dirs = await semanticDirs();
await writeFile(join(dirs.overrideDir, 'worker.md'), '# worker\n\n(`class: worker`)\n', 'utf8');
const validated = await validateRosterV2Semantics(
parseRosterV2(rosterWithClass('worker'), 'yaml'),
dirs,
);
expect(validated.agents[0]?.authority).toMatchObject({
mayMerge: false,
mayOrchestrate: false,
});
});
it('rejects a LIBRARY-only class with no readable resolved persona', async () => {
const dirs = await semanticDirs();
await writeFile(
join(dirs.rolesDir, 'LIBRARY.md'),
'| Persona | Purpose |\n| --- | --- |\n| phantom | Missing |\n',
'utf8',
);
await expect(
validateRosterV2Semantics(parseRosterV2(rosterWithClass('phantom'), 'yaml'), dirs),
).rejects.toThrow(/readable persona/i);
});
it('rejects an unreadable resolved persona', async () => {
const dirs = await semanticDirs();
await mkdir(join(dirs.overrideDir, 'worker.md'));
await expect(
validateRosterV2Semantics(parseRosterV2(rosterWithClass('worker'), 'yaml'), dirs),
).rejects.toThrow(/readable persona/i);
});
it.each([
['merge-gate', 'code'],
['validator', 'merge-gate'],
['orchestrator', 'interaction'],
['team-leader', 'orchestrator'],
['interaction', 'orchestrator'],
['code', 'merge-gate'],
['worker', 'validator'],
])(
'denies protected class/tool-policy mismatch %s with %s',
async (klass: string, toolPolicy: string) => {
const dirs = await semanticDirs();
if (klass === 'worker') {
await writeFile(
join(dirs.overrideDir, 'worker.md'),
'# worker\n\n(`class: worker`)\n',
'utf8',
);
}
await expect(
validateRosterV2Semantics(parseRosterV2(rosterWithClass(klass, toolPolicy), 'yaml'), dirs),
).rejects.toThrow(/tool policy.*must match|mismatch/i);
},
);
});
describe('roster v2 structural compiler', (): void => {
it('parses YAML into a normalized typed model and renders canonical YAML', (): void => {
const roster = parseRosterV2(validRoster, 'yaml');
expect(roster).toEqual({
version: 2,
generation: 7,
transport: 'tmux',
tmux: { socketName: 'mosaic-fleet', holderSession: '_holder' },
defaults: { workingDirectory: '~/src', runtime: 'pi' },
runtimes: { pi: { resetCommand: '/new' } },
agents: [
{
name: 'coder0',
alias: 'Coder 0',
className: 'code',
runtime: 'pi',
provider: 'openai',
model: 'gpt-5.6-sol',
reasoning: 'high',
toolPolicy: 'code',
workingDirectory: '~/src',
persistentPersona: false,
resetBetweenTasks: true,
lifecycle: { enabled: true, desiredState: 'stopped' },
launch: { yolo: true },
},
],
});
expect(renderRosterV2Yaml(roster)).toBe(validRoster.trimStart());
});
it('parses JSON and produces the same normalized model', (): void => {
const yaml = parseRosterV2(validRoster, 'yaml');
const json = JSON.stringify({
version: 2,
generation: 7,
transport: 'tmux',
tmux: { socket_name: 'mosaic-fleet', holder_session: '_holder' },
defaults: { working_directory: '~/src', runtime: 'pi' },
runtimes: { pi: { reset_command: '/new' } },
agents: [
{
name: 'coder0',
alias: 'Coder 0',
class: 'code',
runtime: 'pi',
provider: 'openai',
model: 'gpt-5.6-sol',
reasoning: 'high',
tool_policy: 'code',
working_directory: '~/src',
persistent_persona: false,
reset_between_tasks: true,
lifecycle: { enabled: true, desired_state: 'stopped' },
launch: { yolo: true },
},
],
});
expect(parseRosterV2(json, 'json')).toEqual(yaml);
});
it('sorts runtime and agent maps in the deterministic renderer', (): void => {
const roster = parseRosterV2(
validRoster
.replace(
'runtimes:\n pi:\n reset_command: /new',
'runtimes:\n pi:\n reset_command: /new\n codex:\n reset_command: /clear',
)
.replace(
'agents:\n - name: coder0',
'agents:\n - name: reviewer\n alias: Reviewer\n class: review\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: medium\n tool_policy: review\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true\n - name: coder0',
),
'yaml',
);
const rendered = renderRosterV2Yaml(roster);
expect(rendered.indexOf(' codex:')).toBeLessThan(rendered.indexOf(' pi:'));
expect(rendered.indexOf(' - name: coder0')).toBeLessThan(
rendered.indexOf(' - name: reviewer'),
);
});
it.each([
['v1 document', validRoster.replace('version: 2', 'version: 1'), /v1.*existing v1 path/i],
[
'unknown connector',
`${validRoster}\nconnector:\n kind: matrix\n`,
/unsupported field.*connector/i,
],
[
'remote host',
validRoster.replace(
'runtime: pi\n provider',
'runtime: pi\n host: remote\n provider',
),
/unsupported field.*host/i,
],
[
'secret reference',
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n secret_ref: vault://x'),
/unsupported field.*secret_ref/i,
],
[
'channel override',
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n channels: discord'),
/unsupported field.*channels/i,
],
[
'arbitrary command',
validRoster.replace('model: gpt-5.6-sol', 'model: gpt-5.6-sol\n command: whoami'),
/unsupported field.*command/i,
],
[
'gateway field',
`${validRoster}\ngateway:\n url: https://gateway.example\n`,
/unsupported field.*gateway/i,
],
[
'missing required field',
validRoster.replace(' model: gpt-5.6-sol\n', ''),
/model.*required/i,
],
[
'invalid type',
validRoster.replace('generation: 7', 'generation: seven'),
/generation.*integer/i,
],
[
'unsafe generation',
validRoster.replace('generation: 7', 'generation: 9007199254740992'),
/generation.*integer/i,
],
[
'duplicate names',
validRoster.replace(
' - name: coder0',
' - name: coder0\n alias: Duplicate\n class: code\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: high\n tool_policy: code\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true\n - name: coder0',
),
/duplicate agent name/i,
],
['invalid name', validRoster.replace('name: coder0', 'name: ../coder0'), /invalid agent name/i],
[
'invalid transport',
validRoster.replace('transport: tmux', 'transport: matrix'),
/transport.*tmux/i,
],
[
'invalid runtime',
validRoster.replace('runtime: pi', 'runtime: matrix'),
/runtime.*supported/i,
],
[
'invalid reasoning',
validRoster.replace('reasoning: high', 'reasoning: extreme'),
/reasoning.*low.*medium.*high/i,
],
[
'ambiguous socket',
validRoster.replace('socket_name: mosaic-fleet', 'socket_name: default/socket'),
/socket_name/i,
],
[
'agent socket override',
validRoster.replace(
'runtime: pi\n provider',
'runtime: pi\n socket: another\n provider',
),
/unsupported field.*socket/i,
],
])('rejects %s', (_name: string, source: string, expected: RegExp): void => {
expect((): void => {
parseRosterV2(source, 'yaml');
}).toThrow(expected);
});
it('rejects malformed JSON as a validation error', (): void => {
expect((): void => {
parseRosterV2('{', 'json');
}).toThrow(RosterV2ValidationError);
});
it('declares supported runtime map keys in the executable schema', (): void => {
expect(ROSTER_V2_JSON_SCHEMA).toMatchObject({
properties: {
runtimes: { propertyNames: { enum: ['claude', 'codex', 'opencode', 'pi'] } },
},
});
});
it('keeps the checked-in documentation schema structurally identical to the executable schema', async (): Promise<void> => {
const schemaPath = fileURLToPath(
new URL('../../../../docs/fleet/reference/roster-v2.schema.json', import.meta.url),
);
const documented = await readFile(schemaPath, 'utf8');
expect(JSON.parse(documented) as unknown).toEqual(ROSTER_V2_JSON_SCHEMA);
});
});
+571
View File
@@ -0,0 +1,571 @@
import YAML from 'yaml';
import {
authorityForCanonicalClass,
canonicalizeRoleClass,
defaultOverrideDir,
defaultRolesDir,
extractClassesFromDir,
resolvePersonaFrom,
type PersonaDirs,
type PersonaResolution,
type RoleAuthority,
} from '../commands/fleet-personas.js';
import { compareCodePoints } from './deterministic-order.js';
export const ROSTER_V2_SUPPORTED_RUNTIMES = ['claude', 'codex', 'opencode', 'pi'] as const;
export const ROSTER_V2_REASONING_LEVELS = ['low', 'medium', 'high'] as const;
export const ROSTER_V2_DESIRED_STATES = ['running', 'stopped'] as const;
export type RosterV2RuntimeName = (typeof ROSTER_V2_SUPPORTED_RUNTIMES)[number];
export type RosterV2ReasoningLevel = (typeof ROSTER_V2_REASONING_LEVELS)[number];
export type RosterV2DesiredState = (typeof ROSTER_V2_DESIRED_STATES)[number];
export type RosterV2InputFormat = 'json' | 'yaml';
export interface FleetRosterV2Tmux {
readonly socketName: string;
readonly holderSession: string;
}
export interface FleetRosterV2Defaults {
readonly workingDirectory: string;
readonly runtime: RosterV2RuntimeName;
}
export interface FleetRosterV2Runtime {
readonly resetCommand: string;
}
export interface FleetRosterV2Lifecycle {
readonly enabled: boolean;
readonly desiredState: RosterV2DesiredState;
}
export interface FleetRosterV2Launch {
readonly yolo: boolean;
}
export interface FleetRosterV2Agent {
readonly name: string;
readonly alias: string;
readonly className: string;
readonly runtime: RosterV2RuntimeName;
readonly provider: string;
readonly model: string;
readonly reasoning: RosterV2ReasoningLevel;
readonly toolPolicy: string;
readonly workingDirectory: string;
readonly persistentPersona: boolean;
readonly resetBetweenTasks: boolean;
readonly lifecycle: FleetRosterV2Lifecycle;
readonly launch: FleetRosterV2Launch;
}
export interface FleetRosterV2 {
readonly version: 2;
readonly generation: number;
readonly transport: 'tmux';
readonly tmux: FleetRosterV2Tmux;
readonly defaults: FleetRosterV2Defaults;
readonly runtimes: Readonly<Record<string, FleetRosterV2Runtime>>;
readonly agents: readonly FleetRosterV2Agent[];
}
export interface SemanticallyValidatedRosterV2Agent extends FleetRosterV2Agent {
readonly requestedClass: string;
readonly canonicalClass: string;
readonly canonicalToolPolicy: string;
readonly persona: PersonaResolution;
readonly authority: RoleAuthority;
}
export interface SemanticallyValidatedRosterV2 extends Omit<FleetRosterV2, 'agents'> {
readonly agents: readonly SemanticallyValidatedRosterV2Agent[];
}
const PROTECTED_TOOL_POLICY_CLASSES = new Set([
'merge-gate',
'validator',
'orchestrator',
'team-leader',
'interaction',
]);
/**
* Validate filesystem-backed roster semantics after synchronous structural parsing.
* Directory scans are batched once and every class must resolve to readable content.
*/
export async function validateRosterV2Semantics(
roster: FleetRosterV2,
opts: PersonaDirs = {},
): Promise<SemanticallyValidatedRosterV2> {
const rolesDir = opts.rolesDir ?? defaultRolesDir(opts.mosaicHome);
const overrideDir = opts.overrideDir ?? defaultOverrideDir(opts.mosaicHome);
const [base, over] = await Promise.all([
extractClassesFromDir(rolesDir),
extractClassesFromDir(overrideDir),
]);
const agents: SemanticallyValidatedRosterV2Agent[] = [];
for (const agent of roster.agents) {
const requestedClass = agent.className;
const { canonicalClass } = canonicalizeRoleClass(requestedClass);
const canonicalToolPolicy = canonicalizeRoleClass(agent.toolPolicy).canonicalClass;
const persona = await resolvePersonaFrom(requestedClass, {
rolesDir,
overrideDir,
base,
over,
});
if (!persona || persona.content.trim() === '') {
throw new RosterV2ValidationError(
`Roster v2 agent "${agent.name}" class "${requestedClass}" does not resolve to a readable persona.`,
);
}
if (
(PROTECTED_TOOL_POLICY_CLASSES.has(canonicalClass) ||
PROTECTED_TOOL_POLICY_CLASSES.has(canonicalToolPolicy)) &&
canonicalToolPolicy !== canonicalClass
) {
throw new RosterV2ValidationError(
`Roster v2 agent "${agent.name}" protected class "${canonicalClass}" tool policy must match its canonical class; received "${agent.toolPolicy}".`,
);
}
agents.push(
Object.freeze({
...agent,
requestedClass,
canonicalClass,
canonicalToolPolicy,
persona,
authority: authorityForCanonicalClass(canonicalClass),
}),
);
}
return Object.freeze({ ...roster, agents: Object.freeze(agents) });
}
export class RosterV2ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'RosterV2ValidationError';
}
}
type JsonSchema = string | number | boolean | null | JsonSchema[] | { [key: string]: JsonSchema };
/**
* Executable v2 structural contract. The checked-in documentation schema is
* structurally compared to this value in roster-v2.spec.ts.
*/
export const ROSTER_V2_JSON_SCHEMA: JsonSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://mosaicstack.dev/schemas/fleet/roster-v2.schema.json',
title: 'Mosaic local tmux fleet roster v2',
type: 'object',
additionalProperties: false,
required: ['version', 'generation', 'transport', 'tmux', 'defaults', 'runtimes', 'agents'],
properties: {
version: { const: 2 },
generation: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER },
transport: { const: 'tmux' },
tmux: {
type: 'object',
additionalProperties: false,
required: ['socket_name', 'holder_session'],
properties: {
socket_name: { type: 'string', pattern: '^[A-Za-z0-9_.-]*$' },
holder_session: { type: 'string', pattern: '^[A-Za-z0-9_.-]+$' },
},
},
defaults: {
type: 'object',
additionalProperties: false,
required: ['working_directory', 'runtime'],
properties: {
working_directory: { type: 'string', minLength: 1 },
runtime: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
},
},
runtimes: {
type: 'object',
minProperties: 1,
propertyNames: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
additionalProperties: {
type: 'object',
additionalProperties: false,
required: ['reset_command'],
properties: { reset_command: { type: 'string', minLength: 1 } },
},
},
agents: {
type: 'array',
minItems: 1,
items: {
type: 'object',
additionalProperties: false,
required: [
'name',
'alias',
'class',
'runtime',
'provider',
'model',
'reasoning',
'tool_policy',
'working_directory',
'persistent_persona',
'reset_between_tasks',
'lifecycle',
'launch',
],
properties: {
name: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9_.-]*$' },
alias: { type: 'string', minLength: 1 },
class: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
runtime: { enum: [...ROSTER_V2_SUPPORTED_RUNTIMES] },
provider: { type: 'string', minLength: 1 },
model: { type: 'string', minLength: 1 },
reasoning: { enum: [...ROSTER_V2_REASONING_LEVELS] },
tool_policy: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
working_directory: { type: 'string', minLength: 1 },
persistent_persona: { type: 'boolean' },
reset_between_tasks: { type: 'boolean' },
lifecycle: {
type: 'object',
additionalProperties: false,
required: ['enabled', 'desired_state'],
properties: {
enabled: { type: 'boolean' },
desired_state: { enum: [...ROSTER_V2_DESIRED_STATES] },
},
},
launch: {
type: 'object',
additionalProperties: false,
required: ['yolo'],
properties: { yolo: { type: 'boolean' } },
},
},
},
},
},
};
const ROOT_KEYS = ['version', 'generation', 'transport', 'tmux', 'defaults', 'runtimes', 'agents'];
const TMUX_KEYS = ['socket_name', 'holder_session'];
const DEFAULT_KEYS = ['working_directory', 'runtime'];
const RUNTIME_KEYS = ['reset_command'];
const AGENT_KEYS = [
'name',
'alias',
'class',
'runtime',
'provider',
'model',
'reasoning',
'tool_policy',
'working_directory',
'persistent_persona',
'reset_between_tasks',
'lifecycle',
'launch',
];
const LIFECYCLE_KEYS = ['enabled', 'desired_state'];
const LAUNCH_KEYS = ['yolo'];
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const TMUX_SOCKET_IDENTIFIER = /^[A-Za-z0-9_.-]*$/;
const TMUX_IDENTIFIER = /^[A-Za-z0-9_.-]+$/;
const POLICY_IDENTIFIER = /^[a-z][a-z0-9-]*$/;
/** Parses YAML or JSON and compiles only the local-tmux roster v2 contract. */
export function parseRosterV2(source: string, format?: RosterV2InputFormat): FleetRosterV2 {
const parsed = parseSource(source, format);
return normalizeRosterV2(parsed);
}
/** Renders canonical snake_case YAML with sorted runtime and agent entries. */
export function renderRosterV2Yaml(roster: FleetRosterV2): string {
const normalized = normalizeRosterV2(toSourceShape(roster));
return YAML.stringify(toSourceShape(normalized));
}
function parseSource(source: string, format?: RosterV2InputFormat): unknown {
const resolvedFormat = format ?? (source.trimStart().startsWith('{') ? 'json' : 'yaml');
try {
if (resolvedFormat === 'json') return JSON.parse(source) as unknown;
return YAML.parse(source) as unknown;
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new RosterV2ValidationError(`Roster v2 ${resolvedFormat} parse failed: ${detail}`);
}
}
export function normalizeRosterV2(raw: unknown): FleetRosterV2 {
const root = requiredObject(raw, 'Roster v2');
assertKnownKeys(root, 'Roster v2', ROOT_KEYS);
if (root.version === 1) {
throw new RosterV2ValidationError(
'Roster v2 compiler rejects v1 input; use the existing v1 path until migration.',
);
}
if (root.version !== 2) throw new RosterV2ValidationError('Roster v2 version must be 2.');
const generation = requiredPositiveInteger(root.generation, 'Roster v2 generation');
const transport = requiredEnum(root.transport, 'Roster v2 transport', ['tmux'] as const);
const tmux = normalizeTmux(root.tmux);
const defaults = normalizeDefaults(root.defaults);
const runtimes = normalizeRuntimes(root.runtimes);
const agents = normalizeAgents(root.agents, runtimes);
if (!runtimes[defaults.runtime]) {
throw new RosterV2ValidationError(
`Roster v2 defaults runtime "${defaults.runtime}" must be declared in runtimes.`,
);
}
return { version: 2, generation, transport, tmux, defaults, runtimes, agents };
}
function normalizeTmux(value: unknown): FleetRosterV2Tmux {
const raw = requiredObject(value, 'Roster v2 tmux');
assertKnownKeys(raw, 'Roster v2 tmux', TMUX_KEYS);
return {
socketName: requiredTmuxSocket(raw.socket_name, 'Roster v2 tmux socket_name'),
holderSession: requiredTmuxIdentifier(raw.holder_session, 'Roster v2 tmux holder_session'),
};
}
function normalizeDefaults(value: unknown): FleetRosterV2Defaults {
const raw = requiredObject(value, 'Roster v2 defaults');
assertKnownKeys(raw, 'Roster v2 defaults', DEFAULT_KEYS);
return {
workingDirectory: requiredString(raw.working_directory, 'Roster v2 defaults working_directory'),
runtime: requiredRuntime(raw.runtime, 'Roster v2 defaults runtime'),
};
}
function normalizeRuntimes(value: unknown): Readonly<Record<string, FleetRosterV2Runtime>> {
const raw = requiredObject(value, 'Roster v2 runtimes');
const names = Object.keys(raw);
if (names.length === 0)
throw new RosterV2ValidationError('Roster v2 runtimes must not be empty.');
const result: Record<string, FleetRosterV2Runtime> = {};
for (const name of names.sort(compareCodePoints)) {
const runtime = requiredRuntime(name, 'Roster v2 runtime name');
const config = requiredObject(raw[name], `Roster v2 runtime "${runtime}"`);
assertKnownKeys(config, `Roster v2 runtime "${runtime}"`, RUNTIME_KEYS);
result[runtime] = {
resetCommand: requiredString(
config.reset_command,
`Roster v2 runtime "${runtime}" reset_command`,
),
};
}
return result;
}
function normalizeAgents(
value: unknown,
runtimes: Readonly<Record<string, FleetRosterV2Runtime>>,
): readonly FleetRosterV2Agent[] {
if (!Array.isArray(value) || value.length === 0) {
throw new RosterV2ValidationError('Roster v2 agents must be a non-empty array.');
}
const seen = new Set<string>();
const agents = value.map((candidate: unknown, index: number): FleetRosterV2Agent => {
const raw = requiredObject(candidate, `Roster v2 agents[${index}]`);
assertKnownKeys(raw, `Roster v2 agents[${index}]`, AGENT_KEYS);
const name = requiredIdentifier(raw.name, `Roster v2 agents[${index}] name`);
if (seen.has(name))
throw new RosterV2ValidationError(`Roster v2 has duplicate agent name: ${name}.`);
seen.add(name);
const runtime = requiredRuntime(raw.runtime, `Roster v2 agent "${name}" runtime`);
if (!runtimes[runtime]) {
throw new RosterV2ValidationError(
`Roster v2 agent "${name}" runtime "${runtime}" must be declared in runtimes.`,
);
}
return {
name,
alias: requiredString(raw.alias, `Roster v2 agent "${name}" alias`),
className: requiredPolicyIdentifier(raw.class, `Roster v2 agent "${name}" class`),
runtime,
provider: requiredString(raw.provider, `Roster v2 agent "${name}" provider`),
model: requiredString(raw.model, `Roster v2 agent "${name}" model`),
reasoning: requiredEnum(
raw.reasoning,
`Roster v2 agent "${name}" reasoning`,
ROSTER_V2_REASONING_LEVELS,
),
toolPolicy: requiredPolicyIdentifier(
raw.tool_policy,
`Roster v2 agent "${name}" tool_policy`,
),
workingDirectory: requiredString(
raw.working_directory,
`Roster v2 agent "${name}" working_directory`,
),
persistentPersona: requiredBoolean(
raw.persistent_persona,
`Roster v2 agent "${name}" persistent_persona`,
),
resetBetweenTasks: requiredBoolean(
raw.reset_between_tasks,
`Roster v2 agent "${name}" reset_between_tasks`,
),
lifecycle: normalizeLifecycle(raw.lifecycle, name),
launch: normalizeLaunch(raw.launch, name),
};
});
return agents.sort((left: FleetRosterV2Agent, right: FleetRosterV2Agent): number =>
compareCodePoints(left.name, right.name),
);
}
function normalizeLifecycle(value: unknown, agentName: string): FleetRosterV2Lifecycle {
const raw = requiredObject(value, `Roster v2 agent "${agentName}" lifecycle`);
assertKnownKeys(raw, `Roster v2 agent "${agentName}" lifecycle`, LIFECYCLE_KEYS);
return {
enabled: requiredBoolean(raw.enabled, `Roster v2 agent "${agentName}" lifecycle enabled`),
desiredState: requiredEnum(
raw.desired_state,
`Roster v2 agent "${agentName}" lifecycle desired_state`,
ROSTER_V2_DESIRED_STATES,
),
};
}
function normalizeLaunch(value: unknown, agentName: string): FleetRosterV2Launch {
const raw = requiredObject(value, `Roster v2 agent "${agentName}" launch`);
assertKnownKeys(raw, `Roster v2 agent "${agentName}" launch`, LAUNCH_KEYS);
return { yolo: requiredBoolean(raw.yolo, `Roster v2 agent "${agentName}" launch yolo`) };
}
function requiredObject(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new RosterV2ValidationError(`${label} must be an object.`);
}
return value as Record<string, unknown>;
}
function assertKnownKeys(
value: Record<string, unknown>,
label: string,
allowedKeys: readonly string[],
): void {
const allowed = new Set(allowedKeys);
const unknown = Object.keys(value).filter((key: string): boolean => !allowed.has(key));
if (unknown.length > 0) {
throw new RosterV2ValidationError(`${label} has unsupported field(s): ${unknown.join(', ')}.`);
}
}
function requiredString(value: unknown, label: string): string {
if (typeof value !== 'string' || value.trim() === '') {
throw new RosterV2ValidationError(`${label} is required and must be a non-empty string.`);
}
return value.trim();
}
function requiredIdentifier(value: unknown, label: string): string {
const result = requiredString(value, label);
if (!IDENTIFIER.test(result)) {
throw new RosterV2ValidationError(`Invalid agent name (${label}): ${result}.`);
}
return result;
}
function requiredTmuxSocket(value: unknown, label: string): string {
if (typeof value !== 'string') {
throw new RosterV2ValidationError(`${label} is required and must be a string.`);
}
const result = value.trim();
if (!TMUX_SOCKET_IDENTIFIER.test(result)) {
throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
}
return result;
}
function requiredTmuxIdentifier(value: unknown, label: string): string {
const result = requiredString(value, label);
if (!TMUX_IDENTIFIER.test(result))
throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
return result;
}
function requiredPolicyIdentifier(value: unknown, label: string): string {
const result = requiredString(value, label);
if (!POLICY_IDENTIFIER.test(result))
throw new RosterV2ValidationError(`Invalid ${label}: ${result}.`);
return result;
}
function requiredPositiveInteger(value: unknown, label: string): number {
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
throw new RosterV2ValidationError(`${label} must be a positive integer.`);
}
return value;
}
function requiredBoolean(value: unknown, label: string): boolean {
if (typeof value !== 'boolean') throw new RosterV2ValidationError(`${label} must be a boolean.`);
return value;
}
function requiredRuntime(value: unknown, label: string): RosterV2RuntimeName {
return requiredEnum(value, label, ROSTER_V2_SUPPORTED_RUNTIMES);
}
function requiredEnum<T extends string>(value: unknown, label: string, allowed: readonly T[]): T {
if (typeof value !== 'string' || !allowed.includes(value as T)) {
throw new RosterV2ValidationError(
`${label} must be one of the supported values: ${allowed.join(', ')}.`,
);
}
return value as T;
}
function toSourceShape(roster: FleetRosterV2): Record<string, unknown> {
return {
version: roster.version,
generation: roster.generation,
transport: roster.transport,
tmux: { socket_name: roster.tmux.socketName, holder_session: roster.tmux.holderSession },
defaults: {
working_directory: roster.defaults.workingDirectory,
runtime: roster.defaults.runtime,
},
runtimes: Object.fromEntries(
Object.entries(roster.runtimes)
.sort(([left], [right]): number => compareCodePoints(left, right))
.map(([name, runtime]): [string, unknown] => [
name,
{ reset_command: runtime.resetCommand },
]),
),
agents: [...roster.agents]
.sort((left: FleetRosterV2Agent, right: FleetRosterV2Agent): number =>
compareCodePoints(left.name, right.name),
)
.map(
(agent: FleetRosterV2Agent): Record<string, unknown> => ({
name: agent.name,
alias: agent.alias,
class: agent.className,
runtime: agent.runtime,
provider: agent.provider,
model: agent.model,
reasoning: agent.reasoning,
tool_policy: agent.toolPolicy,
working_directory: agent.workingDirectory,
persistent_persona: agent.persistentPersona,
reset_between_tasks: agent.resetBetweenTasks,
lifecycle: {
enabled: agent.lifecycle.enabled,
desired_state: agent.lifecycle.desiredState,
},
launch: { yolo: agent.launch.yolo },
}),
),
};
}
@@ -0,0 +1,249 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
chmodSync,
mkdirSync,
mkdtempSync,
renameSync,
rmSync,
symlinkSync,
writeFileSync,
type PathLike,
} from 'node:fs';
import type * as NodeFs from 'node:fs';
import type { Stats } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
interface FilesystemRaceState {
afterLstat?: (path: string) => void;
afterOpen?: (path: string) => void;
afterStat?: (path: string, stats: Stats) => Stats;
}
const filesystemRaceState = vi.hoisted<FilesystemRaceState>(() => ({}));
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof NodeFs>();
return {
...actual,
lstatSync: (path: PathLike) => {
const result = actual.lstatSync(path);
filesystemRaceState.afterLstat?.(String(path));
return result;
},
statSync: (path: PathLike) => {
const result = actual.statSync(path);
return filesystemRaceState.afterStat?.(String(path), result) ?? result;
},
openSync: (path: PathLike, flags: string | number, mode?: number) => {
const fd = actual.openSync(path, flags, mode);
filesystemRaceState.afterOpen?.(String(path));
return fd;
},
};
});
import { assertCanonicalContainment, readRegularFileSecure } from './secure-file.js';
describe('secure file reads', () => {
let root: string;
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'mosaic-secure-file-'));
filesystemRaceState.afterLstat = undefined;
filesystemRaceState.afterOpen = undefined;
filesystemRaceState.afterStat = undefined;
});
afterEach(() => {
filesystemRaceState.afterLstat = undefined;
filesystemRaceState.afterOpen = undefined;
filesystemRaceState.afterStat = undefined;
rmSync(root, { recursive: true, force: true });
});
it('rejects canonical path escape', () => {
expect(() => assertCanonicalContainment(root, join(root, '..', 'outside'))).toThrow(
'path escapes managed root',
);
});
// stack#1380: the guard resolves symlinks and validates the resolved target
// instead of refusing any symlink component.
it('permits a symlink ancestor whose resolved target is inside the root', () => {
const external = join(root, 'external');
mkdirSync(external);
writeFileSync(join(external, 'file'), 'external\n');
symlinkSync(external, join(root, 'linked'));
const snapshot = readRegularFileSecure(join(root, 'linked', 'file'), { root });
expect(snapshot.content.toString('utf8')).toBe('external\n');
});
it('permits a symlinked file whose resolved target is inside the root', () => {
const external = join(root, 'external-file');
writeFileSync(external, 'external\n');
symlinkSync(external, join(root, 'linked-file'));
const snapshot = readRegularFileSecure(join(root, 'linked-file'), { root });
expect(snapshot.content.toString('utf8')).toBe('external\n');
});
it('permits a symlink resolving into an additional sanctioned root (split-home roster shape)', () => {
const brain = `${root}-brain`;
mkdirSync(join(brain, 'fleet'), { recursive: true });
writeFileSync(join(brain, 'fleet', 'roster.yaml'), 'roster\n', { mode: 0o600 });
mkdirSync(join(root, 'fleet'));
symlinkSync(join(brain, 'fleet', 'roster.yaml'), join(root, 'fleet', 'roster.yaml'));
const snapshot = readRegularFileSecure(join(root, 'fleet', 'roster.yaml'), {
root,
symlinkTargetRoots: [brain],
});
expect(snapshot.content.toString('utf8')).toBe('roster\n');
});
it('refuses a symlink whose resolved target escapes every sanctioned root', () => {
const outside = mkdtempSync(join(tmpdir(), 'mosaic-secure-outside-'));
try {
mkdirSync(join(root, 'fleet'), { recursive: true });
writeFileSync(join(outside, 'roster.yaml'), 'escaped\n', { mode: 0o600 });
symlinkSync(join(outside, 'roster.yaml'), join(root, 'fleet', 'roster.yaml'));
expect(() => readRegularFileSecure(join(root, 'fleet', 'roster.yaml'), { root })).toThrow(
/symlink target escapes managed roots/,
);
} finally {
rmSync(outside, { recursive: true, force: true });
}
});
it('refuses a group-writable symlink target', () => {
const loose = join(root, 'loose');
mkdirSync(loose);
chmodSync(loose, 0o770); // group-writable bit survives umask via explicit chmod
writeFileSync(join(loose, 'file'), 'loose\n');
symlinkSync(loose, join(root, 'linked-loose'));
expect(() => readRegularFileSecure(join(root, 'linked-loose', 'file'), { root })).toThrow(
/group- or world-writable/,
);
});
it('refuses a symlink target owned by another user', () => {
const external = join(root, 'foreign');
mkdirSync(external);
writeFileSync(join(external, 'file'), 'foreign\n');
symlinkSync(external, join(root, 'linked-foreign'));
filesystemRaceState.afterStat = (path, stats): Stats => {
if (resolve(path) === resolve(external)) {
return { ...stats, uid: stats.uid + 4242 } as Stats;
}
return stats;
};
try {
expect(() => readRegularFileSecure(join(root, 'linked-foreign', 'file'), { root })).toThrow(
/not owned by the current user/,
);
} finally {
filesystemRaceState.afterStat = undefined;
}
});
it('keeps ancestor traversal bound when an opened directory is substituted', () => {
const tools = join(root, 'tools');
const displacedTools = join(root, 'tools.displaced');
const external = join(root, 'external');
const helper = join(tools, 'helper.sh');
mkdirSync(tools);
mkdirSync(external);
writeFileSync(helper, 'trusted\n', { mode: 0o755 });
writeFileSync(join(external, 'helper.sh'), 'external marker\n', { mode: 0o755 });
let substituted = false;
filesystemRaceState.afterOpen = (openedPath: string): void => {
if (substituted || !openedPath.startsWith('/proc/self/fd/')) return;
if (openedPath.split('/').at(-1) !== 'tools') return;
substituted = true;
renameSync(tools, displacedTools);
symlinkSync(external, tools);
};
const result = readRegularFileSecure(helper, { root, executable: true });
expect(substituted).toBe(true);
expect(result.content.toString('utf8')).toBe('trusted\n');
});
it('keeps root selection bound when the opened root is substituted', () => {
const displacedRoot = `${root}.displaced`;
const externalRoot = `${root}.external`;
const helper = join(root, 'helper.sh');
mkdirSync(externalRoot);
writeFileSync(helper, 'trusted root\n', { mode: 0o755 });
writeFileSync(join(externalRoot, 'helper.sh'), 'external root marker\n', { mode: 0o755 });
let substituted = false;
filesystemRaceState.afterOpen = (openedPath: string): void => {
if (substituted || !openedPath.startsWith('/proc/self/fd/')) return;
const match = openedPath.match(/\/([^/]+)$/);
if (match?.[1] !== root.split('/').filter(Boolean).at(-1)) return;
substituted = true;
renameSync(root, displacedRoot);
symlinkSync(externalRoot, root);
};
const result = readRegularFileSecure(helper, { root, executable: true });
expect(substituted).toBe(true);
expect(result.content.toString('utf8')).toBe('trusted root\n');
filesystemRaceState.afterOpen = undefined;
rmSync(root);
renameSync(displacedRoot, root);
});
it('keeps target read and execute validation bound to the opened file', () => {
const file = join(root, 'helper.sh');
const displaced = join(root, 'helper.displaced.sh');
const external = join(root, 'external-helper.sh');
writeFileSync(file, 'trusted target\n', { mode: 0o755 });
writeFileSync(external, 'external target marker\n', { mode: 0o755 });
let substituted = false;
filesystemRaceState.afterOpen = (openedPath: string): void => {
if (substituted || !openedPath.startsWith('/proc/self/fd/')) return;
if (openedPath.split('/').at(-1) !== 'helper.sh') return;
substituted = true;
renameSync(file, displaced);
symlinkSync(external, file);
};
const result = readRegularFileSecure(file, { root, executable: true });
expect(substituted).toBe(true);
expect(result.content.toString('utf8')).toBe('trusted target\n');
});
it('uses a stable redacted executable error while retaining the error code', () => {
const file = join(root, 'helper.sh');
writeFileSync(file, '#!/bin/sh\n', { mode: 0o644 });
try {
readRegularFileSecure(file, { root, executable: true });
throw new Error('expected executable validation to fail');
} catch (error) {
expect(error).toMatchObject({ message: 'managed file is not executable', code: 'EACCES' });
expect(String(error)).not.toContain('/proc/self/fd/');
expect(String(error)).not.toContain(root);
}
});
it('uses effective-identity execute access after regular-file validation', () => {
const file = join(root, 'helper.sh');
writeFileSync(file, '#!/bin/sh\n', { mode: 0o644 });
expect(() => readRegularFileSecure(file, { root, executable: true })).toThrow();
chmodSync(file, 0o755);
expect(readRegularFileSecure(file, { root, executable: true }).content.toString()).toBe(
'#!/bin/sh\n',
);
});
});
+316
View File
@@ -0,0 +1,316 @@
import {
accessSync,
closeSync,
constants,
fstatSync,
lstatSync,
mkdirSync,
openSync,
readFileSync,
readlinkSync,
statSync,
} from 'node:fs';
import { platform } from 'node:os';
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
export interface SecureFileReadOptions {
root: string;
maxBytes?: number;
executable?: boolean;
/**
* Additional roots a symlink component may resolve into (stack#1380).
* Default: only the managed root itself. Every symlink hop is validated —
* containment under the root or one of these roots, current-user ownership,
* no group/world-writable mode — and refusal stays the default for anything
* else. Callers that operate the split-home layout pass the brain home so
* the framework-created roster symlink resolves.
*/
symlinkTargetRoots?: string[];
}
export interface SecureFileSnapshot {
content: Buffer;
mode: number;
dev: number | bigint;
ino: number | bigint;
}
function sameIdentity(
left: { dev: number | bigint; ino: number | bigint },
right: { dev: number | bigint; ino: number | bigint },
): boolean {
return left.dev === right.dev && left.ino === right.ino;
}
function secureFilesystemError(message: string, cause: unknown): Error {
const error = new Error(message);
if (cause instanceof Error && 'code' in cause && typeof cause.code === 'string') {
Object.defineProperty(error, 'code', { value: cause.code, enumerable: true });
}
return error;
}
function closeDescriptors(descriptors: number[]): void {
for (const fd of descriptors.reverse()) {
try {
closeSync(fd);
} catch {
// Best-effort cleanup must not replace the security decision already made.
}
}
}
function procDescriptorPath(fd: number, component?: string): string {
const descriptor = `/proc/self/fd/${fd}`;
return component === undefined ? descriptor : `${descriptor}/${component}`;
}
/**
* Hold each directory while opening its child through Linux proc-fd. The only
* symlink followed is the kernel-owned descriptor link; O_NOFOLLOW protects
* every appended filesystem component from substitution.
*/
function openDirectoryChain(absoluteDirectory: string): { fd: number; descriptors: number[] } {
if (platform() !== 'linux') {
throw new Error('secure descriptor traversal is unsupported on this platform');
}
const descriptors: number[] = [];
try {
let fd = openSync(sep, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
descriptors.push(fd);
for (const component of absoluteDirectory.split(sep).filter(Boolean)) {
fd = openSync(
procDescriptorPath(fd, component),
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
descriptors.push(fd);
if (!fstatSync(fd).isDirectory()) {
throw new Error('secure descriptor traversal encountered a non-directory component');
}
}
return { fd, descriptors };
} catch (error) {
closeDescriptors(descriptors);
throw secureFilesystemError(
'secure descriptor traversal failed: symbolic link, unavailable, or not a directory',
error,
);
}
}
function containedUnder(root: string, target: string): boolean {
const rel = relative(resolve(root), resolve(target));
return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel) && rel !== '';
}
const MAX_SYMLINK_HOPS = 40;
/**
* Resolve every symlink on `lexical` component-wise, validating each hop
* (stack#1380 resolve-then-validate): the hop target must stay under one of
* the sanctioned roots, must be owned by the current user (or root), and must
* not be group- or world-writable. Returns a symlink-free absolute path.
*/
function resolveRealPath(lexical: string, sanctionedRoots: string[]): string {
const hopTargets: string[] = [];
let current: string = sep;
for (const piece of resolve(lexical).split(sep).filter(Boolean)) {
current = resolve(current, piece);
for (let hops = 0; lstatSync(current).isSymbolicLink(); ) {
if (++hops > MAX_SYMLINK_HOPS) {
throw new Error(`symlink chain exceeds ${MAX_SYMLINK_HOPS} hops: ${lexical}`);
}
const linkTarget = readlinkSync(current);
const absolute = resolve(dirname(current), linkTarget);
if (!sanctionedRoots.some((root) => containedUnder(root, absolute))) {
throw new Error(
`symlink target escapes managed roots [${sanctionedRoots.join(', ')}]: ${absolute}`,
);
}
hopTargets.push(absolute);
current = absolute;
}
}
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
for (const hop of hopTargets) {
const stat = statSync(hop);
if (stat.uid !== uid && stat.uid !== 0) {
throw new Error(`symlink target is not owned by the current user: ${hop}`);
}
if (stat.mode & 0o022) {
throw new Error(`symlink target is group- or world-writable: ${hop}`);
}
}
return current;
}
function openFileBeneathRoot(
root: string,
target: string,
symlinkTargetRoots: string[] = [],
): { fd: number; descriptors: number[] } {
const canonicalRoot = resolve(root);
const canonicalTarget = resolve(target);
assertCanonicalContainment(canonicalRoot, canonicalTarget);
const components = relative(canonicalRoot, canonicalTarget).split(sep).filter(Boolean);
const fileName = components.pop();
if (fileName === undefined) throw new Error('managed file path names the managed root');
// stack#1380: resolve-then-validate. The lexical path must name the managed
// root (above); symlink components are then resolved hop-by-hop under the
// sanctioned roots (validated per hop), and the descriptor traversal walks
// the symlink-free real path — keeping the O_NOFOLLOW chain as the race
// guard for anything substituted after resolution.
let realRoot: string;
try {
realRoot = resolveRealPath(canonicalRoot, [canonicalRoot]);
} catch (error) {
throw secureFilesystemError(
'secure descriptor traversal failed: symbolic link, unavailable, or not a directory',
error,
);
}
const sanctioned = [realRoot, ...symlinkTargetRoots.map((extra) => resolve(extra))];
let realTarget: string;
try {
realTarget = resolveRealPath(canonicalTarget, sanctioned);
} catch (error) {
if (error instanceof Error && !('code' in error)) throw error;
throw secureFilesystemError(
'secure descriptor traversal failed: symbolic link, unavailable, or not a directory',
error,
);
}
if (!sanctioned.some((sr) => containedUnder(sr, realTarget) || resolve(sr) === realTarget)) {
throw new Error(
`resolved path escapes managed roots [${sanctioned.join(', ')}]: ${realTarget}`,
);
}
const chain = openDirectoryChain(dirname(realTarget));
try {
let fd: number;
try {
fd = openSync(
procDescriptorPath(chain.fd, basename(realTarget)),
constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW,
);
} catch (error) {
throw secureFilesystemError('file is a symbolic link or unavailable', error);
}
chain.descriptors.push(fd);
return { fd, descriptors: chain.descriptors };
} catch (error) {
closeDescriptors(chain.descriptors);
if (error instanceof Error) throw error;
throw new Error('secure managed file open failed');
}
}
export function assertCanonicalContainment(root: string, target: string): void {
const canonicalRoot = resolve(root);
const canonicalTarget = resolve(target);
const rel = relative(canonicalRoot, canonicalTarget);
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
throw new Error(`path escapes managed root ${canonicalRoot}: ${canonicalTarget}`);
}
}
/** Reject every symlink from the filesystem root through the target's parent. */
export function assertNoSymlinkAncestors(target: string): void {
const absolute = resolve(target);
const parent = dirname(absolute);
const pieces = parent.split(sep).filter(Boolean);
let cursor: string = sep;
for (const piece of pieces) {
cursor = resolve(cursor, piece);
const stat = lstatSync(cursor);
if (stat.isSymbolicLink()) throw new Error(`path ancestor is a symbolic link: ${cursor}`);
if (!stat.isDirectory()) throw new Error(`path ancestor is not a directory: ${cursor}`);
}
}
export function ensureManagedDirectory(root: string, directory: string): void {
assertCanonicalContainment(root, directory);
const canonicalRoot = resolve(root);
const canonicalDirectory = resolve(directory);
assertNoSymlinkAncestors(canonicalRoot);
try {
const rootStat = lstatSync(canonicalRoot);
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
throw new Error(`managed root is not a real directory: ${canonicalRoot}`);
}
} catch (error) {
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
mkdirSync(canonicalRoot, { mode: 0o700 });
const rootStat = lstatSync(canonicalRoot);
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
throw new Error(`managed root creation was redirected: ${canonicalRoot}`);
}
}
const rel = relative(canonicalRoot, canonicalDirectory);
let cursor = canonicalRoot;
for (const piece of rel.split(sep).filter(Boolean)) {
cursor = resolve(cursor, piece);
try {
const stat = lstatSync(cursor);
if (stat.isSymbolicLink()) throw new Error(`path ancestor is a symbolic link: ${cursor}`);
if (!stat.isDirectory()) throw new Error(`path ancestor is not a directory: ${cursor}`);
} catch (error) {
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
mkdirSync(cursor, { mode: 0o700 });
const created = lstatSync(cursor);
if (!created.isDirectory() || created.isSymbolicLink()) {
throw new Error(`managed directory creation was redirected: ${cursor}`);
}
}
}
}
/**
* Read a regular file through an O_NOFOLLOW descriptor. The inode is checked
* before and after access/read, and executable access is tested against the
* already-open descriptor so path replacement cannot redirect the check.
*/
export function readRegularFileSecure(
path: string,
options: SecureFileReadOptions,
): SecureFileSnapshot {
const openedFile = openFileBeneathRoot(options.root, path, options.symlinkTargetRoots ?? []);
try {
const opened = fstatSync(openedFile.fd);
if (!opened.isFile()) throw new Error('managed file is not a regular file');
if (options.maxBytes !== undefined && opened.size > options.maxBytes) {
throw new Error(`managed file exceeds ${options.maxBytes} bytes`);
}
if (options.executable) {
try {
accessSync(procDescriptorPath(openedFile.fd), constants.X_OK);
} catch (error) {
throw secureFilesystemError('managed file is not executable', error);
}
const afterAccess = fstatSync(openedFile.fd);
if (!afterAccess.isFile() || !sameIdentity(opened, afterAccess)) {
throw new Error('managed file changed during executable access check');
}
}
const content = readFileSync(openedFile.fd);
const after = fstatSync(openedFile.fd);
if (!after.isFile() || !sameIdentity(opened, after)) {
throw new Error('managed file changed during secure read');
}
if (options.maxBytes !== undefined && content.byteLength > options.maxBytes) {
throw new Error(`managed file exceeds ${options.maxBytes} bytes`);
}
return {
content,
mode: Number(opened.mode),
dev: opened.dev,
ino: opened.ino,
};
} finally {
closeDescriptors(openedFile.descriptors);
}
}
@@ -0,0 +1,161 @@
import { execFile } from 'node:child_process';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { describe, expect, it } from 'vitest';
import { generateAgentEnv, loadFleetRoster } from '../commands/fleet.js';
import {
provisionInteractionService,
readInteractionServiceProfile,
} from './interaction-service-profile.js';
import type { InteractionServiceProfileError } from './interaction-service-profile.js';
const frameworkFleet = resolve(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'framework',
'fleet',
);
const profilePath = join(frameworkFleet, 'services', 'operator-interaction.yaml');
const examplePath = join(frameworkFleet, 'examples', 'operator-interaction.yaml');
const policyScript = join(
frameworkFleet,
'..',
'tools',
'fleet',
'print-interaction-effective-policy.sh',
);
const interactionStartScript = join(
frameworkFleet,
'..',
'tools',
'fleet',
'start-interaction-service.sh',
);
const agentStartScript = join(frameworkFleet, '..', 'tools', 'fleet', 'start-agent-session.sh');
const execFileAsync = promisify(execFile);
describe('operator interaction service profile', (): void => {
it('provisions a user-supplied Nova identity as data with the required effective policy', async (): Promise<void> => {
const profile = await readInteractionServiceProfile(profilePath);
const provisioned = provisionInteractionService(profile, { agentName: 'Nova' });
expect(provisioned.rosterAgent).toEqual({
name: 'Nova',
runtime: 'pi',
className: 'operator-interaction',
modelHint: 'openai/gpt-5.6-sol',
reasoningLevel: 'high',
toolPolicy: 'operator-interaction',
persistentPersona: true,
});
expect(provisioned.effectivePolicy).toEqual({
agentName: 'Nova',
runtime: 'pi',
model: 'openai/gpt-5.6-sol',
reasoning: 'high',
toolPolicy: 'operator-interaction',
});
expect(JSON.stringify(provisioned.effectivePolicy)).not.toMatch(
/token|secret|credential|api.?key/i,
);
});
it('accepts a renamed example roster and surfaces only its effective policy', async (): Promise<void> => {
const directory = await mkdtemp(join(tmpdir(), 'mosaic-interaction-profile-'));
const rosterPath = join(directory, 'roster.yaml');
try {
const example = await readFile(examplePath, 'utf8');
await writeFile(rosterPath, example.replace('name: Tess', 'name: Nova'), 'utf8');
const roster = await loadFleetRoster(rosterPath);
const agent = roster.agents[0]!;
expect(agent.name).toBe('Nova');
expect(agent.reasoningLevel).toBe('high');
expect(agent.toolPolicy).toBe('operator-interaction');
expect(generateAgentEnv(roster, agent)).toContain('MOSAIC_AGENT_NAME=Nova');
expect(generateAgentEnv(roster, agent)).toContain('MOSAIC_AGENT_REASONING=high');
const { stdout } = await execFileAsync(policyScript, [], {
env: {
...process.env,
MOSAIC_AGENT_NAME: agent.name,
MOSAIC_AGENT_RUNTIME: agent.runtime,
MOSAIC_AGENT_MODEL: agent.modelHint,
MOSAIC_AGENT_REASONING: agent.reasoningLevel,
MOSAIC_AGENT_TOOL_POLICY: agent.toolPolicy,
},
});
expect(JSON.parse(stdout)).toEqual({
agentName: 'Nova',
runtime: 'pi',
model: 'openai/gpt-5.6-sol',
reasoning: 'high',
toolPolicy: 'operator-interaction',
});
} finally {
await rm(directory, { recursive: true, force: true });
}
});
it('fails before launch when service environment drifts from the pinned policy', async (): Promise<void> => {
await expect(
execFileAsync(interactionStartScript, ['Nova'], {
env: {
...process.env,
MOSAIC_AGENT_NAME: 'Nova',
MOSAIC_AGENT_RUNTIME: 'pi',
MOSAIC_AGENT_MODEL: 'other/model',
MOSAIC_AGENT_REASONING: 'high',
MOSAIC_AGENT_TOOL_POLICY: 'operator-interaction',
},
}),
).rejects.toMatchObject({ code: 64 });
});
it('rejects unsafe names from the policy printer and reasoning before tmux launch', async (): Promise<void> => {
await expect(
execFileAsync(policyScript, [], {
env: {
...process.env,
MOSAIC_AGENT_NAME: 'Nova"bad',
MOSAIC_AGENT_RUNTIME: 'pi',
MOSAIC_AGENT_MODEL: 'openai/gpt-5.6-sol',
MOSAIC_AGENT_REASONING: 'high',
MOSAIC_AGENT_TOOL_POLICY: 'operator-interaction',
},
}),
).rejects.toMatchObject({ code: 64 });
await expect(
execFileAsync(agentStartScript, ['Nova'], {
env: { ...process.env, MOSAIC_AGENT_REASONING: 'high; id' },
}),
).rejects.toMatchObject({ code: 64 });
});
it('keeps the product name confined to an example instance, not service source or defaults', async (): Promise<void> => {
const [profile, source, example] = await Promise.all([
readFile(profilePath, 'utf8'),
readFile(new URL('./interaction-service-profile.ts', import.meta.url), 'utf8'),
readFile(examplePath, 'utf8'),
]);
expect(profile).not.toMatch(/tess/i);
expect(source).not.toMatch(/tess/i);
expect(example).toMatch(/name: Tess/);
});
it('fails fast when a required policy field is absent or changed from the pinned policy', async (): Promise<void> => {
await expect(readInteractionServiceProfile(profilePath, { model: '' })).rejects.toMatchObject({
code: 'invalid_profile',
} satisfies Partial<InteractionServiceProfileError>);
await expect(
readInteractionServiceProfile(profilePath, { reasoning: 'medium' }),
).rejects.toMatchObject({
code: 'invalid_profile',
} satisfies Partial<InteractionServiceProfileError>);
});
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,160 @@
import { describe, expect, it, vi } from 'vitest';
import type { CommandResult, CommandRunner, FleetRoster } from '../commands/fleet.js';
import { FleetTmuxRuntimeTransport } from './tmux-runtime-transport.js';
import type { FleetRuntimeTransportError } from './tmux-runtime-transport.js';
const roster: FleetRoster = {
version: 1,
transport: 'tmux',
tmux: { socketName: 'tess-fleet', holderSession: '_holder' },
defaults: { workingDirectory: '~/src' },
runtimes: { codex: { resetCommand: '/clear' } },
agents: [{ name: 'coder0', runtime: 'codex', className: 'code' }],
};
function commandResult(stdout = '', exitCode = 0, stderr = ''): CommandResult {
return { stdout, stderr, exitCode };
}
describe('FleetTmuxRuntimeTransport security boundary', (): void => {
it('rejects a prefix target before it invokes tmux', async (): Promise<void> => {
const runner = vi.fn<CommandRunner>(async (): Promise<CommandResult> => commandResult());
const transport = new FleetTmuxRuntimeTransport({
rosterLoader: async (): Promise<FleetRoster> => roster,
runner,
mosaicHome: '/mosaic',
});
await expect(transport.verifySession('coder')).rejects.toMatchObject({
code: 'not_found',
} satisfies Partial<FleetRuntimeTransportError>);
expect(runner).not.toHaveBeenCalled();
});
it('uses the roster socket and exact pane target while verifying peer identity', async (): Promise<void> => {
const runner = vi.fn<CommandRunner>(
async (): Promise<CommandResult> => commandResult('111 codex 0 0 0 0\n'),
);
const transport = new FleetTmuxRuntimeTransport({
rosterLoader: async (): Promise<FleetRoster> => roster,
runner,
mosaicHome: '/mosaic',
});
await expect(transport.verifySession('coder0')).resolves.toEqual({
id: 'coder0',
runtimeId: 'codex',
socketName: 'tess-fleet',
});
expect(runner).toHaveBeenCalledWith('tmux', [
'-L',
'tess-fleet',
'list-panes',
'-t',
'=coder0:0.0',
'-F',
'#{pane_pid} #{pane_current_command} #{pane_dead} #{pane_activity} #{window_activity} #{session_activity}',
]);
});
it('denies a live pane whose runtime does not match the roster identity', async (): Promise<void> => {
const runner = vi.fn<CommandRunner>(
async (): Promise<CommandResult> => commandResult('111 python3 0 0 0 0\n'),
);
const transport = new FleetTmuxRuntimeTransport({
rosterLoader: async (): Promise<FleetRoster> => roster,
runner,
mosaicHome: '/mosaic',
});
await expect(transport.verifySession('coder0')).rejects.toMatchObject({
code: 'forbidden',
} satisfies Partial<FleetRuntimeTransportError>);
expect(runner).toHaveBeenCalledTimes(1);
});
it('surfaces a runtime identity mismatch instead of hiding it from fleet status', async (): Promise<void> => {
const rosterWithMismatchedPeer: FleetRoster = {
...roster,
agents: [...roster.agents, { name: 'coder1', runtime: 'codex', className: 'code' }],
};
const runner = vi.fn<CommandRunner>(
async (_command: string, args: string[]): Promise<CommandResult> =>
commandResult(
args.includes('=coder1:0.0') ? '222 python3 0 0 0 0\n' : '111 codex 0 0 0 0\n',
),
);
const transport = new FleetTmuxRuntimeTransport({
rosterLoader: async (): Promise<FleetRoster> => rosterWithMismatchedPeer,
runner,
mosaicHome: '/mosaic',
});
await expect(transport.listSessions()).rejects.toMatchObject({
code: 'forbidden',
} satisfies Partial<FleetRuntimeTransportError>);
});
it('denies sending when the roster socket does not contain the exact target', async (): Promise<void> => {
const runner = vi.fn<CommandRunner>(
async (): Promise<CommandResult> => commandResult('', 1, "can't find session: coder0"),
);
const transport = new FleetTmuxRuntimeTransport({
rosterLoader: async (): Promise<FleetRoster> => roster,
runner,
mosaicHome: '/mosaic',
});
await expect(transport.sendMessage('coder0', 'hello', 'tess')).rejects.toMatchObject({
code: 'unavailable',
} satisfies Partial<FleetRuntimeTransportError>);
expect(runner).toHaveBeenCalledTimes(1);
});
it('sends only through the maintained sender after exact target and identity verification', async (): Promise<void> => {
const runner = vi
.fn<CommandRunner>()
.mockResolvedValueOnce(commandResult('111 codex 0 0 0 0\n'))
.mockResolvedValueOnce(commandResult());
const transport = new FleetTmuxRuntimeTransport({
rosterLoader: async (): Promise<FleetRoster> => roster,
runner,
mosaicHome: '/mosaic',
});
await transport.sendMessage('coder0', 'hello', 'tess');
expect(runner).toHaveBeenNthCalledWith(2, '/mosaic/tools/tmux/agent-send.sh', [
'-L',
'tess-fleet',
'-S',
'tess',
'-s',
'coder0',
'-m',
'hello',
]);
});
it('terminates only the exact roster target after identity verification', async (): Promise<void> => {
const runner = vi
.fn<CommandRunner>()
.mockResolvedValueOnce(commandResult('111 codex 0 0 0 0\n'))
.mockResolvedValueOnce(commandResult());
const transport = new FleetTmuxRuntimeTransport({
rosterLoader: async (): Promise<FleetRoster> => roster,
runner,
mosaicHome: '/mosaic',
});
await transport.terminate('coder0');
expect(runner).toHaveBeenNthCalledWith(2, 'tmux', [
'-L',
'tess-fleet',
'kill-session',
'-t',
'=coder0',
]);
});
});
@@ -0,0 +1,200 @@
import {
buildAgentSendCommand,
buildTmuxListPanesCommand,
getRosterAgent,
loadFleetRoster,
parseTmuxListPanes,
resolveFleetPaths,
RUNTIME_ACCEPTABLE_COMMANDS,
socketArgs,
type CommandResult,
type CommandRunner,
type FleetRoster,
} from '../commands/fleet.js';
export type FleetRuntimeTransportErrorCode =
| 'forbidden'
| 'invalid_request'
| 'not_found'
| 'unavailable';
/** A roster-bound, verified tmux target. It never accepts a caller-selected socket. */
export interface FleetRuntimeTarget {
id: string;
runtimeId: string;
socketName: string;
}
/** Narrow transport boundary consumed by the runtime provider. */
export interface FleetRuntimeTransport {
verifySession(sessionId: string): Promise<FleetRuntimeTarget>;
listSessions(): Promise<FleetRuntimeTarget[]>;
sendMessage(sessionId: string, message: string, sourceLabel: string): Promise<void>;
terminate(sessionId: string): Promise<void>;
}
export interface FleetTmuxRuntimeTransportOptions {
mosaicHome: string;
rosterPath?: string;
rosterLoader?: () => Promise<FleetRoster>;
runner: CommandRunner;
}
/** A typed, fail-closed error for roster, target, identity, and tmux failures. */
export class FleetRuntimeTransportError extends Error {
constructor(
readonly code: FleetRuntimeTransportErrorCode,
message: string,
) {
super(message);
this.name = FleetRuntimeTransportError.name;
}
}
/**
* Roster-bound transport for the local fleet tmux server. Every side-effecting
* operation verifies an exact roster name, the roster socket, and the runtime
* command in the exact pane before it invokes the maintained sender or tmux.
*/
export class FleetTmuxRuntimeTransport implements FleetRuntimeTransport {
private readonly rosterLoader: () => Promise<FleetRoster>;
constructor(private readonly options: FleetTmuxRuntimeTransportOptions) {
this.rosterLoader =
options.rosterLoader ??
(() =>
loadFleetRoster(options.rosterPath ?? resolveFleetPaths(options.mosaicHome).rosterPath));
}
async verifySession(sessionId: string): Promise<FleetRuntimeTarget> {
const roster = await this.loadRoster();
return this.verifyRosterSession(roster, sessionId);
}
async listSessions(): Promise<FleetRuntimeTarget[]> {
const roster = await this.loadRoster();
const targets = await Promise.all(
roster.agents.map(async (agent): Promise<FleetRuntimeTarget | undefined> => {
try {
return await this.verifyRosterSession(roster, agent.name);
} catch (error: unknown) {
if (error instanceof FleetRuntimeTransportError && error.code === 'unavailable') {
return undefined;
}
throw error;
}
}),
);
return targets.filter((target): target is FleetRuntimeTarget => target !== undefined);
}
async sendMessage(sessionId: string, message: string, sourceLabel: string): Promise<void> {
if (message.length === 0) {
throw new FleetRuntimeTransportError('invalid_request', 'Fleet message content is required');
}
if (sourceLabel.length === 0) {
throw new FleetRuntimeTransportError(
'invalid_request',
'Fleet message source label is required',
);
}
const target = await this.verifySession(sessionId);
const command = buildAgentSendCommand(
resolveFleetPaths(this.options.mosaicHome),
target.id,
message,
target.socketName,
sourceLabel,
);
await this.runChecked(command, 'Fleet message delivery failed');
}
async terminate(sessionId: string): Promise<void> {
const target = await this.verifySession(sessionId);
await this.runChecked(
['tmux', ...socketArgs(target.socketName), 'kill-session', '-t', `=${target.id}`],
'Fleet session termination failed',
);
}
private async loadRoster(): Promise<FleetRoster> {
try {
return await this.rosterLoader();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Fleet roster is unavailable';
throw new FleetRuntimeTransportError(
'unavailable',
`Fleet roster is unavailable: ${message}`,
);
}
}
private async verifyRosterSession(
roster: FleetRoster,
sessionId: string,
): Promise<FleetRuntimeTarget> {
const agent = this.exactRosterAgent(roster, sessionId);
const socketName = roster.tmux.socketName;
const result = await this.run(buildTmuxListPanesCommand(agent.name, socketName));
if (result.exitCode !== 0) {
throw new FleetRuntimeTransportError(
'unavailable',
`Fleet session is unavailable: ${agent.name}`,
);
}
const pane = parseTmuxListPanes(result.stdout);
if (pane.dead || pane.command === null) {
throw new FleetRuntimeTransportError(
'unavailable',
`Fleet session is unavailable: ${agent.name}`,
);
}
if (!hasExactRuntimeIdentity(agent.runtime, pane.command)) {
throw new FleetRuntimeTransportError(
'forbidden',
`Fleet runtime identity mismatch: ${agent.name}`,
);
}
return { id: agent.name, runtimeId: agent.runtime, socketName };
}
private exactRosterAgent(roster: FleetRoster, sessionId: string): FleetRoster['agents'][number] {
try {
const agent = getRosterAgent(roster, sessionId);
if (agent.name !== sessionId) {
throw new FleetRuntimeTransportError('not_found', 'Fleet session target must be exact');
}
return agent;
} catch (error: unknown) {
if (error instanceof FleetRuntimeTransportError) {
throw error;
}
throw new FleetRuntimeTransportError('not_found', 'Fleet session target is not roster-bound');
}
}
private async run(command: string[]): Promise<CommandResult> {
const [executable, ...args] = command;
if (executable === undefined) {
throw new FleetRuntimeTransportError('invalid_request', 'Fleet command is required');
}
try {
return await this.options.runner(executable, args);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Fleet command failed';
throw new FleetRuntimeTransportError('unavailable', message);
}
}
private async runChecked(command: string[], failureMessage: string): Promise<void> {
const result = await this.run(command);
if (result.exitCode !== 0) {
throw new FleetRuntimeTransportError('unavailable', failureMessage);
}
}
}
function hasExactRuntimeIdentity(runtimeId: string, paneCommand: string): boolean {
const expectedCommands = RUNTIME_ACCEPTABLE_COMMANDS[runtimeId];
return expectedCommands !== undefined && expectedCommands.includes(paneCommand);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff