From 8b4e043164d944fd59308b0ba0d477dc7c0b68fc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 16 Jun 2026 20:50:49 +0100 Subject: [PATCH] Publish VoidBot daemon-owned provider witness --- scripts/render-voidbot-swarm-dashboard.mjs | 141 ++++++++++++++++++++- scripts/run-gamecult-orchestrator.ps1 | 80 +++++++++--- scripts/run-repo-face-heartbeats.ts | 20 --- 3 files changed, 195 insertions(+), 46 deletions(-) diff --git a/scripts/render-voidbot-swarm-dashboard.mjs b/scripts/render-voidbot-swarm-dashboard.mjs index 80f8b8f..d3ee5ce 100644 --- a/scripts/render-voidbot-swarm-dashboard.mjs +++ b/scripts/render-voidbot-swarm-dashboard.mjs @@ -21,6 +21,12 @@ const eveBindingDocumentType = "gamecult.eve.interface_binding"; const eveBindingSchemaId = "gamecult.eve.interface_binding.v1"; const surfaceDocumentType = "gamecult.eve.surface_state"; const surfaceSchemaId = "gamecult.eve.surface_state.v1"; +const providerCatalogDocumentType = "voidbot.provider_advertisement_catalog"; +const providerCatalogSchemaId = "voidbot.provider_advertisement_catalog.v0"; +const transportProfileDocumentType = "idunn.daemon_transport_profile"; +const transportProfileSchemaId = "idunn.daemon_transport_profile.v1"; +const commandBoundaryDocumentType = "idunn.command_boundary"; +const commandBoundarySchemaId = "idunn.command_boundary.v1"; const args = parseArgs(process.argv.slice(2)); @@ -716,7 +722,7 @@ async function writeCultMeshPublication(snapshot, eveState, storePath, allowStor schemaId: providerAdvertisementSchemaId, schemaVersion: providerAdvertisementSchemaId, contentHash: providerAdvertisementSchemaId, - global: true, + global: false, name: "providerId", schema: { parse: parseObjectDocument("Eve provider advertisement") }, }); @@ -736,24 +742,79 @@ async function writeCultMeshPublication(snapshot, eveState, storePath, allowStor schemaId: eveBindingSchemaId, schemaVersion: eveBindingSchemaId, contentHash: eveBindingSchemaId, - global: true, + global: false, name: "bindingId", schema: { parse: parseObjectDocument("Eve interface binding") }, }); - const node = await CultMesh.createNode(storePath, { - documents: [snapshotDefinition, providerDefinition, surfaceDefinition, eveBindingDefinition], + const providerCatalogDefinition = defineDocumentType({ + type: providerCatalogDocumentType, + schemaName: providerCatalogDocumentType, + schemaId: providerCatalogSchemaId, + schemaVersion: providerCatalogSchemaId, + contentHash: providerCatalogSchemaId, + global: true, + schema: { parse: parseObjectDocument("VoidBot provider advertisement catalog") }, }); - const providerAdvertisement = buildProviderAdvertisement(snapshot); + const transportProfileDefinition = defineDocumentType({ + type: transportProfileDocumentType, + schemaName: transportProfileDocumentType, + schemaId: transportProfileSchemaId, + schemaVersion: transportProfileSchemaId, + contentHash: transportProfileSchemaId, + global: false, + name: (value) => value?.profile_id || value?.daemon_id || "voidbot", + schema: { parse: parseObjectDocument("Idunn daemon transport profile") }, + }); + const commandBoundaryDefinition = defineDocumentType({ + type: commandBoundaryDocumentType, + schemaName: commandBoundaryDocumentType, + schemaId: commandBoundarySchemaId, + schemaVersion: commandBoundarySchemaId, + contentHash: commandBoundarySchemaId, + global: false, + name: (value) => value?.boundary_id || value?.daemon_id || "voidbot", + schema: { parse: parseObjectDocument("Idunn command boundary") }, + }); + const node = await CultMesh.createNode(storePath, { + documents: [ + snapshotDefinition, + providerDefinition, + surfaceDefinition, + eveBindingDefinition, + providerCatalogDefinition, + transportProfileDefinition, + commandBoundaryDefinition, + ], + }); + const providerCatalog = loadProviderAdvertisementCatalog(snapshot); + const providerAdvertisements = buildLiveProviderAdvertisements(providerCatalog, snapshot); + const providerAdvertisement = providerAdvertisements.find((item) => item.providerId === providerId) + ?? buildProviderAdvertisement(snapshot); const eveBinding = buildEveInterfaceBinding(snapshot, eveState); + const commandBoundary = buildCommandBoundary(snapshot); + const transportProfile = buildTransportProfile(snapshot); await node.put(snapshotDefinition, "voidbot-swarm", snapshot); - await node.put(providerDefinition, providerId, providerAdvertisement); + await node.put(providerCatalogDefinition, "voidbot.providers", providerCatalog); + for (const advertisement of providerAdvertisements) { + await node.put(providerDefinition, advertisement.providerId, advertisement); + } await node.put(surfaceDefinition, providerId, eveState); await node.put(eveBindingDefinition, providerId, eveBinding); + await node.put(commandBoundaryDefinition, commandBoundary.boundary_id, commandBoundary); + await node.put(transportProfileDefinition, transportProfile.profile_id, transportProfile); await node.flush?.(true); return { writeStatus: "ok", storePath, - documents: [snapshotDocumentType, providerAdvertisementDocumentType, surfaceDocumentType, eveBindingDocumentType], + documents: [ + snapshotDocumentType, + providerCatalogDocumentType, + providerAdvertisementDocumentType, + surfaceDocumentType, + eveBindingDocumentType, + commandBoundaryDocumentType, + transportProfileDocumentType, + ], providerId, verseId, eveBinding: { @@ -845,6 +906,39 @@ function parseObjectDocument(label) { }; } +function loadProviderAdvertisementCatalog(snapshot) { + const fixturePath = resolve(repoRoot, "docs", "architecture", "voidbot-provider-advertisements.fixture.json"); + const fixture = JSON.parse(readFileSync(fixturePath, "utf8")); + fixture.fixtureKind = "daemon-live-publication"; + fixture.generatedAt = snapshot.generatedAt; + fixture.cultMeshKeys.liveSwarmStore = cultMeshStorePath; + return fixture; +} + +function buildLiveProviderAdvertisements(catalog, snapshot) { + const providers = Array.isArray(catalog.providers) ? catalog.providers : []; + return providers.map((provider) => { + if (provider.providerId === providerId) { + return { + ...provider, + status: snapshot.summary?.state ?? provider.status ?? "unknown", + updatedAt: snapshot.generatedAt, + witnesses: Array.isArray(provider.witnesses) + ? provider.witnesses.map((witness) => + witness.id === "voidbot.swarm_state_snapshot" + ? { ...witness, path: cultMeshStorePath } + : witness) + : provider.witnesses, + }; + } + + return { + ...provider, + updatedAt: snapshot.generatedAt, + }; + }); +} + function buildProviderAdvertisement(snapshot) { const endpoints = [ primaryCultMeshEndpoint(), @@ -884,6 +978,39 @@ function buildProviderAdvertisement(snapshot) { }; } +function buildCommandBoundary(snapshot) { + return { + boundary_id: "command-boundary:voidbot", + daemon_id: "voidbot", + owner: "VoidBot local stack", + restart_authority: "E:\\Projects\\Odin\\scripts\\restart-voidbot.cmd", + deploy_authority: "none", + health_authority: "voidbot.cultnet-rudp-stack-health", + alarm_authority: "bifrost.operator-notification", + compatibility_commands: [ + "E:\\Projects\\Odin\\scripts\\health-voidbot.cmd", + "E:\\Projects\\Odin\\scripts\\restart-voidbot.cmd", + ], + forbidden_authority: "Health commands, operations watchdog probes, static HTML lowerings, and Odin renderers do not own VoidBot provider state or restart truth.", + observed_at: snapshot.generatedAt, + }; +} + +function buildTransportProfile(snapshot) { + return { + profile_id: "transport:voidbot", + daemon_id: "voidbot", + target_transport: "cultnet.transport.rudp.v0", + current_transport: "daemon-published-rudp-health + daemon-owned-cultmesh-provider-store + compatibility.local-command fallback", + state: "partial-rudp-health-and-provider-store-live", + health_contract: "voidbot.cultnet-rudp-stack-health", + publication_schema: "gamecult.eve.provider_advertisement.v1 + idunn.command_boundary.v1 + idunn.daemon_transport_profile.v1", + compatibility_mechanism: "E:\\Projects\\Odin\\scripts\\health-voidbot.cmd + operations-health.json watchdog probe", + cut_line: `VoidBot now publishes provider advertisements, command boundary, and transport profile from ${cultMeshStorePath}; the remaining compatibility path is the local operations probe, which becomes fallback evidence only.`, + observed_at: snapshot.generatedAt, + }; +} + function buildEveInterfaceBinding(snapshot, eveState) { return { schemaVersion: eveBindingSchemaId, diff --git a/scripts/run-gamecult-orchestrator.ps1 b/scripts/run-gamecult-orchestrator.ps1 index f8349c7..5da9d81 100644 --- a/scripts/run-gamecult-orchestrator.ps1 +++ b/scripts/run-gamecult-orchestrator.ps1 @@ -134,6 +134,7 @@ function Publish-IdunnRudpHealth { Add-Content -LiteralPath (Join-Path $logDir "idunn-rudp-health.log") -Encoding UTF8 -Value $message } } + function Get-OrganState { param($State, [string] $Id) $property = $State.organs.PSObject.Properties[$Id] @@ -148,6 +149,19 @@ function Set-OrganState { $State.organs | Add-Member -NotePropertyName $Id -NotePropertyValue $Value -Force } +function Set-SkippedOrganState { + param($State, $Organ, [datetime] $Now, [string] $Status, $LogPath = $null) + Set-OrganState -State $State -Id $Organ.Id -Value ([pscustomobject]@{ + label = $Organ.Label + intervalMinutes = $Organ.IntervalMinutes + lastStartedAt = $Now.ToString("o") + lastFinishedAt = $Now.ToString("o") + lastExitCode = 0 + lastStatus = $Status + lastLogPath = $LogPath + }) +} + function Test-Due { param($State, [string] $Id, [int] $IntervalMinutes, [datetime] $Now) if ($Force) { @@ -251,6 +265,25 @@ function Test-OrchestratorLock { return $false } $startedAt = [datetime]::Parse($lock.startedAt).ToUniversalTime() + $lockPid = if ($lock.PSObject.Properties["pid"]) { [int]$lock.pid } else { 0 } + $lockAgeMinutes = (([DateTime]::UtcNow) - $startedAt).TotalMinutes + if ($lockPid -gt 0) { + $lockProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $lockPid" -ErrorAction SilentlyContinue + if ($null -eq $lockProcess) { + Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue + return $false + } + if ($lockAgeMinutes -gt 2) { + $children = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { + [int]$_.ParentProcessId -eq $lockPid -and $_.Name -ne "conhost.exe" + }) + if ($children.Count -eq 0) { + Stop-Process -Id $lockPid -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue + return $false + } + } + } if ((([DateTime]::UtcNow) - $startedAt).TotalMinutes -gt 45) { Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue return $false @@ -302,6 +335,7 @@ try { "bifrost-dispatch" = $true "repo-face-heartbeats" = $true "void-mood-drift" = $true + "void-moderation-heartbeat" = $true "void-moderation-rumination" = $true } $onlySet = @{} @@ -323,6 +357,15 @@ try { Executable = $node Arguments = @((Join-Path $bifrostRoot "tools\dispatch-agent-requests.mjs"), "dispatch", "--repo", "*", "--max", "1") }, + [pscustomobject]@{ + Id = "voidbot-swarm-surface" + Label = "VoidBot swarm surface" + IntervalMinutes = 1 + TimeoutMinutes = 5 + Cwd = $repoRoot + Executable = $node + Arguments = @((Join-Path $PSScriptRoot "render-voidbot-swarm-dashboard.mjs")) + }, [pscustomobject]@{ Id = "repo-face-heartbeats" Label = "Repo Face CTB turns" @@ -345,7 +388,7 @@ try { Id = "void-moderation-heartbeat" Label = "Void rules moderation heartbeat" IntervalMinutes = Get-ConfigInt -Config $config -Name "VOIDBOT_MODERATION_HEARTBEAT_INTERVAL_MINUTES" -Default 1 -Minimum 1 - TimeoutMinutes = 10 + TimeoutMinutes = Get-ConfigInt -Config $config -Name "VOIDBOT_MODERATION_HEARTBEAT_TIMEOUT_MINUTES" -Default 3 -Minimum 1 Cwd = $repoRoot Executable = $powershell Arguments = @("-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", (Join-Path $PSScriptRoot "run-void-moderator-rumination.ps1"), "-ModerationHeartbeatOnly") @@ -376,15 +419,22 @@ try { continue } if ($organ.Id -eq "bifrost-dispatch" -and -not (Get-ConfigBool -Config $config -Name "BIFROST_DISPATCH_ENABLED" -Default $false)) { - Set-OrganState -State $state -Id $organ.Id -Value ([pscustomobject]@{ - label = $organ.Label - intervalMinutes = $organ.IntervalMinutes - lastStartedAt = $now.ToString("o") - lastFinishedAt = $now.ToString("o") - lastExitCode = 0 - lastStatus = "skipped_disabled" - lastLogPath = $null - }) + Set-SkippedOrganState -State $state -Organ $organ -Now $now -Status "skipped_disabled" + Write-JsonFile -Path $statePath -Data $state + continue + } + if ($organ.Id -eq "void-moderation-heartbeat" -and -not (Get-ConfigBool -Config $config -Name "VOIDBOT_MODERATION_HEARTBEAT_ENABLED" -Default $true)) { + Set-SkippedOrganState -State $state -Organ $organ -Now $now -Status "skipped_disabled" + Write-JsonFile -Path $statePath -Data $state + continue + } + if ($organ.Id -eq "void-moderation-rumination" -and -not (Get-ConfigBool -Config $config -Name "VOIDBOT_MODERATION_RUMINATION_ENABLED" -Default $true)) { + Set-SkippedOrganState -State $state -Organ $organ -Now $now -Status "skipped_disabled" + Write-JsonFile -Path $statePath -Data $state + continue + } + if ($organ.Id -eq "voidbot-operations-watchdog" -and -not (Get-ConfigBool -Config $config -Name "VOIDBOT_OPERATIONS_WATCHDOG_ENABLED" -Default $true)) { + Set-SkippedOrganState -State $state -Organ $organ -Now $now -Status "skipped_disabled" Write-JsonFile -Path $statePath -Data $state continue } @@ -393,15 +443,7 @@ try { } if ($agentSwarmPaused -and $agentSwarmOrganIds.ContainsKey($organ.Id)) { - Set-OrganState -State $state -Id $organ.Id -Value ([pscustomobject]@{ - label = $organ.Label - intervalMinutes = $organ.IntervalMinutes - lastStartedAt = $now.ToString("o") - lastFinishedAt = $now.ToString("o") - lastExitCode = 0 - lastStatus = "skipped_agent_swarm_paused" - lastLogPath = $agentSwarmPausePath - }) + Set-SkippedOrganState -State $state -Organ $organ -Now $now -Status "skipped_agent_swarm_paused" -LogPath $agentSwarmPausePath Write-JsonFile -Path $statePath -Data $state continue } diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index 13801f8..e612f08 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -375,7 +375,6 @@ async function main(): Promise { state.lastTickAt = now.toISOString(); if (!dryRun) { await writeHeartbeatState(config.repoFaceHeartbeats.statePath, state); - publishSwarmDashboardSurface(); } process.stdout.write( `${JSON.stringify({ @@ -5535,25 +5534,6 @@ async function writeHeartbeatState(path: string, state: FaceHeartbeatState): Pro await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, "utf8"); } -function publishSwarmDashboardSurface(): void { - const scriptPath = resolve(process.cwd(), "scripts", "render-voidbot-swarm-dashboard.mjs"); - const result = spawnSync(process.execPath, [scriptPath], { - cwd: process.cwd(), - encoding: "utf8", - windowsHide: true, - }); - if (result.status === 0) { - return; - } - - const stderr = result.stderr?.trim(); - const stdout = result.stdout?.trim(); - const detail = [stderr, stdout].filter(Boolean).join("\n"); - console.error( - `VoidBot swarm CultMesh surface publish failed with exit code ${result.status ?? "unknown"}${detail ? `:\n${detail}` : "."}`, - ); -} - function migrateLegacyHeartbeatState( parsed: Partial & { baseIntervalMinutes?: number;