VoidBot/scripts/serve-voidbot-swarm-cultmesh.cjs
2026-07-15 20:51:16 +01:00

468 lines
19 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const { createRequire } = require("module");
const {
operatorViewPublication,
projectSwarmOperatorView,
readSwarmOperatorInputs,
watchSwarmOperatorInputs,
} = require("./voidbot-swarm-operator-view.cjs");
const { parseEndpoint: parseIdunnEndpoint, publishIdunnRudpHealth } = require("./publish-idunn-rudp-health.cjs");
const repoRoot = path.resolve(__dirname, "..");
const defaultStorePath = path.join(repoRoot, ".voidbot", "status", "cultmesh", "voidbot-swarm-state.cc");
const defaultControlStorePath = path.join(repoRoot, ".voidbot", "private", "swarm-controls.cc");
const defaultBind = "127.0.0.1:17873";
const defaultOdinCultMeshUri = "cultmesh://odin/rendezvous/provider-catalog";
const defaultOdinRudpEndpoint = "rudp://127.0.0.1:17871";
const connectionId = 0x43554c54;
const odinConnectionId = 0x0d1d0002;
const args = parseArgs(process.argv.slice(2));
const storePath = path.resolve(args.store || process.env.VOIDBOT_SWARM_CULTMESH_STORE || defaultStorePath);
const controlStorePath = path.resolve(args.controlStore || process.env.VOIDBOT_SWARM_CONTROL_STORE || defaultControlStorePath);
const operatorInputPaths = {
heartbeat: path.resolve(process.env.REPO_FACE_HEARTBEAT_STATE_PATH || path.join(repoRoot, ".voidbot", "status", "repo-face-heartbeats.json")),
orchestrator: path.resolve(process.env.VOIDBOT_ORCHESTRATOR_STATE_PATH || path.join(repoRoot, ".voidbot", "status", "gamecult-orchestrator.json")),
pause: path.resolve(process.env.VOIDBOT_SWARM_PAUSE_STATE_PATH || path.join(repoRoot, "state", "agent-swarm-paused.json")),
control: controlStorePath,
};
const bind = parseBind(args.bind || process.env.VOIDBOT_SWARM_CULTMESH_BIND || defaultBind);
const odinCultMeshUri = args.odinCultMeshUri || args["odin-cultmesh-uri"] || process.env.VOIDBOT_ODIN_CULTMESH_URI || process.env.ODIN_CULTMESH_URI || defaultOdinCultMeshUri;
const odinRudpEndpoint = args.odinRudpEndpoint || args["odin-rudp-endpoint"] || process.env.VOIDBOT_ODIN_RUDP || process.env.CULTMESH_URI_ODIN_RUDP || defaultOdinRudpEndpoint;
const idunnHealthEndpoint = String(process.env.VOIDBOT_IDUNN_RUDP_HEALTH || "").trim();
const idunnHealthIntervalMs = 30_000;
const {
CultMesh,
CultMeshMemoryProviderReceiptStore,
CultMeshProviderRudpTransport,
CultMeshProviderSession,
CultNetDocumentRegistry,
defineCultNetDocumentBinding,
defineDocumentType,
} = loadCultRuntime();
const documents = defineDocuments(defineDocumentType);
const documentDefinitions = Object.values(documents);
const bindings = {
provider: defineCultNetDocumentBinding({ definition: documents.provider }),
surface: defineCultNetDocumentBinding({ definition: documents.surface }),
commandReceipt: defineCultNetDocumentBinding({ definition: documents.commandReceipt }),
};
const documentRegistry = new CultNetDocumentRegistry(
documentDefinitions.map((definition) => defineCultNetDocumentBinding({ definition })),
);
let server = null;
let announceTimer = null;
let liveProviderSession = null;
let stopOperatorWatch = null;
let operatorPublishChain = Promise.resolve();
let idunnHealthTimer = null;
let idunnHealthPublishChain = Promise.resolve();
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exitCode = 1;
});
async function main() {
await publishCurrentControlState();
server = CultMesh.createRudpDocumentServer("voidbot-swarm-cultmesh", connectionId, {
bindHost: bind.host,
bindPort: bind.port,
documents: documentRegistry,
getCache: async () => {
if (!fs.existsSync(storePath)) {
throw new Error(`VoidBot swarm CultMesh store is missing at ${storePath}`);
}
const node = await CultMesh.createNode(storePath, {
documents: documentDefinitions,
});
return node.cache;
},
onError: (error) => {
console.error(`VoidBot swarm CultMesh/RUDP server error: ${error instanceof Error ? error.message : String(error)}`);
},
onDocumentPutRaw: applySwarmCommand,
});
await server.start();
liveProviderSession = await startLiveProviderSession();
await publishCurrentOperatorView();
stopOperatorWatch = watchSwarmOperatorInputs(operatorInputPaths, queueOperatorViewPublication);
console.log(`VoidBot swarm CultMesh/RUDP serving ${storePath} at rudp://${bind.host}:${bind.port}`);
announceToOdin().catch((error) => {
console.error(`VoidBot swarm Odin announcement failed: ${error instanceof Error ? error.message : String(error)}`);
});
announceTimer = setInterval(() => {
announceToOdin().catch((error) => {
console.error(`VoidBot swarm Odin announcement failed: ${error instanceof Error ? error.message : String(error)}`);
});
}, 30_000);
announceTimer.unref?.();
if (idunnHealthEndpoint) {
await queueIdunnHealthPublication();
idunnHealthTimer = setInterval(queueIdunnHealthPublication, idunnHealthIntervalMs);
idunnHealthTimer.unref?.();
} else {
console.log("VoidBot Idunn health publication is disabled because VOIDBOT_IDUNN_RUDP_HEALTH is not configured.");
}
const shutdown = () => {
try {
if (announceTimer) clearInterval(announceTimer);
if (idunnHealthTimer) clearInterval(idunnHealthTimer);
server?.close?.();
stopOperatorWatch?.();
void liveProviderSession?.stop?.();
} finally {
process.exit(0);
}
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
}
function queueIdunnHealthPublication() {
idunnHealthPublishChain = idunnHealthPublishChain
.then(() => publishIdunnRudpHealth({
endpoint: parseIdunnEndpoint(idunnHealthEndpoint),
daemonId: process.env.VOIDBOT_IDUNN_DAEMON || "voidbot",
healthContract: process.env.VOIDBOT_IDUNN_HEALTH_CONTRACT || "voidbot.cultnet-rudp-stack-health",
}, {
state: "healthy",
detail: "VoidBot swarm CultMesh publisher is serving retained runtime state.",
observedAt: new Date().toISOString(),
}))
.catch((error) => console.error(`VoidBot Idunn health publication failed: ${error instanceof Error ? error.message : String(error)}`));
return idunnHealthPublishChain;
}
async function publishCurrentControlState() {
if (!fs.existsSync(controlStorePath)) return;
const controlNode = await CultMesh.createNode(controlStorePath, { documents: [documents.swarmControl] });
const control = controlNode.get(documents.swarmControl, "voidbot-swarm");
if (control) await projectAcceptedHeat(control);
}
async function applySwarmCommand(document) {
if (document.schemaId !== "gamecult.eve.command.v1") return;
const command = document.payload;
if (!command || command.providerId !== "voidbot.swarm" || command.command !== "swarm.set_heat") return;
const value = Number(command.payload?.value);
if (!Number.isFinite(value) || value < 0.05 || value > 2) {
throw new Error(`swarm.set_heat value must be between 0.05 and 2, got ${command.payload?.value}`);
}
const node = await CultMesh.createNode(controlStorePath, { documents: [documents.swarmControl] });
const control = {
schemaVersion: "voidbot.swarm_control_state.v1",
globalHeat: Math.round(value * 100) / 100,
commandId: command.commandId,
updatedAt: new Date().toISOString(),
updatedBy: command.clientId || command.publishedBy || "unknown",
};
await node.put(documents.swarmControl, "voidbot-swarm", control);
await node.flush?.(true);
await projectAcceptedHeat(control);
console.log(`Applied swarm heat ${control.globalHeat} from ${control.commandId}`);
return {
binding: bindings.commandReceipt,
recordKey: control.commandId,
value: {
schema: "gamecult.eve.command_receipt.v1",
receiptId: `voidbot.swarm:${control.commandId}`,
commandId: control.commandId,
command: command.command,
state: "reconciled",
ownerRepo: "VoidBot",
authority: "voidbot.swarm_control_state.v1:voidbot-swarm",
providerId: command.providerId,
surfaceId: command.surfaceId || "voidbot.swarm.surface",
sourceVersion: Date.parse(control.updatedAt),
issuedAtUtc: control.updatedAt,
message: `Swarm heat applied at ${control.globalHeat}.`,
diagnostics: [{ code: "state-published", pointerId: "globalHeat" }],
},
sourceRuntimeId: "voidbot-swarm-cultmesh",
sourceRole: "swarm-control-owner",
tags: ["eve", "command-receipt", "swarm-control"],
};
}
async function projectAcceptedHeat(control) {
const node = await CultMesh.createNode(storePath, { documents: documentDefinitions });
await node.put(documents.swarmControl, "voidbot-swarm", control);
await node.flush?.(true);
await liveProviderSession?.upsertPublication?.(controlPublication(control));
await queueOperatorViewPublication(control);
}
function queueOperatorViewPublication(control) {
operatorPublishChain = operatorPublishChain
.then(() => publishCurrentOperatorView(control))
.catch((error) => console.error(`VoidBot swarm operator view publication failed: ${error instanceof Error ? error.message : String(error)}`));
return operatorPublishChain;
}
async function publishCurrentOperatorView(controlOverride) {
const { control: _controlPath, ...jsonInputPaths } = operatorInputPaths;
const inputs = await readSwarmOperatorInputs(jsonInputPaths);
const controlNode = controlOverride ? null : await CultMesh.createNode(controlStorePath, { documents: [documents.swarmControl] });
const control = controlOverride ?? controlNode?.get(documents.swarmControl, "voidbot-swarm");
const view = projectSwarmOperatorView({
...inputs,
control,
route: {
providerStatus: liveProviderSession ? "connected" : "local-only",
endpoint: `rudp://${bind.host}:${bind.port}`,
},
});
const node = await CultMesh.createNode(storePath, { documents: documentDefinitions });
await node.put(documents.swarmOperatorView, "voidbot-swarm", view);
await node.flush?.(true);
await liveProviderSession?.upsertPublication?.(operatorViewPublication(view));
}
async function startLiveProviderSession() {
const endpoint = String(process.env.HERMODR_PROVIDER_SESSION_ENDPOINT || "").trim();
const sessionToken = String(process.env.HERMODR_PROVIDER_SESSION_TOKEN || "").trim();
if (!endpoint && !sessionToken) return null;
if (!endpoint || !sessionToken) {
throw new Error("HERMODR_PROVIDER_SESSION_ENDPOINT and HERMODR_PROVIDER_SESSION_TOKEN must be configured together.");
}
const controlNode = await CultMesh.createNode(controlStorePath, { documents: [documents.swarmControl] });
const control = controlNode.get(documents.swarmControl, "voidbot-swarm");
const transport = new CultMeshProviderRudpTransport({
endpoint,
runtimeId: "voidbot-swarm-live-provider",
connectionId,
sessionToken,
});
const session = new CultMeshProviderSession({
identity: {
providerId: "voidbot.swarm",
serviceInstanceId: "voidbot-swarm-live-provider",
endpointId: "voidbot.swarm.live.rudp",
verseId: "voidbot.local",
},
transport,
receiptStore: new CultMeshMemoryProviderReceiptStore(),
publications: control ? [controlPublication(control)] : [],
commandHandlers: {},
});
await session.start();
console.log(`VoidBot swarm provider session ${session.state.status} as ${session.identity.providerId}${session.state.lastError ? `: ${session.state.lastError}` : ""}`);
return session;
}
function controlPublication(control) {
return {
publicationId: "voidbot.swarm.control",
documentType: "voidbot.swarm_control_state",
schemaId: "voidbot.swarm_control_state.v1",
recordKey: "voidbot-swarm",
value: control,
};
}
async function announceToOdin() {
const node = await CultMesh.createNode(storePath, {
documents: documentDefinitions,
});
const provider = node.get(documents.provider, "voidbot.swarm");
const surface = node.get(documents.surface, "voidbot.swarm");
if (!provider || !surface) {
throw new Error("VoidBot swarm store must contain provider and surface documents before announcing to Odin.");
}
const options = {
connectTimeoutMs: 2_000,
flushTimeoutMs: 150,
sourceRuntimeId: "voidbot-swarm-cultmesh",
sourceRole: "daemon-provider",
tags: ["voidbot", "eve", "cultmesh-rudp"],
resolveCultMeshRudpEndpoint: (uri) => String(uri || "").startsWith("cultmesh://odin/")
? odinRudpEndpoint
: undefined,
};
await CultMesh.publishRudpDocumentOnce(
"voidbot-swarm-cultmesh",
odinConnectionId,
odinRudpEndpoint,
bindings.provider,
"voidbot.swarm",
provider,
options,
);
await CultMesh.publishRudpDocumentOnce(
"voidbot-swarm-cultmesh",
odinConnectionId,
odinRudpEndpoint,
bindings.surface,
"voidbot.swarm",
surface,
options,
);
console.log(`VoidBot swarm announced provider and surface documents to ${odinCultMeshUri}`);
}
function defineDocuments(defineDocumentType) {
const objectSchema = (label) => ({
parse(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${label} must be an object.`);
}
return value;
},
});
return {
snapshot: defineDocumentType({
type: "voidbot.swarm_state_snapshot",
schemaName: "voidbot.swarm_state_snapshot",
schemaId: "voidbot.swarm_state_snapshot.v1",
schemaVersion: "voidbot.swarm_state_snapshot.v1",
global: true,
schema: objectSchema("VoidBot swarm snapshot"),
}),
provider: defineDocumentType({
type: "gamecult.eve.provider_advertisement",
schemaName: "gamecult.eve.provider_advertisement",
schemaId: "gamecult.eve.provider_advertisement.v1",
schemaVersion: "gamecult.eve.provider_advertisement.v1",
global: false,
name: (value) => value?.providerId || value?.provider?.id || "provider",
schema: objectSchema("Eve provider advertisement"),
}),
surface: defineDocumentType({
type: "gamecult.eve.surface_state",
schemaName: "gamecult.eve.surface_state",
schemaId: "gamecult.eve.surface_state.v1",
schemaVersion: "gamecult.eve.surface_state.v1",
global: false,
name: (value) => value?.providerId || value?.provider_id || "surface",
schema: objectSchema("Eve surface state"),
}),
binding: defineDocumentType({
type: "gamecult.eve.interface_binding",
schemaName: "gamecult.eve.interface_binding",
schemaId: "gamecult.eve.interface_binding.v1",
schemaVersion: "gamecult.eve.interface_binding.v1",
global: false,
name: (value) => value?.bindingId || value?.providerId || "interface",
schema: objectSchema("Eve interface binding"),
}),
providerCatalog: defineDocumentType({
type: "voidbot.provider_advertisement_catalog",
schemaName: "voidbot.provider_advertisement_catalog",
schemaId: "voidbot.provider_advertisement_catalog.v0",
schemaVersion: "voidbot.provider_advertisement_catalog.v0",
global: true,
schema: objectSchema("VoidBot provider advertisement catalog"),
}),
transportProfile: defineDocumentType({
type: "idunn.daemon_transport_profile",
schemaName: "idunn.daemon_transport_profile",
schemaId: "idunn.daemon_transport_profile.v1",
schemaVersion: "idunn.daemon_transport_profile.v1",
global: false,
name: (value) => value?.profile_id || value?.daemon_id || "voidbot",
schema: objectSchema("Idunn daemon transport profile"),
}),
commandBoundary: defineDocumentType({
type: "idunn.command_boundary",
schemaName: "idunn.command_boundary",
schemaId: "idunn.command_boundary.v1",
schemaVersion: "idunn.command_boundary.v1",
global: false,
name: (value) => value?.boundary_id || value?.daemon_id || "voidbot",
schema: objectSchema("Idunn command boundary"),
}),
eveCommand: defineDocumentType({
type: "gamecult.eve.command",
schemaName: "gamecult.eve.command",
schemaId: "gamecult.eve.command.v1",
schemaVersion: "gamecult.eve.command.v1",
global: false,
name: (value) => value?.commandId || value?.command_id || "swarm-command",
schema: objectSchema("Eve command"),
}),
commandReceipt: defineDocumentType({
type: "gamecult.eve.command_receipt",
schemaName: "gamecult.eve.command_receipt",
schemaId: "gamecult.eve.command_receipt.v1",
schemaVersion: "gamecult.eve.command_receipt.v1",
global: false,
name: (value) => value?.commandId || value?.receiptId || "command-receipt",
schema: objectSchema("Eve command receipt"),
}),
swarmControl: defineDocumentType({
type: "voidbot.swarm_control_state",
schemaName: "voidbot.swarm_control_state",
schemaId: "voidbot.swarm_control_state.v1",
schemaVersion: "voidbot.swarm_control_state.v1",
global: false,
name: () => "voidbot-swarm",
schema: objectSchema("VoidBot swarm control state"),
}),
swarmOperatorView: defineDocumentType({
type: "voidbot.swarm_operator_view",
schemaName: "voidbot.swarm_operator_view",
schemaId: "voidbot.swarm_operator_view.v1",
schemaVersion: "voidbot.swarm_operator_view.v1",
global: false,
name: () => "voidbot-swarm",
schema: objectSchema("VoidBot swarm operator view"),
}),
};
}
function loadCultRuntime() {
const cultLibRoot = process.env.VOIDBOT_CULTLIB_ROOT
? path.resolve(process.env.VOIDBOT_CULTLIB_ROOT)
: path.resolve(repoRoot, "..", "CultLib-dev-runtime");
const packageJson = path.resolve(cultLibRoot, "packages", "cultmesh-ts", "package.json");
const requireCult = createRequire(packageJson);
const {
CultMesh,
CultMeshMemoryProviderReceiptStore,
CultMeshProviderRudpTransport,
CultMeshProviderSession,
} = requireCult("./dist/index.js");
const { CultNetDocumentRegistry, defineCultNetDocumentBinding } = createRequire(path.resolve(cultLibRoot, "packages", "cultnet-ts", "package.json"))("./dist/index.js");
const { defineDocumentType } = createRequire(path.resolve(cultLibRoot, "packages", "cultcache-ts", "package.json"))("./dist/index.js");
return {
CultMesh,
CultMeshMemoryProviderReceiptStore,
CultMeshProviderRudpTransport,
CultMeshProviderSession,
CultNetDocumentRegistry,
defineCultNetDocumentBinding,
defineDocumentType,
};
}
function parseArgs(values) {
const parsed = {};
for (let index = 0; index < values.length; index += 1) {
const arg = values[index];
if (arg === "--store") parsed.store = values[++index];
else if (arg === "--control-store") parsed.controlStore = values[++index];
else if (arg === "--bind") parsed.bind = values[++index];
else if (arg === "--odin-cultmesh-uri") parsed.odinCultMeshUri = values[++index];
else if (arg === "--odin-rudp-endpoint") parsed.odinRudpEndpoint = values[++index];
}
return parsed;
}
function parseBind(value) {
const text = String(value || "").trim();
const index = text.lastIndexOf(":");
if (index <= 0) throw new Error(`Bind address must be host:port, got ${value}`);
const host = text.slice(0, index);
const port = Number(text.slice(index + 1));
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
throw new Error(`Invalid port in bind address ${value}`);
}
return { host, port };
}