import { isIP } from "node:net" import { z } from "zod" import type { RouteAiProfile } from "@mmapp/contracts/mcp-routing" import { routeGatewayContext } from "./routing-community-context-model.js" import { fingerprint } from "./security.js" type Row = Record const id = z.string().regex(/^\*[a-z0-9]+$/i).max(100) const text = z.string().min(1).max(200) const ip = text.refine(value => isIP(value) !== 0) const flag = (value: unknown): boolean | undefined => [true, "true", "yes"].includes(value as string | boolean) ? true : [false, "false", "no"].includes(value as string | boolean) ? false : undefined const communities = (row: Row) => Array.isArray(row["bgp.communities"]) ? row["bgp.communities"] as unknown[] : typeof row["bgp.communities"] === "string" ? row["bgp.communities"].split(",") : [] const family = (row: Row) => ["ip", "ip4"].includes(String(row.afi)) ? "ipv4" : ["ipv6", "ip6"].includes(String(row.afi)) ? "ipv6" : null const unique = (rows: Row[], nativeId: string) => rows.filter(row => row[".id"] === nativeId) const tupleSchema = z.object({ connectionId: id, sessionId: id, instance: text, table: text, vrf: text, localAddress: ip, remoteAddress: ip, remoteAsn: z.number().int().positive().max(4294967295), afi: z.enum(["ip", "ipv6"]) }).strict() const prepared = z.object({ status: z.literal("prepared"), revision: z.literal(1), serverId: z.number().int().positive(), routeSelectionScope: z.literal("table-afi-community-observation"), candidateId: text, family: z.enum(["ipv4", "ipv6"]), table: text, community: text, expectedGateway: text, expectedImmediate: z.object({ address: ip, interface: text }).strict(), binding: tupleSchema, bfd: z.object({ applicability: z.literal("not-configured"), evidence: z.literal("explicit-use-bfd-false") }).strict(), routes: z.array(z.object({ nativeId: id, prefix: text, originalGateway: text }).strict()).min(1).max(32), probe: z.object({ key: text, serverId: z.number().int().positive(), destination: ip, source: ip, interface: text, probeVrf: text, expectedHops: z.array(ip).min(1).max(8) }).strict(), }).strict() export const originalEffectSelectionSchema = z.union([prepared, z.object({ status: z.literal("unknown"), reason: text }).strict()]) export type OriginalEffectSelection = z.infer type Prepared = z.infer const state = z.enum(["observed-expected", "observed-other", "unknown"]) const roundSchema = z.object({ observedAt: z.string().datetime(), routes: z.array(z.object({ prefix: text, status: state }).strict()).max(32), bgp: state, bfd: z.enum(["not-configured", "unknown"]), controlProbe: z.enum(["passed", "failed", "unknown"]), relatedTopicRows: z.number().int().nonnegative().max(1000) }).strict() export const originalEffectReportSchema = z.object({ scope: z.literal("prepared-route-fib-adjacency-control-effects"), status: state, reason: text.nullable(), rounds: z.array(roundSchema).max(3), logSource: z.literal("routeros-rest-ring"), logCoverage: z.literal("unknown"), selectionIdentity: z.string().regex(/^[a-f0-9]{64}$/).nullable(), routeSelectionScope: z.literal("table-afi-community-observation"), transitionAttribution: z.literal("unproven"), switchEffectObserved: z.boolean(), transition: z.object({ baselineObservedAt: z.string().datetime(), changedPrefixes: z.array(text).max(32) }).strict().optional(), budget: z.object({ deadline: z.number().int().positive(), calls: z.number().int().nonnegative().max(256), bytes: z.number().int().nonnegative().max(8 * 1048576), rows: z.number().int().nonnegative().max(10000) }).strict().optional(), semanticProof: z.literal(false), routeOriginVerified: z.literal(false), recursiveChainVerified: z.literal(false), forwardingVerified: z.literal(false), autoRecoveryAllowed: z.literal(false) }).strict() export type OriginalEffectReport = z.infer export const unknownOriginalEffects = (reason: string): OriginalEffectReport => ({ scope: "prepared-route-fib-adjacency-control-effects", status: "unknown", reason, rounds: [], logSource: "routeros-rest-ring", logCoverage: "unknown", selectionIdentity: null, routeSelectionScope: "table-afi-community-observation", transitionAttribution: "unproven", switchEffectObserved: false, semanticProof: false, routeOriginVerified: false, recursiveChainVerified: false, forwardingVerified: false, autoRecoveryAllowed: false }) function tuple(row: Row, expected: Prepared["binding"], session: boolean) { return row.instance === expected.instance && row["routing-table"] === expected.table && row.vrf === expected.vrf && row["local.address"] === expected.localAddress && row["remote.address"] === expected.remoteAddress && Number(row["remote.as"]) === expected.remoteAsn && (session ? row["local.afi"] === expected.afi && row["remote.afi"] === expected.afi : row.afi === expected.afi) } function selectedRows(rows: Row[], selection: Pick) { return rows.filter(row => row["routing-table"] === selection.table && family(row) === selection.family && flag(row.bgp) === true && communities(row).includes(selection.community)) } /** Called only by the real private context reader. This selects observed route * effects by table/AFI/Community, never a route→session provenance relationship. * The initial bounded subset requires explicit flat peer identity, explicit * BFD-off and an already configured explicit probe source. Other manual paths * keep their existing configuration/probe behavior, with effects unknown. */ export function prepareOriginalEffectSelection(profile: RouteAiProfile, candidateId: string, binding: { connectionId: string; instance: string | null; vrf: string; table: string } | null, raw: { routes: Row[]; connections: Row[]; sessions: Row[]; candidate: Row | undefined; probe: { destination: string; expected: string[]; source?: string } | undefined }): OriginalEffectSelection { try { if (!binding || !binding.instance) throw Error() const candidate = profile.candidates.find(value => value.id === candidateId), probe = raw.probe if (!candidate?.controlProbe || !probe?.source || !raw.candidate) throw Error() const connection = unique(raw.connections, binding.connectionId) if (connection.length !== 1 || !(connection[0].templates === "" || Array.isArray(connection[0].templates) && connection[0].templates.length === 0) || flag(connection[0].disabled) !== false || flag(connection[0]["use-bfd"]) !== false) throw Error() const peer = connection[0], afi = profile.family === "ipv4" ? "ip" : "ipv6" const wanted = tupleSchema.parse({ connectionId: peer[".id"], sessionId: "*0", instance: binding.instance, table: binding.table, vrf: binding.vrf, localAddress: peer["local.address"], remoteAddress: peer["remote.address"], remoteAsn: Number(peer["remote.as"]), afi }) if (!tuple(peer, wanted, false)) throw Error() const sessions = raw.sessions.filter(row => tuple(row, wanted, true)) if (sessions.length !== 1 || flag(sessions[0].established) === undefined) throw Error() wanted.sessionId = id.parse(sessions[0][".id"]) const affected = selectedRows(raw.routes, profile) if (!affected.length || affected.length > 32 || new Set(affected.map(row => row["dst-address"])).size !== affected.length || new Set(affected.map(row => row[".id"])).size !== affected.length) throw Error() for (const row of affected) { const [address, mask, extra] = String(row["dst-address"]).split("/") if (extra !== undefined || isIP(address) !== (profile.family === "ipv4" ? 4 : 6) || !/^\d+$/.test(mask ?? "") || Number(mask) > (profile.family === "ipv4" ? 32 : 128)) throw Error() routeGatewayContext(String(row.gateway)) } // A selected destination cannot silently choose among competing origins. if (affected.some(row => raw.routes.filter(other => other["routing-table"] === profile.table && other["dst-address"] === row["dst-address"]).length !== 1)) throw Error() const endpoint = candidate.gatewayRef.resource === "gre" ? { address: routeGatewayContext(candidate.gateway).address, interface: String(raw.candidate.name) } : routeGatewayContext(String(raw.candidate["immediate-gw"])) if (!endpoint.interface) throw Error() return prepared.parse({ status: "prepared", revision: 1, serverId: profile.serverId, candidateId, family: profile.family, table: profile.table, routeSelectionScope: "table-afi-community-observation", community: profile.community, expectedGateway: candidate.gateway, expectedImmediate: { address: endpoint.address, interface: endpoint.interface }, binding: wanted, bfd: { applicability: "not-configured", evidence: "explicit-use-bfd-false" }, routes: affected.map(row => ({ nativeId: row[".id"], prefix: row["dst-address"], originalGateway: row.gateway })).sort((a, b) => String(a.prefix).localeCompare(String(b.prefix))), probe: { key: candidateId, serverId: profile.serverId, destination: probe.destination, source: probe.source, interface: candidate.wanInterface, probeVrf: profile.probeVrf, expectedHops: probe.expected } }) } catch { return { status: "unknown", reason: "prepared-effect-selection-unsupported-or-ambiguous" } } } type Snapshot = { observedAt: string; servers: Array<{ serverId: number; routes: { payload: Row[] }; bgp: { payload: Row[]; connectionsPayload: Row[] }; logs: { payload: Row[] } }>; probes: Array<{ key: string; pingPayload: Row[]; tracePayload?: Row[] }> } export function originalEffectBaselineMatches(selection: Prepared, sample: Snapshot) { const source = sample.servers.find(row => row.serverId === selection.serverId) if (!source) return false const actual = selectedRows(source.routes.payload, selection) return actual.length === selection.routes.length && selection.routes.every(wanted => actual.filter(row => row[".id"] === wanted.nativeId && row["dst-address"] === wanted.prefix && row.gateway === wanted.originalGateway).length === 1) } /** Actual bounded native captures only; all stronger causal/whole-path claims * remain false. Related log inspection is reported without completeness. */ export async function inspectOriginalEffectRounds(rawSelection: unknown, samples: Snapshot[], baseline: Snapshot) { const selection = prepared.parse(rawSelection), rounds: z.infer[] = [] if (!originalEffectBaselineMatches(selection, baseline)) return unknownOriginalEffects("prepared-effect-baseline-drift") const { inspectControlPing, inspectTrace } = await import("./path-verification.js") for (const sample of samples) { const source = sample.servers.find(row => row.serverId === selection.serverId) if (!source) return unknownOriginalEffects("effect-source-missing") const scope = selectedRows(source.routes.payload, selection) const scopeChanged = fingerprint(scope.map(row => row["dst-address"]).sort()) !== fingerprint(selection.routes.map(row => row.prefix).sort()) const routes = selection.routes.map(wanted => { const matching = source.routes.payload.filter(row => row["routing-table"] === selection.table && row["dst-address"] === wanted.prefix && family(row) === selection.family) let status: z.infer = "unknown" if (!scopeChanged && matching.length === 1) { const row = matching[0] if (flag(row.bgp) === true && flag(row.connect) !== true && flag(row.static) !== true && flag(row.active) !== undefined && row.ecmp !== undefined && flag(row.ecmp) === false && ["disabled", "filtered", "unreachable", "blackhole", "invalid"].every(key => row[key] === undefined || flag(row[key]) === false)) { try { const final = routeGatewayContext(String(row["immediate-gw"])) status = flag(row.active) === true && row.contribution === "active" && row.gateway === selection.expectedGateway && final.address === selection.expectedImmediate.address && final.interface === selection.expectedImmediate.interface ? "observed-expected" : "observed-other" } catch { /* no final resolution evidence */ } } } return { prefix: wanted.prefix, status } }) const connections = unique(source.bgp.connectionsPayload, selection.binding.connectionId) const sessions = source.bgp.payload.filter(row => tuple(row, selection.binding, true)) let bgp: z.infer = "unknown" if (connections.length === 1 && tuple(connections[0], selection.binding, false) && flag(connections[0].disabled) === false && flag(connections[0]["use-bfd"]) === false && sessions.length === 1 && sessions[0][".id"] === selection.binding.sessionId && flag(sessions[0].established) !== undefined) bgp = flag(sessions[0].established) ? "observed-expected" : "observed-other" const probe = sample.probes.find(row => row.key === selection.probe.key) let controlProbe: "passed" | "failed" | "unknown" = "unknown" if (probe) { const ping = inspectControlPing(z.array(z.record(z.string(), z.string())).max(256).parse(probe.pingPayload)) if (ping.confirmedFailure) controlProbe = "failed" else if (ping.ping.measured && ping.ping.loss === 0 && inspectTrace(probe.tracePayload, selection.probe.destination, selection.probe.expectedHops).verified) controlProbe = "passed" } rounds.push(roundSchema.parse({ observedAt: sample.observedAt, routes, bgp, bfd: connections.length === 1 && flag(connections[0]["use-bfd"]) === false ? "not-configured" : "unknown", controlProbe, relatedTopicRows: source.logs.payload.filter(row => typeof row.topics === "string" && row.topics.split(",").some(topic => ["bgp", "bfd", "route", "interface"].includes(topic))).length })) } if (rounds.length !== 2) return unknownOriginalEffects("two-effect-rounds-required") const all = rounds.flatMap(round => [...round.routes.map(route => route.status), round.bgp, round.bfd === "unknown" ? "unknown" : "observed-expected", round.controlProbe === "passed" ? "observed-expected" : round.controlProbe === "failed" ? "observed-other" : "unknown"]) const before = baseline.servers.find(row => row.serverId === selection.serverId)! // Native IDs alone may churn; they never count as a forwarding transition. // Require an actual selected active destination's final forwarding endpoint // to differ from the expected post-state seen consistently in both rounds. // A changed logical gateway resolving to the SAME endpoint is not a FIB switch. const changedPrefixes = selection.routes.filter(wanted => { if (!rounds.every(round => round.routes.find(row => row.prefix === wanted.prefix)?.status === "observed-expected")) return false const row = before.routes.payload.find(row => row[".id"] === wanted.nativeId && row["dst-address"] === wanted.prefix)! if (flag(row.bgp) !== true || flag(row.active) !== true || row.contribution !== "active" || flag(row.connect) === true || flag(row.static) === true || flag(row.ecmp) !== false || ["disabled", "filtered", "unreachable", "blackhole", "invalid"].some(key => row[key] !== undefined && flag(row[key]) !== false)) return false try { const endpoint = routeGatewayContext(String(row["immediate-gw"])) routeGatewayContext(String(row.gateway)) if (!endpoint.interface) return false return endpoint.address !== selection.expectedImmediate.address || endpoint.interface !== selection.expectedImmediate.interface } catch { return false } }).map(row => row.prefix) const postState = all.includes("unknown") ? "unknown" : all.includes("observed-other") ? "observed-other" : "observed-expected" return originalEffectReportSchema.parse({ ...unknownOriginalEffects(""), rounds, reason: null, selectionIdentity: fingerprint(selection), transition: { baselineObservedAt: baseline.observedAt, changedPrefixes }, switchEffectObserved: postState === "observed-expected" && changedPrefixes.length > 0, status: postState === "observed-expected" && !changedPrefixes.length ? "unknown" : postState, ...(postState === "observed-expected" && !changedPrefixes.length ? { reason: "poststate-matched-without-selected-fib-transition" } : {}) }) }