From 0601e280480cccc28209fbf0630e921f3ef9056d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 18 Jul 2026 14:54:03 +0100 Subject: [PATCH] Move coverage recovery into projector authority --- .../src/bin/epiphany-daemon-supervisor.rs | 60 +- epiphany-core/src/cultmesh_integration.rs | 9 +- epiphany-core/src/lib.rs | 19 +- .../workspace_coverage_process_documents.rs | 904 +++++++++++++++++- .../src/workspace_coverage_projector.rs | 96 +- .../workspace_coverage_projector_service.rs | 81 +- notes/fresh-workspace-handoff.md | 77 ++ state/ledgers.msgpack | Bin 527478 -> 527934 bytes state/map.yaml | 2 +- 9 files changed, 1119 insertions(+), 129 deletions(-) diff --git a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs index 1e7720e1..6dc380c9 100644 --- a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs +++ b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs @@ -57,17 +57,18 @@ use epiphany_core::{EpiphanyPackagedReleaseEntry, authenticate_epiphany_packaged use epiphany_core::{ ProcessInstanceIdentity, ProcessInstanceObservation, WORKSPACE_COVERAGE_PROCESS_LAUNCH_SCHEMA_VERSION, WorkspaceCoverageManagedProcessLaunchEntry, - WorkspaceCoverageProcessBootstrap, authenticate_workspace_coverage_managed_process_launch, + WorkspaceCoverageProcessBootstrap, authenticate_current_workspace_coverage_claim_sight, + authenticate_workspace_coverage_managed_process_launch, authenticate_workspace_coverage_provider_heartbeat, authenticate_workspace_coverage_termination_with_envelope_digest, capture_process_instance, - current_workspace_coverage_recovery_target, load_latest_workspace_coverage_managed_process_launch, load_latest_workspace_coverage_provider_heartbeat, load_workspace_coverage_process_termination_observation, native_boot_identity, - observe_process_instance, open_default_host_identity, recover_workspace_coverage_projection, - sign_workspace_coverage_launch, workspace_coverage_host_identity_record_digest, + observe_process_instance, open_default_host_identity, sign_workspace_coverage_launch, + workspace_coverage_host_identity_record_digest, write_workspace_coverage_managed_process_launch, write_workspace_coverage_process_bootstrap, write_workspace_coverage_process_termination_observation, + write_workspace_coverage_recovery_directive, }; use epiphany_core::{ authenticate_current_workspace_coverage_advancement_sight, @@ -2226,11 +2227,18 @@ fn reconcile_workspace_coverage_projector( ); return Ok(()); } - let runtime_store = args - .runtime_store - .clone() - .context("workspace coverage reconciliation requires --runtime-store")?; - let Some(target) = current_workspace_coverage_recovery_target(&runtime_store)? else { + let runtime_store = args.runtime_store.clone().context( + "workspace coverage reconciliation requires --runtime-store for Body/route sight", + )?; + let host = open_default_host_identity() + .context("workspace coverage reconciliation requires an enrolled host identity")?; + let Some(target) = authenticate_current_workspace_coverage_claim_sight( + &args.store, + &runtime_store, + &args.runtime_id, + host.entry(), + )? + else { let latest = load_latest_workspace_coverage_managed_process_launch( &args.store, args.runtime_id.clone(), @@ -2315,19 +2323,17 @@ fn reconcile_workspace_coverage_projector( } }; - let host = open_default_host_identity() - .context("workspace coverage reconciliation requires an enrolled host identity")?; if load_workspace_coverage_process_termination_observation( &args.store, args.runtime_id.clone(), - &target.managed_process_launch_id, + &target.launch_id, )? .is_none() { if let Err(error) = write_workspace_coverage_process_termination_observation( &args.store, args.runtime_id.clone(), - &target.managed_process_launch_id, + &target.launch_id, &host, ) { println!( @@ -2349,11 +2355,11 @@ fn reconcile_workspace_coverage_projector( authenticate_workspace_coverage_termination_with_envelope_digest( &args.store, args.runtime_id.clone(), - &target.managed_process_launch_id, + &target.launch_id, host.entry(), )?; let replacement_evidence = CoverageReplacementEvidence { - old_launch_id: target.managed_process_launch_id.clone(), + old_launch_id: target.launch_id.clone(), termination_id: termination.termination_id.clone(), termination_envelope_digest: termination_digest, }; @@ -2362,7 +2368,7 @@ fn reconcile_workspace_coverage_projector( args.runtime_id.clone(), )? .filter(|launch| { - launch.replaces_launch_id.as_deref() == Some(target.managed_process_launch_id.as_str()) + launch.replaces_launch_id.as_deref() == Some(target.launch_id.as_str()) && launch.replaces_termination_id.as_deref() == Some(termination.termination_id.as_str()) }); @@ -2405,28 +2411,24 @@ fn reconcile_workspace_coverage_projector( let Some(ready) = ready else { anyhow::bail!("workspace coverage replacement did not publish signed readiness in time"); }; - let recovered = recover_workspace_coverage_projection( - &runtime_store, + let directive = write_workspace_coverage_recovery_directive( &args.store, + &runtime_store, &args.runtime_id, - host.entry(), - &target.managed_process_launch_id, + &target, &replacement.launch_id, &ready.heartbeat_id, - &target.claim_id, + &host, )?; println!( "{}", serde_json::to_string_pretty(&json!({ "schemaVersion": "epiphany.workspace_coverage_reconcile.v0", - "status": "recovered", + "status": "recovery-directed", "serviceId": policy.service_id, "oldClaimId": target.claim_id, - "newClaimId": recovered.claim_id, - "claimEpoch": recovered.claim_epoch, "replacementLaunchId": replacement.launch_id, - "recoveryReceiptId": recovered.recovery_receipt_id, - "recoveryReceiptDigest": recovered.recovery_receipt_digest, + "directiveId": directive.directive_id, "restarted": true, "privateStateExposed": false, }))? @@ -3935,10 +3937,14 @@ mod semantic_projector_authority_tests { let readiness = body .rfind("load_latest_workspace_coverage_provider_heartbeat") .unwrap(); - let recovery = body.rfind("recover_workspace_coverage_projection").unwrap(); + let recovery = body + .rfind("write_workspace_coverage_recovery_directive") + .unwrap(); assert!(termination < launch && launch < readiness && readiness < recovery); assert!(body.contains("write_workspace_coverage_process_termination_observation")); assert!(body.contains("status == \"ready\"")); + assert!(!body.contains("open_workspace_coverage_authority")); + assert!(!body.contains("recover_workspace_coverage_projection")); } #[test] diff --git a/epiphany-core/src/cultmesh_integration.rs b/epiphany-core/src/cultmesh_integration.rs index b94bce0b..cff9850e 100644 --- a/epiphany-core/src/cultmesh_integration.rs +++ b/epiphany-core/src/cultmesh_integration.rs @@ -8,13 +8,16 @@ use crate::default_substrate_gate_cultnet_contracts; use crate::packaged_release::{EpiphanyPackagedReleaseEntry, EpiphanyPackagedReleaseHead}; use crate::workspace_coverage_process_documents::{ WORKSPACE_COVERAGE_ADVANCEMENT_SIGHT_SCHEMA_VERSION, + WORKSPACE_COVERAGE_CLAIM_SIGHT_SCHEMA_VERSION, WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION, WORKSPACE_COVERAGE_PROCESS_LAUNCH_SCHEMA_VERSION, WORKSPACE_COVERAGE_PROCESS_TERMINATION_SCHEMA_VERSION, WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_SCHEMA_VERSION, + WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_SCHEMA_VERSION, WORKSPACE_COVERAGE_TERMINAL_SIGHT_SCHEMA_VERSION, WorkspaceCoverageAdvancementSightEntry, - WorkspaceCoverageManagedProcessLaunchEntry, WorkspaceCoverageProcessEvidenceHead, - WorkspaceCoverageProcessTerminationObservationEntry, WorkspaceCoverageProviderHeartbeatEntry, + WorkspaceCoverageClaimSightEntry, WorkspaceCoverageManagedProcessLaunchEntry, + WorkspaceCoverageProcessEvidenceHead, WorkspaceCoverageProcessTerminationObservationEntry, + WorkspaceCoverageProviderHeartbeatEntry, WorkspaceCoverageRecoveryDirectiveEntry, WorkspaceCoverageTerminalSightEntry, }; use crate::workspace_coverage_projection_progress::{ @@ -2665,6 +2668,8 @@ cultmesh_documents!(EpiphanyCultMeshDocuments { WorkspaceCoverageProcessTerminationObservationEntry => WORKSPACE_COVERAGE_PROCESS_TERMINATION_SCHEMA_VERSION, WorkspaceCoverageProjectionProgressEntry => WORKSPACE_COVERAGE_PROJECTION_PROGRESS_SCHEMA_VERSION, WorkspaceCoverageAdvancementSightEntry => WORKSPACE_COVERAGE_ADVANCEMENT_SIGHT_SCHEMA_VERSION, + WorkspaceCoverageClaimSightEntry => WORKSPACE_COVERAGE_CLAIM_SIGHT_SCHEMA_VERSION, + WorkspaceCoverageRecoveryDirectiveEntry => WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_SCHEMA_VERSION, WorkspaceCoverageTerminalSightEntry => WORKSPACE_COVERAGE_TERMINAL_SIGHT_SCHEMA_VERSION, EpiphanyPackagedReleaseEntry => crate::packaged_release::EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION, EpiphanyPackagedReleaseHead => crate::packaged_release::EPIPHANY_PACKAGED_RELEASE_HEAD_SCHEMA_VERSION, diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index c0980255..789e404e 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -1197,15 +1197,20 @@ pub use workspace_coverage_process_bootstrap::{ }; pub use workspace_coverage_process_documents::{ WORKSPACE_COVERAGE_ADVANCEMENT_SIGHT_SCHEMA_VERSION, WORKSPACE_COVERAGE_ADVANCEMENT_SIGHT_TYPE, + WORKSPACE_COVERAGE_CLAIM_SIGHT_SCHEMA_VERSION, WORKSPACE_COVERAGE_CLAIM_SIGHT_TYPE, WORKSPACE_COVERAGE_PROCESS_LAUNCH_LATEST_KEY, WORKSPACE_COVERAGE_PROCESS_LAUNCH_SCHEMA_VERSION, WORKSPACE_COVERAGE_PROCESS_LAUNCH_TYPE, WORKSPACE_COVERAGE_PROCESS_TERMINATION_SCHEMA_VERSION, WORKSPACE_COVERAGE_PROCESS_TERMINATION_TYPE, WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_LATEST_KEY, WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_SCHEMA_VERSION, - WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_TYPE, WORKSPACE_COVERAGE_TERMINAL_SIGHT_SCHEMA_VERSION, + WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_TYPE, + WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_SCHEMA_VERSION, + WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_TYPE, WORKSPACE_COVERAGE_TERMINAL_SIGHT_SCHEMA_VERSION, WORKSPACE_COVERAGE_TERMINAL_SIGHT_TYPE, WorkspaceCoverageAdvancementSightEntry, - WorkspaceCoverageManagedProcessLaunchEntry, + WorkspaceCoverageClaimSightEntry, WorkspaceCoverageManagedProcessLaunchEntry, WorkspaceCoverageProcessTerminationObservationEntry, WorkspaceCoverageProviderHeartbeatEntry, - WorkspaceCoverageTerminalSightEntry, authenticate_current_workspace_coverage_advancement_sight, + WorkspaceCoverageRecoveryDirectiveEntry, WorkspaceCoverageTerminalSightEntry, + authenticate_current_workspace_coverage_advancement_sight, + authenticate_current_workspace_coverage_claim_sight, authenticate_current_workspace_coverage_terminal_sight, authenticate_workspace_coverage_managed_process_launch, authenticate_workspace_coverage_process_termination_observation, @@ -1222,7 +1227,7 @@ pub use workspace_coverage_process_documents::{ workspace_coverage_host_identity_record_digest, workspace_coverage_launch_statement, workspace_coverage_termination_statement, write_workspace_coverage_managed_process_launch, write_workspace_coverage_process_termination_observation, - write_workspace_coverage_provider_heartbeat, + write_workspace_coverage_provider_heartbeat, write_workspace_coverage_recovery_directive, }; pub use workspace_coverage_projection_progress::{ WORKSPACE_COVERAGE_PROJECTION_PROGRESS_SCHEMA_VERSION, @@ -1234,10 +1239,8 @@ pub use workspace_coverage_projection_progress::{ }; pub use workspace_coverage_projector::{ WORKSPACE_COVERAGE_MAXIMUM_FILE_BYTES, WorkspaceCoverageRecoveryOutcome, - WorkspaceCoverageRecoveryTarget, WorkspaceCoverageTerminalAuthority, - authenticate_current_workspace_coverage_terminal_authority, - authenticate_workspace_coverage_recovery_receipt, current_workspace_coverage_recovery_target, - recover_workspace_coverage_projection, + WorkspaceCoverageTerminalAuthority, authenticate_current_workspace_coverage_terminal_authority, + authenticate_workspace_coverage_recovery_receipt, }; pub use workspace_coverage_projector_service::WorkspaceCoverageProjectorConfig; pub use workspace_coverage_projector_service::WorkspaceCoverageProjectorPulseStatus; diff --git a/epiphany-core/src/workspace_coverage_process_documents.rs b/epiphany-core/src/workspace_coverage_process_documents.rs index 376e8d24..dc39ce02 100644 --- a/epiphany-core/src/workspace_coverage_process_documents.rs +++ b/epiphany-core/src/workspace_coverage_process_documents.rs @@ -46,6 +46,10 @@ const COVERAGE_ADVANCEMENT_SIGHT_DOMAIN: &[u8] = b"epiphany.workspace-coverage.advancement-sight.signature.v0\0"; const COVERAGE_TERMINAL_SIGHT_DOMAIN: &[u8] = b"epiphany.workspace-coverage.terminal-sight.signature.v0\0"; +const COVERAGE_CLAIM_SIGHT_DOMAIN: &[u8] = + b"epiphany.workspace-coverage.claim-sight.signature.v0\0"; +const HOST_RECOVERY_DIRECTIVE_PURPOSE: &str = + "epiphany.workspace-coverage.host-recovery-directive.v0"; pub const WORKSPACE_COVERAGE_ADVANCEMENT_SIGHT_TYPE: &str = "epiphany.workspace_coverage.advancement_sight"; @@ -55,6 +59,135 @@ pub const WORKSPACE_COVERAGE_TERMINAL_SIGHT_TYPE: &str = "epiphany.workspace_coverage.terminal_sight"; pub const WORKSPACE_COVERAGE_TERMINAL_SIGHT_SCHEMA_VERSION: &str = "epiphany.workspace_coverage.terminal_sight.v0"; +pub const WORKSPACE_COVERAGE_CLAIM_SIGHT_TYPE: &str = "epiphany.workspace_coverage.claim_sight"; +pub const WORKSPACE_COVERAGE_CLAIM_SIGHT_SCHEMA_VERSION: &str = + "epiphany.workspace_coverage.claim_sight.v0"; +pub const WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_TYPE: &str = + "epiphany.workspace_coverage.recovery_directive"; +pub const WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_SCHEMA_VERSION: &str = + "epiphany.workspace_coverage.recovery_directive.v0"; + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.workspace_coverage.claim_sight", + schema = "WorkspaceCoverageClaimSightEntry" +)] +pub struct WorkspaceCoverageClaimSightEntry { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub runtime_id: String, + #[cultcache(key = 2)] + pub workspace_id: String, + #[cultcache(key = 3)] + pub launch_id: String, + #[cultcache(key = 4)] + pub launch_envelope_digest: String, + #[cultcache(key = 5)] + pub provider_incarnation_id: String, + #[cultcache(key = 6)] + pub provider_public_key: Vec, + #[cultcache(key = 7)] + pub coverage_store_binding_id: String, + #[cultcache(key = 8)] + pub coverage_store_binding_envelope_digest: String, + #[cultcache(key = 9)] + pub coverage_store_file_identity: String, + #[cultcache(key = 10)] + pub runtime_coverage_route_envelope_digest: String, + #[cultcache(key = 11)] + pub body_binding_sha256: String, + #[cultcache(key = 12)] + pub body_observation_id: String, + #[cultcache(key = 13)] + pub body_generation: u64, + #[cultcache(key = 14)] + pub manifest_root_sha256: String, + #[cultcache(key = 15)] + pub claim_id: String, + #[cultcache(key = 16)] + pub claim_epoch: u64, + #[cultcache(key = 17)] + pub plan_id: String, + #[cultcache(key = 18)] + pub attempt_id: String, + #[cultcache(key = 19)] + pub attempt_envelope_digest: String, + #[cultcache(key = 20)] + pub recovery_receipt_id: Option, + #[cultcache(key = 21)] + pub recovery_receipt_digest: Option, + #[cultcache(key = 22)] + pub observed_at_utc: String, + #[cultcache(key = 23)] + pub provider_signature: Vec, + #[cultcache(key = 24)] + pub signature_algorithm: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.workspace_coverage.recovery_directive", + schema = "WorkspaceCoverageRecoveryDirectiveEntry" +)] +pub struct WorkspaceCoverageRecoveryDirectiveEntry { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub directive_id: String, + #[cultcache(key = 2)] + pub runtime_id: String, + #[cultcache(key = 3)] + pub old_claim_id: String, + #[cultcache(key = 4)] + pub old_launch_id: String, + #[cultcache(key = 5)] + pub replacement_launch_id: String, + #[cultcache(key = 6)] + pub replacement_ready_heartbeat_id: String, + #[cultcache(key = 7)] + pub termination_id: String, + #[cultcache(key = 8)] + pub claim_sight_envelope_digest: String, + #[cultcache(key = 9)] + pub old_claim_epoch: u64, + #[cultcache(key = 10)] + pub old_plan_id: String, + #[cultcache(key = 11)] + pub termination_envelope_digest: String, + #[cultcache(key = 12)] + pub replacement_launch_envelope_digest: String, + #[cultcache(key = 13)] + pub replacement_ready_heartbeat_envelope_digest: String, + #[cultcache(key = 14)] + pub workspace_id: String, + #[cultcache(key = 15)] + pub body_binding_sha256: String, + #[cultcache(key = 16)] + pub body_observation_id: String, + #[cultcache(key = 17)] + pub body_generation: u64, + #[cultcache(key = 18)] + pub manifest_root_sha256: String, + #[cultcache(key = 19)] + pub coverage_store_binding_id: String, + #[cultcache(key = 20)] + pub coverage_store_binding_envelope_digest: String, + #[cultcache(key = 21)] + pub coverage_store_file_identity: String, + #[cultcache(key = 22)] + pub runtime_coverage_route_envelope_digest: String, + #[cultcache(key = 23)] + pub issued_at_utc: String, + #[cultcache(key = 24)] + pub host_identity_id: String, + #[cultcache(key = 25)] + pub host_identity_record_digest: String, + #[cultcache(key = 26)] + pub host_signature: Vec, + #[cultcache(key = 27)] + pub signature_algorithm: String, +} #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] #[cultcache( @@ -781,6 +914,7 @@ pub fn write_workspace_coverage_provider_heartbeat( rmp_serde::from_slice(&launch_envelope.payload)?; authenticate_heartbeat_against_launch(&entry, &launch, &envelope_digest(&launch_envelope))?; let latest_key = heartbeat_latest_key(&entry.launch_id); + let evidence_key = heartbeat_evidence_key(&entry.launch_id, &entry.heartbeat_id); let latest = node .cache() .get_envelope::(&latest_key)?; @@ -823,6 +957,13 @@ pub fn write_workspace_coverage_provider_heartbeat( .prepare_entry(&evidence_head_key, &next_evidence_head)? .0, ]; + if node + .cache() + .get_envelope::(&evidence_key)? + .is_some() + { + bail!("workspace coverage heartbeat immutable evidence identity collision"); + } match latest.as_ref() { Some(envelope) => { let prior: WorkspaceCoverageProviderHeartbeatEntry = @@ -850,6 +991,7 @@ pub fn write_workspace_coverage_provider_heartbeat( None => {} } replacements.push(node.cache().prepare_entry(&latest_key, &entry)?.0); + replacements.push(node.cache().prepare_entry(&evidence_key, &entry)?.0); if !SingleFileMessagePackBackingStore::new(store_path) .compare_and_swap_batch(&expected, replacements)? { @@ -1667,6 +1809,510 @@ fn require_nonempty(name: &str, value: &str) -> Result<()> { Ok(()) } +fn claim_sight_key(runtime_id: &str, body_generation: u64, body_observation_id: &str) -> String { + format!( + "epiphany-local/workspace-coverage/claim-sight/{runtime_id}/{body_generation}/{body_observation_id}" + ) +} + +fn validate_claim_sight_shape( + entry: &WorkspaceCoverageClaimSightEntry, + signed: bool, +) -> Result<()> { + if entry.schema_version != WORKSPACE_COVERAGE_CLAIM_SIGHT_SCHEMA_VERSION + || entry.claim_epoch == 0 + || entry.signature_algorithm != "ed25519" + || entry.provider_public_key.len() != 32 + || signed != (entry.provider_signature.len() == 64) + { + bail!("workspace coverage claim sight violates its reserved schema or signature shape"); + } + for (name, value) in [ + ("runtime id", entry.runtime_id.as_str()), + ("workspace id", entry.workspace_id.as_str()), + ("launch id", entry.launch_id.as_str()), + ("claim id", entry.claim_id.as_str()), + ("attempt id", entry.attempt_id.as_str()), + ("plan id", entry.plan_id.as_str()), + ("Body observation id", entry.body_observation_id.as_str()), + ] { + require_nonempty(name, value)?; + } + uuid::Uuid::parse_str(&entry.launch_id).context("claim sight launch id must be UUID")?; + uuid::Uuid::parse_str(&entry.claim_id).context("claim sight claim id must be UUID")?; + uuid::Uuid::parse_str(&entry.attempt_id).context("claim sight attempt id must be UUID")?; + DateTime::parse_from_rfc3339(&entry.observed_at_utc) + .context("claim sight observation time must be RFC3339")?; + Ok(()) +} + +pub(crate) fn publish_workspace_coverage_claim_sight( + local_verse_store: &Path, + runtime_store: &Path, + coverage: &crate::WorkspaceCoverageAuthority, + acquisition: &crate::workspace_coverage_projector::WorkspaceCoverageAcquisition, + runtime_id: &str, + launch_id: &str, + trusted_host: &HostIncarnationIdentityEntry, + provider: &SigningKey, + observed_at: chrono::DateTime, +) -> Result { + if acquisition.claim.status != "running" + || acquisition.claim.managed_process_launch_id != launch_id + || acquisition.claim.attempt_id != acquisition.attempt.attempt_id + { + bail!("claim sight requires the projector's exact running claim and attempt"); + } + let opening = coverage.store.pull_all()?; + let attempt_env = opening + .iter() + .find(|env| { + env.r#type == "gamecult.epiphany.workspace_coverage_projection_attempt" + && env.key == acquisition.attempt.attempt_id + }) + .ok_or_else(|| anyhow!("claim sight attempt envelope is absent"))?; + let (launch, launch_digest) = + authenticate_workspace_coverage_managed_process_launch_with_envelope_digest( + local_verse_store, + runtime_id, + launch_id, + trusted_host, + )?; + if launch.provider_public_key.as_slice() != provider.verifying_key().to_bytes() + || launch.provider_incarnation_id != acquisition.claim.executor_incarnation + { + bail!("claim sight provider disagrees with its authenticated launch"); + } + let basis = crate::load_current_runtime_repository_body_basis(runtime_store)?; + let mut entry = WorkspaceCoverageClaimSightEntry { + schema_version: WORKSPACE_COVERAGE_CLAIM_SIGHT_SCHEMA_VERSION.into(), + runtime_id: runtime_id.into(), + workspace_id: basis.workspace_id, + launch_id: launch_id.into(), + launch_envelope_digest: launch_digest, + provider_incarnation_id: launch.provider_incarnation_id, + provider_public_key: launch.provider_public_key, + coverage_store_binding_id: coverage.store_binding.binding_id.clone(), + coverage_store_binding_envelope_digest: coverage.store_binding_envelope_sha256.clone(), + coverage_store_file_identity: coverage.store_binding.store_file_identity.clone(), + runtime_coverage_route_envelope_digest: coverage + .runtime_coverage_route_envelope_sha256 + .clone(), + body_binding_sha256: basis.body_binding_sha256, + body_observation_id: acquisition.claim.body_observation_id.clone(), + body_generation: acquisition.claim.body_generation, + manifest_root_sha256: acquisition.claim.manifest_root_sha256.clone(), + claim_id: acquisition.claim.claim_id.clone(), + claim_epoch: acquisition.claim.claim_epoch, + plan_id: acquisition.claim.plan_id.clone(), + attempt_id: acquisition.attempt.attempt_id.clone(), + attempt_envelope_digest: envelope_digest(attempt_env), + recovery_receipt_id: acquisition.claim.recovery_receipt_id.clone(), + recovery_receipt_digest: acquisition.claim.recovery_receipt_digest.clone(), + observed_at_utc: observed_at.to_rfc3339(), + provider_signature: Vec::new(), + signature_algorithm: "ed25519".into(), + }; + validate_workspace_coverage_sight_closing_authority( + runtime_store, + coverage, + &entry.workspace_id, + &entry.body_binding_sha256, + &entry.body_observation_id, + entry.body_generation, + &entry.manifest_root_sha256, + )?; + validate_claim_sight_shape(&entry, false)?; + entry.provider_signature = provider + .sign(&signed_sight_statement( + COVERAGE_CLAIM_SIGHT_DOMAIN, + &entry, + )?) + .to_bytes() + .to_vec(); + validate_claim_sight_shape(&entry, true)?; + let backing = SingleFileMessagePackBackingStore::new(local_verse_store); + let node = open_epiphany_cultmesh_node(local_verse_store, runtime_id.to_string())?; + let key = claim_sight_key( + runtime_id, + entry.body_generation, + &entry.body_observation_id, + ); + let opening = backing.pull_all()?; + if let Some(prior_env) = opening + .iter() + .find(|env| env.r#type == WORKSPACE_COVERAGE_CLAIM_SIGHT_TYPE && env.key == key) + { + let prior: WorkspaceCoverageClaimSightEntry = rmp_serde::from_slice(&prior_env.payload)?; + authenticate_workspace_coverage_claim_sight_entry(&prior)?; + if entry.claim_epoch < prior.claim_epoch + || (entry.claim_epoch == prior.claim_epoch && entry.claim_id != prior.claim_id) + { + bail!("workspace coverage claim sight cannot regress or fork its epoch"); + } + } + replace_latest_sight( + node.cache(), + &backing, + WORKSPACE_COVERAGE_CLAIM_SIGHT_TYPE, + &key, + &entry, + &opening, + )?; + Ok(entry) +} + +fn authenticate_workspace_coverage_claim_sight_entry( + entry: &WorkspaceCoverageClaimSightEntry, +) -> Result<()> { + validate_claim_sight_shape(entry, true)?; + let key: [u8; 32] = entry + .provider_public_key + .clone() + .try_into() + .map_err(|_| anyhow!("claim sight provider key length is invalid"))?; + let signature = Signature::from_slice(&entry.provider_signature)?; + let mut unsigned = entry.clone(); + unsigned.provider_signature.clear(); + VerifyingKey::from_bytes(&key)? + .verify( + &signed_sight_statement(COVERAGE_CLAIM_SIGHT_DOMAIN, &unsigned)?, + &signature, + ) + .map_err(|_| anyhow!("workspace coverage claim sight signature verification failed"))?; + Ok(()) +} + +pub fn authenticate_current_workspace_coverage_claim_sight( + local_verse_store: impl AsRef, + runtime_store: impl AsRef, + runtime_id: &str, + trusted_host: &HostIncarnationIdentityEntry, +) -> Result> { + let runtime_store = runtime_store.as_ref(); + let basis = crate::load_current_runtime_repository_body_basis(runtime_store)?; + let route = crate::runtime_workspace_coverage_store_binding(runtime_store)? + .ok_or_else(|| anyhow!("runtime has no workspace coverage route"))?; + let runtime_opening = SingleFileMessagePackBackingStore::new(runtime_store).pull_all()?; + let route_env = runtime_opening + .iter() + .find(|env| { + env.r#type == crate::RUNTIME_WORKSPACE_COVERAGE_STORE_BINDING_TYPE + && env.key == crate::RUNTIME_WORKSPACE_COVERAGE_STORE_BINDING_KEY + }) + .ok_or_else(|| anyhow!("runtime workspace coverage route envelope is absent"))?; + let node = open_epiphany_cultmesh_node(local_verse_store.as_ref(), runtime_id.to_string())?; + let Some(env) = node + .cache() + .get_envelope::(&claim_sight_key( + runtime_id, + basis.generation, + &basis.observation_id, + ))? + else { + return Ok(None); + }; + if env.r#type != WORKSPACE_COVERAGE_CLAIM_SIGHT_TYPE { + bail!("claim sight key has alien type"); + } + let entry: WorkspaceCoverageClaimSightEntry = rmp_serde::from_slice(&env.payload)?; + authenticate_workspace_coverage_claim_sight_entry(&entry)?; + let (launch, digest) = + authenticate_workspace_coverage_managed_process_launch_with_envelope_digest( + local_verse_store.as_ref(), + runtime_id, + &entry.launch_id, + trusted_host, + )?; + if entry.runtime_id != runtime_id + || entry.launch_envelope_digest != digest + || entry.provider_incarnation_id != launch.provider_incarnation_id + || entry.provider_public_key != launch.provider_public_key + || entry.workspace_id != basis.workspace_id + || entry.body_binding_sha256 != basis.body_binding_sha256 + || entry.body_observation_id != basis.observation_id + || entry.body_generation != basis.generation + || entry.manifest_root_sha256 != basis.manifest_root_sha256 + || entry.coverage_store_binding_id != route.binding_id + || entry.coverage_store_file_identity != route.store_file_identity + || entry.runtime_coverage_route_envelope_digest + != format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(route_env)?)) + { + bail!("claim sight disagrees with authenticated launch"); + } + Ok(Some(entry)) +} + +fn recovery_directive_key(runtime_id: &str, replacement_launch_id: &str) -> String { + format!( + "epiphany-local/workspace-coverage/recovery-directive/{runtime_id}/{replacement_launch_id}" + ) +} + +fn recovery_directive_statement( + entry: &WorkspaceCoverageRecoveryDirectiveEntry, +) -> Result> { + let mut unsigned = entry.clone(); + unsigned.host_signature.clear(); + Ok(rmp_serde::to_vec_named(&unsigned)?) +} + +pub fn write_workspace_coverage_recovery_directive( + local_verse_store: impl AsRef, + runtime_store: impl AsRef, + runtime_id: &str, + claim_sight: &WorkspaceCoverageClaimSightEntry, + replacement_launch_id: &str, + replacement_ready_heartbeat_id: &str, + host: &HostIdentitySigner, +) -> Result { + let local_verse_store = local_verse_store.as_ref(); + let runtime_store = runtime_store.as_ref(); + let authenticated = authenticate_current_workspace_coverage_claim_sight( + local_verse_store, + runtime_store, + runtime_id, + host.entry(), + )? + .ok_or_else(|| anyhow!("workspace coverage recovery claim sight is absent"))?; + if &authenticated != claim_sight { + bail!("workspace coverage recovery claim sight moved"); + } + let node = open_epiphany_cultmesh_node(local_verse_store, runtime_id.to_string())?; + let directive_key = recovery_directive_key(runtime_id, replacement_launch_id); + if node + .cache() + .get_envelope::(&directive_key)? + .is_some() + { + let existing = authenticate_workspace_coverage_recovery_directive( + local_verse_store, + runtime_store, + runtime_id, + replacement_launch_id, + host.entry(), + )? + .ok_or_else(|| anyhow!("existing recovery directive belongs to a stale Body"))?; + if existing.old_claim_id != claim_sight.claim_id + || existing.old_claim_epoch != claim_sight.claim_epoch + || existing.old_plan_id != claim_sight.plan_id + || existing.old_launch_id != claim_sight.launch_id + || existing.workspace_id != claim_sight.workspace_id + || existing.body_binding_sha256 != claim_sight.body_binding_sha256 + || existing.body_observation_id != claim_sight.body_observation_id + || existing.body_generation != claim_sight.body_generation + || existing.manifest_root_sha256 != claim_sight.manifest_root_sha256 + || existing.coverage_store_binding_id != claim_sight.coverage_store_binding_id + || existing.coverage_store_binding_envelope_digest + != claim_sight.coverage_store_binding_envelope_digest + || existing.coverage_store_file_identity != claim_sight.coverage_store_file_identity + || existing.runtime_coverage_route_envelope_digest + != claim_sight.runtime_coverage_route_envelope_digest + { + bail!( + "recovery directive replacement identity already belongs to another claim lineage" + ); + } + return Ok(existing); + } + let claim_env = node + .cache() + .get_envelope::(&claim_sight_key( + runtime_id, + claim_sight.body_generation, + &claim_sight.body_observation_id, + ))? + .ok_or_else(|| anyhow!("workspace coverage recovery claim sight envelope is absent"))?; + let (termination, termination_digest) = + authenticate_workspace_coverage_termination_with_envelope_digest( + local_verse_store, + runtime_id.to_string(), + &claim_sight.launch_id, + host.entry(), + )?; + let (replacement, replacement_digest) = + authenticate_workspace_coverage_managed_process_launch_with_envelope_digest( + local_verse_store, + runtime_id, + replacement_launch_id, + host.entry(), + )?; + let (ready, ready_digest) = + authenticate_workspace_coverage_provider_heartbeat_with_envelope_digest( + local_verse_store, + runtime_id, + replacement_ready_heartbeat_id, + host.entry(), + )?; + if replacement.replaces_launch_id.as_deref() != Some(claim_sight.launch_id.as_str()) + || replacement.replaces_termination_id.as_deref() + != Some(termination.termination_id.as_str()) + || ready.launch_id != replacement.launch_id + || ready.status != "ready" + { + bail!("workspace coverage recovery directive evidence is not an exact ready replacement"); + } + let mut entry = WorkspaceCoverageRecoveryDirectiveEntry { + schema_version: WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_SCHEMA_VERSION.into(), + directive_id: uuid::Uuid::new_v4().to_string(), + runtime_id: runtime_id.into(), + old_claim_id: claim_sight.claim_id.clone(), + old_launch_id: claim_sight.launch_id.clone(), + replacement_launch_id: replacement.launch_id, + replacement_ready_heartbeat_id: ready.heartbeat_id, + termination_id: termination.termination_id, + claim_sight_envelope_digest: envelope_digest(&claim_env), + old_claim_epoch: claim_sight.claim_epoch, + old_plan_id: claim_sight.plan_id.clone(), + termination_envelope_digest: termination_digest, + replacement_launch_envelope_digest: replacement_digest, + replacement_ready_heartbeat_envelope_digest: ready_digest, + workspace_id: claim_sight.workspace_id.clone(), + body_binding_sha256: claim_sight.body_binding_sha256.clone(), + body_observation_id: claim_sight.body_observation_id.clone(), + body_generation: claim_sight.body_generation, + manifest_root_sha256: claim_sight.manifest_root_sha256.clone(), + coverage_store_binding_id: claim_sight.coverage_store_binding_id.clone(), + coverage_store_binding_envelope_digest: claim_sight + .coverage_store_binding_envelope_digest + .clone(), + coverage_store_file_identity: claim_sight.coverage_store_file_identity.clone(), + runtime_coverage_route_envelope_digest: claim_sight + .runtime_coverage_route_envelope_digest + .clone(), + issued_at_utc: chrono::Utc::now().to_rfc3339(), + host_identity_id: host.entry().identity_id.clone(), + host_identity_record_digest: workspace_coverage_host_identity_record_digest(host.entry())?, + host_signature: Vec::new(), + signature_algorithm: "ed25519".into(), + }; + entry.host_signature = host + .sign( + HOST_RECOVERY_DIRECTIVE_PURPOSE, + &recovery_directive_statement(&entry)?, + )? + .signature; + let backing = SingleFileMessagePackBackingStore::new(local_verse_store); + let opening = backing.pull_all()?; + replace_latest_sight( + node.cache(), + &backing, + WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_TYPE, + &directive_key, + &entry, + &opening, + )?; + Ok(entry) +} + +pub fn authenticate_workspace_coverage_recovery_directive( + local_verse_store: impl AsRef, + runtime_store: impl AsRef, + runtime_id: &str, + replacement_launch_id: &str, + trusted_host: &HostIncarnationIdentityEntry, +) -> Result> { + let node = open_epiphany_cultmesh_node(local_verse_store.as_ref(), runtime_id.to_string())?; + let Some(env) = node + .cache() + .get_envelope::(&recovery_directive_key( + runtime_id, + replacement_launch_id, + ))? + else { + return Ok(None); + }; + let entry: WorkspaceCoverageRecoveryDirectiveEntry = rmp_serde::from_slice(&env.payload)?; + if entry.schema_version != WORKSPACE_COVERAGE_RECOVERY_DIRECTIVE_SCHEMA_VERSION + || entry.runtime_id != runtime_id + || entry.replacement_launch_id != replacement_launch_id + || entry.host_identity_id != trusted_host.identity_id + || entry.host_identity_record_digest + != workspace_coverage_host_identity_record_digest(trusted_host)? + || entry.signature_algorithm != "ed25519" + { + bail!("workspace coverage recovery directive identity is invalid"); + } + verify_host_identity_signature( + trusted_host, + HOST_RECOVERY_DIRECTIVE_PURPOSE, + &recovery_directive_statement(&entry)?, + &HostIdentitySignature { + identity_id: entry.host_identity_id.clone(), + signature: entry.host_signature.clone(), + }, + )?; + let basis = crate::load_current_runtime_repository_body_basis(runtime_store.as_ref())?; + if entry.body_observation_id != basis.observation_id + || entry.body_generation != basis.generation + || entry.manifest_root_sha256 != basis.manifest_root_sha256 + || entry.body_binding_sha256 != basis.body_binding_sha256 + || entry.workspace_id != basis.workspace_id + { + // A directive is authority only for its exact Body generation. Keeping + // the immutable document is useful evidence; treating it as current + // pressure after Body advances would strand the replacement process. + return Ok(None); + } + let claim_node = + open_epiphany_cultmesh_node(local_verse_store.as_ref(), runtime_id.to_string())?; + let claim_env = claim_node + .cache() + .get_envelope::(&claim_sight_key( + runtime_id, + basis.generation, + &basis.observation_id, + ))? + .ok_or_else(|| anyhow!("recovery directive claim sight is absent"))?; + let claim: WorkspaceCoverageClaimSightEntry = rmp_serde::from_slice(&claim_env.payload)?; + authenticate_workspace_coverage_claim_sight_entry(&claim)?; + let (termination, termination_digest) = + authenticate_workspace_coverage_termination_with_envelope_digest( + local_verse_store.as_ref(), + runtime_id.to_string(), + &entry.old_launch_id, + trusted_host, + )?; + let (_, replacement_digest) = + authenticate_workspace_coverage_managed_process_launch_with_envelope_digest( + local_verse_store.as_ref(), + runtime_id, + &entry.replacement_launch_id, + trusted_host, + )?; + let (_, ready_digest) = + authenticate_workspace_coverage_provider_heartbeat_with_envelope_digest( + local_verse_store.as_ref(), + runtime_id, + &entry.replacement_ready_heartbeat_id, + trusted_host, + )?; + if entry.claim_sight_envelope_digest != envelope_digest(&claim_env) + || entry.old_claim_id != claim.claim_id + || entry.old_claim_epoch != claim.claim_epoch + || entry.old_plan_id != claim.plan_id + || entry.old_launch_id != claim.launch_id + || entry.termination_id != termination.termination_id + || entry.termination_envelope_digest != termination_digest + || entry.replacement_launch_envelope_digest != replacement_digest + || entry.replacement_ready_heartbeat_envelope_digest != ready_digest + || entry.workspace_id != claim.workspace_id + || entry.body_binding_sha256 != claim.body_binding_sha256 + || entry.body_observation_id != claim.body_observation_id + || entry.body_generation != claim.body_generation + || entry.manifest_root_sha256 != claim.manifest_root_sha256 + || entry.coverage_store_binding_id != claim.coverage_store_binding_id + || entry.coverage_store_binding_envelope_digest + != claim.coverage_store_binding_envelope_digest + || entry.coverage_store_file_identity != claim.coverage_store_file_identity + || entry.runtime_coverage_route_envelope_digest + != claim.runtime_coverage_route_envelope_digest + { + bail!("workspace coverage recovery directive evidence moved or was substituted"); + } + Ok(Some(entry)) +} + fn advancement_sight_key(launch_id: &str, claim_id: &str) -> String { format!("epiphany-local/workspace-coverage/advancement-sight/{launch_id}/{claim_id}") } @@ -2466,36 +3112,41 @@ fn launch_key(id: &str) -> String { fn heartbeat_latest_key(launch_id: &str) -> String { format!("{WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_LATEST_KEY}{launch_id}") } +fn heartbeat_evidence_key(launch_id: &str, heartbeat_id: &str) -> String { + format!("epiphany-local/workspace-coverage/provider-heartbeat/{launch_id}/{heartbeat_id}") +} fn latest_heartbeat_envelope_by_id( store_path: impl AsRef, runtime_id: &str, heartbeat_id: &str, ) -> Result> { - // Open the typed node first so schema-catalog corruption is still refused; - // then select only launch-latest heartbeat projections. Historical unique - // heartbeat events are intentionally not authority. - let _ = open_epiphany_cultmesh_node(store_path.as_ref(), runtime_id.to_string())?; - let mut matches = Vec::new(); - for envelope in SingleFileMessagePackBackingStore::new(store_path.as_ref()).pull_all()? { - if envelope.r#type != WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_TYPE - || !envelope + let _node = open_epiphany_cultmesh_node(store_path.as_ref(), runtime_id.to_string())?; + let opening = SingleFileMessagePackBackingStore::new(store_path.as_ref()).pull_all()?; + let matches = opening + .iter() + .filter(|envelope| { + envelope.r#type == WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_TYPE + && rmp_serde::from_slice::( + &envelope.payload, + ) + .is_ok_and(|heartbeat| { + heartbeat.runtime_id == runtime_id && heartbeat.heartbeat_id == heartbeat_id + }) + }) + .collect::>(); + let immutable = matches + .into_iter() + .filter(|envelope| { + !envelope .key .starts_with(WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_LATEST_KEY) - { - continue; - } - let heartbeat: WorkspaceCoverageProviderHeartbeatEntry = - rmp_serde::from_slice(&envelope.payload)?; - validate_heartbeat_shape(&heartbeat)?; - if heartbeat.runtime_id == runtime_id && heartbeat.heartbeat_id == heartbeat_id { - matches.push(envelope); - } + }) + .collect::>(); + if immutable.len() != 1 { + bail!("workspace coverage heartbeat immutable evidence is absent or duplicated"); } - if matches.len() > 1 { - bail!("workspace coverage heartbeat identity is duplicated across latest projections"); - } - Ok(matches.pop()) + Ok(Some(immutable[0].clone())) } fn termination_key(launch_id: &str) -> String { format!("epiphany-local/workspace-coverage/process-termination/{launch_id}") @@ -2708,6 +3359,7 @@ mod tests { std::path::PathBuf, HostIdentitySigner, WorkspaceCoverageManagedProcessLaunchEntry, + SigningKey, )> { let store = root.join("verse.ccmp"); let host = enroll_host_identity_at(&root.join("host.ccmp"))?; @@ -2734,7 +3386,7 @@ mod tests { .context("test launch envelope absent")?; let pulse = heartbeat(&launch, envelope_digest(&launch_envelope), &provider, 1)?; write_workspace_coverage_provider_heartbeat(&store, "local", pulse)?; - Ok((store, host, launch)) + Ok((store, host, launch, provider)) } fn advancement_sight( @@ -3061,9 +3713,9 @@ mod tests { load_workspace_coverage_provider_heartbeat(&store, "local", &second.heartbeat_id)?, Some(second) ); - assert!( - load_workspace_coverage_provider_heartbeat(&store, "local", &first.heartbeat_id)? - .is_none() + assert_eq!( + load_workspace_coverage_provider_heartbeat(&store, "local", &first.heartbeat_id)?, + Some(first.clone()) ); assert_eq!( SingleFileMessagePackBackingStore::new(&store) @@ -3071,8 +3723,8 @@ mod tests { .into_iter() .filter(|entry| entry.r#type == WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_TYPE) .count(), - 1, - "heartbeat authority is one latest projection, not an append-only event log" + 3, + "latest is a derived head over immutable heartbeat evidence" ); let mut forged = first.clone(); @@ -3144,7 +3796,7 @@ mod tests { ]; for (expected_outcome, process) in cases { let temp = tempfile::tempdir()?; - let (store, host, launch) = persisted_chain(temp.path())?; + let (store, host, launch, _provider) = persisted_chain(temp.path())?; let source = FakeObservation { boot: Some(launch.boot_identity.clone()), process, @@ -3199,7 +3851,7 @@ mod tests { } let temp = tempfile::tempdir()?; - let (store, host, launch) = persisted_chain(temp.path())?; + let (store, host, launch, _provider) = persisted_chain(temp.path())?; let source = FakeObservation { boot: Some("proved-new-boot".to_string()), process: ProcessInstanceObservation::ExactAlive, @@ -3240,7 +3892,7 @@ mod tests { ]; for source in cases { let temp = tempfile::tempdir()?; - let (store, host, launch) = persisted_chain(temp.path())?; + let (store, host, launch, _provider) = persisted_chain(temp.path())?; assert!( write_workspace_coverage_process_termination_observation_with_source( &store, @@ -3320,7 +3972,7 @@ mod tests { #[test] fn body_recovery_is_one_exact_evidence_fenced_transaction() -> Result<()> { let temp = tempfile::tempdir()?; - let (verse, host, old_launch) = persisted_chain(temp.path())?; + let (verse, host, old_launch, old_provider) = persisted_chain(temp.path())?; let repo = temp.path().join("repo"); std::fs::create_dir_all(&repo)?; @@ -3375,6 +4027,26 @@ mod tests { ) => value, _ => bail!("fixture did not acquire old claim"), }; + publish_workspace_coverage_claim_sight( + &verse, + &runtime, + &coverage, + &acquired, + "local", + &old_launch.launch_id, + host.entry(), + &old_provider, + chrono::Utc::now(), + )?; + assert!( + authenticate_current_workspace_coverage_claim_sight( + &verse, + &runtime, + "local", + host.entry(), + )? + .is_some() + ); let source = FakeObservation { boot: Some(old_launch.boot_identity.clone()), @@ -3437,7 +4109,8 @@ mod tests { )?; write_workspace_coverage_provider_heartbeat(&verse, "local", out_of_order_ready.clone())?; assert!( - crate::workspace_coverage_projector::recover_workspace_coverage_projection( + crate::workspace_coverage_projector::recover_workspace_coverage_projection_with_authority( + &crate::open_workspace_coverage_authority(&runtime)?, &runtime, &verse, "local", @@ -3516,7 +4189,8 @@ mod tests { sign_workspace_coverage_heartbeat(&mut degraded, &replacement_provider)?; write_workspace_coverage_provider_heartbeat(&verse, "local", degraded.clone())?; assert!( - crate::workspace_coverage_projector::recover_workspace_coverage_projection( + crate::workspace_coverage_projector::recover_workspace_coverage_projection_with_authority( + &crate::open_workspace_coverage_authority(&runtime)?, &runtime, &verse, "local", @@ -3538,8 +4212,62 @@ mod tests { 3, )?; write_workspace_coverage_provider_heartbeat(&verse, "local", ready.clone())?; + let later_ready = heartbeat( + &replacement, + envelope_digest(&replacement_envelope), + &replacement_provider, + 4, + )?; + write_workspace_coverage_provider_heartbeat(&verse, "local", later_ready)?; + let directive = write_workspace_coverage_recovery_directive( + &verse, + &runtime, + "local", + &authenticate_current_workspace_coverage_claim_sight( + &verse, + &runtime, + "local", + host.entry(), + )? + .context("current claim sight missing before recovery directive")?, + &replacement.launch_id, + &ready.heartbeat_id, + &host, + )?; + assert!( + authenticate_workspace_coverage_recovery_directive( + &verse, + &runtime, + "local", + &replacement.launch_id, + host.entry(), + )? + .is_some() + ); + let mut conflicting_lineage = authenticate_current_workspace_coverage_claim_sight( + &verse, + &runtime, + "local", + host.entry(), + )? + .context("claim sight missing for conflict check")?; + conflicting_lineage.claim_id = uuid::Uuid::new_v4().to_string(); + assert!( + write_workspace_coverage_recovery_directive( + &verse, + &runtime, + "local", + &conflicting_lineage, + &replacement.launch_id, + &ready.heartbeat_id, + &host, + ) + .is_err(), + "a replacement directive must refuse conflicting claim lineage" + ); - let recovered = crate::workspace_coverage_projector::recover_workspace_coverage_projection( + let recovered = crate::workspace_coverage_projector::recover_workspace_coverage_projection_with_authority( + &crate::open_workspace_coverage_authority(&runtime)?, &runtime, &verse, "local", @@ -3555,19 +4283,63 @@ mod tests { recovered.executor_incarnation, replacement.provider_incarnation_id ); - let coverage = crate::open_workspace_coverage_authority(&runtime)?; - match crate::workspace_coverage_projector::acquire_workspace_coverage_projection( - &prepared, - &coverage.store, - EPIPHANY_WORKSPACE_COVERAGE_PROJECTOR_DAEMON_ID, - &replacement.provider_incarnation_id, + let reissued = write_workspace_coverage_recovery_directive( + &verse, + &runtime, + "local", + &authenticate_current_workspace_coverage_claim_sight( + &verse, + &runtime, + "local", + host.entry(), + )? + .context("old claim sight must survive the CAS-to-sight crash boundary")?, &replacement.launch_id, - )? { - crate::workspace_coverage_projector::WorkspaceCoverageAcquireResult::Acquired( - resumed, - ) => assert_eq!(resumed.claim.claim_id, recovered.claim_id), - _ => bail!("exact replacement incarnation did not resume its recovered claim"), - } + &ready.heartbeat_id, + &host, + )?; + assert_eq!( + reissued, directive, + "directive reissue must be byte-identical" + ); + let coverage = crate::open_workspace_coverage_authority(&runtime)?; + let resumed = + match crate::workspace_coverage_projector::acquire_workspace_coverage_projection( + &prepared, + &coverage.store, + EPIPHANY_WORKSPACE_COVERAGE_PROJECTOR_DAEMON_ID, + &replacement.provider_incarnation_id, + &replacement.launch_id, + )? { + crate::workspace_coverage_projector::WorkspaceCoverageAcquireResult::Acquired( + resumed, + ) => resumed, + _ => bail!("exact replacement incarnation did not resume its recovered claim"), + }; + assert_eq!(resumed.claim.claim_id, recovered.claim_id); + publish_workspace_coverage_claim_sight( + &verse, + &runtime, + &coverage, + &resumed, + "local", + &replacement.launch_id, + host.entry(), + &replacement_provider, + chrono::Utc::now(), + )?; + assert_eq!( + authenticate_current_workspace_coverage_claim_sight( + &verse, + &runtime, + "local", + host.entry(), + )? + .context("successor sight missing after recovery resume")? + .claim_id, + recovered.claim_id, + ); + drop(resumed); assert!(matches!( crate::workspace_coverage_projector::acquire_workspace_coverage_projection( &prepared, @@ -3611,8 +4383,9 @@ mod tests { old_history.recovery_receipt_digest.as_deref(), Some(recovery_digest) ); - assert!( - crate::workspace_coverage_projector::recover_workspace_coverage_projection( + let replayed = + crate::workspace_coverage_projector::recover_workspace_coverage_projection_with_authority( + &crate::open_workspace_coverage_authority(&runtime)?, &runtime, &verse, "local", @@ -3621,9 +4394,38 @@ mod tests { &replacement.launch_id, &ready.heartbeat_id, &old_claim_id, - ) - .is_err(), - "moved claim/attempt must refuse replay" + )?; + assert_eq!( + replayed, recovered, + "exact crash-boundary replay must be idempotent" + ); + std::fs::write(repo.join("after-recovery.rs"), "fn newer_body() {}\n")?; + Command::new("git") + .args(["add", "."]) + .current_dir(&repo) + .output()?; + let newer_basis = crate::observe_runtime_repository_body_basis(&runtime)?; + assert!(newer_basis.generation > basis.generation); + assert!( + authenticate_current_workspace_coverage_claim_sight( + &verse, + &runtime, + "local", + host.entry(), + )? + .is_none(), + "a prior-Body claim sight must not become the current recovery target" + ); + assert!( + authenticate_workspace_coverage_recovery_directive( + &verse, + &runtime, + "local", + &directive.replacement_launch_id, + host.entry(), + )? + .is_none(), + "a prior-Body directive must become inert after Body advances" ); Ok(()) } diff --git a/epiphany-core/src/workspace_coverage_projector.rs b/epiphany-core/src/workspace_coverage_projector.rs index cedc159c..b55a551f 100644 --- a/epiphany-core/src/workspace_coverage_projector.rs +++ b/epiphany-core/src/workspace_coverage_projector.rs @@ -107,14 +107,6 @@ pub(crate) struct WorkspaceCoverageObservedBinding { vector_binding_set_sha256: String, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct WorkspaceCoverageRecoveryTarget { - pub claim_id: String, - pub claim_epoch: u64, - pub plan_id: String, - pub managed_process_launch_id: String, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct WorkspaceCoverageRecoveryOutcome { pub claim_id: String, @@ -2107,34 +2099,8 @@ pub(crate) fn acquire_workspace_coverage_projection( /// Atomically transfers a projection lease only after CultMesh proves the old /// process dead and the replacement process ready. This does not launch either /// process; the supervisor must establish the evidence ordering first. -pub fn current_workspace_coverage_recovery_target( - runtime_store: impl AsRef, -) -> Result> { - let runtime_store = runtime_store.as_ref(); - let body_basis = observe_runtime_repository_body_basis(runtime_store)?; - let coverage = crate::open_workspace_coverage_authority(runtime_store)?; - let opening = coverage.store.pull_all()?; - let claim_key = workspace_coverage_claim_key(&body_basis.observation_id, body_basis.generation); - let Some(claim_env) = find(&opening, CLAIM_TYPE, &claim_key) else { - return Ok(None); - }; - let claim: WorkspaceCoverageProjectionClaim = decode(claim_env)?; - let attempt_env = find(&opening, ATTEMPT_TYPE, &claim.attempt_id) - .ok_or_else(|| anyhow!("workspace coverage current attempt is absent"))?; - let attempt: WorkspaceCoverageProjectionAttempt = decode(attempt_env)?; - validate_claim_attempt_link(&claim, &attempt)?; - if claim.status != "running" { - return Ok(None); - } - Ok(Some(WorkspaceCoverageRecoveryTarget { - claim_id: claim.claim_id, - claim_epoch: claim.claim_epoch, - plan_id: claim.plan_id, - managed_process_launch_id: claim.managed_process_launch_id, - })) -} - -pub fn recover_workspace_coverage_projection( +pub(crate) fn recover_workspace_coverage_projection_with_authority( + coverage: &crate::WorkspaceCoverageAuthority, runtime_store: impl AsRef, cultmesh_store: impl AsRef, runtime_id: &str, @@ -2185,6 +2151,14 @@ pub fn recover_workspace_coverage_projection( &replacement_launch.launch_id, )? .ok_or_else(|| anyhow!("workspace coverage recovery replacement has no current heartbeat"))?; + let (latest, _) = authenticate_workspace_coverage_provider_heartbeat_with_envelope_digest( + cultmesh_store, + runtime_id, + &latest.heartbeat_id, + host, + )?; + let ready_at = chrono::DateTime::parse_from_rfc3339(&ready.observed_at_utc)?; + let latest_at = chrono::DateTime::parse_from_rfc3339(&latest.observed_at_utc)?; if old_launch.launch_id == replacement_launch.launch_id || replacement_launch.replaces_launch_id.as_deref() != Some(old_launch.launch_id.as_str()) || replacement_launch.replaces_termination_id.as_deref() @@ -2194,9 +2168,14 @@ pub fn recover_workspace_coverage_projection( .as_deref() != Some(termination_digest.as_str()) || ready.launch_id != replacement_launch.launch_id - || latest.heartbeat_id != ready.heartbeat_id || ready.status != "ready" || ready.sequence == 0 + || latest.launch_id != ready.launch_id + || latest.provider_incarnation_id != ready.provider_incarnation_id + || latest.provider_public_key != ready.provider_public_key + || latest.status != "ready" + || latest.sequence < ready.sequence + || latest_at < ready_at { bail!("workspace coverage recovery replacement is not exact and ready"); } @@ -2212,7 +2191,6 @@ pub fn recover_workspace_coverage_projection( RepositoryBodyReadSession::open(runtime_store, &body_basis)?; let body_opening = SingleFileMessagePackBackingStore::new(&body_store).pull_all()?; let body_authority = exact_body_authority(&body_opening, &body_basis)?; - let coverage = crate::open_workspace_coverage_authority(runtime_store)?; let opening = coverage.store.pull_all()?; let claim_key = workspace_coverage_claim_key(&body_basis.observation_id, body_basis.generation); let claim_env = find(&opening, CLAIM_TYPE, &claim_key) @@ -2222,6 +2200,47 @@ pub fn recover_workspace_coverage_projection( .ok_or_else(|| anyhow!("workspace coverage recovery running attempt is absent"))?; let old_attempt: WorkspaceCoverageProjectionAttempt = decode(attempt_env)?; validate_claim_attempt_link(&old_claim, &old_attempt)?; + // The coverage CAS may have committed before the successor projector could + // publish its claim sight. Replaying the exact host directive must observe + // that committed transfer, not attempt a second transfer or strand recovery. + if old_claim.claim_id != expected_old_claim_id + && let (Some(recovery_id), Some(expected_digest)) = ( + old_claim.recovery_receipt_id.as_deref(), + old_claim.recovery_receipt_digest.as_deref(), + ) + { + let receipt_env = find(&opening, RECOVERY_TYPE, recovery_id) + .ok_or_else(|| anyhow!("recovered successor receipt is absent"))?; + let receipt: WorkspaceCoverageRecoveryReceipt = decode(receipt_env)?; + validate_recovery_receipt(&receipt)?; + let receipt_digest = cultcache_envelope_digest(receipt_env); + if receipt_digest != expected_digest + || receipt.old_claim_id != expected_old_claim_id + || receipt.termination_id != termination.termination_id + || receipt.termination_envelope_digest != termination_digest.as_str() + || receipt.replacement_launch_id != replacement_launch.launch_id + || receipt.replacement_launch_envelope_digest != replacement_launch_digest.as_str() + || receipt.ready_heartbeat_id != ready.heartbeat_id + || receipt.ready_heartbeat_envelope_digest != ready_digest.as_str() + || receipt.new_claim_id != old_claim.claim_id + || receipt.new_attempt_id != old_attempt.attempt_id + || receipt.new_claim_epoch != old_claim.claim_epoch + || old_claim.managed_process_launch_id != replacement_launch.launch_id + || old_claim.executor_incarnation != replacement_launch.provider_incarnation_id + || old_claim.status != "running" + || old_attempt.status != "running" + { + bail!("workspace coverage recovered successor disagrees with exact directive evidence"); + } + return Ok(WorkspaceCoverageRecoveryOutcome { + claim_id: old_claim.claim_id, + claim_epoch: old_claim.claim_epoch, + managed_process_launch_id: old_claim.managed_process_launch_id, + executor_incarnation: old_claim.executor_incarnation, + recovery_receipt_id: recovery_id.to_string(), + recovery_receipt_digest: receipt_digest, + }); + } if old_claim.schema_version != CLAIM_SCHEMA || old_attempt.schema_version != ATTEMPT_SCHEMA || old_claim.status != "running" @@ -5157,7 +5176,6 @@ mod tests { .is_none() ); drop(authority); - assert!(current_workspace_coverage_recovery_target(&runtime)?.is_none()); Ok(()) } diff --git a/epiphany-core/src/workspace_coverage_projector_service.rs b/epiphany-core/src/workspace_coverage_projector_service.rs index ddf35d4e..79af268b 100644 --- a/epiphany-core/src/workspace_coverage_projector_service.rs +++ b/epiphany-core/src/workspace_coverage_projector_service.rs @@ -1,6 +1,10 @@ use crate::repository_body_observer::RepositoryBodyReadSession; use crate::semantic_backend::{OllamaConfig, OllamaEmbedder, QdrantBackend, QdrantConfig}; -use crate::workspace_coverage_process_documents::publish_workspace_coverage_terminal_sight; +use crate::workspace_coverage_process_documents::{ + authenticate_current_workspace_coverage_claim_sight, + authenticate_workspace_coverage_recovery_directive, publish_workspace_coverage_claim_sight, + publish_workspace_coverage_terminal_sight, +}; use crate::workspace_coverage_projector::{ WorkspaceCoverageAcquireResult, WorkspaceCoverageCurrentState, WorkspaceCoverageProjectionExecutionAuthority, acquire_workspace_coverage_projection, @@ -214,6 +218,53 @@ impl WorkspaceCoverageProjectorServiceBody { } fn pulse_inner(&mut self) -> Result { + let authority = self.execution_authority.as_ref().ok_or_else(|| { + anyhow!("workspace coverage projector has no installed execution authority") + })?; + let has_successor_sight = authenticate_current_workspace_coverage_claim_sight( + &authority.local_verse_store, + &self.runtime_store, + &authority.runtime_id, + &authority.trusted_host, + )? + .is_some_and(|sight| { + sight.launch_id == self.managed_process_launch_id && sight.recovery_receipt_id.is_some() + }); + if !has_successor_sight + && let Some(directive) = authenticate_workspace_coverage_recovery_directive( + &authority.local_verse_store, + &self.runtime_store, + &authority.runtime_id, + &self.managed_process_launch_id, + &authority.trusted_host, + )? + { + let basis = load_current_runtime_repository_body_basis(&self.runtime_store)?; + if directive.workspace_id != basis.workspace_id + || directive.body_binding_sha256 != basis.body_binding_sha256 + || directive.body_observation_id != basis.observation_id + || directive.body_generation != basis.generation + || directive.manifest_root_sha256 != basis.manifest_root_sha256 + || directive.coverage_store_binding_id + != self.coverage_authority.store_binding.binding_id + || directive.coverage_store_binding_envelope_digest + != self.coverage_authority.store_binding_envelope_sha256 + || directive.coverage_store_file_identity + != self.coverage_authority.store_binding.store_file_identity + || directive.runtime_coverage_route_envelope_digest + != self + .coverage_authority + .runtime_coverage_route_envelope_sha256 + { + bail!("workspace coverage recovery directive disagrees with held authority"); + } + crate::workspace_coverage_projector::recover_workspace_coverage_projection_with_authority( + &self.coverage_authority, &self.runtime_store, &authority.local_verse_store, + &authority.runtime_id, &authority.trusted_host, &directive.old_launch_id, + &directive.replacement_launch_id, &directive.replacement_ready_heartbeat_id, + &directive.old_claim_id, + )?; + } // Retirement is derived from typed Body history and is deliberately // performed before the idle fast path. Qdrant never nominates its own // garbage and incompatible same-name collections stop the pulse. @@ -308,6 +359,20 @@ impl WorkspaceCoverageProjectorServiceBody { let authority = self.execution_authority.as_ref().ok_or_else(|| { anyhow!("workspace coverage projector has no installed execution authority") })?; + // Publish the exact provider-signed lease before the first backend + // operation. Recovery sight must exist even when the projector dies + // before it can mint a progress checkpoint. + publish_workspace_coverage_claim_sight( + &authority.local_verse_store, + &self.runtime_store, + &self.coverage_authority, + &acquisition, + &authority.runtime_id, + &self.managed_process_launch_id, + &authority.trusted_host, + &authority.provider_signing_key, + chrono::Utc::now(), + )?; let receipt = execute_workspace_coverage_projection( &acquisition, &prepared, @@ -441,4 +506,18 @@ mod tests { )?; Ok(()) } + + #[test] + fn acquired_claim_is_visible_before_any_projection_backend_work() { + let source = include_str!("workspace_coverage_projector_service.rs"); + let acquired = source + .find("WorkspaceCoverageAcquireResult::Acquired(acquisition)") + .unwrap(); + let body = &source[acquired..source.find("#[cfg(test)]").unwrap()]; + let sight = body + .find("publish_workspace_coverage_claim_sight(") + .unwrap(); + let execution = body.find("execute_workspace_coverage_projection(").unwrap(); + assert!(sight < execution, "claim sight must precede backend work"); + } } diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index d379d942..fd7251f8 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -2413,3 +2413,80 @@ supervisor binaries pass. The old all-at-once executor survives only under deployed body. The next authority cut is Idunn and aggregate health: consume the exact provider progress document with a no-advance lease, never a generic job progress noun or total projection wall clock. + +## Organizational Epiphany is the product, not a remote-control experiment (2026-07-18) + +The Yggdrasil deployment is the first ordinary organizational Epiphany body. +It must remain awake without continual instruction: Modeling persistently maps +the bounded domain into typed Mind state, Self extrapolates pressure and +direction from that map, and Imagination authors inspectable improvement and +feature proposals. This initiative is advisory until the appropriate adoption +and consequence authorities act; operator inactivity permits observation, +modeling, and proposals, not merge, publication, deployment, or scope growth. + +The repository Persona is the organization's shared social crossing. Members +may talk to it, correct it, and provide feedback through ordinary Discord +conversation. Persona owns discussion and legibility. Conversation produces +typed feedback pressure for Modeling, Imagination, and Mind review; it does not +directly mutate canonical Mind, invoke Hands, authorize a release, or command +Idunn. Authenticated operational capabilities such as brake, objective intake, +adoption approval, release authorization, and deployment remain distinct typed +Bifrost routes with explicit receipts. VoidBot observes the room and Bifrost +routes the typed crossing; do not invent a second Discord bot or grant ordinary +chat ambient actuator authority. + +The July 22 continuity criterion is stronger than remote command. Yggdrasil +must host the resident scheduler/coordinator, model runtime, tool spine, Persona +crossing, writable domain worktree, persistent Mind/Modeling state, and local +credentials. Starfire may be absent through August 5. The offline rehearsal +must prove conversation/feedback -> persistent domain map -> Imagination +proposal -> explicit adoption/review -> Hands consequence -> Bifrost exact +release authority -> Idunn deployment receipt, plus the negative that +conversation alone cannot cross any later boundary. + +The immediate live wound precedes that work. The workspace projector is the +lifetime owner of `workspace-coverage.cc`, but the supervisor currently opens +that private store to discover and perform recovery. Replace that transitive +authority leak with a provider-signed claim sight published immediately after +acquisition and a host-signed recovery directive consumed by the replacement +projector. Heartbeat remains liveness only. The supervisor must never open or +mutate the coverage store; it declares recovery only after authenticating the +replacement claim sight. Keep Epiphany and Idunn stopped on Yggdrasil until the +corrected exact release is authorized through Bifrost and deployed through +Idunn. No host reboot is authorized. + +## Workspace-coverage recovery ownership cut complete locally (2026-07-18) + +The supervisor no longer opens or mutates `workspace-coverage.cc`, directly or +through an imported recovery helper. The lifetime projector owner publishes a +provider-signed claim sight before its first backend operation. Claim sight is +keyed to the exact current Body observation and generation and binds the exact +launch, provider key, coverage-store binding/file/route, Body basis, claim, +attempt envelope, plan, and recovery lineage. A signed sight from an older Body +is not current recovery authority. + +After exact termination and replacement readiness, the supervisor publishes +one immutable host-signed recovery directive. It binds the authenticated old +claim sight, termination, replacement launch, readiness heartbeat, Body, and +coverage route by exact envelope digests. Reconciliation reauthenticates and +returns the same signed directive byte-for-byte; a later heartbeat cannot +rewrite it, and conflicting lineage is refused. Provider heartbeats now retain +immutable per-heartbeat evidence beside the latest pointer because durable +termination and recovery documents may cite an earlier heartbeat. Causally safe +bounded retirement remains future work; naive deletion is forbidden. + +The replacement projector authenticates the directive and performs the +recovery CAS through its already-held `WorkspaceCoverageAuthority`. Recovery is +idempotent across the CAS-to-successor-sight crash window: an exact committed +successor and receipt are returned on replay, then ordinary acquisition +republishes successor claim sight. A directive naming readiness heartbeat H +remains valid when the same launch/provider has advanced normally to ready +H+1. Heartbeat remains liveness evidence and does not own the lease. + +Focused proof passes: eight process-document tests, the exclusive lifetime +owner test, supervisor authority-order/prohibition test, pre-checkpoint sight, +stale-Body refusal, H/H+1 consumption, immutable directive reissue, conflicting +lineage refusal, exact CAS replay, successor sight publication, `cargo check +--bins`, and `git diff --check`. Soul found no remaining P0/P1/P2. This code is +ready to commit and push, then cross Bifrost/Idunn as an exact release while the +host remains braked. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 89beb52c2181d25716faec4867830d83b4da6899..76e6328404cd72573f1bddcd93c4284edd20ae31 100644 GIT binary patch delta 468 zcmY*VJ#Q015ao=6LlFr1wVjT zcENuDi2~82%#WZWKOsfxjGZVV#nXIf=Djy_HHWkB@bg*nWdB`nxBq&#|7!ok`GeK9 z?yncgagtn|Oj}>4t%R|N4;gL73gZ!@^LyG_{rC9k>7SkLoj2L`Ue>$i`F8Z9+y0x( z-_O=BJKZmKY|wc{4agY!&^Y@MWv~vKm}~M1Jsj9#l6h$xEWlG~I7lt1UKA=ojzjAZ zav>`;1&pn$2H<0ZZbbeb5!H1JBG(ARsPzPL8!O&$DD4Q{1PbCb&Vzm~HH>JpU}w`>e1s1^gs+ z^emSxpOD_LPEr;T8neaODI9JczU*=9zk8fjlv5yUQV{GvC*wgXl$D1@ct3z48KL2= OjT%HODDb~fn9XZqUZ(m0 delta 65 zcmdltN8#HHg$=@t(`RvUaIr-lW8mNp-mJ`6$jJDxc`8%;R3=6s2GPtw%mT!$K+Fcj M?Axa@aXjP%0J-E8IRF3v diff --git a/state/map.yaml b/state/map.yaml index d79ba606..2361d6ec 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -739,4 +739,4 @@ current_status: - Release `sha256-c94109f94ee42c1257089830f399cb87cd5ad672772c274194371995ce4df923` is the first deployed immutable sibling generation. Native packaging builds from detached exact source commit `dd80f4a51c18425a6665710698a357be61154abf`, initializes pinned submodules, uses the tracked lockfile under `--locked`, and publishes four exact binaries plus a typed CultCache witness/head only after atomic directory publication and byte verification. Task Scheduler pins release id/digest and the witnessed supervisor path; Idunn reauthenticates the complete set and reserved policy role paths. Current-boot proof: reconciler PID 21228 launched direct child 22736; witnessed Mind/Modeling queries ranked semantically; killing that exact-path child caused Idunn to launch PID 19748 with receipt `86a42d51-9930-4b5d-9e61-e74c82a0d808`, ready heartbeat `4fc91977-b635-474b-812b-c364847e2b4b`, and witnessed executable digest `sha256-1060dd64e8c2b520ec8744b67e8306db04a6618d463fc2117c352adf9e30e6b0`. This closes current-boot artifact generation and recurrence, not reboot/logon survival. - Release `sha256-6f9ab4848207a110e7e3836f41e6a74c0f463369ad4b04ce2d0d3831a6b262ae` packages repository-readiness source commit `360dcb433dda9599fca14282592608d26b8b6cc8`; witnessed Idunn PID 29476 directly owns witnessed projector PID 22840 with launch receipt `50879209-b65f-4411-ae3f-6f647d37bf42` and ready heartbeat `134df620-9e20-4220-ad4e-117ea4ca8188`. Deployment exposed receipt-v2 migration pressure: v1 receipts became correctly ineligible, but the old Succeeded pulse shortcut kept both partitions idle. The shortcut is now cut in source. Valid v2 terminal evidence derives Ready; an exact same-obligation succeeded claim opens Repair only after its consumed grant/recovery authority and exact succeeded attempt are authenticated. Failed repair retries through execute at the next epoch. Forged predecessor authority cannot be laundered by a repair grant. - Yggdrasil exact-SHA pressure release `586147a751c6eec2d59cd8dc10bd17ce0d02a4d1` proved useful GPU work but failed deployment honestly. GTX 1080 stayed near saturation at 2,419 MiB; bounded four-text requests completed continuously. Mind published 215 vectors, while workspace coverage completed 3,389 Ollama HTTP calls total (about 13.3k workspace chunks after Mind) before Idunn's 50-minute candidate-health deadline at 2026-07-17T01:23:50+02:00. Idunn then stopped the service, published no deployment witness, and reported no exact-candidate daemon-health proof; the Modeling collection remained at the inherited 22 points because the monolithic executor had not yet published its accumulated vectors. This is failure evidence for the execution/deployment contract, not for GPU acceleration. Total wall-time extension is forbidden as the repair. - next_action: Migrate workspace projection obligation/plan/claim/attempt/recovery/checkpoint/progress/receipt/head ownership into the pinned transactional workspace-coverage store, bind/bootstrap it on every deployment path, and cut all Body/Verse projection writers and fallback reads. Preserve Body as observations only. Do not redeploy c94fa580 or any descendant as a live candidate until that cut is complete and verified against body.cc rewrite, store-substitution, stale-Body claim, atomic checkpoint/progress, restart, and terminal-proof negatives. No reboot is authorized. + next_action: "Commit and push the verified workspace-coverage recovery ownership cut, authorize its exact SHA through Bifrost, deploy only through Idunn, and prove Ygg-local exclusive ownership, recovery replay, bounded Body/coverage I/O, Qdrant progress, GPU work, terminal health, and service restart while the host remains braked until admission. Then package the resident organizational swarm (scheduler/coordinator, model runtime, tool spine, Persona), provision its writable domain worktree and local credentials, wire VoidBot/Bifrost Discord feedback and separately authenticated operations, and complete a Starfire-offline rehearsal before 2026-07-22. Ordinary Persona conversation may create typed feedback pressure but cannot adopt Mind state, invoke Hands, authorize release, or deploy. No host reboot is authorized."