diff --git a/epiphany-core/src/bin/epiphany-mind-repair.rs b/epiphany-core/src/bin/epiphany-mind-repair.rs index 38f0d9a6..5a7fb872 100644 --- a/epiphany-core/src/bin/epiphany-mind-repair.rs +++ b/epiphany-core/src/bin/epiphany-mind-repair.rs @@ -2,7 +2,8 @@ use anyhow::{Result, anyhow}; use epiphany_core::{ RepoFrontierExecutionAmendment, RepoFrontierRoute, amend_repo_frontier_execution, apply_supervisor_modeling_acceptance_correction, read_coordinator_state, - runtime_repo_model_admission_receipt, runtime_repo_model_admission_review, runtime_spine_cache, + runtime_repo_frontier_plan_decision, runtime_repo_model_admission_receipt, + runtime_repo_model_admission_review, runtime_spine_cache, }; use sha2::{Digest, Sha256}; use std::{collections::BTreeMap, env, path::PathBuf}; @@ -83,6 +84,11 @@ fn main() -> Result<()> { .ok_or_else(|| anyhow!("route {route_id} admission receipt is absent"))?; let review = runtime_repo_model_admission_review(&store, &admission.review_id)? .ok_or_else(|| anyhow!("route {route_id} admission review is absent"))?; + let plan_decision = if admission.frontier_plan_decision_id.is_empty() { + None + } else { + runtime_repo_frontier_plan_decision(&store, &admission.frontier_plan_decision_id)? + }; println!( "{}", serde_json::to_string_pretty(&serde_json::json!({ @@ -93,6 +99,12 @@ fn main() -> Result<()> { "admissionResultId": admission.result_id, "admissionReviewId": admission.review_id, "admissionSource": review.admission_source, + "admissionPreviousRevision": admission.previous_revision, + "admissionPreviousHash": admission.previous_hash, + "admissionAdmittedRevision": admission.admitted_revision, + "admissionAdmittedHash": admission.admitted_hash, + "frontierPlanDecisionId": admission.frontier_plan_decision_id, + "frontierPlanDecision": plan_decision, "frontierItemId": route.frontier_item_id, "frontierItemHash": route.frontier_item_hash, "originalAction": plan.action, diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 103a6d33..89569a17 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -2583,6 +2583,29 @@ pub(crate) mod tests { let route = select_and_commit_repo_frontier_route(&store, "2026-07-15T09:00:06Z")?; assert_eq!(route.adopted_plan.as_ref(), Some(adopted)); assert_eq!(route.source_scope, adopted.safe_paths); + let admission_review = crate::runtime_repo_model_admission_review( + &store, + &admission.review_id, + )? + .expect("plan decision admission review"); + crate::coordinator_launch_context::validate_frontier_plan_decision_chain( + &decision, + &admission, + &admission_review, + &route, + ) + .map_err(anyhow::Error::msg)?; + let mut substituted_route = route.clone(); + substituted_route.model_hash = "substituted-admitted-model".into(); + assert!( + crate::coordinator_launch_context::validate_frontier_plan_decision_chain( + &decision, + &admission, + &admission_review, + &substituted_route, + ) + .is_err() + ); let grant = crate::substrate_gate_coordinator_implementation_grant( "adopt-hands-grant".into(), "adopt-hands-job".into(), diff --git a/epiphany-core/src/coordinator_launch_context.rs b/epiphany-core/src/coordinator_launch_context.rs index 326f27b8..0ae9928f 100644 --- a/epiphany-core/src/coordinator_launch_context.rs +++ b/epiphany-core/src/coordinator_launch_context.rs @@ -202,32 +202,12 @@ pub fn append_verification_hands_receipt_context( ) .map_err(|error| format!("failed to load Soul frontier plan decision: {error}"))? .ok_or_else(|| "Soul frontier admission has no persisted plan decision".to_string())?; - let source_matches = match decision.decision_source.as_ref() { - Some(crate::RepoFrontierPlanDecisionSource::MindWorker { result_id, job_id }) => { - admission_receipt.result_id.as_deref() == Some(result_id.as_str()) - && admission_review.result_id.as_deref() == Some(result_id.as_str()) - && admission_review.job_id.as_deref() == Some(job_id.as_str()) - } - Some(crate::RepoFrontierPlanDecisionSource::AuthenticatedOperatorReview { .. }) => { - admission_receipt.result_id.is_none() - && admission_review.result_id.is_none() - && admission_review.job_id.is_none() - } - None => false, - }; - if decision.decision != crate::RepoFrontierPlanDecision::Adopt - || decision.model_admission_receipt_id != admission_receipt.receipt_id - || decision.model_revision != admission_receipt.previous_revision - || decision.model_hash != admission_receipt.previous_hash - || decision.frontier_item_id != route.frontier_item_id - || decision.frontier_item_hash != route.frontier_item_hash - || !source_matches - { - return Err( - "Soul frontier plan decision does not exactly bind admission, route, and decision source" - .to_string(), - ); - } + validate_frontier_plan_decision_chain( + &decision, + &admission_receipt, + &admission_review, + &route, + )?; plan_decision_chain = Some(decision); } else if let Some(admission_result_id) = admission_receipt.result_id.as_deref() { let modeling_acceptances = state @@ -509,6 +489,51 @@ pub fn append_verification_hands_receipt_context( Ok(context) } +pub(crate) fn validate_frontier_plan_decision_chain( + decision: &crate::RepoFrontierPlanDecisionReceipt, + admission_receipt: &crate::RepoModelAdmissionReceipt, + admission_review: &crate::RepoModelAdmissionReview, + route: &crate::RepoFrontierRoute, +) -> Result<(), String> { + let source_matches = match decision.decision_source.as_ref() { + Some(crate::RepoFrontierPlanDecisionSource::MindWorker { result_id, job_id }) => { + admission_receipt.result_id.as_deref() == Some(result_id.as_str()) + && admission_review.result_id.as_deref() == Some(result_id.as_str()) + && admission_review.job_id.as_deref() == Some(job_id.as_str()) + } + Some(crate::RepoFrontierPlanDecisionSource::AuthenticatedOperatorReview { .. }) => { + admission_receipt.result_id.is_none() + && admission_review.result_id.is_none() + && admission_review.job_id.is_none() + } + None => false, + }; + if decision.decision != crate::RepoFrontierPlanDecision::Adopt + || decision.model_admission_receipt_id != admission_receipt.receipt_id + || decision.model_revision != admission_receipt.previous_revision + || decision.model_hash != admission_receipt.previous_hash + || decision.frontier_item_id != route.frontier_item_id + || admission_receipt.admitted_revision != route.model_revision + || admission_receipt.admitted_hash != route.model_hash + || !matches!( + &admission_receipt.purpose, + crate::RepoModelPatchPurpose::AdoptFrontierPlan { + planning_request_id, + candidate_id, + .. + } if planning_request_id == &decision.planning_request_id + && candidate_id == &decision.candidate_id + ) + || !source_matches + { + return Err( + "Soul frontier plan decision does not exactly bind admission, route, and decision source" + .to_string(), + ); + } + Ok(()) +} + pub fn has_complete_hands_consequence_after_latest_accepted_boundary( runtime_store_path: &Path, state: &EpiphanyThreadState, diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index 9f77941f..426cd235 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -1497,8 +1497,9 @@ an authenticated operator supplied the decision. Soul selects and verifies provenance by typed route anatomy. A non-empty `frontier_plan_decision_id` requires the exact decision receipt, Adopt verdict, -admission receipt binding, base model revision/hash, frontier identity/hash, -and exact Mind-worker result/job or authenticated-operator source. Only an +admission receipt binding, pre-admission model revision/hash and frontier +identity, admission-purpose planning/candidate identity, post-admission route +model revision/hash, and exact Mind-worker result/job or authenticated-operator source. Only an ordinary worker-result admission may require the Modeling acceptance, Mind-state-commit, and gateway-review chain. Execution amendments retain their separate exact receipt chain. @@ -1511,7 +1512,10 @@ validation path despite the legacy generic `WorkerResult` source tag. Forbidden writer: Soul cannot infer Modeling from result presence, and a plan decision cannot bypass provenance merely because its admission and review contain a model-produced result. Missing, substituted, non-Adopt, or -route-mismatched decisions fail closed before worker launch. +route-mismatched decisions fail closed before worker launch. The decision's +pre-adoption frontier-item hash is deliberately not equated with the route's +post-adoption frontier-item hash; Mind's admitted patch is the transition that +changes that item. ## Exact release build cache ownership diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index b3cddd95..4583b54d 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -72,6 +72,15 @@ The active source repair types all new plan admissions as uses the already-persisted `frontier_plan_decision_id` as the compatibility discriminator for v53. It does not rewrite evidence or weaken Soul. +Authenticated release `sha256-53dc65ecf1971fc39a535c3a597723e02ebe73a28936581441b4fe007c98c601` +from `d2538d46` proved the typed branch live, then failed closed because its +first exact-binding predicate incorrectly equated the decision's pre-adoption +frontier hash with the route's post-adoption hash. The follow-up source repair +binds the decision to the pre-admission model, the admission purpose to the +planning request and candidate, and the route to the admitted model. A focused +positive/negative test proves the real transition passes and a substituted +admitted route hash fails. + ## Build-time finding Commit `0f0b006d` made the three named owner manifests drive separate locked Cargo diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 325749a4..8202f0d3 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ