From 755649ff29c729a4980751e075894a02f856c83e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 7 Jun 2026 04:21:42 +0100 Subject: [PATCH] Add CultMesh operator notification publisher --- tools/operator-notification.mjs | 242 ++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 tools/operator-notification.mjs diff --git a/tools/operator-notification.mjs b/tools/operator-notification.mjs new file mode 100644 index 0000000..a995dcb --- /dev/null +++ b/tools/operator-notification.mjs @@ -0,0 +1,242 @@ +#!/usr/bin/env node +import { createRequire } from "node:module"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { createHash } from "node:crypto"; + +const repoRoot = resolve(import.meta.dirname, ".."); +const projectsRoot = resolve(repoRoot, ".."); +const defaultStorePath = resolve(projectsRoot, "VoidBot", ".voidbot", "status", "cultmesh", "voidbot-swarm-state.cc"); +const requestDocumentType = "gamecult.operator_dm_request"; +const requestSchemaId = "gamecult.operator_dm_request.v1"; + +async function main() { + const [command, ...rawArgs] = process.argv.slice(2); + const options = parseArgs(rawArgs); + + if (!command || command === "help" || command === "--help" || command === "-h") { + printHelp(); + return; + } + + switch (command) { + case "publish-idunn-alarm": + await publishIdunnAlarm(options); + return; + default: + throw new Error(`Unknown command "${command}". Run "node tools/operator-notification.mjs help".`); + } +} + +async function publishIdunnAlarm(options) { + const storePath = resolve(options.store ?? defaultStorePath); + const alarm = { + alarmId: readOptionOrEnv(options, "alarm-id", "IDUNN_ALARM_ID", "unknown-alarm"), + daemonId: readOptionOrEnv(options, "daemon-id", "IDUNN_ALARM_DAEMON_ID", "unknown-daemon"), + severity: readOptionOrEnv(options, "severity", "IDUNN_ALARM_SEVERITY", "operator-action-required"), + reason: readOptionOrEnv(options, "reason", "IDUNN_ALARM_REASON", "Idunn raised an operator alarm."), + raisedAt: readOptionOrEnv(options, "raised-at", "IDUNN_ALARM_RAISED_AT", new Date().toISOString()), + }; + const request = buildOperatorDmRequest(alarm); + + if (options["dry-run"] === "true") { + printJson({ + dryRun: true, + action: "publish-idunn-alarm", + storePath, + documentType: requestDocumentType, + schemaId: requestSchemaId, + request, + }); + return; + } + + const { CultMesh, defineDocumentType } = loadCultMeshRuntime(); + const requestDefinition = defineDocumentType({ + type: requestDocumentType, + schemaName: requestDocumentType, + schemaId: requestSchemaId, + schemaVersion: requestSchemaId, + contentHash: requestSchemaId, + global: false, + name: "requestId", + schema: { parse: parseObjectDocument("Operator DM request") }, + }); + const node = await CultMesh.createNode(storePath, { + documents: [ + requestDefinition, + defineDocumentType({ + type: "voidbot.swarm_state_snapshot", + schemaName: "voidbot.swarm_state_snapshot", + schemaId: "voidbot.swarm_state_snapshot.v1", + schemaVersion: "voidbot.swarm_state_snapshot.v1", + contentHash: "voidbot.swarm_state_snapshot.v1", + global: false, + schema: { parse: parseObjectDocument("VoidBot swarm snapshot") }, + }), + defineDocumentType({ + type: "gamecult.eve.provider_advertisement", + schemaName: "gamecult.eve.provider_advertisement", + schemaId: "gamecult.eve.provider_advertisement.v1", + schemaVersion: "gamecult.eve.provider_advertisement.v1", + contentHash: "gamecult.eve.provider_advertisement.v1", + global: false, + name: "providerId", + schema: { parse: parseObjectDocument("Eve provider advertisement") }, + }), + defineDocumentType({ + type: "gamecult.eve.surface_state", + schemaName: "gamecult.eve.surface_state", + schemaId: "gamecult.eve.surface_state.v1", + schemaVersion: "gamecult.eve.surface_state.v1", + contentHash: "gamecult.eve.surface_state.v1", + global: false, + name: "providerId", + schema: { parse: parseObjectDocument("Eve surface state") }, + }), + defineDocumentType({ + type: "gamecult.eve.interface_binding", + schemaName: "gamecult.eve.interface_binding", + schemaId: "gamecult.eve.interface_binding.v1", + schemaVersion: "gamecult.eve.interface_binding.v1", + contentHash: "gamecult.eve.interface_binding.v1", + global: false, + name: "bindingId", + schema: { parse: parseObjectDocument("Eve interface binding") }, + }), + ], + }); + await node.put(requestDefinition, request.requestId, request); + await node.flush?.(); + + printJson({ + ok: true, + action: "publish-idunn-alarm", + storePath, + documentType: requestDocumentType, + schemaId: requestSchemaId, + requestId: request.requestId, + }); +} + +function buildOperatorDmRequest(alarm) { + const requestId = `idunn-alarm-${createHash("sha1") + .update(JSON.stringify(alarm)) + .digest("hex") + .slice(0, 16)}`; + const message = [ + "Idunn needs operator intervention.", + "", + `Service: ${alarm.daemonId}`, + `Severity: ${alarm.severity}`, + `Reason: ${alarm.reason}`, + `Alarm: ${alarm.alarmId}`, + `Raised: ${alarm.raisedAt}`, + "", + "Automatic recovery either was not authorized or failed.", + ].join("\n"); + + return { + requestId, + command: "owner.dm.send", + status: "pending", + service: alarm.daemonId, + sourceId: alarm.alarmId, + severity: alarm.severity, + reason: alarm.reason, + message, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + requestedBy: "idunn", + transportOwner: "bifrost", + schemaName: requestDocumentType, + schemaVersion: requestSchemaId, + }; +} + +function loadCultMeshRuntime() { + const candidates = [ + resolve(projectsRoot, "CultLib", "packages", "cultmesh-ts", "package.json"), + resolve(projectsRoot, "CultMeshTS", "package.json"), + ]; + + for (const packageJson of candidates) { + if (!existsSync(packageJson)) { + continue; + } + try { + const requireCult = createRequire(packageJson); + const { CultMesh } = requireCult("cultmesh-ts"); + const { defineDocumentType } = requireCult("cultcache-ts"); + if (CultMesh && defineDocumentType) { + return { CultMesh, defineDocumentType }; + } + } catch { + } + } + + throw new Error("CultMesh/CultCache TypeScript runtime is unavailable."); +} + +function parseObjectDocument(label) { + return { + parse(input) { + if (!input || typeof input !== "object") { + throw new Error(`${label} must be an object.`); + } + return input; + }, + }; +} + +function readOptionOrEnv(options, optionName, envName, fallback) { + return optionalString(options[optionName]) ?? optionalString(process.env[envName]) ?? fallback; +} + +function parseArgs(args) { + const options = {}; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (!token.startsWith("--")) { + throw new Error(`Unexpected argument "${token}".`); + } + const key = token.slice(2); + const next = args[index + 1]; + if (!next || next.startsWith("--")) { + options[key] = "true"; + continue; + } + options[key] = next; + index += 1; + } + return options; +} + +function optionalString(value) { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +function printJson(value) { + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); +} + +function printHelp() { + process.stdout.write(`Bifrost operator notification CultMesh publisher + +Commands: + publish-idunn-alarm Publish an Idunn alarm as gamecult.operator_dm_request.v1 + +Examples: + node tools/operator-notification.mjs publish-idunn-alarm --dry-run + node tools/operator-notification.mjs publish-idunn-alarm --daemon-id voidbot --reason "restart failed" +`); +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; +});