From b43d2a3136eb42e56c2ad83bf1887dc397c5243b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 17 Jun 2026 01:28:55 +0100 Subject: [PATCH] Serve Odin snapshots and retire Gjallar bridge debt --- README.md | 19 ++- crates/idunn-daemon/src/main.rs | 55 ++++---- scripts/start-odin.ps1 | 3 + src/Idunn/README.md | 86 +++++------- src/odin-coordinator.cjs | 17 +++ src/odin/config.cjs | 2 + src/odin/cultnet-rudp.cjs | 229 ++++++++++++++++++++++++++++++++ 7 files changed, 322 insertions(+), 89 deletions(-) create mode 100644 src/odin/cultnet-rudp.cjs diff --git a/README.md b/README.md index fdce124..73e3404 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,17 @@ documents, ingest ports, normalization, and repository persistence so unit tests can use mocked inputs and pipeline smokes can prove typed handoff without booting the whole daemon. Gjallar is not part of that Rust record spine: it is the Nightwing-resident terminal compositor in `E:\Projects\Gjallar` that -consumes Odin's published Eve/CultUI deck and renders the live display. +consumes Odin's accepted `gamecult.eve.surface_state` snapshot over CultNet/RUDP +and renders the live display. ## Gjallar Gjallar is the herald display daemon that runs on Nightwing. Odin sees the Verses, accepts provider surfaces, and publishes the `odin.providers` catalog. -Gjallar talks to Odin, enumerates active providers, composes the multi-scale -tiled dashboard, lowers Odin's canonical marquee tape into continuous gutter -text, owns dense character-level update behavior, and writes the visible -framebuffer. +Gjallar consumes Odin's accepted surface snapshot over CultNet/RUDP, composes +the multi-scale tiled dashboard from that typed state, lowers Odin's canonical +marquee tape into continuous gutter text, owns dense character-level update +behavior, and writes the visible framebuffer. Local package surfaces: @@ -95,7 +96,13 @@ The Eve/CultUI deck endpoint is a compatibility bridge: ws://127.0.0.1:8797/eve/deck ``` -Nightwing can consume the LAN bridge endpoint: +The native CultNet/RUDP snapshot endpoint is: + +```text +127.0.0.1:17871 +``` + +Legacy browser/deck lowerers can consume the LAN bridge endpoint: ```text ws://192.168.1.66:8797/eve/deck diff --git a/crates/idunn-daemon/src/main.rs b/crates/idunn-daemon/src/main.rs index a311bc8..cfcce56 100644 --- a/crates/idunn-daemon/src/main.rs +++ b/crates/idunn-daemon/src/main.rs @@ -951,14 +951,7 @@ fn swarm_surgery_plan( plans: &[IdunnDaemonSurgeryPlanRecord], updated_at: &str, ) -> IdunnSwarmSurgeryPlanRecord { - let next_target = preferred_next_target(targets, plans); - let next_target = if let Some(target) = next_target { - target - } else if let Some(target) = targets.iter().find(|target| target.enabled) { - target.daemon_id.as_str() - } else { - "none" - }; + let next_target = preferred_next_target(targets, plans).unwrap_or("none"); IdunnSwarmSurgeryPlanRecord { plan_id: format!("swarm-surgery:{profile}"), @@ -987,11 +980,11 @@ fn swarm_surgery_plan( "5. Delete or demote compatibility probes once every target has daemon-owned publication and advertised lifecycle authority.".to_string(), ], current_phase: - "Phase 24: Nightwing Gjallar now publishes a daemon-owned gamecult.eve.surface_state witness, Odin proxies a real Nightwing Gjallar panel from the live remote store, and the remaining Gjallar debt is replacing the compatibility Odin WebSocket deck input with native CultMesh/CultNet intake." + "Phase 25: Nightwing Gjallar now consumes Odin's accepted gamecult.eve.surface_state snapshot over CultNet/RUDP, so every active Starfire-local daemon target publishes daemon-owned RUDP health and typed boundary state; the remaining debt is demoting compatibility probes and bridge-only lowerings without letting them retake ownership." .to_string(), next_target: next_target.to_string(), cut_line: - "Muninn, Vili, Idunn, Odin, Stonks, Weksa, VoidBot, Nightwing Gjallar, Mimir Eve dashboard, Nightwing Eve dashboard, Nightwing Eve browser reference, yggdrasil-streampixels, yggdrasil-heimdall, and yggdrasil-repixelizer now exercise daemon-owned RUDP health. Raven GameCult\\Vili has been refreshed from Odin, the hidden scheduled task now launches start-vili-daemon.ps1 with Idunn RUDP arguments, and live Idunn accepts vili.cultnet-rudp-animation-health from 10.77.0.4. VoidBot now publishes a daemon-owned provider catalog, provider advertisements, command_boundary, and transport_profile records from E:\\Projects\\VoidBot\\.voidbot\\status\\cultmesh\\voidbot-swarm-state.cc while health-voidbot.cmd is demoted to a fast compatibility witness. Raven Muninn now publishes provider advertisement, command_boundary, and transport_profile records from C:\\Meta\\Odin\\state\\muninn.telemetry.cc, Starfire Muninn now publishes the same daemon-owned witness records from C:\\Meta\\Odin\\state\\starfire.muninn.telemetry.cc, Nightwing Muninn now publishes the same daemon-owned witness records from /home/metacrat/.local/state/gamecult/muninn/muninn.telemetry.cc, and live Idunn accepts muninn from 10.77.0.4, starfire-muninn from 127.0.0.1, and nightwing-muninn from 10.77.0.3. Raven's GameCult-Muninn, GameCult-Muninn-Activate, and GameCult-Muninn-VideoProof tasks now execute wscript.exe actions whose hidden VBS launchers call noninteractive hidden PowerShell entrypoints directly; .cmd wrappers, where present, are manual compatibility entrypoints only. Live yggdrasil-streampixels publishes streampixels.cultnet-rudp-service-health from 10.77.0.1, live yggdrasil-heimdall publishes heimdall.cultnet-rudp-provider-health from 10.77.0.1 with a boundary store at /srv/heimdall/cultcache/heimdall.service.cc, live yggdrasil-repixelizer publishes repixelizer.cultnet-rudp-service-health from 10.77.0.1 with a boundary store at /srv/repixelizer/cultcache/repixelizer.service.cc, live Nightwing Gjallar publishes a boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc plus a daemon-owned gamecult.eve.surface_state witness, live Odin now ingests remote Raven Vili and Nightwing Gjallar witness stores into the accepted provider catalog, and /eve/deck/nightwing-gjallar now proxies a real Nightwing Gjallar panel from the live remote store. Live Mimir Eve dashboard now publishes CultMesh state at /var/lib/gamecult/eve-dashboard/cultmesh/eve-dashboard.ccmp plus a boundary store at /var/lib/gamecult/eve-dashboard/cultcache/eve-dashboard.service.cc, and live Nightwing Eve browser reference now publishes a boundary store at /var/lib/gamecult/eve-browser-reference/cultcache/eve-browser-reference.service.cc. Gjallar remains the next compatibility debt until its native CultMesh/CultNet input path replaces the Odin WebSocket deck bridge." + "Muninn, Vili, Idunn, Odin, Stonks, Weksa, VoidBot, Nightwing Gjallar, Mimir Eve dashboard, Nightwing Eve dashboard, Nightwing Eve browser reference, yggdrasil-streampixels, yggdrasil-heimdall, and yggdrasil-repixelizer now exercise daemon-owned RUDP health. Raven GameCult\\Vili has been refreshed from Odin, the hidden scheduled task now launches start-vili-daemon.ps1 with Idunn RUDP arguments, and live Idunn accepts vili.cultnet-rudp-animation-health from 10.77.0.4. VoidBot now publishes a daemon-owned provider catalog, provider advertisements, command_boundary, and transport_profile records from E:\\Projects\\VoidBot\\.voidbot\\status\\cultmesh\\voidbot-swarm-state.cc while health-voidbot.cmd is demoted to a fast compatibility witness. Raven Muninn now publishes provider advertisement, command_boundary, and transport_profile records from C:\\Meta\\Odin\\state\\muninn.telemetry.cc, Starfire Muninn now publishes the same daemon-owned witness records from C:\\Meta\\Odin\\state\\starfire.muninn.telemetry.cc, Nightwing Muninn now publishes the same daemon-owned witness records from /home/metacrat/.local/state/gamecult/muninn/muninn.telemetry.cc, and live Idunn accepts muninn from 10.77.0.4, starfire-muninn from 127.0.0.1, and nightwing-muninn from 10.77.0.3. Raven's GameCult-Muninn, GameCult-Muninn-Activate, and GameCult-Muninn-VideoProof tasks now execute wscript.exe actions whose hidden VBS launchers call noninteractive hidden PowerShell entrypoints directly; .cmd wrappers, where present, are manual compatibility entrypoints only. Live yggdrasil-streampixels publishes streampixels.cultnet-rudp-service-health from 10.77.0.1, live yggdrasil-heimdall publishes heimdall.cultnet-rudp-provider-health from 10.77.0.1 with a boundary store at /srv/heimdall/cultcache/heimdall.service.cc, live yggdrasil-repixelizer publishes repixelizer.cultnet-rudp-service-health from 10.77.0.1 with a boundary store at /srv/repixelizer/cultcache/repixelizer.service.cc, live Nightwing Gjallar publishes a boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc plus a daemon-owned gamecult.eve.surface_state witness, live Gjallar now consumes Odin's accepted surface:gamecult.network.status snapshot over CultNet/RUDP from 192.168.1.66:17871, live Odin now ingests remote Raven Vili and Nightwing Gjallar witness stores into the accepted provider catalog, and /eve/deck/nightwing-gjallar now proxies a renderer-only lowering of the live remote store. Live Mimir Eve dashboard now publishes CultMesh state at /var/lib/gamecult/eve-dashboard/cultmesh/eve-dashboard.ccmp plus a boundary store at /var/lib/gamecult/eve-dashboard/cultcache/eve-dashboard.service.cc, and live Nightwing Eve browser reference now publishes a boundary store at /var/lib/gamecult/eve-browser-reference/cultcache/eve-browser-reference.service.cc. The remaining debt is demoting compatibility probes and bridge-only lowerings without letting them retake daemon truth." .to_string(), verification_layer: "CultMesh keepalive store records plus live Idunn decision cycles, not process exit codes or chat summaries." @@ -1238,9 +1231,9 @@ fn daemon_transport_profile( "VoidBot stack health is published over CultNet/RUDP from the local orchestrator pulse, and the daemon-owned CultMesh witness store at E:\\Projects\\VoidBot\\.voidbot\\status\\cultmesh\\voidbot-swarm-state.cc now carries provider advertisement catalog, provider advertisement, command_boundary, and transport_profile records. The operations probe remains fallback evidence only.", ), "nightwing-gjallar" => ( - "daemon-published-rudp-health + daemon-owned-cultcache-service-boundary + compatibility.local-command fallback", + "daemon-published-rudp-health + daemon-owned-cultcache-service-boundary + native-odin-cultnet-rudp-snapshot-input + compatibility.local-command fallback", "partial-rudp-health-and-provider-store-live", - "Gjallar framebuffer composition health is published over CultNet/RUDP from Nightwing, and the daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc carries provider advertisement, runtime_config, frame_status, command_boundary, transport_profile, and daemon-health summary state. Native C# CultNet/RUDP deck input remains migration debt before the service/status probe can be deleted.", + "Gjallar framebuffer composition health is published over CultNet/RUDP from Nightwing, the daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc carries provider advertisement, runtime_config, frame_status, gamecult.eve.surface_state, command_boundary, transport_profile, and daemon-health summary state, and live composition now consumes Odin's accepted surface:gamecult.network.status snapshot over CultNet/RUDP. The service/status probe is fallback evidence only.", ), "nightwing-eve-dashboard" => ( "daemon-published-rudp-health + daemon-owned-cultmesh-state + daemon-owned-cultcache-boundary-store + compatibility.local-command fallback", @@ -1401,23 +1394,23 @@ fn daemon_surgery_plan(target: &DaemonTarget, updated_at: &str) -> IdunnDaemonSu .to_string(); } "nightwing-gjallar" => { - status = "catalog-coherence-probe"; + status = "partial-rudp-health-and-provider-store-live"; severity = "critical"; - owner = "Gjallar C# runtime plus Odin provider feed"; + owner = "Gjallar C# runtime plus Odin accepted snapshot surface"; current_mechanism = - "Nightwing Gjallar publishes gjallar.cultnet-rudp-framebuffer-composition-health over CultNet/RUDP from the C# runtime, and the daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc now contains provider advertisement, gamecult.eve.surface_state, runtime_config, frame_status, command_boundary, transport_profile, and daemon-health summary records. Live Odin ingests that witness store and now proxies /eve/deck/nightwing-gjallar as a real panel." + "Nightwing Gjallar publishes gjallar.cultnet-rudp-framebuffer-composition-health over CultNet/RUDP from the C# runtime, the daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc now contains provider advertisement, gamecult.eve.surface_state, runtime_config, frame_status, command_boundary, transport_profile, and daemon-health summary records, and the live compositor now polls Odin's accepted surface:gamecult.network.status snapshot over cultnet.transport.rudp.v0 from 192.168.1.66:17871. Live Odin ingests that witness store and /eve/deck/nightwing-gjallar remains a renderer-only lowering." .to_string(); intended_authority = - "Gjallar subscribes to Odin/provider deck state over CultNet/RUDP and publishes framebuffer composition health, provider advertisement, provider-owned Eve surface, runtime config, command boundary, and transport profile as typed CultMesh/CultNet state." + "Gjallar consumes Odin's accepted provider surface snapshot over CultNet/RUDP and publishes framebuffer composition health, provider advertisement, provider-owned Eve surface, runtime config, command boundary, and transport profile as typed CultMesh/CultNet state." .to_string(); cut_line = - "Keep the service/status probe as fallback only until Gjallar's native CultMesh/RUDP input path replaces the Odin WebSocket deck bridge; the daemon-owned witness now owns provider advertisement, gamecult.eve.surface_state, command_boundary, and transport_profile truth, and the remaining debt is deleting the compatibility deck bridge from the live input path." + "Keep the service/status probe as fallback only; the daemon-owned witness now owns provider advertisement, gamecult.eve.surface_state, command_boundary, and transport_profile truth, live input now comes from Odin's accepted CultNet/RUDP snapshot surface, and the remaining debt is deleting or demoting compatibility probes and renderer-only deck lowerings without letting them reclaim daemon truth." .to_string(); steps = vec![ "Keep live gjallar.cultnet-rudp-framebuffer-composition-health publication running from Nightwing's Gjallar service.".to_string(), "Keep Gjallar's daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc publishing provider advertisement, gamecult.eve.surface_state, runtime_config, frame_status, command_boundary, transport_profile, and daemon-health summary records.".to_string(), - "Replace Odin WebSocket deck consumption with a native C# CultMesh/RUDP input path when the deck contract is ready.".to_string(), - "Keep Odin's hashed-store ingestion shim strictly as a xenos-boundary bridge until Gjallar's native CultMesh/CultNet input path and interoperable witness path make the compatibility deck bridge disposable.".to_string(), + "Keep the native C# CultNet/RUDP snapshot polling path pointed at Odin's accepted surface:gamecult.network.status surface on 192.168.1.66:17871.".to_string(), + "Keep /eve/deck/nightwing-gjallar as a renderer-only lowering; it must not become Gjallar's live input path again.".to_string(), "Delete or demote health-nightwing-gjallar.cmd to a manual deployment witness with no lifecycle truth.".to_string(), ]; } @@ -2829,9 +2822,9 @@ mod tests { assert_eq!(plan.plan_id, "swarm-surgery:starfire-local"); assert_eq!(plan.status, "active-transport-migration"); - assert_eq!(plan.next_target, "nightwing-gjallar"); + assert_eq!(plan.next_target, "none"); assert!(plan.current_phase.contains( - "Nightwing Gjallar now publishes a daemon-owned gamecult.eve.surface_state witness" + "Nightwing Gjallar now consumes Odin's accepted gamecult.eve.surface_state snapshot over CultNet/RUDP" )); assert!( plan.cut_line.contains( @@ -2904,10 +2897,9 @@ mod tests { assert!(plan.cut_line.contains( "/var/lib/gamecult/eve-browser-reference/cultcache/eve-browser-reference.service.cc" )); - assert!( - plan.cut_line - .contains("Gjallar remains the next compatibility debt") - ); + assert!(plan.cut_line.contains("surface:gamecult.network.status")); + assert!(plan.cut_line.contains("192.168.1.66:17871")); + assert!(plan.cut_line.contains("renderer-only lowering")); assert!(plan.verification_layer.contains("CultMesh keepalive store")); assert!( plan.invariants @@ -3033,13 +3025,21 @@ mod tests { ); let gjallar_plan = daemon_surgery_plan(&nightwing_gjallar, "unix:100"); - assert_eq!(gjallar_plan.status, "catalog-coherence-probe"); + assert_eq!( + gjallar_plan.status, + "partial-rudp-health-and-provider-store-live" + ); assert!( gjallar_plan .current_mechanism .contains("gjallar.cultnet-rudp-framebuffer-composition-health") ); assert!(gjallar_plan.cut_line.contains("gamecult.eve.surface_state")); + assert!( + gjallar_plan + .current_mechanism + .contains("surface:gamecult.network.status") + ); let nightwing_eve_dashboard_plan = daemon_surgery_plan(&nightwing_eve_dashboard, "unix:100"); @@ -3423,13 +3423,14 @@ mod tests { assert_eq!(profile.state, "partial-rudp-health-and-provider-store-live"); assert_eq!( profile.current_transport, - "daemon-published-rudp-health + daemon-owned-cultcache-service-boundary + compatibility.local-command fallback" + "daemon-published-rudp-health + daemon-owned-cultcache-service-boundary + native-odin-cultnet-rudp-snapshot-input + compatibility.local-command fallback" ); assert!( profile .cut_line .contains("Gjallar framebuffer composition health is published over CultNet/RUDP") ); + assert!(profile.cut_line.contains("surface:gamecult.network.status")); } #[test] diff --git a/scripts/start-odin.ps1 b/scripts/start-odin.ps1 index 12249c6..1552c06 100644 --- a/scripts/start-odin.ps1 +++ b/scripts/start-odin.ps1 @@ -1,6 +1,7 @@ param( [int] $Port = 8797, [string] $StateDir = "E:\Projects\Odin\scratch\odin", + [string] $CultNetRudpBind = "0.0.0.0:17871", [string] $IdunnRudpHealth = "127.0.0.1:17870", [string] $IdunnDaemon = "odin", [string] $IdunnHealthContract = "odin.cultnet-rudp-provider-health", @@ -33,6 +34,7 @@ $args = @( $scriptPath, "--port", "$Port", "--stateDir", $StateDir, + "--cultnet-rudp-bind", $CultNetRudpBind, "--idunn-rudp-health", $IdunnRudpHealth, "--idunn-daemon", $IdunnDaemon, "--idunn-health-contract", $IdunnHealthContract @@ -56,3 +58,4 @@ if ($proc.HasExited) { Write-Host "Odin started as PID $($proc.Id)." Write-Host "Health: http://127.0.0.1:$Port/health" Write-Host "Eve deck: ws://127.0.0.1:$Port/eve/deck" +Write-Host "CultNet/RUDP snapshot: $CultNetRudpBind" diff --git a/src/Idunn/README.md b/src/Idunn/README.md index 96bd826..52c7834 100644 --- a/src/Idunn/README.md +++ b/src/Idunn/README.md @@ -56,9 +56,11 @@ Idunn is a Rust daemon inside Odin's Cargo workspace. The live local runtime is one long-lived `idunn.exe` process that owns the whole Starfire-local swarm: Odin, local adjunct daemons, the Yggdrasil deploy lanes, and the Nightwing display services. Each target declares a daemon-owned health contract and keeps -its own interval and deploy/restart authority. The current shell health -commands are compatibility probes until each daemon updates its CultLib -dependency and publishes health through CultNet over the shared RUDP transport. +its own interval and deploy/restart authority. The shell health commands now +exist as compatibility probes and actuators; daemon truth is expected to come +first from typed CultNet/RUDP publication and daemon-owned boundary stores, and +the local commands only matter when that witness is absent or when an explicit +restart/deploy boundary is advertised. The scheduler and continuity witness now belong to one Rust process instead of a PowerShell-herded pile of one-daemon workers. @@ -220,61 +222,33 @@ probe. The record must match the daemon id, health contract, RUDP transport, and freshness window; otherwise the compatibility probe remains fallback evidence. -The current active cut has moved past Raven daemon launcher surgery and into the -remaining Yggdrasil deployments, with repixelizer now the explicit next target. -Heimdall has crossed the first live transport cut: the Yggdrasil runtime now -publishes `heimdall.cultnet-rudp-provider-health`, writes a daemon-owned -boundary witness at `/srv/heimdall/cultcache/heimdall.service.cc`, and ships -the required `CultLib` snapshot through the source-artifact lane. Heimdall -still owes Odin ingestion of that typed store plus the later redacted -auth-document witness export, but it is no longer blocked on basic daemon -health publication. Vili now publishes RUDP health from Raven -through the hidden `GameCult\Vili` scheduled task, and live Idunn accepts -`vili.cultnet-rudp-animation-health` from `10.77.0.4`. The Raven repair -actuator in [restart-vili.ps1](/E:/Projects/Odin/scripts/restart-vili.ps1) -syncs the authoritative Vili runtime plus flattened CultLib node modules before -reinstalling the hidden task. Vili also writes a daemon-owned -`.vili\vili.service.cc` store with provider advertisement, operator state, Eve -surface, command boundary, and transport profile records; Odin local discovery -can ingest that typed store. Weksa publishes RUDP health and a daemon-owned -provider store containing provider advertisement, operator state, Eve surface, -command boundary, and transport profile records; Odin local discovery can ingest -that typed store too. Weksa still owes CultNet/RUDP command document ingress -for MiMo VoiceDesign before its HTTP command route can become debug-only. -Stonks publishes RUDP health and a daemon-owned CultCache store containing -provider advertisement, market snapshot, Eve surface, command boundary, and -transport profile records; Odin local discovery can ingest that store as -provider truth. StreamPixels now publishes a service-owned CultCache boundary -store with provider advertisement, command boundary, transport profile, and -Idunn health summary; Odin local discovery can ingest that store, and the live -Yggdrasil service now publishes daemon health over CultNet/RUDP with -`STREAMPIXELS_IDUNN_RUDP_HEALTH=10.77.0.2:17870` and contract -`streampixels.cultnet-rudp-service-health` in -`/srv/streampixels/env/service.env`. Live Idunn accepts -`yggdrasil-streampixels` from `10.77.0.1`, and the service-owned boundary store -now lives at -`/srv/streampixels/app/.streampixels-data/cultcache/streampixels.service.cc`. -The remaining debt is only to demote SSH/systemd/HTTP probes from compatibility -proof to deployment/debug witness once Odin consumes the typed store without -fallback. Repixelizer is still plain GUI/systemd compatibility debt for now, -which makes it the next Yggdrasil surgery target: -`repixelizer-gui.service`, `/api/health`, `/api/config`, and nginx routing are -still the live witnesses until the runtime publishes internal RUDP health and -typed queue/provider state. Raven Muninn task actions are also an explicit ops invariant: Task -Scheduler must execute `wscript.exe //B //Nologo` hidden launcher actions -directly for `GameCult-Muninn`, `GameCult-Muninn-Activate`, and +The active transport cut is no longer "make the daemons speak RUDP at all." +Nightwing Gjallar now consumes Odin's accepted +`surface:gamecult.network.status` snapshot over CultNet/RUDP, so every active +Starfire-local daemon target now publishes daemon-owned RUDP health plus typed +boundary state. The remaining debt is deleting or demoting compatibility +probes, HTTP/WebSocket/TCP lowerings, and xenos-boundary ingestion shims +without letting them reclaim ownership. Weksa still owes CultNet/RUDP command +document ingress for MiMo VoiceDesign before its HTTP command route can become +debug-only, and Odin still owes removal of hashed-store/C# witness shims once +direct interoperable witness publication is ready. + +Raven Muninn task actions are also an explicit ops invariant: Task Scheduler +must execute `wscript.exe //B //Nologo` hidden launcher actions directly for +`GameCult-Muninn`, `GameCult-Muninn-Activate`, and `GameCult-Muninn-VideoProof`, never raw `.cmd` wrappers, and the hidden VBS layer must call `.ps1` launchers directly instead of `cmdPath` trampolines. -Those three live Raven tasks have been repaired and verified. The repair actuator now uploads its -Raven PowerShell body with `sftp` and runs a tiny cleanup wrapper, so the -hidden-task repair does not hit Windows command-line limits before it can run. -The long-running Muninn serve bodies on Raven, Nightwing, and Starfire now also -carry their own `--idunn-rudp-health`, `--idunn-daemon`, and -`--idunn-health-contract` arguments, and live Idunn accepts those daemon-owned -health records directly instead of relying on one-shot health commands for the -truth. -After those live cuts, continue daemon-by-daemon until compatibility HTTP, TCP, -WebSocket, and command probes are fallback witnesses only. +Those three live Raven tasks have been repaired and verified. The repair +actuator now uploads its Raven PowerShell body with `sftp` and runs a tiny +cleanup wrapper, so the hidden-task repair does not hit Windows command-line +limits before it can run. The long-running Muninn serve bodies on Raven, +Nightwing, and Starfire now also carry their own `--idunn-rudp-health`, +`--idunn-daemon`, and `--idunn-health-contract` arguments, and live Idunn +accepts those daemon-owned health records directly instead of relying on +one-shot health commands for the truth. + +From here on out, the work is not "teach another daemon to speak." It is +"delete the witness masks and keep ownership where it belongs." Rust no longer gets to claim the transport is imaginary: `vendor/cultnet-rs` includes the canonical CNR0 `cultnet.transport.rudp.v0` diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 11ccb56..7f1e20d 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const { buildConfig, loadCultRuntime } = require("./odin/config.cjs"); +const { createCultNetRudpSurfaceServer } = require("./odin/cultnet-rudp.cjs"); const { defineOdinDocuments } = require("./odin/documents.cjs"); const { createInterfaceDiscovery } = require("./odin/interfaces.cjs"); const { createIdunnRudpHealthPublisher, publishIdunnRudpHealth } = require("./odin/idunn-rudp.cjs"); @@ -41,6 +42,7 @@ const stateBuilder = createStateBuilder({ }); let meshNodePromise = null; +let cultNetRudpSurfaceServer = null; let currentState = stateBuilder.buildPendingState("Coordinator starting"); let lastRefresh = { completedAt: null, @@ -59,6 +61,20 @@ async function main() { if (cultRuntime.CultMesh && documents.surfaceDefinition) { meshNodePromise = createDurableSurfaceNode(); } + if (config.cultnetRudpBind) { + cultNetRudpSurfaceServer = createCultNetRudpSurfaceServer({ + bind: config.cultnetRudpBind, + documents, + getCache: async () => { + if (!meshNodePromise) { + throw new Error("CultMesh runtime is unavailable; Odin cannot serve a CultNet/RUDP snapshot."); + } + return (await meshNodePromise).cache; + }, + }); + await cultNetRudpSurfaceServer.start(); + console.log(`CultNet/RUDP snapshot: ${config.cultnetRudpBind}`); + } const dashboardServer = createDashboardServer({ applyClientCommand: (command) => layoutStore.applyClientCommand(command), @@ -177,6 +193,7 @@ function health(clients) { error: cultRuntime.error?.message || null, }, discovery: { + cultNetRudpBind: config.cultnetRudpBind || null, seedDeckUrls: config.seedDeckUrls, discoveredDeckUrls: interfaceDiscovery.getDiscoveredDeckUrls(), interfaceBindingStores: config.interfaceBindingStores, diff --git a/src/odin/config.cjs b/src/odin/config.cjs index 461d529..03e9dd1 100644 --- a/src/odin/config.cjs +++ b/src/odin/config.cjs @@ -16,6 +16,7 @@ function buildConfig(argv) { const args = parseArgs(argv); const port = Number(args.port || 8797); const host = args.host || "0.0.0.0"; + const cultnetRudpBind = args["cultnet-rudp-bind"] ? String(args["cultnet-rudp-bind"]) : ""; const intervalMs = Number(args.intervalMs || 5000); const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); @@ -57,6 +58,7 @@ function buildConfig(argv) { return { args, cachePath, + cultnetRudpBind, gamecultTextDocumentStorePath, host, interfaceBindingStores, diff --git a/src/odin/cultnet-rudp.cjs b/src/odin/cultnet-rudp.cjs new file mode 100644 index 0000000..5a31ef7 --- /dev/null +++ b/src/odin/cultnet-rudp.cjs @@ -0,0 +1,229 @@ +"use strict"; + +const dgram = require("dgram"); +const path = require("path"); +const { createRequire } = require("module"); + +const requireCultNet = createRequire(path.resolve( + __dirname, + "..", + "..", + "..", + "CultLib", + "packages", + "cultnet-ts", + "package.json", +)); + +const { + CultNetDocumentRegistry, + CultNetRudpSession, + cultNetBuiltinSchemaRegistry, + decodeRudpPacket, + defineCultNetDocumentBinding, + encodeCultNetMessageForWire, + encodeRudpPacket, + parseCultNetMessage, +} = requireCultNet("./dist/index.js"); +const { decode, encode } = requireCultNet("@msgpack/msgpack"); + +const ODIN_RUDP_CONNECTION_ID = 0x0d1d0002; +const DEFAULT_SESSION_TIMEOUT_MS = 30_000; + +function createCultNetRudpSurfaceServer(options) { + if (!options?.bind) return null; + if (!options.documents?.surfaceDefinition) { + throw new Error("Odin CultNet/RUDP surface server requires a surface document definition."); + } + if (typeof options.getCache !== "function") { + throw new Error("Odin CultNet/RUDP surface server requires an async getCache function."); + } + + const bind = parseEndpoint(options.bind); + const documentRegistry = new CultNetDocumentRegistry([ + defineCultNetDocumentBinding({ definition: options.documents.surfaceDefinition }), + ]); + const sessions = new Map(); + const socket = dgram.createSocket(bind.host.includes(":") ? "udp6" : "udp4"); + const resendPollMs = Math.max(10, Number(options.resendPollMs || 25)); + const sessionTimeoutMs = Math.max(1_000, Number(options.sessionTimeoutMs || DEFAULT_SESSION_TIMEOUT_MS)); + let resendTimer = null; + + socket.on("message", (wire, remote) => { + try { + const packet = decodeRudpPacket(wire); + handlePacket(packet, remote).catch((error) => { + logError(options, `CultNet/RUDP packet handling failed for ${remote.address}:${remote.port}`, error); + }); + } catch (error) { + logError(options, "CultNet/RUDP packet decode failed", error); + } + }); + + socket.on("error", (error) => { + logError(options, "CultNet/RUDP socket error", error); + }); + + async function start() { + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.bind(bind.port, bind.host, () => { + socket.off("error", reject); + resolve(); + }); + }); + resendTimer = setInterval(() => { + try { + const nowMs = Date.now(); + for (const [key, record] of sessions) { + if (record.session.checkTimeout(nowMs, sessionTimeoutMs)) { + sessions.delete(key); + continue; + } + for (const packet of record.session.dueResends(nowMs)) { + sendPacket(record.remote, packet); + } + } + } catch (error) { + logError(options, "CultNet/RUDP resend loop failed", error); + } + }, resendPollMs); + resendTimer.unref?.(); + } + + function close() { + if (resendTimer) { + clearInterval(resendTimer); + resendTimer = null; + } + sessions.clear(); + socket.close(); + } + + async function handlePacket(packet, remote) { + const key = remoteKey(remote); + const nowMs = Date.now(); + let record = sessions.get(key); + + if (packet.packetType === "connect") { + record = { + remote: { address: remote.address, family: remote.family, port: remote.port }, + session: new CultNetRudpSession({ + connectionId: ODIN_RUDP_CONNECTION_ID, + initialSequence: 1, + resendDelayMs: 100, + }), + }; + sessions.set(key, record); + sendPacket(record.remote, record.session.acceptConnect(packet, nowMs, new Uint8Array())); + return; + } + + if (!record) { + return; + } + + const result = record.session.receive(packet, nowMs); + if (result.reply) { + sendPacket(record.remote, result.reply); + } + for (const frame of result.delivered) { + if (frame.channelId !== "schema") { + continue; + } + await handleSchemaFrame(record, frame.payload); + } + if (result.disconnected) { + sessions.delete(key); + return; + } + if (packet.packetType === "accept" || result.delivered.length > 0) { + sendPacket(record.remote, record.session.createAck()); + } + } + + async function handleSchemaFrame(record, payload) { + const message = parseCultNetMessage(decode(payload), "cultnet.schema.v0"); + switch (message.schemaVersion) { + case "cultnet.snapshot_request.v0": { + try { + const cache = await options.getCache(); + const response = documentRegistry.createRawSnapshotResponse(cache, message.messageId, message); + sendSchemaMessage(record, response); + } catch (error) { + sendSchemaMessage(record, { + schemaVersion: "cultnet.error.v0", + error: error.message, + }); + } + return; + } + case "cultnet.schema_catalog_request.v0": + sendSchemaMessage(record, cultNetBuiltinSchemaRegistry.createCatalogResponse(message)); + return; + default: + sendSchemaMessage(record, { + schemaVersion: "cultnet.error.v0", + error: `Unsupported Odin CultNet/RUDP request ${message.schemaVersion}.`, + }); + } + } + + function sendSchemaMessage(record, message) { + const payload = encode(encodeCultNetMessageForWire(message, "cultnet.schema.v0")); + for (const packet of record.session.sendMany("schema", payload, { + reliable: true, + ordered: true, + nowMs: Date.now(), + })) { + sendPacket(record.remote, packet); + } + } + + function sendPacket(remote, packet) { + const wire = Buffer.from(encodeRudpPacket(packet)); + socket.send(wire, remote.port, remote.address); + } + + return { + bind, + start, + close, + }; +} + +function parseEndpoint(value) { + const text = String(value || "").trim(); + const ipv6 = text.match(/^\[([^\]]+)\]:(\d+)$/); + if (ipv6) return { host: ipv6[1], port: parsePort(ipv6[2]) }; + const index = text.lastIndexOf(":"); + if (index <= 0) { + throw new Error(`CultNet/RUDP bind must be host:port, got "${value}".`); + } + return { host: text.slice(0, index), port: parsePort(text.slice(index + 1)) }; +} + +function parsePort(value) { + const port = Number(value); + if (!Number.isInteger(port) || port <= 0 || port > 65535) { + throw new Error(`CultNet/RUDP port is invalid: ${value}`); + } + return port; +} + +function remoteKey(remote) { + return `${remote.address}:${remote.port}`; +} + +function logError(options, label, error) { + const message = error instanceof Error ? error.message : String(error); + if (typeof options?.logError === "function") { + options.logError(`${label}: ${message}`); + return; + } + console.error(`${label}: ${message}`); +} + +module.exports = { + createCultNetRudpSurfaceServer, +};