Seal pre-readiness process death

This commit is contained in:
Meta Vens 2026-07-16 02:32:19 +01:00
commit 9f9f9fe304
6 changed files with 313 additions and 42 deletions

View file

@ -6,10 +6,11 @@ use crate::default_mind_cultnet_contracts;
use crate::default_soul_cultnet_contracts;
use crate::default_substrate_gate_cultnet_contracts;
use crate::workspace_coverage_process_documents::{
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,
WorkspaceCoverageManagedProcessLaunchEntry,
WorkspaceCoverageManagedProcessLaunchEntry, WorkspaceCoverageProcessEvidenceHead,
WorkspaceCoverageProcessTerminationObservationEntry, WorkspaceCoverageProviderHeartbeatEntry,
};
use anyhow::Context;
@ -2646,6 +2647,7 @@ cultmesh_documents!(EpiphanyCultMeshDocuments {
EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry => EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_SCHEMA_VERSION,
EpiphanyCultMeshManagedServicePolicyEntry => EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_SCHEMA_VERSION,
WorkspaceCoverageManagedProcessLaunchEntry => WORKSPACE_COVERAGE_PROCESS_LAUNCH_SCHEMA_VERSION,
WorkspaceCoverageProcessEvidenceHead => WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION,
WorkspaceCoverageProviderHeartbeatEntry => WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_SCHEMA_VERSION,
WorkspaceCoverageProcessTerminationObservationEntry => WORKSPACE_COVERAGE_PROCESS_TERMINATION_SCHEMA_VERSION,
EpiphanyCultMeshIdunnDeploymentReceiptEntry => EPIPHANY_CULTMESH_IDUNN_DEPLOYMENT_RECEIPT_SCHEMA_VERSION,

View file

@ -30,7 +30,9 @@ pub const WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_LATEST_KEY: &str =
pub const WORKSPACE_COVERAGE_PROCESS_TERMINATION_TYPE: &str =
"epiphany.workspace_coverage.process_termination_observation";
pub const WORKSPACE_COVERAGE_PROCESS_TERMINATION_SCHEMA_VERSION: &str =
"epiphany.workspace_coverage.process_termination_observation.v0";
"epiphany.workspace_coverage.process_termination_observation.v1";
pub(crate) const WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION: &str =
"epiphany.workspace_coverage.process_evidence_head.v0";
const HOST_LAUNCH_PURPOSE: &str = "epiphany.workspace-coverage.managed-process-launch.v0";
const PROVIDER_HEARTBEAT_DOMAIN: &[u8] =
@ -170,9 +172,9 @@ pub struct WorkspaceCoverageProcessTerminationObservationEntry {
#[cultcache(key = 3)]
pub launch_envelope_digest: String,
#[cultcache(key = 4)]
pub heartbeat_id: String,
#[cultcache(key = 5)]
pub heartbeat_envelope_digest: String,
pub heartbeat_id: Option<String>,
#[cultcache(key = 5, default)]
pub heartbeat_envelope_digest: Option<String>,
#[cultcache(key = 6)]
pub policy_id: String,
#[cultcache(key = 7)]
@ -215,6 +217,26 @@ pub struct WorkspaceCoverageProcessTerminationObservationEntry {
pub signature_algorithm: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.workspace_coverage.process_evidence_head",
schema = "WorkspaceCoverageProcessEvidenceHead"
)]
pub(crate) struct WorkspaceCoverageProcessEvidenceHead {
#[cultcache(key = 0)]
schema_version: String,
#[cultcache(key = 1)]
launch_id: String,
#[cultcache(key = 2)]
generation: u64,
#[cultcache(key = 3)]
state: String,
#[cultcache(key = 4, default)]
heartbeat_id: Option<String>,
#[cultcache(key = 5, default)]
termination_id: Option<String>,
}
trait WorkspaceCoverageProcessObservationSource {
fn boot_identity(&self) -> Option<String>;
fn observe(&self, expected: &ProcessInstanceIdentity) -> ProcessInstanceObservation;
@ -292,8 +314,8 @@ struct TerminationStatement<'a> {
termination_id: &'a str,
launch_id: &'a str,
launch_envelope_digest: &'a str,
heartbeat_id: &'a str,
heartbeat_envelope_digest: &'a str,
heartbeat_id: &'a Option<String>,
heartbeat_envelope_digest: &'a Option<String>,
policy_id: &'a str,
policy_envelope_digest: &'a str,
runtime_id: &'a str,
@ -511,10 +533,29 @@ pub fn write_workspace_coverage_managed_process_launch(
}
bail!("workspace coverage launch identity collision");
}
let evidence_head_key = process_evidence_head_key(&entry.launch_id);
if node
.cache()
.get_envelope::<WorkspaceCoverageProcessEvidenceHead>(&evidence_head_key)?
.is_some()
{
bail!("workspace coverage launch evidence head identity collision");
}
let evidence_head = WorkspaceCoverageProcessEvidenceHead {
schema_version: WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION.into(),
launch_id: entry.launch_id.clone(),
generation: 1,
state: "launched".into(),
heartbeat_id: None,
termination_id: None,
};
let mut expected = vec![policy_envelope.clone()];
let mut replacements = vec![
policy_envelope,
node.cache().prepare_entry(&identity_key, &entry)?.0,
node.cache()
.prepare_entry(&evidence_head_key, &evidence_head)?
.0,
];
if let (Some(old_launch_id), Some(termination_id), Some(termination_digest)) = (
entry.replaces_launch_id.as_deref(),
@ -601,9 +642,34 @@ pub fn write_workspace_coverage_provider_heartbeat(
}
bail!("workspace coverage heartbeat identity collision");
}
let mut expected = vec![launch_envelope.clone()];
let evidence_head_key = process_evidence_head_key(&entry.launch_id);
let evidence_head_envelope = node
.cache()
.get_envelope::<WorkspaceCoverageProcessEvidenceHead>(&evidence_head_key)?
.ok_or_else(|| anyhow!("workspace coverage heartbeat process evidence head is absent"))?;
let evidence_head: WorkspaceCoverageProcessEvidenceHead =
rmp_serde::from_slice(&evidence_head_envelope.payload)?;
validate_process_evidence_head(&evidence_head, &entry.launch_id)?;
if evidence_head.state == "terminated" {
bail!("workspace coverage heartbeat cannot advance a terminated process");
}
let next_evidence_head = WorkspaceCoverageProcessEvidenceHead {
schema_version: WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION.into(),
launch_id: entry.launch_id.clone(),
generation: evidence_head
.generation
.checked_add(1)
.ok_or_else(|| anyhow!("workspace coverage process evidence generation exhausted"))?,
state: "heartbeat".into(),
heartbeat_id: Some(entry.heartbeat_id.clone()),
termination_id: None,
};
let mut expected = vec![launch_envelope.clone(), evidence_head_envelope.clone()];
let mut replacements = vec![
launch_envelope,
node.cache()
.prepare_entry(&evidence_head_key, &next_evidence_head)?
.0,
node.cache().prepare_entry(&identity_key, &entry)?.0,
];
let latest_key = heartbeat_latest_key(&entry.launch_id);
@ -858,13 +924,38 @@ fn write_workspace_coverage_process_termination_observation_with_source(
bail!("workspace coverage termination launch disagrees with current managed policy");
}
let heartbeat_latest_envelope = node
let evidence_head_key = process_evidence_head_key(launch_id);
let evidence_head_envelope = node
.cache()
.get_envelope::<WorkspaceCoverageProviderHeartbeatEntry>(&heartbeat_latest_key(launch_id))?
.ok_or_else(|| anyhow!("workspace coverage launch has no provider heartbeat"))?;
let heartbeat: WorkspaceCoverageProviderHeartbeatEntry =
rmp_serde::from_slice(&heartbeat_latest_envelope.payload)?;
authenticate_heartbeat_against_launch(&heartbeat, &launch, &envelope_digest(&launch_envelope))?;
.get_envelope::<WorkspaceCoverageProcessEvidenceHead>(&evidence_head_key)?
.ok_or_else(|| anyhow!("workspace coverage termination process evidence head is absent"))?;
let evidence_head: WorkspaceCoverageProcessEvidenceHead =
rmp_serde::from_slice(&evidence_head_envelope.payload)?;
validate_process_evidence_head(&evidence_head, launch_id)?;
if evidence_head.state == "terminated" {
bail!("workspace coverage process already has terminal evidence");
}
let heartbeat_evidence = if let Some(heartbeat_id) = evidence_head.heartbeat_id.as_deref() {
let envelope = node
.cache()
.get_envelope::<WorkspaceCoverageProviderHeartbeatEntry>(&heartbeat_latest_key(
launch_id,
))?
.ok_or_else(|| anyhow!("workspace coverage evidence head heartbeat is absent"))?;
let heartbeat: WorkspaceCoverageProviderHeartbeatEntry =
rmp_serde::from_slice(&envelope.payload)?;
authenticate_heartbeat_against_launch(
&heartbeat,
&launch,
&envelope_digest(&launch_envelope),
)?;
if heartbeat.heartbeat_id != heartbeat_id {
bail!("workspace coverage evidence head disagrees with latest heartbeat");
}
Some((envelope, heartbeat))
} else {
None
};
let observed_boot_identity = source
.boot_identity()
@ -897,8 +988,12 @@ fn write_workspace_coverage_process_termination_observation_with_source(
termination_id: launch.launch_id.clone(),
launch_id: launch.launch_id.clone(),
launch_envelope_digest: envelope_digest(&launch_envelope),
heartbeat_id: heartbeat.heartbeat_id.clone(),
heartbeat_envelope_digest: envelope_digest(&heartbeat_latest_envelope),
heartbeat_id: heartbeat_evidence
.as_ref()
.map(|(_, heartbeat)| heartbeat.heartbeat_id.clone()),
heartbeat_envelope_digest: heartbeat_evidence
.as_ref()
.map(|(envelope, _)| envelope_digest(envelope)),
policy_id: policy.policy_id.clone(),
policy_envelope_digest: envelope_digest(&policy_envelope),
runtime_id,
@ -928,20 +1023,38 @@ fn write_workspace_coverage_process_termination_observation_with_source(
validate_termination(&entry, host.entry())?;
let key = termination_key(launch_id);
let terminal_head = WorkspaceCoverageProcessEvidenceHead {
schema_version: WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION.into(),
launch_id: launch_id.into(),
generation: evidence_head
.generation
.checked_add(1)
.ok_or_else(|| anyhow!("workspace coverage process evidence generation exhausted"))?,
state: "terminated".into(),
heartbeat_id: evidence_head.heartbeat_id.clone(),
termination_id: Some(entry.termination_id.clone()),
};
let replacement = node.cache().prepare_entry(&key, &entry)?.0;
if !SingleFileMessagePackBackingStore::new(store_path).compare_and_swap_batch(
&[
policy_envelope.clone(),
launch_envelope.clone(),
heartbeat_latest_envelope.clone(),
],
vec![
policy_envelope,
launch_envelope,
heartbeat_latest_envelope,
replacement,
],
)? {
let mut expected = vec![
policy_envelope.clone(),
launch_envelope.clone(),
evidence_head_envelope,
];
let mut replacements = vec![
policy_envelope,
launch_envelope,
node.cache()
.prepare_entry(&evidence_head_key, &terminal_head)?
.0,
replacement,
];
if let Some((heartbeat_envelope, _)) = heartbeat_evidence {
expected.push(heartbeat_envelope.clone());
replacements.push(heartbeat_envelope);
}
if !SingleFileMessagePackBackingStore::new(store_path)
.compare_and_swap_batch(&expected, replacements)?
{
bail!("workspace coverage termination lost exact policy/launch/heartbeat CAS or collided");
}
Ok(entry)
@ -989,20 +1102,51 @@ pub fn authenticate_workspace_coverage_process_termination_observation(
let launch: WorkspaceCoverageManagedProcessLaunchEntry =
rmp_serde::from_slice(&launch_envelope.payload)?;
validate_launch(&launch, host)?;
let heartbeat_envelope = node
.cache()
.get_envelope::<WorkspaceCoverageProviderHeartbeatEntry>(&heartbeat_latest_key(launch_id))?
.ok_or_else(|| anyhow!("workspace coverage termination heartbeat evidence is absent"))?;
let heartbeat: WorkspaceCoverageProviderHeartbeatEntry =
rmp_serde::from_slice(&heartbeat_envelope.payload)?;
authenticate_heartbeat_against_launch(&heartbeat, &launch, &envelope_digest(&launch_envelope))?;
let evidence_head: WorkspaceCoverageProcessEvidenceHead = node
.get(&process_evidence_head_key(launch_id))?
.ok_or_else(|| anyhow!("workspace coverage termination process evidence head is absent"))?;
validate_process_evidence_head(&evidence_head, launch_id)?;
if evidence_head.state != "terminated"
|| evidence_head.termination_id.as_deref() != Some(entry.termination_id.as_str())
|| evidence_head.heartbeat_id != entry.heartbeat_id
{
bail!("workspace coverage termination disagrees with process evidence head");
}
let heartbeat_evidence = match (
entry.heartbeat_id.as_deref(),
entry.heartbeat_envelope_digest.as_deref(),
) {
(Some(heartbeat_id), Some(expected_digest)) => {
let envelope = node
.cache()
.get_envelope::<WorkspaceCoverageProviderHeartbeatEntry>(&heartbeat_latest_key(
launch_id,
))?
.ok_or_else(|| {
anyhow!("workspace coverage termination heartbeat evidence is absent")
})?;
let heartbeat: WorkspaceCoverageProviderHeartbeatEntry =
rmp_serde::from_slice(&envelope.payload)?;
authenticate_heartbeat_against_launch(
&heartbeat,
&launch,
&envelope_digest(&launch_envelope),
)?;
if heartbeat.heartbeat_id != heartbeat_id
|| envelope_digest(&envelope) != expected_digest
{
bail!("workspace coverage termination heartbeat evidence disagrees");
}
Some(heartbeat)
}
(None, None) => None,
_ => bail!("workspace coverage termination has partial heartbeat evidence"),
};
if entry.policy_id != policy.policy_id
|| entry.policy_envelope_digest != envelope_digest(&policy_envelope)
|| launch.policy_id != policy.policy_id
|| launch.policy_envelope_digest != entry.policy_envelope_digest
|| entry.launch_envelope_digest != envelope_digest(&launch_envelope)
|| entry.heartbeat_id != heartbeat.heartbeat_id
|| entry.heartbeat_envelope_digest != envelope_digest(&heartbeat_envelope)
|| entry.host_identity_id != launch.host_identity_id
|| entry.host_identity_record_digest != launch.host_identity_record_digest
|| entry.expected_boot_identity != launch.boot_identity
@ -1012,6 +1156,13 @@ pub fn authenticate_workspace_coverage_process_termination_observation(
{
bail!("workspace coverage termination evidence chain disagrees with its exact sources");
}
if heartbeat_evidence
.as_ref()
.map(|heartbeat| heartbeat.heartbeat_id.as_str())
!= entry.heartbeat_id.as_deref()
{
bail!("workspace coverage termination heartbeat identity disagrees");
}
Ok(entry)
}
@ -1246,12 +1397,21 @@ fn validate_termination(
bail!("workspace coverage termination violates its reserved authority");
}
uuid::Uuid::parse_str(&entry.launch_id).context("termination launch id must be UUID")?;
uuid::Uuid::parse_str(&entry.heartbeat_id).context("termination heartbeat id must be UUID")?;
match (
entry.heartbeat_id.as_deref(),
entry.heartbeat_envelope_digest.as_deref(),
) {
(Some(id), Some(digest)) => {
uuid::Uuid::parse_str(id).context("termination heartbeat id must be UUID")?;
validate_digest("heartbeat", digest)?;
}
(None, None) => {}
_ => bail!("termination heartbeat evidence is partial"),
}
DateTime::parse_from_rfc3339(&entry.observed_at_utc)
.context("termination observation time must be RFC3339")?;
for (label, digest) in [
("launch", &entry.launch_envelope_digest),
("heartbeat", &entry.heartbeat_envelope_digest),
("policy", &entry.policy_envelope_digest),
("host identity record", &entry.host_identity_record_digest),
] {
@ -1384,6 +1544,31 @@ fn heartbeat_latest_key(launch_id: &str) -> String {
fn termination_key(launch_id: &str) -> String {
format!("epiphany-local/workspace-coverage/process-termination/{launch_id}")
}
fn process_evidence_head_key(launch_id: &str) -> String {
format!("epiphany-local/workspace-coverage/process-evidence-head/{launch_id}")
}
fn validate_process_evidence_head(
head: &WorkspaceCoverageProcessEvidenceHead,
launch_id: &str,
) -> Result<()> {
if head.schema_version != WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION
|| head.launch_id != launch_id
|| head.generation == 0
{
bail!("workspace coverage process evidence head is invalid");
}
match head.state.as_str() {
"launched"
if head.generation == 1
&& head.heartbeat_id.is_none()
&& head.termination_id.is_none() => {}
"heartbeat" if head.heartbeat_id.is_some() && head.termination_id.is_none() => {}
"terminated" if head.termination_id.is_some() => {}
_ => bail!("workspace coverage process evidence head state is incoherent"),
}
Ok(())
}
fn managed_policy_key() -> String {
format!(
"epiphany-local/managed-service-policy/{}",
@ -1729,7 +1914,7 @@ mod tests {
)
.expect_err("termination key is immutable")
.to_string()
.contains("collided")
.contains("already has terminal evidence")
);
let mut advanced_policy = policy()?;
advanced_policy.updated_at_utc = "2026-07-16T23:59:59Z".to_string();
@ -1816,6 +2001,60 @@ mod tests {
Ok(())
}
#[test]
fn termination_before_first_heartbeat_seals_the_process_evidence_head() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("verse.ccmp");
let host = enroll_host_identity_at(&temp.path().join("host.ccmp"))?;
let policy = policy()?;
let mut node = open_epiphany_cultmesh_node(&store, "local")?;
node.put(managed_policy_key(), &policy)?;
let policy_envelope = node
.cache()
.get_envelope::<EpiphanyCultMeshManagedServicePolicyEntry>(&managed_policy_key())?
.context("test policy envelope absent")?;
let provider = provider_key();
let launch = launch(&policy, envelope_digest(&policy_envelope), &host, &provider)?;
write_workspace_coverage_managed_process_launch(
&store,
"local",
launch.clone(),
host.entry(),
)?;
let source = FakeObservation {
boot: Some(launch.boot_identity.clone()),
process: ProcessInstanceObservation::Missing,
};
let termination = write_workspace_coverage_process_termination_observation_with_source(
&store,
"local",
&launch.launch_id,
&host,
&source,
)?;
assert!(termination.heartbeat_id.is_none());
assert!(termination.heartbeat_envelope_digest.is_none());
authenticate_workspace_coverage_process_termination_observation(
&store,
"local",
&launch.launch_id,
host.entry(),
)?;
let launch_envelope = open_epiphany_cultmesh_node(&store, "local")?
.cache()
.get_envelope::<WorkspaceCoverageManagedProcessLaunchEntry>(&launch_key(
&launch.launch_id,
))?
.context("test launch envelope absent")?;
let pulse = heartbeat(&launch, envelope_digest(&launch_envelope), &provider, 1)?;
assert!(
write_workspace_coverage_provider_heartbeat(&store, "local", pulse).is_err(),
"a late heartbeat must not resurrect a terminated launch"
);
Ok(())
}
#[test]
fn body_recovery_is_one_exact_evidence_fenced_transaction() -> Result<()> {
let temp = tempfile::tempdir()?;

View file

@ -1905,3 +1905,23 @@ Rebuild termination so signed launch plus enrolled host/boot/process observation
is sufficient, with heartbeat optional additional evidence. Then run a
GUID-scoped live initial launch -> death -> replacement -> Body recovery smoke.
Reboot/logon proof remains permission-bound.
## Pre-readiness workspace coverage death is recoverable (2026-07-16)
The heartbeat prerequisite was rebuilt, not relaxed with nullable fields alone.
Each specialized launch owns one typed process-evidence head. Launch initializes
it; every signed heartbeat advances it by exact CAS; termination advances and
seals it by exact CAS. Termination v1 uses signed launch plus enrolled
host/boot/process observation as sufficient death proof and includes heartbeat
id/digest only when the head names a current signed heartbeat. Heartbeat after
termination refuses, so publication and death cannot win on separate keys.
Focused proof now includes death before heartbeat sequence one, successful
heartbeat-free termination authentication, and refusal of a late heartbeat.
All six process-document/recovery tests, fourteen supervisor tests, fifteen
projector tests plus one ignored live test, and all-target check pass.
Next: create/run a GUID-scoped live smoke through real packaged supervisor and
projector binaries, local Qdrant/Ollama, exact child kill, causal replacement,
epoch+1 recovery, and restart-time receipt authentication. Restore the prior
Qdrant container state afterward. Reboot/logon remains permission-bound.

Binary file not shown.

View file

@ -730,4 +730,5 @@ current_status:
- The live workspace-coverage process path now uses only specialized authority. Supervisor generates a per-launch provider key, spawns with reserved stdin piped and all other child stdin null, captures exact process identity, sends one fixed-size launch-id/seed frame, host-signs and CAS-persists the specialized launch, and kills/waits on any bootstrap or persistence failure. The child requires exact frame plus EOF, authenticates the enrolled host, current policy, boot, its PID creation token/path, launch digest, and provider public key before constructing the service or publishing its signed per-launch heartbeat. Generic lifecycle and generic daemon-heartbeat writers reject the reserved coverage identities; their old authenticator is deleted. Body claim/attempt v1 binds `managed_process_launch_id`; v0 lifecycle semantics cannot deserialize as the new owner. This Windows installation is explicitly enrolled at the default DPAPI CurrentUser host store and status re-open proved identity `17f041421045aa66d4c8ab0488f462dbb5ea1a7d8507dbf75216f5ec368dbb7a` without exposing private state.
- Idunn now has one immutable, host-signed process-termination observation per reserved coverage launch, with no latest/global mirror. Its writer reloads and exact-CAS-preserves the current specialized policy, exact launch, and exact per-launch latest signed heartbeat; it immediately observes the enrolled host's proven boot and exact PID incarnation. A distinct proven boot yields `boot_superseded`; same-boot proof permits only `exact_exited`, `process_missing`, or a complete different `process_replaced` identity. ExactAlive, Inaccessible, Indeterminate, unknown boot, host mismatch, identity collision, and moved source envelopes refuse publication. The injectable observation seam is private to module tests. Recovery-facing authentication reconstructs the entire persisted policy/launch/heartbeat chain rather than trusting the signed termination blob alone. Four hostile document/termination tests and all-target check pass.
- Abandoned workspace-coverage authority now has a complete typed transfer. Recovery derives the Body store from the runtime route, reopens the authenticated Body basis, joins obligation/plan/claim to that basis, authenticates current policy plus exact old launch and immutable termination, requires a signed causal termination-to-replacement edge and the replacement's latest ready heartbeat, then performs one Body CAS over Body authority, plan, old claim/attempt, immutable recovery receipt, failed claim history, and epoch+1 successor. The receipt stores exact termination, launch, and heartbeat envelope digests and can reconstruct both CultMesh evidence and current Body authority after restart. Claim/attempt schema and evidence pairs cannot split; impossible completion-before-start timelines refuse. Idunn's reserved reconciler no longer consults generic lifecycle PID receipts for this service. It persists/reuses termination before spawn, uses the specialized bootstrap launch, waits for signed readiness, and then invokes the recovery CAS. One termination admits one replacement slot; concurrent losers are killed when launch persistence loses CAS.
next_action: Close the remaining pre-readiness death seam: a reserved child that dies after launch persistence but before its first heartbeat currently cannot produce termination evidence because termination v0 requires a heartbeat. Rebuild termination evidence so exact launch plus native host/boot/process proof is sufficient and latest heartbeat is optional additional evidence, while preserving immutable causal replacement and recovery receipts. Then run an isolated live initial-launch/death/replacement/Body-recovery smoke. Reboot/logon proof remains permission-bound.
- Workspace-coverage launch, heartbeat, and termination now serialize through one typed per-launch process-evidence head. Launch initializes generation 1 as `launched`; each signed heartbeat exact-CAS advances it to `heartbeat`; host-signed termination exact-CAS seals it as `terminated`. Termination v1 accepts exact launch plus enrolled host/boot/process proof before heartbeat sequence one and records heartbeat id/digest only when the head names current signed heartbeat evidence. A heartbeat cannot advance a terminated head. This removes the pre-readiness death strand without allowing heartbeat/termination split authority.
next_action: Build and run a GUID-scoped live workspace-coverage recovery smoke through the real packaged supervisor/projector binaries: initial specialized launch, acquired Body claim, exact child death, immutable termination, unique causal replacement, signed readiness, epoch+1 Body recovery, and restart-time recovery-receipt authentication. Use local Qdrant/Ollama only inside isolated fixture state and restore Qdrant container state afterward. Reboot/logon proof remains permission-bound.

View file

@ -157,3 +157,12 @@ signed launch persistence but before heartbeat sequence one can strand the
chain. Make heartbeat optional additional termination evidence, then run an
isolated live death/replacement/recovery smoke. Reboot still needs live operator
approval.
## Pre-readiness death seam closed (2026-07-16)
Launch, heartbeat, and termination now contend on one typed per-launch process
evidence head. Launch creates generation 1; heartbeat advances it; termination
seals it. Termination v1 can carry no heartbeat when the exact signed launch and
native host/boot/process proof already establish death. A late heartbeat cannot
resurrect the launch. Six focused process tests, supervisor/projector suites,
and all-target check pass. Next: real GUID-scoped recovery smoke.