Stop declaring Heimdall as an Idunn dependency

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<HeimdallDependencyBinding> 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<HeimdallDependencyBinding>`
  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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-23 20:29:00 +02:00
commit c5bf090d62
4 changed files with 42 additions and 138 deletions

View file

@ -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 {

View file

@ -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<RuntimeDependencyBindings> {
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> {
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"

View file

@ -408,14 +408,16 @@ pub(crate) async fn run(state_root_binding: Option<PathBuf>) -> 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),

View file

@ -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"