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