Expose Idunn swarm surgery plan state
This commit is contained in:
parent
7a8ad9fcd0
commit
00aef68402
4 changed files with 198 additions and 19 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue