/* RFC-0292: the Fleet Agent Catalog — a deterministic projection of every site's public Agent Surface Manifest into one workspace-level discovery document. Pure, framework-free assembly: no I/O, no timestamps, sorted-key canonical hashing so two runs on unchanged input are byte-identical. Consumed by fleet.agent.catalog.generate (site-kernel-checks) and by the Leitstand. Do not read files or network — callers (kernel commands) load each site's agent.json and pass already-parsed data here. Do not aggregate knowledge payloads, leads, or any per-site data — only public discovery metadata (RFC-0176/0177 isolation). Do not add fields absent from the site's own agent.json — the catalog is a projection, never a new source of facts (AS-2 at fleet scale). RFC-0292: initial fleet catalog contract and pure builder. */ import { createHash } from "node:crypto"; import { canonicalJson } from "./manifest.ts"; export const FLEET_AGENT_CATALOG_SCHEMA = "gogol.fleet.agent-catalog@1"; export interface FleetAgentCatalogEntry { site: string; baseUrl: string; surfaceVersion: string; contentHash: string; signed: boolean; enabled: boolean; knowledgeDomains: string[]; actions: string[]; interfaces: { openapi: boolean; mcp: boolean }; } export interface FleetAgentCatalog { schema: typeof FLEET_AGENT_CATALOG_SCHEMA; /** sha256 over sorted-key JSON of `sites` — no timestamps (byte-stable). */ contentHash: string; sites: FleetAgentCatalogEntry[]; } export interface FleetAgentCatalogSiteInput { site: string; /** Parsed agent.json, or null when the site has no agent surface (agent.enabled: false). */ doc: Record | null; } function sortKeysDeep(value: unknown): unknown { if (Array.isArray(value)) return value.map(sortKeysDeep); if (value !== null && typeof value === "object") { const out: Record = {}; for (const key of Object.keys(value as Record).sort()) { out[key] = sortKeysDeep((value as Record)[key]); } return out; } return value; } function asString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } function _asStringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.filter((v): v is string => typeof v === "string"); } /** * Extract a catalog entry from one site's parsed agent.json. * Missing doc ⇒ enabled: false entry with empty arrays. */ function entryFromSiteInput(input: FleetAgentCatalogSiteInput): FleetAgentCatalogEntry { if (!input.doc) { return { site: input.site, baseUrl: "", surfaceVersion: "", contentHash: "", signed: false, enabled: false, knowledgeDomains: [], actions: [], interfaces: { openapi: false, mcp: false }, }; } const doc = input.doc; const knowledge = Array.isArray(doc.knowledge) ? doc.knowledge : []; const actions = Array.isArray(doc.actions) ? doc.actions : []; const interfaces = (doc.interfaces ?? {}) as Record; return { site: input.site, baseUrl: asString(doc.baseUrl) ?? "", surfaceVersion: asString(doc.surfaceVersion) ?? "", contentHash: asString(doc.contentHash) ?? "", signed: doc.proof != null, enabled: true, knowledgeDomains: knowledge .map((ref) => asString((ref as Record)?.domain)) .filter((d): d is string => typeof d === "string") .sort((a, b) => a.localeCompare(b)), actions: actions .map((ref) => asString((ref as Record)?.id)) .filter((d): d is string => typeof d === "string") .sort((a, b) => a.localeCompare(b)), interfaces: { openapi: interfaces.openapi != null, mcp: interfaces.mcp != null, }, }; } /** * Pure: (fleet sites × loaded discovery docs) → catalog. * Missing doc ⇒ enabled: false entry. * Sites are sorted by site id; entries are deterministic. */ export function buildFleetAgentCatalog(input: FleetAgentCatalogSiteInput[]): FleetAgentCatalog { const sites = input .slice() .sort((a, b) => a.site.localeCompare(b.site)) .map(entryFromSiteInput); const contentHash = createHash("sha256").update(canonicalJson(sites), "utf8").digest("hex"); return { schema: FLEET_AGENT_CATALOG_SCHEMA, contentHash, sites, }; } /** * Re-compute the catalog contentHash from a parsed catalog's sites array. * Used by the validator to detect staleness/tampering (FAC-01). */ export function computeFleetCatalogContentHash(sites: unknown[]): string { return createHash("sha256") .update(JSON.stringify(sortKeysDeep(sites)), "utf8") .digest("hex"); }