# eggup-core
`eggup-core` is the policy-neutral local substrate for verified, multi-artifact
updates. It is intentionally transport-neutral: callers acquire artifacts,
prove destination ownership, and choose release and service policies; the core
provides bounded local validation, private staging, SHA-256 integrity
verification, bounded candidate validation, locked ownership and staged-digest
revalidation, and synchronous commit/rollback with structured receipts.
Ownership uses `Absent | Owned | Foreign | Unknown`. Destructive replacement
requires `Owned`, or `Absent` with explicit creation authorization;
`Foreign` and `Unknown` fail closed. No destination parent is created
automatically. Staged bytes are re-hashed under lock before any live mutation.
Terminal receipts preserve failure phase, category, member, post-commit check
failure, and real recovery evidence. `commit_with_post_commit` retains the
mutation lock and rollback set until one caller check succeeds or its explicit
`KeepInstalled | RollBack` policy is resolved.
A program can replace the executable it is currently running through the same
transaction model: `InstallPlan::for_current_executable` binds the canonical
live image, plans `StagePlacement::InsideInstallationRoot` so no write authority
above that directory is ever needed, and re-proves the image's identity under
the lock immediately before the first live rename. An invocation symlink is
followed to its real target and never overwritten; a hard-linked or non-regular
destination is refused. On Windows the previous generation stays mapped until the
process exits, so `KeepInstalled` reports
`CleanupDisposition::DeferredToProcessExit` and schedules its removal, rather
than calling it cleaned or stranded.
Locks are fail-closed by default: `MutationLock::acquire` never recovers a stale
record and `inspect` is read-only. Recovery is opt-in through
`MutationLock::acquire_with_recovery`, which displaces a record only when a
caller-supplied `StaleLockVerifier` returns `StaleLockDecision::ProvenStale`
for the exact observation Core reported. That opt-in API returns
`RecoveryResult<T>`: ordinary failures arrive as `RecoveryError::Core(Error)`,
and retained evidence as `RecoveryError::RecoveryRequired`, which carries the
real path via `RecoveryError::evidence()`. Default `acquire` keeps returning
`Result<T, Error>`. Core never decides staleness from PID
liveness, record age, executable name, or service state; see
`examples/stale_lock_recovery.rs`.
Integrity is checksum evidence only. No authenticity or signature verification
exists in this crate.
Bounded candidate execution clears the inherited environment, bounds stdout and
stderr per stream, and enforces an unconditional wall-clock deadline that kills
and reaps the child. A caller may attach a bounded stdin payload with
`CommandSpec::stdin_data`; the payload is bounded by `MAX_STDIN_BYTES`
inclusively, an oversized payload is rejected before any child is spawned, and a
write that fails before the deadline fails closed with a typed `Error::Io` whose
`operation` is `"writing bounded stdin"`. A write that fails at or after the
deadline is reported by the deadline outcome instead — `Ok` with `timed_out`
set — because the deadline already decided it; the write's own observation
instant decides which of the two applies, never the runner's polling phase. The
`operation` strings `"spawning bounded candidate"`, `"polling bounded
candidate"`, and `"writing bounded stdin"` are a stable classification surface a
downstream adapter may branch on.