From 2a382fb4a8b92a5a1d0e07274fc25f9d122c8714 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 23 Jun 2026 23:09:16 +0100 Subject: [PATCH] Publish Bifrost provider respect to Odin --- docs/verse-service-contract.md | 7 +- tools/provider-advertisement.mjs | 122 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/docs/verse-service-contract.md b/docs/verse-service-contract.md index 682efb2..cc29656 100644 --- a/docs/verse-service-contract.md +++ b/docs/verse-service-contract.md @@ -104,6 +104,7 @@ Bifrost has a read-only first-cut provider advertisement command: ```powershell node tools/provider-advertisement.mjs export --out .bifrost/provider-advertisement.cc +node tools/provider-advertisement.mjs publish-odin --odin-cultmesh-rudp 127.0.0.1:17871 ``` It writes Bifrost-owned Eve discovery documents into @@ -124,6 +125,9 @@ This export does not migrate Postgres state, read secrets, execute bridge actions, or make Razor Pages, HTTP probes, Discord mirrors, Reddit threads, or local dispatch JSON canonical. The operator surface may display those probes, but the provider owned `.cc` witness remains the discovery and dashboard source. +When `BIFROST_ODIN_CULTMESH_RUDP` or `--odin-cultmesh-rudp` is configured, the +same provider advertisement can be published once to Odin's CultMesh/RUDP +document catalog through the provider-advertisement tool. For protocol-debug inspection without writing a witness: @@ -229,7 +233,8 @@ Bifrost. service registration, and Eve surface publication. 3. Add Postgres-to-CultCache witness export for alpha entities before replacing the transactional store. -4. Publish `gamecult.bifrost.service` and schema catalog through CultMesh. +4. Extend the existing Odin provider-advertisement publication into + `gamecult.bifrost.service` and schema catalog publication through CultMesh. 5. Publish operator surfaces first: readiness, witness freshness, bridge queue, failed crossings, and migration drift. 6. Publish product surfaces for account, project, work, motion, patron, and diff --git a/tools/provider-advertisement.mjs b/tools/provider-advertisement.mjs index c224693..c5d580f 100644 --- a/tools/provider-advertisement.mjs +++ b/tools/provider-advertisement.mjs @@ -12,12 +12,18 @@ const projectsRoot = resolve(repoRoot, ".."); const defaultStorePath = resolve(repoRoot, ".bifrost", "provider-advertisement.cc"); const cultCacheRequire = createRequire(resolveCultCachePackagePath()); +const cultMeshRequire = createRequire(resolveCultMeshPackagePath()); +const cultNetRequire = createRequire(resolveCultNetPackagePath()); const cultCacheRuntime = loadCultCacheRuntime(); +const cultMeshRuntime = loadCultMeshRuntime(); +const cultNetRuntime = loadCultNetRuntime(); const { CultCache, SingleFileMessagePackBackingStore, defineDocumentType, } = cultCacheRuntime; +const { CultMesh } = cultMeshRuntime; +const { defineCultNetDocumentBinding } = cultNetRuntime; function loadCultCacheRuntime() { const candidates = [ @@ -55,6 +61,78 @@ function resolveCultCachePackagePath() { throw new Error(`CultCache TypeScript runtime is unavailable. Tried: ${candidates.join(", ")}`); } +function loadCultMeshRuntime() { + const candidates = [ + resolve(projectsRoot, "CultLib", "packages", "cultmesh-ts", "dist", "index.js"), + resolve(projectsRoot, "CultMeshTS", "dist", "index.js"), + ]; + + for (const candidate of candidates) { + try { + const runtime = cultMeshRequire(candidate); + if (runtime.CultMesh) { + return runtime; + } + } catch { + // Try the next local CultMesh runtime candidate. + } + } + + throw new Error("CultMesh TypeScript runtime is unavailable."); +} + +function resolveCultMeshPackagePath() { + const candidates = [ + resolve(projectsRoot, "CultLib", "packages", "cultmesh-ts", "package.json"), + resolve(projectsRoot, "CultMeshTS", "package.json"), + resolve(projectsRoot, "CultMeshTS", "node_modules", "cultmesh-ts", "package.json"), + ]; + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + + throw new Error(`CultMesh TypeScript runtime is unavailable. Tried: ${candidates.join(", ")}`); +} + +function loadCultNetRuntime() { + const candidates = [ + resolve(projectsRoot, "CultLib", "packages", "cultnet-ts", "dist", "index.js"), + resolve(projectsRoot, "CultNetTS", "dist", "index.js"), + ]; + + for (const candidate of candidates) { + try { + const runtime = cultNetRequire(candidate); + if (runtime.defineCultNetDocumentBinding) { + return runtime; + } + } catch { + // Try the next local CultNet runtime candidate. + } + } + + throw new Error("CultNet TypeScript runtime is unavailable."); +} + +function resolveCultNetPackagePath() { + const candidates = [ + resolve(projectsRoot, "CultLib", "packages", "cultnet-ts", "package.json"), + resolve(projectsRoot, "CultNetTS", "package.json"), + resolve(projectsRoot, "CultNetTS", "node_modules", "cultnet-ts", "package.json"), + ]; + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + + throw new Error(`CultNet TypeScript runtime is unavailable. Tried: ${candidates.join(", ")}`); +} + const documentType = "gamecult.eve.provider_advertisement"; const schemaId = "gamecult.eve.provider_advertisement.v1"; const documentId = "bifrost"; @@ -141,6 +219,9 @@ async function main() { case "export": await exportAdvertisement(options); return; + case "publish-odin": + await publishOdinAdvertisement(options); + return; case "print": printJson(buildAdvertisement(options)); return; @@ -555,6 +636,35 @@ function witnessStat(relativePath) { }; } +async function publishOdinAdvertisement(options) { + const endpoint = options["odin-cultmesh-rudp"] ?? options.odin ?? process.env.BIFROST_ODIN_CULTMESH_RUDP; + if (!endpoint) { + throw new Error("Bifrost Odin publication requires --odin-cultmesh-rudp or BIFROST_ODIN_CULTMESH_RUDP."); + } + + const advertisement = buildAdvertisement(options); + await CultMesh.publishRudpDocumentOnce( + "bifrost", + 0x0d1d0002, + normalizeRudpEndpoint(endpoint), + defineCultNetDocumentBinding({ definition: advertisementDefinition }), + advertisement.providerId, + advertisement, + { + sourceRole: "bifrost.provider", + tags: ["startup-respect", "odin-verse-discovery"], + }, + ); + + printJson({ + ok: true, + documentType, + schemaId, + providerId: advertisement.providerId, + odinCultMeshRudp: endpoint, + }); +} + function buildBridgeStats() { const discordCredentialSource = process.env.BIFROST_DISCORD_BOT_TOKEN ? "BIFROST_DISCORD_BOT_TOKEN" @@ -1038,6 +1148,14 @@ function resolveOptionPath(path) { return resolve(process.cwd(), path); } +function normalizeRudpEndpoint(endpoint) { + const text = String(endpoint || "").trim(); + if (!text) { + throw new Error("Odin CultMesh/RUDP endpoint must be non-empty."); + } + return text.toLowerCase().startsWith("rudp://") ? text : `rudp://${text}`; +} + function printJson(value) { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); } @@ -1047,6 +1165,7 @@ function printHelp() { Commands: export Write advertisement, operator surface, and interface binding to a CultCache .cc witness + publish-odin Publish the provider advertisement once to Odin over CultMesh/RUDP print Print the advertisement as protocol-debug JSON without writing state print-surface Print the current operator Eve surface JSON without writing state print-binding Print the current Eve interface binding JSON without writing state @@ -1054,12 +1173,15 @@ Commands: Options: --out Override export path; defaults to .bifrost/provider-advertisement.cc + --odin-cultmesh-rudp + Odin CultMesh/RUDP document catalog endpoint; also reads BIFROST_ODIN_CULTMESH_RUDP --generated-at Pin generatedAt for deterministic fixture checks Examples: node tools/provider-advertisement.mjs print node tools/provider-advertisement.mjs print-binding node tools/provider-advertisement.mjs export --out .bifrost/provider-advertisement.cc + node tools/provider-advertisement.mjs publish-odin --odin-cultmesh-rudp 127.0.0.1:17871 `); }