424 lines
14 KiB
TypeScript
424 lines
14 KiB
TypeScript
/**
|
|
* 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 } from 'node:path';
|
|
import { readRegularFileSecure } from './secure-file.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 });
|
|
return undefined;
|
|
} catch (error) {
|
|
const reason = error instanceof Error ? error.message : String(error);
|
|
return `helper is unavailable or unsafe: ${path} (${reason})`;
|
|
}
|
|
}
|
|
|
|
function helperFailureGuidance(reason: string): string {
|
|
return `${reason}. Run \`mosaic update --repair-tools\` to restore the supported current-version helper and TOOLS contract, then retry exact-member composition; 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 agentSendPath = join(mosaicHome, 'tools', 'tmux', 'agent-send.sh');
|
|
const helperError = validateAgentSendHelper(agentSendPath, mosaicHome);
|
|
if (helperError) return { ok: false, error: helperFailureGuidance(helperError) };
|
|
|
|
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 }).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 {
|
|
return `Run \`mosaic update --repair-tools\` to make a digest-qualified backup and restore the supported current-version TOOLS contract, then have an authorized 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');
|