Expose Idunn swarm surgery plan state

This commit is contained in:
Meta Vens 2026-06-15 23:36:52 +01:00
commit 00aef68402
4 changed files with 198 additions and 19 deletions

View file

@ -8,7 +8,7 @@ use odin_core::{
IdunnCommandBoundaryRecord, IdunnDaemonHealthRecord, IdunnDaemonSurgeryPlanRecord,
IdunnDaemonTransportProfileRecord, IdunnDeploymentResultRecord, IdunnDesiredDaemonRecord,
IdunnOperatorAlarmRecord, IdunnRestartResultRecord, IdunnRudpHealthIngressRecord,
IdunnRuntimeTransportCheckRecord, OdinDocuments, plan_keepalive,
IdunnRuntimeTransportCheckRecord, IdunnSwarmSurgeryPlanRecord, OdinDocuments, plan_keepalive,
};
use std::env;
use std::net::{SocketAddr, UdpSocket};
@ -112,7 +112,7 @@ fn run_swarm(options: &SwarmOptions, common: &CommonOptions) -> Result<()> {
let now = timestamp()?;
publish_runtime_transport_check(common, &store_lock, &now)?;
start_rudp_health_ingress(common, &store_lock, &now)?;
publish_daemon_surgery_plans(&targets, common, &store_lock, &now)?;
publish_surgery_plans(&options.profile, &targets, common, &store_lock, &now)?;
let mut workers = Vec::with_capacity(targets.len());
for target in targets {
@ -607,14 +607,17 @@ fn health_from_rudp_message(message: &CultNetMessage) -> Result<IdunnDaemonHealt
Ok(health)
}
fn publish_daemon_surgery_plans(
fn publish_surgery_plans(
profile: &str,
targets: &[DaemonTarget],
options: &CommonOptions,
store_lock: &Arc<Mutex<()>>,
updated_at: &str,
) -> Result<()> {
let swarm_plan = swarm_surgery_plan(profile, targets, updated_at);
let plans = daemon_surgery_plans(targets, updated_at);
with_store_node(options, store_lock, |node| {
node.put(&swarm_plan.plan_id, &swarm_plan)?;
for target in targets {
let transport_profile = daemon_transport_profile(target, updated_at);
let command_boundary = command_boundary(target, updated_at);
@ -628,6 +631,60 @@ fn publish_daemon_surgery_plans(
})
}
fn swarm_surgery_plan(
profile: &str,
targets: &[DaemonTarget],
updated_at: &str,
) -> IdunnSwarmSurgeryPlanRecord {
let next_target = if targets
.iter()
.any(|target| target.daemon_id == "starfire-muninn")
{
"starfire-muninn"
} else if let Some(target) = targets.iter().find(|target| target.enabled) {
target.daemon_id.as_str()
} else {
"none"
};
IdunnSwarmSurgeryPlanRecord {
plan_id: format!("swarm-surgery:{profile}"),
profile: profile.to_string(),
status: "active-transport-migration".to_string(),
owner: "Idunn swarm supervisor".to_string(),
objective:
"Move daemon awareness from compatibility probes to daemon-published typed CultNet/RUDP state."
.to_string(),
current_mechanism:
"Idunn publishes per-daemon desired state, surgery plans, transport profiles, command boundaries, runtime RUDP self-checks, and RUDP health ingress; compatibility command probes remain fallback evidence until each daemon publishes its own health."
.to_string(),
invariants: vec![
"Daemon truth is typed CultCache/CultMesh state carried over cultnet.transport.rudp.v0.".to_string(),
"Compatibility commands, HTTP, WebSocket, SSH, and systemd probes are evidence only and must not own daemon health.".to_string(),
"Idunn consumes daemon-published RUDP health before compatibility probes and actuates only advertised lifecycle authority.".to_string(),
"Each migrated daemon must publish the same health contract that Idunn expects for its target.".to_string(),
],
phases: vec![
"1. Publish Idunn's own RUDP substrate and ingress state.".to_string(),
"2. Install daemon-published RUDP health in one Rust daemon and prove Idunn consumes it live.".to_string(),
"3. Extend CultLib RUDP publication support across TypeScript, C#, and remaining daemon runtimes.".to_string(),
"4. Promote provider advertisements, command boundaries, and transport profiles to daemon-owned CultNet/RUDP records.".to_string(),
"5. Delete or demote compatibility probes once every target has daemon-owned publication and advertised lifecycle authority.".to_string(),
],
current_phase:
"Phase 2: install daemon-published RUDP health in Muninn as the first Rust daemon cut."
.to_string(),
next_target: next_target.to_string(),
cut_line:
"The old command-probe path is cut per daemon only after fresh daemon-published RUDP health matches daemon_id, health_contract, publication_source, transport, and freshness window."
.to_string(),
verification_layer:
"CultMesh keepalive store records plus live Idunn decision cycles, not process exit codes or chat summaries."
.to_string(),
updated_at: updated_at.to_string(),
}
}
fn transport_profile_id(target: &DaemonTarget) -> String {
format!("transport:{}", target.daemon_id)
}
@ -1630,6 +1687,48 @@ mod tests {
);
}
#[test]
fn swarm_surgery_plan_names_current_cut_and_verification_layer() {
let muninn = DaemonTarget {
daemon_id: "starfire-muninn".to_string(),
verse_id: "starfire.local".to_string(),
name: "Starfire Muninn".to_string(),
health_contract: health_contract(
"muninn.cultnet-rudp-local-telemetry-and-quest-access",
"degraded",
),
health_command: Some("health-starfire-muninn.cmd".to_string()),
deploy_command: None,
restart_command: Some("restart-starfire-muninn.cmd".to_string()),
enabled: true,
interval_seconds: 30,
};
let odin = DaemonTarget {
daemon_id: "odin".to_string(),
verse_id: "starfire.local".to_string(),
name: "Odin".to_string(),
health_contract: health_contract("odin.cultnet-rudp-provider-health", "failed"),
health_command: Some("health-odin.cmd".to_string()),
deploy_command: None,
restart_command: Some("restart-odin.cmd".to_string()),
enabled: true,
interval_seconds: 30,
};
let plan = swarm_surgery_plan("starfire-local", &[odin, muninn], "unix:100");
assert_eq!(plan.plan_id, "swarm-surgery:starfire-local");
assert_eq!(plan.status, "active-transport-migration");
assert_eq!(plan.next_target, "starfire-muninn");
assert!(plan.current_phase.contains("Muninn"));
assert!(plan.verification_layer.contains("CultMesh keepalive store"));
assert!(
plan.invariants
.iter()
.any(|invariant| invariant.contains("cultnet.transport.rudp.v0"))
);
}
#[test]
fn fresh_daemon_published_rudp_health_can_replace_probe_health() {
let desired = IdunnDesiredDaemonRecord {

View file

@ -19,6 +19,7 @@ pub const IDUNN_RESTART_RESULT_SCHEMA: &str = "idunn.restart_result.v1";
pub const IDUNN_DEPLOYMENT_REQUEST_SCHEMA: &str = "idunn.deployment_request.v1";
pub const IDUNN_DEPLOYMENT_RESULT_SCHEMA: &str = "idunn.deployment_result.v1";
pub const IDUNN_OPERATOR_ALARM_SCHEMA: &str = "idunn.operator_alarm.v1";
pub const IDUNN_SWARM_SURGERY_PLAN_SCHEMA: &str = "idunn.swarm_surgery_plan.v1";
pub const IDUNN_DAEMON_SURGERY_PLAN_SCHEMA: &str = "idunn.daemon_surgery_plan.v1";
pub const IDUNN_DAEMON_TRANSPORT_PROFILE_SCHEMA: &str = "idunn.daemon_transport_profile.v1";
pub const IDUNN_COMMAND_BOUNDARY_SCHEMA: &str = "idunn.command_boundary.v1";
@ -354,6 +355,40 @@ pub struct IdunnOperatorAlarmRecord {
pub raised_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "idunn.swarm_surgery_plan",
schema = "idunn.swarm_surgery_plan.v1"
)]
pub struct IdunnSwarmSurgeryPlanRecord {
#[cultcache(key = 0)]
pub plan_id: String,
#[cultcache(key = 1)]
pub profile: String,
#[cultcache(key = 2)]
pub status: String,
#[cultcache(key = 3)]
pub owner: String,
#[cultcache(key = 4)]
pub objective: String,
#[cultcache(key = 5)]
pub current_mechanism: String,
#[cultcache(key = 6)]
pub invariants: Vec<String>,
#[cultcache(key = 7)]
pub phases: Vec<String>,
#[cultcache(key = 8)]
pub current_phase: String,
#[cultcache(key = 9)]
pub next_target: String,
#[cultcache(key = 10)]
pub cut_line: String,
#[cultcache(key = 11)]
pub verification_layer: String,
#[cultcache(key = 12)]
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "idunn.daemon_surgery_plan",
@ -716,6 +751,7 @@ cultmesh_rs::cultmesh_documents!(OdinDocuments {
IdunnDeploymentRequestRecord => IDUNN_DEPLOYMENT_REQUEST_SCHEMA,
IdunnDeploymentResultRecord => IDUNN_DEPLOYMENT_RESULT_SCHEMA,
IdunnOperatorAlarmRecord => IDUNN_OPERATOR_ALARM_SCHEMA,
IdunnSwarmSurgeryPlanRecord => IDUNN_SWARM_SURGERY_PLAN_SCHEMA,
IdunnDaemonSurgeryPlanRecord => IDUNN_DAEMON_SURGERY_PLAN_SCHEMA,
IdunnDaemonTransportProfileRecord => IDUNN_DAEMON_TRANSPORT_PROFILE_SCHEMA,
IdunnCommandBoundaryRecord => IDUNN_COMMAND_BOUNDARY_SCHEMA,

View file

@ -178,8 +178,9 @@ mod tests {
use crate::documents::{
IDUNN_COMMAND_BOUNDARY_SCHEMA, IDUNN_DAEMON_SURGERY_PLAN_SCHEMA,
IDUNN_DAEMON_TRANSPORT_PROFILE_SCHEMA, IDUNN_DESIRED_DAEMON_SCHEMA,
IdunnCommandBoundaryRecord, IdunnDaemonSurgeryPlanRecord,
IdunnDaemonTransportProfileRecord, IdunnRuntimeTransportCheckRecord, OdinDocuments,
IDUNN_SWARM_SURGERY_PLAN_SCHEMA, IdunnCommandBoundaryRecord, IdunnDaemonSurgeryPlanRecord,
IdunnDaemonTransportProfileRecord, IdunnRuntimeTransportCheckRecord,
IdunnSwarmSurgeryPlanRecord, OdinDocuments,
};
use anyhow::Result;
use cultmesh_rs::{CultMesh, CultMeshNodeOptions};
@ -354,6 +355,22 @@ mod tests {
blockers: vec!["runtime CultLib update".to_string()],
updated_at: "2026-06-04T00:00:02Z".to_string(),
};
let swarm_surgery_plan = IdunnSwarmSurgeryPlanRecord {
plan_id: "swarm-surgery:starfire-local".to_string(),
profile: "starfire-local".to_string(),
status: "active-transport-migration".to_string(),
owner: "Idunn swarm supervisor".to_string(),
objective: "Move daemon awareness to CultNet/RUDP".to_string(),
current_mechanism: "compatibility probes with RUDP fallback".to_string(),
invariants: vec!["daemon truth is typed RUDP state".to_string()],
phases: vec!["install Muninn RUDP health".to_string()],
current_phase: "Phase 2".to_string(),
next_target: "starfire-muninn".to_string(),
cut_line: "cut command probe after RUDP health exists".to_string(),
verification_layer: "CultMesh keepalive store".to_string(),
updated_at: "2026-06-04T00:00:02Z".to_string(),
};
node.put(&swarm_surgery_plan.plan_id, &swarm_surgery_plan)?;
node.put(&surgery_plan.plan_id, &surgery_plan)?;
let transport_profile = IdunnDaemonTransportProfileRecord {
profile_id: "transport:voidbot".to_string(),
@ -408,6 +425,14 @@ mod tests {
.as_deref(),
Some(IDUNN_DESIRED_DAEMON_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.swarm_surgery_plan")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_SWARM_SURGERY_PLAN_SCHEMA)
);
assert_eq!(
reloaded
.documents()
@ -445,6 +470,12 @@ mod tests {
.action,
"restart"
);
assert_eq!(
reloaded
.get_required::<IdunnSwarmSurgeryPlanRecord>(&swarm_surgery_plan.plan_id)?
.next_target,
"starfire-muninn"
);
assert_eq!(
reloaded
.get_required::<IdunnDaemonSurgeryPlanRecord>(&surgery_plan.plan_id)?

View file

@ -86,6 +86,7 @@ idunn.deployment_result.v1
idunn.restart_request.v1
idunn.restart_result.v1
idunn.operator_alarm.v1
idunn.swarm_surgery_plan.v1
idunn.daemon_surgery_plan.v1
idunn.daemon_transport_profile.v1
idunn.command_boundary.v1
@ -155,7 +156,13 @@ idunn.rudp_health_ingress.v1
contract, `publication_source=daemon-published`, transport
`cultnet.transport.rudp.v0`, and `max_silence_seconds` freshness window all
match. If any of those fail, the command probe remains fallback evidence.
- Idunn publishes the transport migration plan as
- Idunn publishes the active swarm transport migration plan as
`idunn.swarm_surgery_plan.v1`. That record names the active profile, owner,
objective, current mechanism, invariants, ordered phases, current phase, next
target, cut line, and verification layer. It is the state-surface answer to
"what surgery is Idunn doing next?" and must be lowered by Nightwing/Gjallar
before any chat summary or dashboard prose claims ownership of the plan.
- Idunn publishes per-target transport migration plans as
`idunn.daemon_surgery_plan.v1` records in the keepalive store. Each daemon
plan names severity, status, owner, objective, current mechanism, intended
CultNet/RUDP authority, cut line, ordered steps, blockers, and update time.
@ -228,26 +235,32 @@ The local swarm mode owns:
1. the built-in Starfire-local target catalog;
2. a mandatory health contract per target;
3. a typed `idunn.daemon_surgery_plan.v1` record per target, so transport debt
3. a typed `idunn.swarm_surgery_plan.v1` record, so Idunn's active migration
order, next target, cut line, and verification layer are explicit state;
4. a typed `idunn.daemon_surgery_plan.v1` record per target, so transport debt
is visible in the same state surface as daemon health;
4. a typed `idunn.daemon_transport_profile.v1` and
5. a typed `idunn.daemon_transport_profile.v1` and
`idunn.command_boundary.v1` record per target, so compatibility probes and
local commands cannot pretend to be daemon-owned CultNet/RUDP truth;
5. a startup `idunn.runtime_transport_check.v1` witness proving Idunn's own
6. a startup `idunn.runtime_transport_check.v1` witness proving Idunn's own
Rust RUDP loopback path;
6. a local `idunn.rudp_health_ingress.v1` listener for daemon-owned RUDP
7. a local `idunn.rudp_health_ingress.v1` listener for daemon-owned RUDP
health publication;
7. one in-process schedule per target instead of one watchdog process per target;
8. shared typed keepalive records in one CultMesh store;
9. deploy/restart/alarm execution through the same Rust decision engine;
10. recovery of fast local targets like Odin without waiting behind slow remote
8. one in-process schedule per target instead of one watchdog process per target;
9. shared typed keepalive records in one CultMesh store;
10. deploy/restart/alarm execution through the same Rust decision engine;
11. recovery of fast local targets like Odin without waiting behind slow remote
Yggdrasil checks.
Next: update daemon CultLib dependencies to the cross-runtime
`cultnet.transport.rudp.v0` surface, ingest Odin-owned service/provider
advertisements directly, promote the target catalog out of hardcoded bootstrap
data, add named adapters only for legacy service-manager crossings, then
publish an Eve/CultUI keepalive surface.
Current plan surface: `idunn.swarm_surgery_plan.v1` for profile
`starfire-local` currently points at `starfire-muninn` as the first Rust daemon
cut. The verification layer is the CultMesh keepalive store plus live Idunn
decision cycles, not command exit codes or chat summaries.
Next: update Muninn's CultLib dependency to publish daemon-owned health through
the cross-runtime `cultnet.transport.rudp.v0` surface, prove Idunn consumes that
live record, then continue runtime-by-runtime until compatibility probes can be
deleted or demoted.
No ad hoc JSON manifest, HTTP endpoint, TCP socket, or WebSocket bridge may
become the live state owner. Debug projections are fine when they name the