Bind process authority to native incarnation

This commit is contained in:
Meta Vens 2026-07-16 00:52:21 +01:00
commit fe3ceac8be
7 changed files with 442 additions and 66 deletions

View file

@ -47,7 +47,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread"] }
wiremock = "0.6"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Threading"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"

View file

@ -800,8 +800,13 @@ pub use persona_turn::persona_projected_surface_is_clean;
pub use persona_turn::render_persona_semantic_memory_recall;
pub use persona_turn::semantic_memory_recall_from_heartbeat_action;
pub use process_observation::EpiphanyProcessObservation;
pub use process_observation::ProcessInstanceIdentity;
pub use process_observation::ProcessInstanceObservation;
pub use process_observation::capture_process_instance;
pub use process_observation::native_boot_identity;
pub use process_observation::native_process_executable_path;
pub use process_observation::observe_native_process;
pub use process_observation::observe_process_instance;
pub use promotion::EpiphanyPromotionDecision;
pub use promotion::EpiphanyPromotionInput;
pub use promotion::EpiphanyStateReplacementValidationInput;

View file

@ -1,95 +1,440 @@
use anyhow::Result;
use anyhow::{Context, Result, bail};
use std::path::PathBuf;
/// The immutable operating-system identity of one process incarnation.
///
/// A PID alone is an address which the OS is free to reuse. `creation_token`
/// is the native, monotonic-within-boot incarnation token (Windows FILETIME or
/// Linux `/proc/<pid>/stat` starttime), and the executable path prevents a
/// different image from inheriting authority through an erroneous launch.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProcessInstanceIdentity {
pub process_id: u32,
pub creation_token: u64,
pub created_at_rfc3339: Option<String>,
pub executable_path: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProcessInstanceObservation {
ExactAlive,
ExactExited { exit_code: Option<u32> },
Replaced { observed: ProcessInstanceIdentity },
Missing,
Inaccessible,
Indeterminate { reason: String },
}
/// Compatibility projection for old operator displays. It is deliberately
/// incapable of calling access denial or PID absence "dead".
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EpiphanyProcessObservation {
Alive,
Dead,
Missing,
}
#[cfg(windows)]
pub fn native_process_executable_path(process_id: u32) -> Result<Option<PathBuf>> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW,
};
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process_id);
if handle.is_null() {
return Ok(None);
}
let mut buffer = vec![0_u16; 32_768];
let mut length = buffer.len() as u32;
let read = QueryFullProcessImageNameW(handle, 0, buffer.as_mut_ptr(), &mut length);
CloseHandle(handle);
if read == 0 {
return Ok(None);
}
buffer.truncate(length as usize);
Ok(Some(PathBuf::from(String::from_utf16(&buffer)?)))
}
}
#[cfg(unix)]
pub fn native_process_executable_path(process_id: u32) -> Result<Option<PathBuf>> {
let path = PathBuf::from(format!("/proc/{process_id}/exe"));
match std::fs::read_link(path) {
Ok(path) => Ok(Some(path)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error.into()),
}
}
impl EpiphanyProcessObservation {
pub fn label(self) -> &'static str {
match self {
Self::Alive => "alive",
Self::Dead => "dead",
Self::Missing => "missing",
}
}
}
#[cfg(windows)]
pub fn capture_process_instance(process_id: u32) -> Result<ProcessInstanceIdentity> {
if process_id == 0 {
bail!("process-instance identity requires a nonzero PID");
}
platform::capture(process_id)
}
pub fn observe_process_instance(expected: &ProcessInstanceIdentity) -> ProcessInstanceObservation {
if expected.process_id == 0 || expected.creation_token == 0 {
return ProcessInstanceObservation::Indeterminate {
reason: "invalid expected process-instance identity".to_string(),
};
}
platform::observe(expected)
}
/// Returns a boot-incarnation token only when the platform can prove it.
pub fn native_boot_identity() -> Option<String> {
platform::boot_identity()
}
pub fn native_process_executable_path(process_id: u32) -> Result<Option<PathBuf>> {
match capture_process_instance(process_id) {
Ok(identity) => Ok(Some(identity.executable_path)),
Err(error) if platform::is_missing_error(&error) => Ok(None),
Err(error) => Err(error),
}
}
pub fn observe_native_process(process_id: u32) -> Result<EpiphanyProcessObservation> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
// This legacy API has no expected incarnation and therefore cannot prove
// termination. It remains useful only as a conservative liveness display.
match capture_process_instance(process_id) {
Ok(_) => Ok(EpiphanyProcessObservation::Alive),
Err(error) if platform::is_missing_error(&error) => Ok(EpiphanyProcessObservation::Missing),
Err(error) => Err(error),
}
}
#[cfg(windows)]
mod platform {
use super::*;
use chrono::{DateTime, SecondsFormat, Utc};
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Foundation::{
CloseHandle, ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, ERROR_NO_MORE_FILES, FILETIME,
GetLastError, INVALID_HANDLE_VALUE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT,
};
const STILL_ACTIVE: u32 = 259;
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process_id);
if handle.is_null() {
return Ok(EpiphanyProcessObservation::Dead);
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
};
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
QueryFullProcessImageNameW, WaitForSingleObject,
};
const WINDOWS_TO_UNIX_100NS: u64 = 116_444_736_000_000_000;
const SYNCHRONIZE_ACCESS: u32 = 0x0010_0000;
struct OwnedHandle(windows_sys::Win32::Foundation::HANDLE);
impl Drop for OwnedHandle {
fn drop(&mut self) {
unsafe { CloseHandle(self.0) };
}
let mut exit_code = 0_u32;
let read = GetExitCodeProcess(handle, &mut exit_code);
CloseHandle(handle);
if read == 0 {
anyhow::bail!("failed to inspect managed service process {process_id}");
}
fn filetime_token(value: FILETIME) -> u64 {
((value.dwHighDateTime as u64) << 32) | value.dwLowDateTime as u64
}
fn rfc3339_from_filetime(token: u64) -> Result<String> {
let unix_100ns = token
.checked_sub(WINDOWS_TO_UNIX_100NS)
.context("process creation FILETIME predates Unix epoch")?;
let seconds = (unix_100ns / 10_000_000) as i64;
let nanos = ((unix_100ns % 10_000_000) * 100) as u32;
let value = DateTime::<Utc>::from_timestamp(seconds, nanos)
.context("process creation FILETIME is out of range")?;
Ok(value.to_rfc3339_opts(SecondsFormat::Nanos, true))
}
fn pid_in_snapshot(process_id: u32) -> Result<bool> {
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snapshot == INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error()).context("create process snapshot");
}
let snapshot = OwnedHandle(snapshot);
let mut entry: PROCESSENTRY32W = std::mem::zeroed();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
if Process32FirstW(snapshot.0, &mut entry) == 0 {
return Err(std::io::Error::last_os_error()).context("read process snapshot");
}
loop {
if entry.th32ProcessID == process_id {
return Ok(true);
}
if Process32NextW(snapshot.0, &mut entry) == 0 {
let error = GetLastError();
if error == ERROR_NO_MORE_FILES {
return Ok(false);
}
return Err(std::io::Error::from_raw_os_error(error as i32))
.context("advance process snapshot");
}
}
}
Ok(if exit_code == STILL_ACTIVE {
EpiphanyProcessObservation::Alive
} else {
EpiphanyProcessObservation::Dead
}
fn open(process_id: u32) -> Result<OwnedHandle> {
unsafe {
let handle = OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE_ACCESS,
0,
process_id,
);
if handle.is_null() {
return Err(std::io::Error::last_os_error())
.with_context(|| format!("open process {process_id}"));
}
Ok(OwnedHandle(handle))
}
}
fn identity_from_handle(
process_id: u32,
handle: &OwnedHandle,
) -> Result<ProcessInstanceIdentity> {
unsafe {
let mut creation: FILETIME = std::mem::zeroed();
let mut exit: FILETIME = std::mem::zeroed();
let mut kernel: FILETIME = std::mem::zeroed();
let mut user: FILETIME = std::mem::zeroed();
if GetProcessTimes(handle.0, &mut creation, &mut exit, &mut kernel, &mut user) == 0 {
return Err(std::io::Error::last_os_error()).context("query process creation time");
}
let creation_token = filetime_token(creation);
if creation_token == 0 {
bail!("operating system returned a zero process creation token");
}
let mut buffer = vec![0_u16; 32_768];
let mut length = buffer.len() as u32;
if QueryFullProcessImageNameW(handle.0, 0, buffer.as_mut_ptr(), &mut length) == 0 {
return Err(std::io::Error::last_os_error())
.context("query process executable path");
}
buffer.truncate(length as usize);
let raw = PathBuf::from(std::ffi::OsString::from_wide(&buffer));
let executable_path = std::fs::canonicalize(&raw)
.with_context(|| format!("canonicalize process executable {}", raw.display()))?;
Ok(ProcessInstanceIdentity {
process_id,
creation_token,
created_at_rfc3339: Some(rfc3339_from_filetime(creation_token)?),
executable_path,
})
}
}
pub(super) fn capture(process_id: u32) -> Result<ProcessInstanceIdentity> {
identity_from_handle(process_id, &open(process_id)?)
}
pub(super) fn observe(expected: &ProcessInstanceIdentity) -> ProcessInstanceObservation {
let present = match pid_in_snapshot(expected.process_id) {
Ok(value) => value,
Err(error) => {
return ProcessInstanceObservation::Indeterminate {
reason: error.to_string(),
};
}
};
if !present {
return ProcessInstanceObservation::Missing;
}
let handle = match open(expected.process_id) {
Ok(handle) => handle,
Err(error) => {
return if error
.downcast_ref::<std::io::Error>()
.is_some_and(|e| e.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32))
{
ProcessInstanceObservation::Inaccessible
} else {
ProcessInstanceObservation::Indeterminate {
reason: error.to_string(),
}
};
}
};
let observed = match identity_from_handle(expected.process_id, &handle) {
Ok(observed) if observed != *expected => {
return ProcessInstanceObservation::Replaced { observed };
}
Ok(observed) => observed,
Err(error) => {
return ProcessInstanceObservation::Indeterminate {
reason: error.to_string(),
};
}
};
debug_assert_eq!(observed, *expected);
unsafe {
match WaitForSingleObject(handle.0, 0) {
WAIT_OBJECT_0 => {
let mut code = 0;
let exit_code = (GetExitCodeProcess(handle.0, &mut code) != 0).then_some(code);
ProcessInstanceObservation::ExactExited { exit_code }
}
WAIT_TIMEOUT => ProcessInstanceObservation::ExactAlive,
WAIT_FAILED => ProcessInstanceObservation::Indeterminate {
reason: format!(
"wait for process failed with Windows error {}",
GetLastError()
),
},
other => ProcessInstanceObservation::Indeterminate {
reason: format!("unexpected process wait result {other}"),
},
}
}
}
pub(super) fn boot_identity() -> Option<String> {
capture(4).ok().map(|identity| {
format!(
"windows-system-process-filetime:{}",
identity.creation_token
)
})
}
pub(super) fn is_missing_error(error: &anyhow::Error) -> bool {
error.chain().any(|source| {
source.downcast_ref::<std::io::Error>().is_some_and(|e| {
matches!(e.raw_os_error(), Some(code) if code == ERROR_INVALID_PARAMETER as i32)
})
})
}
}
#[cfg(unix)]
pub fn observe_native_process(process_id: u32) -> Result<EpiphanyProcessObservation> {
let result = unsafe { libc::kill(process_id as i32, 0) };
if result == 0 {
Ok(EpiphanyProcessObservation::Alive)
} else {
let error = std::io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ESRCH) {
Ok(EpiphanyProcessObservation::Dead)
} else if error.raw_os_error() == Some(libc::EPERM) {
Ok(EpiphanyProcessObservation::Alive)
} else {
Err(error.into())
#[cfg(target_os = "linux")]
mod platform {
use super::*;
fn stat(process_id: u32) -> Result<(u64, char)> {
let raw = std::fs::read_to_string(format!("/proc/{process_id}/stat"))
.with_context(|| format!("read /proc/{process_id}/stat"))?;
let close = raw
.rfind(") ")
.context("malformed proc stat command field")?;
let fields: Vec<&str> = raw[close + 2..].split_whitespace().collect();
let state = fields
.first()
.and_then(|value| value.chars().next())
.context("missing proc state")?;
// starttime is field 22; `fields[0]` is original field 3.
let starttime = fields.get(19).context("missing proc starttime")?.parse()?;
Ok((starttime, state))
}
pub(super) fn capture(process_id: u32) -> Result<ProcessInstanceIdentity> {
let (creation_token, _) = stat(process_id)?;
if creation_token == 0 {
bail!("operating system returned a zero process creation token");
}
let executable_path = std::fs::canonicalize(format!("/proc/{process_id}/exe"))
.with_context(|| format!("read process {process_id} executable"))?;
Ok(ProcessInstanceIdentity {
process_id,
creation_token,
created_at_rfc3339: None,
executable_path,
})
}
pub(super) fn observe(expected: &ProcessInstanceIdentity) -> ProcessInstanceObservation {
let (creation_token, state) = match stat(expected.process_id) {
Ok(observed) => observed,
Err(error) if is_missing_error(&error) => return ProcessInstanceObservation::Missing,
Err(error) => {
return ProcessInstanceObservation::Indeterminate {
reason: error.to_string(),
};
}
};
if creation_token == expected.creation_token && matches!(state, 'Z' | 'X') {
return ProcessInstanceObservation::ExactExited { exit_code: None };
}
match capture(expected.process_id) {
Ok(observed) if observed != *expected => {
ProcessInstanceObservation::Replaced { observed }
}
Ok(_) => ProcessInstanceObservation::ExactAlive,
Err(error) => ProcessInstanceObservation::Indeterminate {
reason: error.to_string(),
},
}
}
pub(super) fn boot_identity() -> Option<String> {
std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
pub(super) fn is_missing_error(error: &anyhow::Error) -> bool {
error.chain().any(|source| {
source
.downcast_ref::<std::io::Error>()
.is_some_and(|e| e.kind() == std::io::ErrorKind::NotFound)
})
}
}
#[cfg(not(any(windows, target_os = "linux")))]
mod platform {
use super::*;
pub(super) fn capture(_: u32) -> Result<ProcessInstanceIdentity> {
bail!("native process-instance observation is unsupported on this platform")
}
pub(super) fn observe(_: &ProcessInstanceIdentity) -> ProcessInstanceObservation {
ProcessInstanceObservation::Indeterminate {
reason: "native process-instance observation is unsupported on this platform"
.to_string(),
}
}
pub(super) fn boot_identity() -> Option<String> {
None
}
pub(super) fn is_missing_error(_: &anyhow::Error) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_zero_pid() {
assert!(capture_process_instance(0).is_err());
}
#[test]
fn live_current_process_has_stable_exact_identity() {
let identity = capture_process_instance(std::process::id()).unwrap();
assert_ne!(identity.creation_token, 0);
assert!(identity.executable_path.is_absolute());
assert_eq!(
observe_process_instance(&identity),
ProcessInstanceObservation::ExactAlive
);
}
#[test]
fn forged_incarnation_is_replaced_not_alive() {
let mut identity = capture_process_instance(std::process::id()).unwrap();
identity.creation_token = identity.creation_token.saturating_add(1);
assert!(matches!(
observe_process_instance(&identity),
ProcessInstanceObservation::Replaced { .. }
));
}
#[test]
fn child_exit_is_observed_for_the_exact_instance() {
let mut child = if cfg!(windows) {
std::process::Command::new("powershell.exe")
.args([
"-NoProfile",
"-NonInteractive",
"-Command",
"Start-Sleep -Milliseconds 500; exit 17",
])
.spawn()
.unwrap()
} else {
std::process::Command::new("sh")
.args(["-c", "exit 17"])
.spawn()
.unwrap()
};
let identity = capture_process_instance(child.id()).unwrap();
let _ = child.wait().unwrap();
let observed = observe_process_instance(&identity);
assert!(matches!(
observed,
ProcessInstanceObservation::ExactExited {
exit_code: Some(17)
} | ProcessInstanceObservation::Missing
));
}
}

View file

@ -1796,6 +1796,17 @@ immutable termination observation bound to the exact old launch/PID/executable/
policy/heartbeat/provider. Only that proof plus a current replacement launch may
authorize one Body-store recovery CAS. Do not add timeout recovery.
The native process-instance probe foundation is now implemented and locally
proved. Windows uses PID + process-creation FILETIME + canonical executable,
compares exact identity through a held query/synchronize handle before
alive/exited classification, and separates Toolhelp absence, access denial, and
indeterminate query failure. Linux uses boot id + proc starttime + executable
and recognizes exact zombie exit; other Unix targets refuse to claim Linux
evidence. The old PID-only projection has no `Dead` state. This substrate does
not itself authorize recovery. Next build the enrolled OS-host identity and
specialized reserved launch/heartbeat and termination documents; only then wire
the Body recovery CAS.
The deployment next action is unchanged and permission-bound. Do not reboot
without exact live operator approval. With that approval, run the real
reboot/logon recovery proof already specified above.

Binary file not shown.

View file

@ -725,4 +725,4 @@ current_status:
- The `epiphany.surface.*` Hello fiction is cut. Commit `65445623` remains the completed local Persona projection repair: `epiphany.persona_surface.v0` is a strict, content-free MVP-status projection over typed Persona artifact references, and its schemas remain useful discoverable vocabulary for local projection validation. It is not a CultNet runtime capability. All seventeen unbacked `epiphany.surface.*` mutation contracts and their dead runtime constants are removed; no surface type may appear in Hello until an owning provider can prove `provider publish -> typed CultMesh document -> Snapshot -> schema validation -> Eve lowering`, with a real typed dispatcher and receipt path for every advertised action. Runtime Hello and runtime status derive the current executable contract list. They ignore stale `supported_document_types` persisted in runtime identity, so the live canonical store's historical field cannot decide present wire authority or status. A canonical real hello-frame proved zero `epiphany.surface.*` capabilities. The adjacent generic central Eve substitution is also cut: `epiphany-cluster-daemon` owns liveness only; the central provider publisher and heartbeat call are deleted. Live Odin, Eve, and daemon-tool directories ignore provenance-free v0 rows, and explicit bootstrap retires stale rows of exactly those three families. Topology `eve_surface_id` is stable address/routing metadata only, not availability or composition proof. Provider discovery may return only after an owning provider emits a provenance-bearing typed contract; advertised actions additionally require a real dispatcher and receipt path. This does not displace the reboot/logon deployment proof as the current next action.
- Historical Windows SCM material in this map is rejected-path evidence, not live authority. Arbitrary console `sc.exe` installation, elevated service execution runbooks/audits, cluster-service install/start/stop/control, `swarm-online-runbook`, service-manager action queues, and their wrapper routes no longer describe deployable physiology or actionable next work. Their receipts may remain sealed historical evidence, but they cannot decide liveness, readiness, recovery, or operator action. The console binaries never implemented the Windows service-control dispatcher and therefore could not honestly own SCM service state.
- Static cluster topology owns declared faculty routing only, not attendance. Explicit bootstrap may persist seven declared faculty routes, desired private-Verse addresses, and daemon/Eve targets. Only persisted provider-authored daemon status creates an observed daemon row. The synthetic `unknown_daemon_status` path is deleted; fresh bootstrap therefore means seven declared targets and zero observed daemons. Topology, liveness, restart-policy, swarm overview/triage, compact swarm output, prompt context, and wrapper summaries distinguish `declared*` counts/routes from `observedDaemonCount`; a heartbeat does not become an agent count. Restart-policy rows may carry `daemonStatus=unobserved` solely for Idunn desired-state reconciliation. Configured ids, routes, topology row counts, and policy targets are forbidden from proving deployed daemons, active private Verses, agent presence, Eve availability, or readiness.
next_action: With explicit operator permission for the host-wide action, prove this Task Scheduler boundary across a real reboot and user logon: both tasks must return to Running, the gamecult-ops foreground tunnel must restore `127.0.0.1:16333`/`:16334`, the rebuilt Idunn task must establish a fresh post-boot reconciler -> exactly-one-projector chain rather than inherit a pre-boot child, that projector must publish a fresh launch-correlated provider heartbeat, and packaged Mind and Modeling queries must both achieve semantic ranking using explicit `EPIPHANY_QDRANT_URL` and `EPIPHANY_OLLAMA_BASE_URL`. Record that provider correlation is sight and query admission alone owns readiness. This proves after-login recovery only; the isolated abandoned-claim recovery proof is complete but does not establish global deployability. Do not reboot without exact live operator approval, resume SCM hardening, or resurrect cluster-service/elevated-runbook wrappers.
next_action: Strengthen native process observation with host incarnation, boot incarnation, and exact process-instance identity; Windows OpenProcess/access failure must remain Unknown rather than Dead, and PID reuse must be distinguishable. Bind those identities into reserved managed-service launch receipts, then let Idunn publish one immutable termination observation for the exact prior workspace-coverage projector launch before spawning a replacement. Only that termination proof plus the current replacement launch and correlated ready heartbeat may authorize abandoned-claim recovery. Do not use timeout, heartbeat staleness, a newer launch, Qdrant state, or PID absence without instance identity as death evidence. The separate real reboot/logon proof remains permission-bound and must not run without exact live operator approval.

View file

@ -50,3 +50,18 @@ stopped. The hostile proof fixes passed; a real ignored smoke started Qdrant,
projected through Ollama, exact-scroll-verified payloads and vectors, classified
the result Current, deleted its isolated collection, verified no coverage
collection remained, and stopped Qdrant again.
## Exact native process identity (2026-07-16)
The process-observation foundation now identifies a process incarnation by PID,
native creation token, and canonical executable path. Windows compares identity
through a held process handle before classifying it alive or exited; Toolhelp
absence is distinct from access denial and query failure. Linux uses boot id,
proc starttime, executable identity, and zombie state. Unsupported Unix targets
refuse to pretend they have Linux `/proc` evidence.
The legacy PID-only projection is display-only and can emit only `Alive` or
`Missing`; it cannot mint death evidence. Tests prove current-instance life,
PID-incarnation replacement, and exact child exit. Next add enrolled OS-host
identity, then reserved launch/heartbeat and immutable termination documents
before any recovery CAS.