From c5bf090d624dd0076a3d99c33fcec0f8dce72ded Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 23 Sep 2026 20:29:00 +0200 Subject: [PATCH] Stop declaring Heimdall as an Idunn dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Odin (96a55ec) now computes Ready with an external-operator-binding dependency not ready, but CultLib's shared contract (runtime_authority_contracts.rs:797-805) refuses any correlation claiming Ready while a non-optional dependency is not ready, so Odin rejected every Ghostlight Active presence (30/30 in the last deploy). The external kind existed only because Heimdall isn't v2-managed; that reason doesn't survive contact with Odin's own Ready computation. Ghostlight's deploy stops declaring Heimdall as an Idunn dependency. The daemon discovers it through Odin at use instead: HeimdallClient already took Option with None as the non-managed path, so runtime.rs now passes None directly rather than computing it from a dependency_bindings field that no longer exists, and the loopback-only, non-secret-bearing, app-bound HMAC+AES envelope enforcement is unchanged. When Heimdall becomes a v2 target the dependency returns as `required` managed. - deployment/idunn/recipe.toml: deleted the heimdall.command-boundary [[dependencies]] block. Ghostlight's own required_environment for GHOSTLIGHT_HEIMDALL_APP_SECRET_FILE/GHOSTLIGHT_HEIMDALL_BASE_URL is unrelated (Heimdall's HTTP app integration, not the Idunn dependency) and stays. - idunn_health.rs: runtime_dependency_bindings now requires exactly the Odin dependency (one, not two). Deleted HEIMDALL_CAPABILITY, required_external_dependency and its doc comment, the `heimdall` field on RuntimeDependencyBindings, the fixture's external_dependency helper and its Heimdall entry, and the recipe-drift test's Heimdall block/assertion (now asserts exactly one [[dependencies]] block and fails if a Heimdall dependency reappears without the daemon changing to expect it again). HeimdallDependencyBinding itself moved to heimdall.rs (see below) rather than being deleted outright: it's still the shape require_expected_heimdall_boundary checks a discovered boundary against when Idunn does supply one, which stays true the moment Heimdall returns as a managed dependency. - heimdall.rs: now owns HeimdallDependencyBinding (moved from idunn_health.rs, which no longer has any use for it). No behavior change: HeimdallBoundaryLocator::Odin's `expected: Option` and its None-tolerant checking logic were already exactly the "discover through Odin, verify against a managed binding when one exists" shape this ruling asks for. - runtime.rs: expected_heimdall is gone; `HeimdallClient::from_env` now takes `None` directly instead of a value computed from a dependency_bindings field that no longer exists, with a comment on why. Mutation: re-added the heimdall.command-boundary [[dependencies]] block to the recipe -> recipe_matches_the_daemon_it_deploys fails (asserts exactly one dependency block). Restored; deployment/idunn/recipe.toml is git-diff-clean after, one clean 7-line deletion. Verification: cargo test -p ghostlight-dungeon --bin ghostlight-dungeon -> 192 passed, 0 failed, 4 ignored (unchanged count: removed a test helper, not a test). Structural delta: 4 files changed, 42 insertions(+), 138 deletions(-) in Ghostlight (net -96 lines) — a genuine subtraction, not a relocation with padding; the only addition of substance is the doc comment on the relocated HeimdallDependencyBinding explaining why it still exists. Co-Authored-By: Claude Sonnet 5 --- crates/ghostlight-dungeon/src/heimdall.rs | 15 +- crates/ghostlight-dungeon/src/idunn_health.rs | 140 +++--------------- crates/ghostlight-dungeon/src/runtime.rs | 18 ++- deployment/idunn/recipe.toml | 7 - 4 files changed, 42 insertions(+), 138 deletions(-) diff --git a/crates/ghostlight-dungeon/src/heimdall.rs b/crates/ghostlight-dungeon/src/heimdall.rs index 812aa39..cc9b7f7 100644 --- a/crates/ghostlight-dungeon/src/heimdall.rs +++ b/crates/ghostlight-dungeon/src/heimdall.rs @@ -1,4 +1,3 @@ -use crate::idunn_health::HeimdallDependencyBinding; use aes_gcm::{ Aes256Gcm, KeyInit, aead::{Aead, Payload}, @@ -43,6 +42,20 @@ pub struct HeimdallClient { shared_secret: String, } +/// Idunn's projection of a managed Heimdall dependency, when Heimdall is a +/// v2-managed target (it is not, as of this cut: every v2 deploy of it has +/// failed and it still runs as the legacy `heimdall.service`, so +/// `runtime.rs` always passes `None` here). Not an Idunn dependency in +/// `deployment/idunn/recipe.toml` right now, but the type stays owned here +/// so `require_expected_heimdall_boundary` below can resume checking a +/// discovered boundary against it the moment Heimdall returns as a managed +/// dependency, without re-deriving this shape from scratch. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct HeimdallDependencyBinding { + pub(crate) provider_id: String, + pub(crate) endpoint: SocketAddr, +} + #[derive(Clone)] enum HeimdallBoundaryLocator { Odin { diff --git a/crates/ghostlight-dungeon/src/idunn_health.rs b/crates/ghostlight-dungeon/src/idunn_health.rs index 66770b0..8cb9f22 100644 --- a/crates/ghostlight-dungeon/src/idunn_health.rs +++ b/crates/ghostlight-dungeon/src/idunn_health.rs @@ -69,24 +69,12 @@ const WARMING_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const MAX_RECENT_WARMING_PROOFS: usize = 64; const ODIN_CAPABILITY: (&str, &str, &str) = ("odin.verse-rendezvous", "odin.verse-topology.v1", "v1"); -const HEIMDALL_CAPABILITY: (&str, &str, &str) = ( - "heimdall.command-boundary", - "heimdall.command_boundary.v1", - "v1", -); #[cfg(target_os = "linux")] const SYSTEMD_LISTEN_FDS_START: RawFd = 3; -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct HeimdallDependencyBinding { - pub(crate) provider_id: String, - pub(crate) endpoint: SocketAddr, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct RuntimeDependencyBindings { pub(crate) odin_rudp: SocketAddr, - pub(crate) heimdall: HeimdallDependencyBinding, } pub(crate) struct PublishedRuntimePresence { @@ -475,11 +463,10 @@ fn runtime_dependency_bindings( configured_odin: SocketAddr, ) -> Result { ensure!( - expected.dependencies.len() == 2, - "Ghostlight Expected does not carry its exact two dependencies" + expected.dependencies.len() == 1, + "Ghostlight Expected does not carry its exact one dependency" ); let odin = required_managed_dependency(expected, "shared-infrastructure", ODIN_CAPABILITY)?; - let heimdall = required_external_dependency(expected, HEIMDALL_CAPABILITY)?; let odin_endpoint = parse_dependency_socket_endpoint( odin.provider_endpoint @@ -492,24 +479,9 @@ fn runtime_dependency_bindings( odin_endpoint == configured_odin, "GHOSTLIGHT_ODIN_RUDP differs from Expected Odin" ); - let heimdall_endpoint = parse_dependency_socket_endpoint( - heimdall - .provider_endpoint - .as_deref() - .context("Expected Heimdall dependency has no endpoint")?, - &["rudp://", "udp://"], - "Heimdall dependency", - )?; Ok(RuntimeDependencyBindings { odin_rudp: odin_endpoint, - heimdall: HeimdallDependencyBinding { - provider_id: heimdall - .provider_id - .clone() - .context("Expected Heimdall dependency has no provider")?, - endpoint: heimdall_endpoint, - }, }) } @@ -540,40 +512,6 @@ fn required_managed_dependency<'a>( Ok(dependency) } -/// Heimdall is not an Idunn-managed incarnation (every v2 deploy of it has -/// failed; it still runs as the legacy `heimdall.service`), so its Expected -/// dependency can never carry `managed-incarnation` authority. Idunn projects -/// an operator-declared `[[external_capabilities]]` entry as -/// `provider_authority = "external-operator-binding"` with no -/// `provider_expected_projection_sha256` (`deployment_plan.rs`'s -/// `DependencySelection::expected_projection`) — accept exactly that shape, -/// never the managed one, for Heimdall alone. Odin stays managed. -fn required_external_dependency<'a>( - expected: &'a IdunnExpectedIncarnationRecord, - identity: (&str, &str, &str), -) -> Result<&'a IdunnExpectedDependency> { - let dependency = expected - .dependencies - .iter() - .find(|dependency| { - dependency.capability == identity.0 - && dependency.schema == identity.1 - && dependency.compatibility == identity.2 - }) - .with_context(|| format!("Expected omits dependency {}", identity.0))?; - ensure!( - dependency.kind == "external-operator-binding" - && dependency.startup == "before-promotion" - && dependency.minimum_capacity > 0 - && dependency.provider_id.is_some() - && dependency.provider_authority.as_deref() == Some("external-operator-binding") - && dependency.provider_expected_projection_sha256.is_none(), - "Expected dependency {} is unresolved or authority-incoherent", - identity.0 - ); - Ok(dependency) -} - fn parse_dependency_socket_endpoint( endpoint: &str, allowed_schemes: &[&str], @@ -1296,32 +1234,6 @@ pub(crate) mod tests { } } - /// Exactly what Idunn's `DependencySelection::expected_projection` - /// projects for an `[[external_capabilities]]`-declared provider - /// (`deployment_plan.rs`): `provider_authority = - /// "external-operator-binding"` and no - /// `provider_expected_projection_sha256`, unlike a managed dependency. - fn external_dependency( - capability: &str, - schema: &str, - compatibility: &str, - provider_id: &str, - endpoint: &str, - ) -> IdunnExpectedDependency { - IdunnExpectedDependency { - kind: "external-operator-binding".into(), - capability: capability.into(), - schema: schema.into(), - compatibility: compatibility.into(), - minimum_capacity: 1, - startup: "before-promotion".into(), - provider_id: Some(provider_id.into()), - provider_authority: Some("external-operator-binding".into()), - provider_expected_projection_sha256: None, - provider_endpoint: Some(endpoint.into()), - } - } - fn fixture_with_state_contract(root: &Path, state_contract_sha256: &str) -> Result { fixture_with_state_contract_and_custody(root, state_contract_sha256, false) } @@ -1372,23 +1284,14 @@ pub(crate) mod tests { minimum_capacity: 1, }, ], - dependencies: vec![ - external_dependency( - HEIMDALL_CAPABILITY.0, - HEIMDALL_CAPABILITY.1, - HEIMDALL_CAPABILITY.2, - "yggdrasil-heimdall", - "rudp://127.0.0.1:4101", - ), - managed_dependency( - "shared-infrastructure", - ODIN_CAPABILITY.0, - ODIN_CAPABILITY.1, - ODIN_CAPABILITY.2, - "odin-yggdrasil", - "rudp://127.0.0.1:9", - ), - ], + dependencies: vec![managed_dependency( + "shared-infrastructure", + ODIN_CAPABILITY.0, + ODIN_CAPABILITY.1, + ODIN_CAPABILITY.2, + "odin-yggdrasil", + "rudp://127.0.0.1:9", + )], }; let launch = IdunnRuntimeActivationLaunch::issue(&expected, digest('7'), 100, &idunn)?; let activation = launch.activation().clone(); @@ -1595,7 +1498,7 @@ pub(crate) mod tests { parse_dependency_socket_endpoint( "http://127.0.0.1:4101", &["rudp://", "udp://"], - "Heimdall dependency", + "candidate dependency", ) .is_err() ); @@ -1739,8 +1642,8 @@ pub(crate) mod tests { /// binary does not actually have (F2). Every field is anchored to its own /// table block (Soul's second pass: a bare `.contains()` anywhere in the /// file passed even when the matched value sat under the wrong table — - /// e.g. Odin's and Heimdall's dependency kinds swapped, or a schema - /// correct in `[[provides]]` but wrong in its own state slot). + /// e.g. a schema correct in `[[provides]]` but wrong in its own state + /// slot). const RECIPE_TOML: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/../../deployment/idunn/recipe.toml" @@ -1832,8 +1735,11 @@ pub(crate) mod tests { .collect(); assert_eq!( dependency_blocks.len(), - 2, - "recipe does not declare Ghostlight's exact two dependencies" + 1, + "recipe does not declare Ghostlight's exact one dependency \ + (Heimdall is discovered through Odin at use, not an Idunn \ + dependency, until Heimdall is a v2 target — see \ + runtime_dependency_bindings)" ); let odin_dependency = find_block( @@ -1846,16 +1752,6 @@ pub(crate) mod tests { "recipe Odin dependency kind disagrees with idunn_health's managed-shared-infrastructure rule" ); - let heimdall_dependency = find_block( - &blocks, - "[[dependencies]]", - &format!("capability = \"{}\"", HEIMDALL_CAPABILITY.0), - ); - assert!( - heimdall_dependency.contains("kind = \"external-operator-binding\""), - "recipe Heimdall dependency kind disagrees with required_external_dependency's rule" - ); - assert!( !RECIPE_TOML.contains("gamecult.codex.subscription-inference"), "recipe still declares the retired CodexConnector dependency" diff --git a/crates/ghostlight-dungeon/src/runtime.rs b/crates/ghostlight-dungeon/src/runtime.rs index d83d426..6f83252 100644 --- a/crates/ghostlight-dungeon/src/runtime.rs +++ b/crates/ghostlight-dungeon/src/runtime.rs @@ -408,14 +408,16 @@ pub(crate) async fn run(state_root_binding: Option) -> anyhow::Result<( .map(|bindings| bindings.odin_rudp) .map(Ok) .unwrap_or_else(configured_odin_endpoint)?; - let expected_heimdall = dependency_bindings - .as_ref() - .map(|bindings| bindings.heimdall.clone()); - let heimdall = Arc::new(HeimdallClient::from_env( - &runtime_id, - odin_endpoint, - expected_heimdall, - )?); + // Heimdall is not an Idunn dependency: Odin now refuses every Ghostlight + // Active presence while a non-optional dependency it manages is not + // Ready, and Heimdall is not itself an Idunn-v2-managed target (every + // v2 deploy of it has failed; it still runs as the legacy + // heimdall.service). Ghostlight discovers it through Odin at use + // instead, and still enforces loopback-only, non-secret-bearing, + // app-bound HMAC+AES envelopes regardless. When Heimdall becomes a v2 + // target, this returns to a managed `RuntimeDependencyBindings` field + // the way Odin's own endpoint already is. + let heimdall = Arc::new(HeimdallClient::from_env(&runtime_id, odin_endpoint, None)?); require_no_runtime_custody_failure(&mut fatal_events)?; let mesh = match open_mesh(&service_root, &mesh_identity, Some(odin_endpoint)) { Ok(mesh) => Some(mesh), diff --git a/deployment/idunn/recipe.toml b/deployment/idunn/recipe.toml index 7015fbe..1ef01f3 100644 --- a/deployment/idunn/recipe.toml +++ b/deployment/idunn/recipe.toml @@ -186,10 +186,3 @@ capability = "odin.verse-rendezvous" schema = "odin.verse-topology.v1" compatibility = "v1" startup = "before-promotion" - -[[dependencies]] -kind = "external-operator-binding" -capability = "heimdall.command-boundary" -schema = "heimdall.command_boundary.v1" -compatibility = "v1" -startup = "before-promotion"