Own resident readiness through role singletons
This commit is contained in:
parent
e7e05ece1c
commit
7da02c6771
5 changed files with 199 additions and 30 deletions
|
|
@ -24,13 +24,17 @@ use std::os::windows::ffi::OsStrExt;
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GetLastError, HANDLE};
|
||||
use windows_sys::Win32::Foundation::{
|
||||
CloseHandle, ERROR_ALREADY_EXISTS, GetLastError, HANDLE, WAIT_ABANDONED, WAIT_OBJECT_0,
|
||||
WAIT_TIMEOUT,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use windows_sys::Win32::System::Threading::{CreateMutexW, ReleaseMutex};
|
||||
use windows_sys::Win32::System::Threading::{CreateMutexW, ReleaseMutex, WaitForSingleObject};
|
||||
|
||||
pub const RESIDENT_PROVIDER_READINESS_SCHEMA_VERSION: &str =
|
||||
"epiphany.resident_cognition.provider_readiness.v0";
|
||||
const PROVIDER_READINESS_KEY: &str = "provider-readiness";
|
||||
const MAX_PROVIDER_CLOCK_SKEW_MILLIS: u64 = 1_000;
|
||||
|
||||
pub struct ResidentProcessSingletonGuard {
|
||||
#[cfg(windows)]
|
||||
|
|
@ -99,8 +103,10 @@ pub fn acquire_resident_process_singleton(
|
|||
#[cfg(unix)]
|
||||
{
|
||||
let path = resident_process_singleton_lock_path(role, &store)?;
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
let file = File::options()
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path)?;
|
||||
|
|
@ -118,6 +124,70 @@ pub fn acquire_resident_process_singleton(
|
|||
))
|
||||
}
|
||||
|
||||
fn resident_process_singleton_is_owned(role: &str, store: &Path) -> Result<bool> {
|
||||
if !matches!(role, "heartbeat" | "resident-self") {
|
||||
bail!("unsupported resident singleton role: {role}");
|
||||
}
|
||||
let store = canonical_or_absolute(store);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let identity = format!(
|
||||
"{:x}",
|
||||
Sha256::digest(format!("{role}|{}", store.to_string_lossy().to_lowercase()).as_bytes())
|
||||
);
|
||||
let name = OsStr::new(&format!("Global\\EpiphanyResident-{role}-{identity}"))
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect::<Vec<_>>();
|
||||
let handle = unsafe { CreateMutexW(std::ptr::null_mut(), 0, name.as_ptr()) };
|
||||
if handle.is_null() {
|
||||
bail!("resident {role} singleton mutex probe failed: {}", unsafe {
|
||||
GetLastError()
|
||||
});
|
||||
}
|
||||
let observation = unsafe { WaitForSingleObject(handle, 0) };
|
||||
let owned = match observation {
|
||||
WAIT_TIMEOUT => true,
|
||||
WAIT_OBJECT_0 | WAIT_ABANDONED => {
|
||||
unsafe {
|
||||
let _ = ReleaseMutex(handle);
|
||||
}
|
||||
false
|
||||
}
|
||||
other => {
|
||||
unsafe { CloseHandle(handle) };
|
||||
bail!("resident {role} singleton mutex probe returned {other}");
|
||||
}
|
||||
};
|
||||
unsafe { CloseHandle(handle) };
|
||||
return Ok(owned);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let path = resident_process_singleton_lock_path(role, &store)?;
|
||||
let file = File::options()
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path)?;
|
||||
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } == 0 {
|
||||
unsafe {
|
||||
let _ = libc::flock(file.as_raw_fd(), libc::LOCK_UN);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.kind() == std::io::ErrorKind::WouldBlock {
|
||||
return Ok(true);
|
||||
}
|
||||
return Err(error.into());
|
||||
}
|
||||
#[allow(unreachable_code)]
|
||||
Err(anyhow!(
|
||||
"resident process singleton probe is unsupported on this platform"
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.resident_cognition.provider_readiness",
|
||||
|
|
@ -544,12 +614,15 @@ pub fn authenticate_resident_provider_pair(
|
|||
continue;
|
||||
}
|
||||
};
|
||||
let exact_process = observe_process_instance(&ProcessInstanceIdentity {
|
||||
process_id: value.process_id,
|
||||
creation_token: value.process_creation_token,
|
||||
created_at_rfc3339: None,
|
||||
executable_path: PathBuf::from(&value.process_executable_path),
|
||||
}) == ProcessInstanceObservation::ExactAlive;
|
||||
let role_owner_present = match resident_process_singleton_is_owned(owner, store) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
result.contradictions.push(format!(
|
||||
"{owner} provider singleton observation failed: {error:#}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if provider_matches_authority(
|
||||
&value,
|
||||
owner,
|
||||
|
|
@ -560,7 +633,7 @@ pub fn authenticate_resident_provider_pair(
|
|||
executable.as_deref(),
|
||||
now_millis,
|
||||
freshness_millis,
|
||||
exact_process,
|
||||
role_owner_present,
|
||||
) {
|
||||
result.terminal_current += 1;
|
||||
} else if value.runtime_id == release.runtime_id && value.release_id == release.release_id {
|
||||
|
|
@ -593,12 +666,15 @@ fn provider_is_fresh(
|
|||
return false;
|
||||
}
|
||||
};
|
||||
let exact_process = observe_process_instance(&ProcessInstanceIdentity {
|
||||
process_id: value.process_id,
|
||||
creation_token: value.process_creation_token,
|
||||
created_at_rfc3339: None,
|
||||
executable_path: PathBuf::from(&value.process_executable_path),
|
||||
}) == ProcessInstanceObservation::ExactAlive;
|
||||
let role_owner_present = match resident_process_singleton_is_owned(owner, store) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
reasons.push(format!(
|
||||
"{owner} provider singleton observation failed: {error:#}"
|
||||
));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let failures = provider_authority_failures(
|
||||
&value,
|
||||
owner,
|
||||
|
|
@ -609,7 +685,7 @@ fn provider_is_fresh(
|
|||
expected_executable,
|
||||
request.now_millis,
|
||||
request.freshness_millis,
|
||||
exact_process,
|
||||
role_owner_present,
|
||||
);
|
||||
if !failures.is_empty() {
|
||||
reasons.push(format!(
|
||||
|
|
@ -631,7 +707,7 @@ fn provider_authority_failures(
|
|||
executable: Option<&Path>,
|
||||
now_millis: u64,
|
||||
freshness_millis: u64,
|
||||
exact_process_alive: bool,
|
||||
role_owner_present: bool,
|
||||
) -> Vec<&'static str> {
|
||||
let mut failures = Vec::new();
|
||||
if value.provider != owner {
|
||||
|
|
@ -658,10 +734,10 @@ fn provider_authority_failures(
|
|||
if value.status != "ready" {
|
||||
failures.push("provider-status");
|
||||
}
|
||||
if !exact_process_alive {
|
||||
failures.push("process-incarnation");
|
||||
if !role_owner_present {
|
||||
failures.push("role-singleton");
|
||||
}
|
||||
if now_millis < value.observed_at_millis {
|
||||
if value.observed_at_millis > now_millis.saturating_add(MAX_PROVIDER_CLOCK_SKEW_MILLIS) {
|
||||
failures.push("future-observation");
|
||||
} else if now_millis.saturating_sub(value.observed_at_millis) > freshness_millis {
|
||||
failures.push("freshness");
|
||||
|
|
@ -680,7 +756,7 @@ fn provider_matches_authority(
|
|||
executable: Option<&Path>,
|
||||
now_millis: u64,
|
||||
freshness_millis: u64,
|
||||
exact_process_alive: bool,
|
||||
role_owner_present: bool,
|
||||
) -> bool {
|
||||
provider_authority_failures(
|
||||
value,
|
||||
|
|
@ -692,7 +768,7 @@ fn provider_matches_authority(
|
|||
executable,
|
||||
now_millis,
|
||||
freshness_millis,
|
||||
exact_process_alive,
|
||||
role_owner_present,
|
||||
)
|
||||
.is_empty()
|
||||
}
|
||||
|
|
@ -813,7 +889,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn stale_wrong_release_and_nonexact_process_cannot_be_ready() {
|
||||
fn stale_wrong_release_and_absent_role_owner_cannot_be_ready() {
|
||||
let value = provider();
|
||||
let executable = std::env::current_exe().unwrap();
|
||||
assert!(provider_matches_authority(
|
||||
|
|
@ -866,6 +942,28 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_singleton_probe_observes_only_a_live_owner() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("heartbeat.cc");
|
||||
assert!(!resident_process_singleton_is_owned("heartbeat", &store)?);
|
||||
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(0);
|
||||
let (release_tx, release_rx) = std::sync::mpsc::sync_channel(0);
|
||||
let owned_store = store.clone();
|
||||
let owner = std::thread::spawn(move || -> Result<()> {
|
||||
let _guard = acquire_resident_process_singleton("heartbeat", &owned_store)?;
|
||||
ready_tx.send(())?;
|
||||
release_rx.recv()?;
|
||||
Ok(())
|
||||
});
|
||||
ready_rx.recv()?;
|
||||
assert!(resident_process_singleton_is_owned("heartbeat", &store)?);
|
||||
release_tx.send(())?;
|
||||
owner.join().expect("singleton owner thread")?;
|
||||
assert!(!resident_process_singleton_is_owned("heartbeat", &store)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_provider_is_absent_and_private_state_is_rejected() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
|
|
@ -1084,7 +1182,7 @@ mod tests {
|
|||
value.runtime_id = "wrong-runtime".into();
|
||||
value.release_id = "wrong-release".into();
|
||||
value.status = "warming".into();
|
||||
value.observed_at_millis = 2_000;
|
||||
value.observed_at_millis = 3_000;
|
||||
let executable = PathBuf::from(&value.process_executable_path);
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -1104,12 +1202,44 @@ mod tests {
|
|||
"runtime",
|
||||
"release",
|
||||
"provider-status",
|
||||
"process-incarnation",
|
||||
"role-singleton",
|
||||
"future-observation",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_snapshot_skew_is_fresh_but_a_future_row_is_not() {
|
||||
let mut value = provider();
|
||||
value.observed_at_millis = 1_500;
|
||||
let executable = PathBuf::from(&value.process_executable_path);
|
||||
assert!(provider_matches_authority(
|
||||
&value,
|
||||
"heartbeat",
|
||||
"ygg",
|
||||
"release-a",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_000,
|
||||
100,
|
||||
true,
|
||||
));
|
||||
value.observed_at_millis = 2_001;
|
||||
assert!(!provider_matches_authority(
|
||||
&value,
|
||||
"heartbeat",
|
||||
"ygg",
|
||||
"release-a",
|
||||
&value.release_witness_sha256,
|
||||
Some("commit-a"),
|
||||
Some(&executable),
|
||||
1_000,
|
||||
100,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregate_pair_warms_on_missing_and_rejects_wrong_release() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
|
|
|
|||
|
|
@ -1154,6 +1154,17 @@ exact heartbeat/Self provider pair. Wrong-release evidence contradicts;
|
|||
missing evidence warms; stale or incoherent evidence degrades. Readiness,
|
||||
health, systemd, and PID sight cannot write deployment admission.
|
||||
|
||||
Provider liveness is owned by each role's cross-process singleton beside its
|
||||
mounted store. The provider's CAS-replaced readiness row binds that live role
|
||||
to exact release, witness, source, executable, status, monotonic sequence, and
|
||||
bounded freshness. PID/creation-token observation is same-namespace diagnostic
|
||||
evidence; it is not a cross-container liveness owner because PID namespaces do
|
||||
not preserve that observation. Readiness may accept only a narrowly bounded
|
||||
future skew around its snapshot time and must still reject a genuinely
|
||||
future-dated row. Status and supervisor aggregation share this derivation.
|
||||
Neither a fresh row without the role lock nor a role lock without the exact
|
||||
fresh row can become ready.
|
||||
|
||||
Yggdrasil separates provenance from Body. `/srv/epiphany/source/current` is
|
||||
immutable release source. `/var/lib/gamecult/epiphany/workspace` is the writable
|
||||
domain Body carrying repository binding, persistent Modeling map, Mind state,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,33 @@
|
|||
# Fresh workspace handoff
|
||||
|
||||
## Actual c006 woke and completed retained Modeling once — 2026-08-10
|
||||
|
||||
Exact `0ef0c0b8` was published into actual c006 under its cognitive brake. Exact
|
||||
Self authenticated it at revision 23 with no active lease. Heartbeat and Self
|
||||
then started as distinct long-lived container owners. An explicit typed brake
|
||||
release created no grant; it allowed the already-retained, unconsumed
|
||||
proposal-Modeling grant to launch exactly once. Coordinator thread
|
||||
`linux-eyes-tools-c005` opened runtime job
|
||||
`5ff2704b-64eb-42f1-9ca4-ca7144e33ae4`; the detached Modeling worker published
|
||||
valid typed fulfillment. Self advanced 23 -> 25 -> 26 through launched,
|
||||
awaiting-fulfillment, completed, then sleeping. Heartbeat returned to bounded
|
||||
void routines without a replacement grant. No Hands, Persona, deployment, or
|
||||
other external consequence occurred. Exact before/after manifests, brake
|
||||
transitions, provider logs, and status projections are under
|
||||
`.epiphany-run/c006-wake-0ef0c0b8`.
|
||||
|
||||
The live run exposed one readiness ownership fault. Both providers were alive,
|
||||
but status reported warming because its early snapshot time raced continuously
|
||||
refreshed rows and because one container cannot observe the other's PID
|
||||
incarnation. The cognitive brake was re-engaged at the clean sleeping boundary;
|
||||
both exact processes remain alive. The current source cut makes the shared-store
|
||||
role singleton own cross-container liveness and keeps the exact fresh provider
|
||||
row as release/witness/source/executable identity. PID sight is diagnostic.
|
||||
Rows more than one second beyond snapshot time still refuse. Fifteen focused
|
||||
tests and all 666 non-ignored core library tests pass. Commit/package this cut,
|
||||
prove active/warming transitions with two copied-state containers, then publish
|
||||
under brake and resume endurance. Yggdrasil never builds.
|
||||
|
||||
## Direct proposal Modeling and archived fulfillment accepted — 2026-08-10
|
||||
|
||||
The exact `53678374` Linux body now carries a direct proposal Modeling proof in
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -36,9 +36,9 @@ invariants:
|
|||
|
||||
current_status:
|
||||
summary: Epiphany is a supervised engineering alpha on Starfire, not deployment-ready. Exact b8138a6b active-child SIGTERM and restart, exact 9f369528 deterministic warm packaging, exact c930f505 cross-container ownership, exact ed15f83a resident lifecycle retention, exact 16c4b19f coordinator receipt and Heartbeat artifact retention, exact cce2b8ac narrow model-generation retention, exact eea15e82 model-turn JSON ingress, exact b1e8c89a semantic unattempted-generation retention, exact 6bc3ec3f semantic physical-epoch retirement, and exact 44230a05 Persona conversation retention are proven. Gamecult-ops 059821f owns Yggdrasil stream retention and exact 6ee08a4 owns stable Starfire Persona-permit firewall ingress. Exact 16c4b19f genuine idle and exact a987d2f7 active cognition completed 900-pulse bounded plateaus. Runtime-owned typed fulfillment is accepted live under exact 6dd9b132. Yggdrasil is the small public crossing body, never the release forge.
|
||||
latest_phase: Exact pushed 53678374 release sha256-5d50fa7dab6bbfae78982d6dd8e8a21f2c3642bb6678686660c59fb42aa97d05 with witness sha256-1814d2b7ee0bd93ab93a2878beb6ab7916fa7e1a476b8bde6dc7e06cd79bda73 authenticates 24 binaries with zero warnings. Exact packaged swarm proves failed and fulfilled outer-worker retention on copied Body-bound Linux state. The same body now directly proves proposal Modeling: immutable proposal/request/payload, generation-one Body carrier, exact launch binding/hash, terminal Modeling result, accepted Evolution patch, and one proposal-citing Eyes frontier. Zero-receipt recovery closed the resident grant recovered-fulfilled; packaged retention removed its lifecycle; an eight-envelope worker archive preserved exact fulfillment while removing launch/job/claim. Heartbeat then issued only the causally new admitted-model-direction grant, not a proposal retry. Actual c006 remains braked and stopped.
|
||||
latest_phase: Exact 0ef0c0b8 packages deterministically as 24-binary release sha256-032af7baa251caf32c1843eeace8990001557fe916988d8d1e2f0a27093a2b36 with witness sha256-c0b8dbac33f821e9adc15218ea22a9133e0b9ea78f1981916b403fcad79f3d3b. Removing forced unchanged-submodule refresh reduced the source-changing Cargo pass from 11m04s to 7m19s; an identical replay took 5.8s wall and 0.77s Cargo. Actual c006 authenticated that body under brake, then an explicit wake consumed its one retained proposal-Modeling grant exactly once. Runtime job 5ff2704b published typed fulfillment; Self advanced revision 23 to 25 to 26 and returned sleeping without replacement grant or external consequence. The brake is re-engaged while readiness moves cross-container liveness ownership from PID sight to each role's shared-store singleton plus exact fresh release row.
|
||||
operational_state:
|
||||
local_worktree: exact_0ef0c0b8_fast_deterministic_package_accepted_c006_wake_next
|
||||
local_worktree: c006_wake_accepted_cross_container_readiness_owner_cut_uncommitted
|
||||
operator_wireguard: restored
|
||||
starfire_persona_permit_firewall: persistent_exact_gamecult_ops_6ee08a4
|
||||
yggdrasil: canonical_small_public_crossing_body
|
||||
|
|
@ -68,9 +68,9 @@ current_status:
|
|||
starfire_linux_role_cognition: eyes_and_modeling_tools_results_and_acceptance_proven_zero_consequence
|
||||
starfire_linux_modeling_peak: 109887488_bytes_resident_body_plus_modeling_circuit
|
||||
yggdrasil_epiphany_service_contract: repaired_in_gamecult_ops_812b427_not_installed
|
||||
resident_physiology: c006_exact_c693aaea_settled_braked_no_active_lease_no_attempt_three_old_processes_stopped
|
||||
resident_physiology: c006_exact_0ef0c0b8_alive_braked_revision_26_after_one_fulfilled_proposal_modeling_grant
|
||||
unresolved_authority: Coordinator receipt, Heartbeat pulse-artifact, genuine-idle and active resource plateaus, exact-grant terminality, restart-persistent cooldown refusal, schedule/action issuance cardinality, runtime-owned typed fulfillment, all three packaged fulfilled-before-receipt lanes, narrow model-generation retention, published model-turn JSON ingress, semantic logical/physical retention, Persona conversation retention, stable Starfire firewall ownership, completed model-adapter session archival, normal coordinator lifecycle opening/error closure/legacy repair/archival, co-resident resident lifecycle retention, authenticated outer-worker attempt retention, deployed supervisor/projector journald ownership, and Docker named-volume atomic snapshot visibility are accepted live. Starfire's Docker Desktop F: bind is rejected for mutable authority after an exact packaged lockless reader observed an empty published snapshot during writer interruption. Historical migrated semantic attempts without recorded physical namespace remain durable and non-retirable. One completed Discord consequence remains blocked by the absent Yggdrasil credential.
|
||||
next_action: Publish exact 0ef0c0b8 release sha256-032af7baa251caf32c1843eeace8990001557fe916988d8d1e2f0a27093a2b36 with witness sha256-c0b8dbac33f821e9adc15218ea22a9133e0b9ea78f1981916b403fcad79f3d3b into actual c006, authenticate it under the cognitive brake, then start exact Heartbeat and resident owners and deliberately release the brake. Preserve before/after hashes and process logs; require the existing coherent unconsumed grant to be consumed at most once through its typed lifecycle with no external consequence. Yggdrasil never builds.
|
||||
next_action: Commit and package the cross-container readiness owner cut. Fifteen focused tests and the 666-test core library pass. On copied state start exact Heartbeat and resident owners in distinct PID namespaces; require status active with role singleton plus exact fresh release rows, require a stopped role to warm, and require a row beyond bounded snapshot skew to refuse. Then publish to c006 under brake and resume measured live endurance. Yggdrasil never builds.
|
||||
|
||||
active_subgoals:
|
||||
- Prove Persona, Self, Eyes, Modeling, Imagination, Hands, Soul, Mind, Continuity, and nervous-system behavior individually.
|
||||
|
|
|
|||
Reference in a new issue