Publish Bifrost Eve operator surface

This commit is contained in:
Meta Vens 2026-06-04 20:59:16 +01:00
commit e3dd8c3437
2 changed files with 354 additions and 9 deletions

View file

@ -79,15 +79,25 @@ Bifrost has a read-only first-cut provider advertisement command:
node tools/provider-advertisement.mjs export --out .bifrost/provider-advertisement.cc
```
It writes a single `gamecult.eve.provider_advertisement.v1` CultCache document
for Odin/Eve discovery experiments. The document names Bifrost's Account,
Patron, Project, Work, Motion, and Operator Verse surfaces; current and planned
schema ids; `.cc` witness/export paths; command authority boundaries; style
capabilities; and demoted presentation/probe surfaces.
It writes Bifrost-owned Eve discovery documents into
`.bifrost/provider-advertisement.cc`:
This export is discovery metadata only. It does not migrate Postgres state,
publish to CultMesh, read secrets, execute bridge actions, or make Razor Pages,
HTTP probes, Discord mirrors, or local dispatch JSON canonical.
- `gamecult.eve.provider_advertisement.v1` names Bifrost's Account, Patron,
Project, Work, Motion, and Operator Verse surfaces; current and planned
schema ids; `.cc` witness/export paths; command authority boundaries; style
capabilities; and demoted presentation/probe surfaces.
- `gamecult.eve.surface_state.v1` publishes the live Bifrost operator dashboard
surface with health/readiness, Docker container state, governance topic
counts, agent transport request counts, witness freshness, and bridge
capability status.
- `gamecult.eve.interface_binding.v1` binds that surface to provider id
`bifrost` so Odin can lower it into Nightwing, Eve, browser, or future room
dashboards.
This export does not migrate Postgres state, read secrets, execute bridge
actions, or make Razor Pages, HTTP probes, Discord mirrors, or local dispatch
JSON canonical. The operator surface may display those probes, but the provider
owned `.cc` witness remains the discovery and dashboard source.
For protocol-debug inspection without writing a witness:

View file

@ -1,6 +1,8 @@
#!/usr/bin/env node
import { createRequire } from "node:module";
import { spawnSync } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { existsSync, statSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
@ -19,6 +21,11 @@ const {
const documentType = "gamecult.eve.provider_advertisement";
const schemaId = "gamecult.eve.provider_advertisement.v1";
const documentId = "bifrost";
const surfaceDocumentType = "gamecult.eve.surface_state";
const surfaceSchemaId = "gamecult.eve.surface_state.v1";
const interfaceBindingDocumentType = "gamecult.eve.interface_binding";
const interfaceBindingSchemaId = "gamecult.eve.interface_binding.v1";
const verseId = "bifrost.local";
const advertisementDefinition = defineDocumentType({
type: documentType,
@ -48,6 +55,31 @@ const advertisementDefinition = defineDocumentType({
],
});
const surfaceDefinition = defineDocumentType({
type: surfaceDocumentType,
schemaId: surfaceSchemaId,
schemaName: surfaceDocumentType,
schemaVersion: surfaceSchemaId,
schema: { parse: parseObjectDocument("Eve surface state") },
name: "providerId",
members: [
{ slot: 0, memberName: "providerId", typeName: "string", isName: true },
{ slot: 1, memberName: "title", typeName: "string" },
{ slot: 2, memberName: "version", typeName: "long" },
{ slot: 3, memberName: "updatedAt", typeName: "string" },
{ slot: 4, memberName: "surface", typeName: "object" },
],
});
const interfaceBindingDefinition = defineDocumentType({
type: interfaceBindingDocumentType,
schemaId: interfaceBindingSchemaId,
schemaName: interfaceBindingDocumentType,
schemaVersion: interfaceBindingSchemaId,
schema: { parse: parseObjectDocument("Eve interface binding") },
name: "bindingId",
});
async function main() {
const [command, ...rawArgs] = process.argv.slice(2);
const options = parseArgs(rawArgs);
@ -83,12 +115,19 @@ async function exportAdvertisement(options) {
const cache = CultCache.builder()
.withDocumentType(advertisementDefinition)
.withDocumentType(surfaceDefinition)
.withDocumentType(interfaceBindingDefinition)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.pullAllBackingStores();
const advertisement = buildAdvertisement(options);
const stats = await collectStats(options);
const surface = buildOperatorSurface(stats, options);
const binding = buildInterfaceBinding(surface, stats, options);
await cache.put(advertisementDefinition, advertisement.providerId, advertisement);
await cache.put(surfaceDefinition, surface.providerId, surface);
await cache.put(interfaceBindingDefinition, binding.bindingId, binding);
printJson({
ok: true,
@ -98,6 +137,7 @@ async function exportAdvertisement(options) {
out: storePath,
surfaces: advertisement.surfaces.map((surface) => surface.id),
witnesses: advertisement.witnesses.map((witness) => witness.path),
stats: stats.summary,
});
}
@ -150,6 +190,14 @@ function buildAdvertisement(options) {
witness(".bifrost/eve-surfaces.cc", "gamecult.eve.surface.v1", "planned-export", "product and operator Eve/CultUI surface publications"),
],
surfaces: [
surface("bifrost", "Bifrost Operator Dashboard", "gamecult.bifrost.surface.operator", "gamecult.eve.surface_state.v1", ".bifrost/provider-advertisement.cc", [
"service health",
"docker containers",
"governance topic counts",
"agent transport request counts",
"witness freshness",
"bridge capability status",
]),
surface("bifrost.account", "Account Verse", "gamecult.bifrost.surface.product", "gamecult.eve.surface.v1", ".bifrost/eve-surfaces.cc", [
"membership status",
"Heimdall-linked account projection",
@ -259,6 +307,284 @@ function buildAdvertisement(options) {
});
}
async function collectStats(options) {
const generatedAt = options["generated-at"] ?? new Date().toISOString();
const health = await fetchProbe("http://127.0.0.1:5080/healthz");
const ready = await fetchProbe("http://127.0.0.1:5080/readyz");
const transport = runJsonTool(["tools/agent-transport.mjs", "list", "--json"]);
const governance = runJsonTool(["tools/governance-threads.mjs", "list", "--json"]);
const docker = dockerContainers();
const witnesses = [
witnessStat(".bifrost/provider-advertisement.cc"),
witnessStat(".bifrost/governance-threads.cc"),
witnessStat(".bifrost/agent-transport.cc"),
witnessStat(".bifrost/discord-webhook-cache.json"),
];
const topics = Array.isArray(governance.value) ? governance.value : [];
const requests = Array.isArray(transport.value) ? transport.value : [];
const topicCounts = countBy(topics, "status");
const requestCounts = countBy(requests, "status");
const dockerHealthy = docker.items.filter((item) => /healthy/i.test(item.status)).length;
const dockerRunning = docker.items.filter((item) => /^Up\b/i.test(item.status)).length;
return {
generatedAt,
health,
ready,
docker,
governance: {
ok: governance.ok,
error: governance.error,
count: topics.length,
statusCounts: topicCounts,
latestUpdatedAt: latestTimestamp(topics.map((item) => item.updatedAt)),
},
transport: {
ok: transport.ok,
error: transport.error,
count: requests.length,
statusCounts: requestCounts,
latestUpdatedAt: latestTimestamp(requests.map((item) => item.updatedAt)),
},
witnesses,
bridge: {
discordPost: true,
discordDm: true,
githubDraftPr: true,
githubPrComment: true,
credentialSource: process.env.BIFROST_DISCORD_BOT_TOKEN ? "BIFROST_DISCORD_BOT_TOKEN" : process.env.DISCORD_BOT_TOKEN ? "DISCORD_BOT_TOKEN-fallback" : "missing",
},
summary: {
status: health.ok && ready.ok ? "ready" : "degraded",
health: health.value ?? health.error,
ready: ready.value ?? ready.error,
dockerRunning,
dockerHealthy,
governanceTopics: topics.length,
transportRequests: requests.length,
openTopics: topicCounts.open ?? 0,
queuedRequests: requestCounts.queued ?? 0,
},
};
}
async function fetchProbe(url) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2500);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
const text = (await response.text()).trim();
return {
ok: response.ok,
statusCode: response.status,
value: text,
};
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function runJsonTool(args) {
const result = spawnSync(process.execPath, args, {
cwd: repoRoot,
encoding: "utf8",
windowsHide: true,
});
if (result.status !== 0) {
return {
ok: false,
error: (result.stderr || result.stdout || `exit ${result.status}`).trim(),
value: null,
};
}
try {
return {
ok: true,
value: JSON.parse(result.stdout),
};
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
value: null,
};
}
}
function dockerContainers() {
const result = spawnSync("docker", ["ps", "--filter", "name=bifrost", "--format", "{{json .}}"], {
cwd: repoRoot,
encoding: "utf8",
windowsHide: true,
});
if (result.status !== 0) {
return {
ok: false,
error: (result.stderr || result.stdout || `exit ${result.status}`).trim(),
items: [],
};
}
const items = result.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
try {
const parsed = JSON.parse(line);
return {
id: parsed.ID,
name: parsed.Names,
image: parsed.Image,
status: parsed.Status,
ports: parsed.Ports,
};
} catch {
return null;
}
})
.filter(Boolean);
return { ok: true, items };
}
function witnessStat(relativePath) {
const fullPath = resolve(repoRoot, relativePath);
if (!existsSync(fullPath)) {
return {
path: relativePath,
exists: false,
bytes: 0,
updatedAt: null,
};
}
const stat = statSync(fullPath);
return {
path: relativePath,
exists: true,
bytes: stat.size,
updatedAt: stat.mtime.toISOString(),
};
}
function buildOperatorSurface(stats) {
const statusTone = stats.summary.status === "ready" ? "ok" : "warn";
const children = [
metricNode("status", "Status", stats.summary.status, statusTone),
metricNode("health", "Health", String(stats.summary.health), stats.health.ok ? "ok" : "warn"),
metricNode("ready", "Ready", String(stats.summary.ready), stats.ready.ok ? "ok" : "warn"),
metricNode("docker", "Docker", `${stats.summary.dockerRunning} up / ${stats.summary.dockerHealthy} healthy`, stats.summary.dockerHealthy > 0 ? "ok" : "warn"),
metricNode("topics", "Topics", `${stats.summary.governanceTopics} total / ${stats.summary.openTopics} open`, stats.governance.ok ? "ok" : "warn"),
metricNode("transport", "Transport", `${stats.summary.transportRequests} requests / ${stats.summary.queuedRequests} queued`, stats.transport.ok ? "ok" : "warn"),
metricNode("bridge", "Bridge", `Discord post ${stats.bridge.discordPost ? "yes" : "no"} / DM ${stats.bridge.discordDm ? "yes" : "no"}`, "ok"),
listNode("containers", "Containers", stats.docker.items.map((item) => `${item.name}: ${item.status}`)),
listNode("witnesses", "Witnesses", stats.witnesses.map((item) => `${item.path}: ${item.exists ? `${item.bytes} bytes` : "missing"}`)),
listNode("topic-status", "Topic Status", objectLines(stats.governance.statusCounts)),
listNode("request-status", "Request Status", objectLines(stats.transport.statusCounts)),
];
return {
providerId: "bifrost",
title: "Bifrost Operator Dashboard",
version: Date.parse(stats.generatedAt) || Date.now(),
updatedAt: stats.generatedAt,
stats,
surface: {
schema: "gamecult.eve.surface.v1",
id: "bifrost-operator-dashboard",
title: "Bifrost Operator Dashboard",
root: {
id: "bifrost-root",
kind: "dashboard",
props: {
title: "Bifrost",
subtitle: "Governance, labor, and public crossing bridge",
status: stats.summary.status,
generatedAt: stats.generatedAt,
},
children,
},
},
};
}
function buildInterfaceBinding(surface, stats) {
return {
bindingId: "bifrost",
providerId: "bifrost",
title: surface.title,
kind: "operator-dashboard",
updatedAt: surface.updatedAt,
provider: {
id: "bifrost",
title: "Bifrost",
description: "Bifrost-owned operator stats and bridge health surface.",
version: String(surface.version),
endpoint: `cultmesh://${verseId}/eve/providers/bifrost`,
capabilities: ["operator-stats", "bridge-health", "governance-counts", "agent-transport-counts"],
usesCultMesh: true,
status: stats.summary.status,
transport: "CultMesh Eve interface binding.",
},
surface: surface.surface,
rendererHints: {
preferredLowerings: ["nightwing-tui", "eve-native", "browser"],
tileId: "bifrost",
minWidth: 80,
minHeight: 18,
preferredWidth: 120,
preferredHeight: 32,
density: "operator",
},
};
}
function metricNode(id, label, value, tone) {
return {
id: `metric-${id}`,
kind: "metric",
props: { label, value, tone },
};
}
function listNode(id, title, items) {
return {
id: `list-${id}`,
kind: "list",
props: { title },
children: items.length > 0
? items.map((item, index) => ({
id: `list-${id}-${index}`,
kind: "text",
props: { text: item },
}))
: [{ id: `list-${id}-empty`, kind: "text", props: { text: "none" } }],
};
}
function objectLines(value) {
const entries = Object.entries(value).sort((left, right) => left[0].localeCompare(right[0]));
return entries.length > 0 ? entries.map(([key, count]) => `${key}: ${count}`) : ["none"];
}
function countBy(items, field) {
const counts = {};
for (const item of items) {
const key = String(item?.[field] || "unknown");
counts[key] = (counts[key] || 0) + 1;
}
return counts;
}
function latestTimestamp(values) {
return values
.filter((value) => typeof value === "string" && value.trim().length > 0)
.sort()
.at(-1) || null;
}
function namespace(id, purpose) {
return { id, purpose };
}
@ -314,6 +640,15 @@ function parseAdvertisement(input) {
return advertisement;
}
function parseObjectDocument(label) {
return (input) => {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new Error(`${label} must be an object.`);
}
return input;
};
}
function parseArgs(args) {
const options = {};
for (let index = 0; index < args.length; index += 1) {