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:
parent
203dc3658e
commit
c5bf090d62
4 changed files with 42 additions and 138 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue