fix(fleet): enforce exact comms authority (#787)
This commit was merged in pull request #787.
This commit is contained in:
@@ -1,226 +1,423 @@
|
||||
/**
|
||||
* Fleet onboarding-injection (#620).
|
||||
* Exact roster-resolved fleet communications contract (#766).
|
||||
*
|
||||
* Fleet agents are born not knowing how to reach their peers — the root cause of
|
||||
* a spawned agent's failed first send. When an agent boots via `mosaic yolo
|
||||
* <runtime>` (→ composeContract → system prompt), we append a comms cheat-sheet
|
||||
* + peer roster so it can talk to the orchestrator and other agents immediately.
|
||||
*
|
||||
* Cross-host aware: a peer may carry `host`/`ssh` (a deliberate pre-federation
|
||||
* stopgap — manual cross-host listing; federation/W1 auto-discovers later), so a
|
||||
* w-jarvis agent is born knowing the exact `-H` command to reach a dragon-lin
|
||||
* peer. Same-host peers render the short form.
|
||||
*
|
||||
* Standalone (no fleet.ts import) to keep launch.ts's prompt path free of the
|
||||
* heavy fleet command module. The roster is parsed leniently — the cheat-sheet
|
||||
* is best-effort onboarding, never a hard dependency.
|
||||
* 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 { existsSync, readFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { homedir, hostname } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export interface CommsPeer {
|
||||
name: string;
|
||||
/** Roster `class` (orchestrator | enhancer | implementer | worker | …). */
|
||||
className: string;
|
||||
/** Host the peer runs on; absent ⇒ the fleet host (same host). */
|
||||
host?: string;
|
||||
/** SSH target (user@host) for a cross-host peer; renders the `-H` form. */
|
||||
ssh?: string;
|
||||
/** tmux socket the peer's session lives on; absent ⇒ default socket (no `-L`). */
|
||||
socket?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lenient parse of a fleet `roster.yaml` for agent name/class/host/ssh. Avoids a
|
||||
* dependency on the full fleet roster parser; the format is `- name:` list items
|
||||
* with `class:`/`host:`/`ssh:` siblings under `agents:`.
|
||||
*/
|
||||
export function parseRosterAgents(yamlText: string): CommsPeer[] {
|
||||
const peers: CommsPeer[] = [];
|
||||
let current: CommsPeer | null = null;
|
||||
let inAgents = false;
|
||||
const scalar = (line: string, key: string): string | null => {
|
||||
const m = line.match(new RegExp(`^\\s*${key}:\\s*["']?([^"'#]+?)["']?\\s*$`));
|
||||
return m ? (m[1] as string).trim() : null;
|
||||
};
|
||||
for (const rawLine of yamlText.split('\n')) {
|
||||
const line = rawLine.replace(/\s+$/, '');
|
||||
if (/^agents:\s*$/.test(line)) {
|
||||
inAgents = true;
|
||||
continue;
|
||||
}
|
||||
if (!inAgents) continue;
|
||||
// A new top-level key (no leading space) ends the agents block.
|
||||
if (/^\S/.test(line)) break;
|
||||
|
||||
const nameMatch = line.match(/^\s*-\s*name:\s*["']?([A-Za-z0-9._-]+)["']?\s*$/);
|
||||
if (nameMatch) {
|
||||
if (current) peers.push(current);
|
||||
current = { name: nameMatch[1] as string, className: 'worker' };
|
||||
continue;
|
||||
}
|
||||
if (!current) continue;
|
||||
const cls = scalar(line, 'class');
|
||||
if (cls) current.className = cls;
|
||||
const host = scalar(line, 'host');
|
||||
if (host) current.host = host;
|
||||
const ssh = scalar(line, 'ssh');
|
||||
if (ssh) current.ssh = ssh;
|
||||
const socket = scalar(line, 'socket');
|
||||
if (socket) current.socket = socket;
|
||||
}
|
||||
if (current) peers.push(current);
|
||||
return peers;
|
||||
}
|
||||
import { readRegularFileSecure } from './secure-file.js';
|
||||
import {
|
||||
parseFleetRosterV1,
|
||||
resolveInstalledFleetRosterPath,
|
||||
getRosterAgent,
|
||||
type FleetAgent,
|
||||
type FleetRoster,
|
||||
} from './fleet-roster-v1.js';
|
||||
|
||||
export interface FleetCommsOptions {
|
||||
/** This agent's name (it is excluded from its own peer list). */
|
||||
/** Exact current roster member. */
|
||||
selfName: string;
|
||||
/** All roster agents (including self; filtered out internally). */
|
||||
agents: CommsPeer[];
|
||||
/** Host the fleet runs on (short hostname) — the same-host baseline. */
|
||||
fleetHost: string;
|
||||
/** Absolute path to agent-send.sh in this install. */
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/** Is this peer on a different host than the fleet baseline? */
|
||||
function isRemote(peer: CommsPeer, fleetHost: string): boolean {
|
||||
return peer.host !== undefined && peer.host !== fleetHost;
|
||||
export interface CommsBlockResult {
|
||||
ok: boolean;
|
||||
output: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the exact agent-send command to reach a peer (session = agent name).
|
||||
* Data-driven per peer: a named `socket` → `-L <socket>`; an unset socket → the
|
||||
* default tmux socket (no `-L`). A cross-host peer adds `-H <ssh|host>`.
|
||||
*/
|
||||
export function renderPeerReach(peer: CommsPeer, fleetHost: string, agentSendPath: string): string {
|
||||
const parts = [agentSendPath];
|
||||
if (peer.socket) parts.push('-L', peer.socket); // unset ⇒ default socket, no -L
|
||||
if (isRemote(peer, fleetHost)) parts.push('-H', peer.ssh ?? (peer.host as string));
|
||||
parts.push('-s', peer.name, '-m', '"…"');
|
||||
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(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `# Fleet Comms` onboarding block (pure markdown). Returns '' when
|
||||
* the agent has no peers (a single-agent roster has no one to talk to).
|
||||
*/
|
||||
/** 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 peers = opts.agents.filter((a) => a.name !== opts.selfName);
|
||||
if (peers.length === 0) return '';
|
||||
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`
|
||||
: '';
|
||||
|
||||
const orchestrator = peers.find((p) => p.className === 'orchestrator');
|
||||
const rows = peers
|
||||
.map((p) => {
|
||||
const where = isRemote(p, opts.fleetHost)
|
||||
? `${p.className} · host \`${p.host}\``
|
||||
: p.className;
|
||||
const role = p.className === 'orchestrator' ? `${where} ← point of contact` : where;
|
||||
return `| ${p.name} | ${role} | \`${renderPeerReach(p, opts.fleetHost, opts.agentSendPath)}\` |`;
|
||||
})
|
||||
.join('\n');
|
||||
return `# Fleet Comms — authoritative exact targets
|
||||
|
||||
const orchLine = orchestrator
|
||||
? `Your point of contact is **${orchestrator.name}** (the orchestrator) — route questions, ` +
|
||||
`status, and decisions there.`
|
||||
: `This fleet has no orchestrator in its roster; coordinate with your peers directly.`;
|
||||
## Local identity
|
||||
|
||||
return `# Fleet Comms — reach your peers
|
||||
- Host: \`${selfHost}\`
|
||||
- Agent/session: \`${self.name}\`
|
||||
- Role/class: \`${self.className}\`
|
||||
- tmux socket: \`${displaySocket(selfSocket)}\`
|
||||
- Helper: \`${opts.agentSendPath}\`
|
||||
- Comms generation: \`${generation}\`
|
||||
|
||||
You are **${opts.selfName}** in this fleet. Your comms identity is \`[${opts.fleetHost}:${opts.selfName}]\` —
|
||||
that is the \`<src>\` other agents see and reply to. Reach other agents (durable tmux sessions) with the
|
||||
Mosaic comms tool at \`${opts.agentSendPath}\`. The **Reach** column below is the exact command per peer:
|
||||
same-host peers use the short form (no \`-H\`); cross-host peers include \`-H <user@host>\`.
|
||||
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
|
||||
|
||||
| Agent | Role | Reach (session = agent name) |
|
||||
| ----- | ---- | ---------------------------- |
|
||||
${rows}
|
||||
${peerSection}
|
||||
|
||||
${orchLine}
|
||||
${contact}
|
||||
${soloAuthority}
|
||||
## Context freshness
|
||||
|
||||
## Conventions
|
||||
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.`;
|
||||
}
|
||||
|
||||
- Every message carries a self-identifying preamble \`[<src_host>:<src_session> -> <dst_host>:<dst_session>]\` — \`agent-send.sh\` adds it automatically.
|
||||
- **To reply, FLIP the preamble:** address your reply to the sender's \`src\` (their host:session becomes your \`-s\`/\`-H\`).
|
||||
- \`agent-send.sh\` (a.k.a. \`agent send --verify\`) confirms the message was **ACCEPTED** at the destination prompt — not merely injected. Prefer it for anything that matters.`;
|
||||
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 the fleet roster from `mosaicHome` and build the comms block for
|
||||
* `selfName`. Returns '' when there is no roster, the agent is not in it, or
|
||||
* there are no peers — onboarding is best-effort and never throws.
|
||||
* 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,
|
||||
fleetHost: string = hostname().split('.')[0] || 'localhost',
|
||||
): string {
|
||||
if (!selfName) return '';
|
||||
const rosterPath = join(mosaicHome, 'fleet', 'roster.yaml');
|
||||
if (!existsSync(rosterPath)) return '';
|
||||
let text: string;
|
||||
try {
|
||||
text = readFileSync(rosterPath, 'utf-8');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
const agents = parseRosterAgents(text);
|
||||
if (!agents.some((a) => a.name === selfName)) return ''; // not a member of this fleet
|
||||
return buildFleetCommsBlock({
|
||||
selfName,
|
||||
agents,
|
||||
fleetHost,
|
||||
agentSendPath: join(mosaicHome, 'tools', 'tmux', 'agent-send.sh'),
|
||||
});
|
||||
}
|
||||
|
||||
/** Result of resolving a comms-block emit request — see `mosaic fleet comms-block`. */
|
||||
export interface CommsBlockResult {
|
||||
/** True when a cheat-sheet was produced; false maps to stderr + non-zero exit. */
|
||||
ok: boolean;
|
||||
/** The Fleet-Comms cheat-sheet (empty unless ok). */
|
||||
output: string;
|
||||
/** Operator-facing reason when !ok. */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Fleet-Comms cheat-sheet for an explicit <role>, backing the
|
||||
* `mosaic fleet comms-block <role>` command. Unlike readFleetCommsBlock — which
|
||||
* returns '' on any miss so composeContract can no-op silently during a launch —
|
||||
* this NEVER silently emits empty: an unknown role or missing roster yields
|
||||
* ok:false + an operator-facing reason, so the CLI surfaces it (stderr + exit 1)
|
||||
* rather than printing nothing. That makes it safe to preview any peer's view,
|
||||
* e.g. `mosaic fleet comms-block coder0-0`.
|
||||
*/
|
||||
export function resolveCommsBlock(
|
||||
mosaicHome: string,
|
||||
role: string | undefined,
|
||||
fleetHost?: string,
|
||||
localHost: string = shortHostname(),
|
||||
): CommsBlockResult {
|
||||
if (!role) {
|
||||
return { ok: false, output: '', error: 'comms-block requires a <role> argument' };
|
||||
}
|
||||
const block = fleetHost
|
||||
? readFleetCommsBlock(mosaicHome, role, fleetHost)
|
||||
: readFleetCommsBlock(mosaicHome, role);
|
||||
if (!block) {
|
||||
const rosterPath = join(mosaicHome, 'fleet', 'roster.yaml');
|
||||
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: existsSync(rosterPath)
|
||||
? `role "${role}" is not a member of the fleet roster at ${rosterPath}`
|
||||
: `no fleet roster at ${rosterPath}`,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
return { ok: true, output: block };
|
||||
}
|
||||
|
||||
/** Default mosaic home (mirrors launch.ts), for callers that don't pass one. */
|
||||
/** 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');
|
||||
|
||||
Reference in New Issue
Block a user