From 7386d844951124ffd2b0bc23ed6cd725b2d6f3ef Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 16 Jun 2026 08:05:42 +0100 Subject: [PATCH] Keep Idunn RUDP ingress alive for Mimir health --- crates/idunn-daemon/src/main.rs | 214 ++++++++++++++++++++++++++------ docs/idunn.md | 10 +- 2 files changed, 180 insertions(+), 44 deletions(-) diff --git a/crates/idunn-daemon/src/main.rs b/crates/idunn-daemon/src/main.rs index 78e9ef8..fca185b 100644 --- a/crates/idunn-daemon/src/main.rs +++ b/crates/idunn-daemon/src/main.rs @@ -372,10 +372,10 @@ fn is_fresh_daemon_published_health( if health.transport != CULTNET_RUDP_PROTOCOL_ID { return false; } - let Some(now_seconds) = parse_unix_timestamp(now) else { + let Some(now_seconds) = parse_timestamp_seconds(now) else { return false; }; - let Some(observed_seconds) = parse_unix_timestamp(&health.observed_at) else { + let Some(observed_seconds) = parse_timestamp_seconds(&health.observed_at) else { return false; }; if observed_seconds > now_seconds { @@ -388,6 +388,61 @@ fn parse_unix_timestamp(value: &str) -> Option { value.strip_prefix("unix:")?.parse().ok() } +fn parse_timestamp_seconds(value: &str) -> Option { + parse_unix_timestamp(value).or_else(|| parse_utc_iso_timestamp_seconds(value)) +} + +fn parse_utc_iso_timestamp_seconds(value: &str) -> Option { + if value.len() < 19 { + return None; + } + if !value.ends_with('Z') && !value.contains("+00:00") { + return None; + } + + let year = value.get(0..4)?.parse::().ok()?; + let month = value.get(5..7)?.parse::().ok()?; + let day = value.get(8..10)?.parse::().ok()?; + let hour = value.get(11..13)?.parse::().ok()?; + let minute = value.get(14..16)?.parse::().ok()?; + let second = value.get(17..19)?.parse::().ok()?; + if value.as_bytes().get(4) != Some(&b'-') + || value.as_bytes().get(7) != Some(&b'-') + || value.as_bytes().get(10) != Some(&b'T') + || value.as_bytes().get(13) != Some(&b':') + || value.as_bytes().get(16) != Some(&b':') + || !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || hour > 23 + || minute > 59 + || second > 60 + { + return None; + } + + let days = days_from_civil(year, month, day)?; + let seconds = days + .checked_mul(86_400)? + .checked_add(i64::from(hour) * 3_600)? + .checked_add(i64::from(minute) * 60)? + .checked_add(i64::from(second))?; + u64::try_from(seconds).ok() +} + +fn days_from_civil(year: i32, month: u32, day: u32) -> Option { + let year = i64::from(year) - if month <= 2 { 1 } else { 0 }; + let era = if year >= 0 { year } else { year - 399 } / 400; + let year_of_era = year - era * 400; + let month = i64::from(month); + let day = i64::from(day); + let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + day - 1; + if !(0..=365).contains(&day_of_year) { + return None; + } + let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; + Some(era * 146_097 + day_of_era - 719_468) +} + fn publish_runtime_transport_check( options: &CommonOptions, store_lock: &Arc>, @@ -580,53 +635,67 @@ fn run_rudp_health_ingress_loop( match handle_rudp_health_datagram(&socket, &mut sessions, &buffer[..size], source) { Ok(frames) => { for frame in frames { - let message = decode_cultnet_message_from_slice( + let message = match decode_cultnet_message_from_slice( &frame.payload, CultNetWireContract::CultNetSchemaV0, - )?; + ) { + Ok(message) => message, + Err(error) => { + publish_rudp_ingress_failure( + &options, + &store_lock, + local_addr, + &format!( + "rejected RUDP health schema bytes from {source}: {error}" + ), + &observed_at, + ); + continue; + } + }; match health_from_rudp_message(&message) { Ok(mut health) => { health.publication_source = "daemon-published".to_string(); health.transport = CULTNET_RUDP_PROTOCOL_ID.to_string(); - with_store_node(&options, &store_lock, |node| { - node.put(&health.daemon_id, &health)?; - Ok(()) - })?; - println!( - "Idunn accepted RUDP health for {} from {} over {}.", - health.daemon_id, source, frame.channel_id - ); + if let Err(error) = + with_store_node(&options, &store_lock, |node| { + node.put(&health.daemon_id, &health)?; + Ok(()) + }) + { + eprintln!( + "Idunn RUDP health ingress failed to persist {} from {}: {}", + health.daemon_id, source, error + ); + } else { + println!( + "Idunn accepted RUDP health for {} from {} over {}.", + health.daemon_id, source, frame.channel_id + ); + } } Err(error) => { - let failed = rudp_health_ingress_record( - "idunn-rudp-health-ingress", + publish_rudp_ingress_failure( + &options, + &store_lock, local_addr, - "degraded", &format!( "rejected RUDP health schema frame from {source}: {error}" ), &observed_at, ); - with_store_node(&options, &store_lock, |node| { - node.put(&failed.ingress_id, &failed)?; - Ok(()) - })?; } } } } Err(error) => { - let failed = rudp_health_ingress_record( - "idunn-rudp-health-ingress", + publish_rudp_ingress_failure( + &options, + &store_lock, local_addr, - "degraded", &format!("rejected RUDP health datagram from {source}: {error}"), &observed_at, ); - with_store_node(&options, &store_lock, |node| { - node.put(&failed.ingress_id, &failed)?; - Ok(()) - })?; } } } @@ -643,6 +712,28 @@ fn run_rudp_health_ingress_loop( } } +fn publish_rudp_ingress_failure( + options: &CommonOptions, + store_lock: &Arc>, + local_addr: SocketAddr, + detail: &str, + observed_at: &str, +) { + let failed = rudp_health_ingress_record( + "idunn-rudp-health-ingress", + local_addr, + "degraded", + detail, + observed_at, + ); + if let Err(error) = with_store_node(options, store_lock, |node| { + node.put(&failed.ingress_id, &failed)?; + Ok(()) + }) { + eprintln!("Idunn RUDP health ingress failed to persist ingress failure: {error}"); + } +} + fn handle_rudp_health_datagram( socket: &UdpSocket, sessions: &mut HashMap, @@ -748,11 +839,23 @@ fn swarm_surgery_plan( targets: &[DaemonTarget], updated_at: &str, ) -> IdunnSwarmSurgeryPlanRecord { - let has_mimir_eve_dashboard = targets - .iter() - .any(|target| target.daemon_id == "mimir-eve-dashboard"); - let next_target = if has_mimir_eve_dashboard { - "mimir-eve-dashboard" + let next_target = [ + "nightwing-eve-dashboard", + "nightwing-eve-browser-reference", + "vili", + "yggdrasil-streampixels", + "yggdrasil-repixelizer", + "yggdrasil-heimdall", + ] + .iter() + .copied() + .find(|candidate| { + targets + .iter() + .any(|target| target.enabled && target.daemon_id == *candidate) + }); + 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 { @@ -786,11 +889,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 10: move Mimir Eve dashboard health from local compatibility probes to daemon-published RUDP state, with Raven Muninn scheduled-task action repair queued as a background-only ops invariant." + "Phase 11: move Nightwing Eve runtime surfaces from compatibility service probes to daemon-published RUDP state, with Raven Muninn scheduled-task action repair queued as a background-only ops invariant." .to_string(), next_target: next_target.to_string(), cut_line: - "Muninn, Idunn, Odin, Stonks, Weksa, VoidBot, and Nightwing Gjallar now exercise daemon-owned RUDP health. Mimir Eve dashboard is the next local runtime cut. Live Raven still needs GameCult-Muninn, GameCult-Muninn-Activate, and GameCult-Muninn-VideoProof task actions repaired to execute wscript.exe hidden launchers directly; application is blocked while Raven SSH is unreachable." + "Muninn, Idunn, Odin, Stonks, Weksa, VoidBot, Nightwing Gjallar, and Mimir Eve dashboard now exercise daemon-owned RUDP health. Nightwing Eve runtime surfaces are the next local runtime cut. Live Raven still needs GameCult-Muninn, GameCult-Muninn-Activate, and GameCult-Muninn-VideoProof task actions repaired to execute wscript.exe hidden launchers directly; application is blocked while Raven SSH is unreachable." .to_string(), verification_layer: "CultMesh keepalive store records plus live Idunn decision cycles, not process exit codes or chat summaries." @@ -986,14 +1089,25 @@ fn daemon_surgery_plan(target: &DaemonTarget, updated_at: &str) -> IdunnDaemonSu ]; } "mimir-eve-dashboard" => { + status = "partial-rudp-health-live"; severity = "high"; owner = "Mimir dashboard runtime"; current_mechanism = - "Mimir dashboard health is currently a local compatibility probe with no Idunn restart authority." + "Mimir Eve dashboard publishes mimir.cultnet-rudp-provider-health over CultNet/RUDP from the Nightwing systemd broker; provider advertisement and command boundary still retain compatibility projections." .to_string(); - blockers.push( - "No restart/deploy command authority is currently advertised to Idunn.".to_string(), - ); + intended_authority = + "Mimir dashboard publishes daemon health, provider catalog, Eve dashboard state, command boundary, and transport profile as typed CultMesh/CultNet records over cultnet.transport.rudp.v0; HTTP/WebSocket remain lowerings for clients only." + .to_string(); + cut_line = + "Keep the local HTTP probe as fallback only until Mimir provider advertisement and command_boundary records are also daemon-owned RUDP publications." + .to_string(); + steps = vec![ + "Keep live mimir.cultnet-rudp-provider-health publication running from the Nightwing Eve dashboard broker.".to_string(), + "Publish Mimir Eve dashboard provider advertisement and retained state records over cultnet.transport.rudp.v0.".to_string(), + "Publish Mimir Eve dashboard command_boundary and transport_profile records from the daemon runtime.".to_string(), + "Teach Odin to prefer Mimir RUDP/CultMesh provider records over compatibility HTTP catalog ingestion.".to_string(), + "Delete or demote health-mimir-eve-dashboard.cmd to a manual compatibility probe with no lifecycle truth.".to_string(), + ]; } "weksa" => { status = "partial-rudp-health-live"; @@ -2016,12 +2130,27 @@ mod tests { enabled: true, interval_seconds: 30, }; + let nightwing_eve_dashboard = DaemonTarget { + daemon_id: "nightwing-eve-dashboard".to_string(), + verse_id: "nightwing.local".to_string(), + name: "Nightwing Eve dashboard".to_string(), + health_contract: health_contract( + "nightwing.cultnet-rudp-eve-dashboard-health", + "failed", + ), + health_command: Some("health-nightwing-eve-dashboard.cmd".to_string()), + deploy_command: None, + restart_command: Some("restart-nightwing-eve-dashboard.cmd".to_string()), + enabled: true, + interval_seconds: 30, + }; let plan = swarm_surgery_plan( "starfire-local", &[ odin, mimir, + nightwing_eve_dashboard, stonks, weksa.clone(), voidbot.clone(), @@ -2035,11 +2164,11 @@ mod tests { assert_eq!(plan.plan_id, "swarm-surgery:starfire-local"); assert_eq!(plan.status, "active-transport-migration"); - assert_eq!(plan.next_target, "mimir-eve-dashboard"); - assert!(plan.current_phase.contains("Mimir Eve dashboard")); + assert_eq!(plan.next_target, "nightwing-eve-dashboard"); + assert!(plan.current_phase.contains("Nightwing Eve")); assert!( plan.cut_line - .contains("Muninn, Idunn, Odin, Stonks, Weksa, VoidBot, and Nightwing Gjallar") + .contains("Muninn, Idunn, Odin, Stonks, Weksa, VoidBot, Nightwing Gjallar, and Mimir Eve dashboard") ); assert!(plan.cut_line.contains("GameCult-Muninn-Activate")); assert!(plan.cut_line.contains("GameCult-Muninn-VideoProof")); @@ -2154,6 +2283,11 @@ mod tests { &health, &desired, "unix:100" )); + health.observed_at = "1970-01-01T00:01:35.0000000+00:00".to_string(); + assert!(is_fresh_daemon_published_health( + &health, &desired, "unix:100" + )); + health.observed_at = "unix:1".to_string(); assert!(!is_fresh_daemon_published_health( &health, &desired, "unix:100" diff --git a/docs/idunn.md b/docs/idunn.md index 9e2d8ed..debebc3 100644 --- a/docs/idunn.md +++ b/docs/idunn.md @@ -279,15 +279,17 @@ serialized provider witness refresh. VoidBot publishes `voidbot.cultnet-rudp-stack-health` after each GameCult Local Orchestrator pulse. Gjallar publishes `gjallar.cultnet-rudp-framebuffer-composition-health` from the C# Nightwing -framebuffer service. Live Idunn cycles accept these records before command-probe fallback. +framebuffer service. Mimir Eve dashboard publishes +`mimir.cultnet-rudp-provider-health` from the Nightwing systemd broker. Live +Idunn cycles accept these records before command-probe fallback. Raven's Muninn scheduled-task repair is a separate ops invariant: `GameCult-Muninn`, `GameCult-Muninn-Activate`, and `GameCult-Muninn-VideoProof` must execute `wscript.exe` hidden launcher actions, not raw `.cmd` task actions. -Next: move Mimir Eve dashboard health off local compatibility evidence and onto -daemon-owned RUDP state, then continue runtime-by-runtime until compatibility -probes can be deleted or demoted. Stonks, Weksa, VoidBot, and Gjallar still owe +Next: move the remaining Nightwing Eve runtime surfaces off compatibility +service probes, then continue runtime-by-runtime until compatibility probes can +be deleted or demoted. Stonks, Weksa, VoidBot, Gjallar, and Mimir still owe provider advertisement and command-boundary RUDP publication before their compatibility surfaces can be purely display/debug lowerings; Gjallar also owes native CultMesh/RUDP deck input to replace the current Odin WebSocket lowering