Publish VoidBot daemon-owned provider witness
This commit is contained in:
parent
d54a3974ad
commit
8b4e043164
3 changed files with 194 additions and 45 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -375,7 +375,6 @@ async function main(): Promise<void> {
|
|||
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<FaceHeartbeatState> & {
|
||||
baseIntervalMinutes?: number;
|
||||
|
|
|
|||
Loading…
Reference in a new issue