fable-agent/src/fable5/cyber-reconcile.ts

63 lines
2.6 KiB
TypeScript

import * as fs from "node:fs";
import * as path from "node:path";
import { loadFactoryCapabilities, type FactoryCapabilityService } from "./factory-capabilities.js";
import type { CyberPreflightReceipt } from "./cyber-preflight.js";
export interface CyberReconcileFinding {
capability: string;
expected: string;
evidence: string;
status: "ok" | "missing";
}
export interface CyberReconcileReceipt {
created_at: string;
preflight: string;
capabilities: string;
findings: CyberReconcileFinding[];
decision: "allow_read_only" | "fail_closed";
deploy_allowed: false;
recommendation: string;
}
export function reconcileCyberPreflight(preflightFile: string, capabilitiesFile: string, now = new Date()): CyberReconcileReceipt {
const preflight = JSON.parse(fs.readFileSync(preflightFile, "utf-8")) as CyberPreflightReceipt;
const capabilities = loadFactoryCapabilities(capabilitiesFile);
const findings = capabilities.flatMap((service) => findingsForService(service, preflight));
const allOk = findings.every((finding) => finding.status === "ok") && preflight.decision === "allow_read_only";
return {
created_at: now.toISOString(),
preflight: preflightFile,
capabilities: capabilitiesFile,
findings,
decision: allOk ? "allow_read_only" : "fail_closed",
deploy_allowed: false,
recommendation: allOk ? "read-only posture verified; deploy still requires separate approval" : "hold position; restore telemetry routes or update capability registry",
};
}
export function writeCyberReconcileReceipt(file: string, receipt: CyberReconcileReceipt): void {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
}
function findingsForService(service: FactoryCapabilityService, preflight: CyberPreflightReceipt): CyberReconcileFinding[] {
return service.provides.map((capability) => {
const check = matchingCheck(capability, preflight);
return {
capability,
expected: service.url,
evidence: check ? `${check.name}: ${check.ok ? "ok" : `failed status=${check.status} ${check.stderr.trim()}`}` : "no matching preflight check",
status: check?.ok ? "ok" : "missing",
};
});
}
function matchingCheck(capability: string, preflight: CyberPreflightReceipt) {
if (capability === "deploy") return preflight.checks.find((check) => check.name === "deploy_route_health");
if (capability === "factory-status") return preflight.checks.find((check) => check.name === "git_proxy_factory_status");
if (capability === "discovery") return preflight.checks.find((check) => check.name === "git_proxy_health");
return undefined;
}