A continuity abort demotes the shared key to Expected-only with its own
exact activation, and never withdraws the Expected. a148802 is reverted.
Pre-B1 terminal continuity aborts keep their phase under a
LegacySkippedBeforeB1 marker that only the lift can construct. Boot
reconciliation is the single owner of legacy residue and demotes only by
exact identity. Supervision records a failed pre-restart demotion and
defers instead of minting a continuity. Scheduler faults are isolated per
transaction and backed off, reported once and kept in the record. Queued
commands freeze every tick, so one wedged target cannot starve the
others. An unreadable history fails closed for freezing. boot is a
separate, tested step.
Soul made four passes. The first found boot-wedging on legacy records.
The second traced it to Self's ruling to reopen terminal records, which
was replaced with one owner for the cleanup. Pass three found
cross-target starvation, and pass four found fault re-arming. 241 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fault reports clear only when a step ran and succeeded, not on a backoff
skip or a handled failure.
- Fault notes are written through cleanup_evidence::last_error_envelope, so
an unretirable pre-B1 terminal record shows its fault; a failed note is
printed with the fault instead of swallowed.
- backoff_wait and is_waiting are pure and unit-tested; tick and resume
return values are pinned.
- migrate_transaction_record is the migration's only entry; the encoder that
can write a marker is private to cleanup_evidence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- A tick's progress is a state change, never a last_error rewrite; freezing a
queued command no longer waits on other targets' progress (still at most
one freeze per tick, busy targets refused as before).
- Failing transaction steps back off (process-local, doubling from the poll
interval to 60s) instead of retrying every tick.
- Boot reconciliation runs the resident-issuer pass with history unreadable.
- Resume and retire faults are reported once per transaction and left in
last_error, which status renders.
- The legacy marker is a private-constructor type inside cleanup_evidence;
transaction_envelope refuses a record carrying it, the schema migration
writes through its own path.
- Tests: changing-error wedge, backoff, dead-candidate post-fence branch,
fault note, marker refusal, torn-history boot residue.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A transaction seeded at AwaitingReady fails validation (no fencing evidence),
and nothing at Fencing makes a workload error, so the post-fence abort is
begun directly and the test pins that its failing step only waits.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cargo-mutants on the previous commit missed the per-candidate progress flag
and the post-fence branch selection in resume_candidate. A repeated error
reports no progress; a dead candidate past the fence aborts; a wedged
post-fence abort only waits.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul's pass on the B1 fix batch found the lift's reopen of a terminal pre-B1
continuity abort could stop Idunn booting (the reopened record claims target
authority beside a live transaction, and the store was already migrated to v4),
and that one wedged abort stopped the whole scheduler.
- The lift no longer reopens anything. A terminal pre-B1 continuity abort that
issued an activation keeps its phase and completion and carries the typed
marker CleanupEvidence::LegacySkippedBeforeB1 on its topology_reconciliation.
Validation accepts that shape only for a terminal continuity abort. Boot
reconciliation, already the exact-activation owner of that residue, is the
only cleanup path. A pre-B1 abort still in flight lifts to Pending: it
already holds its target's authority, so the lift adds no claimant.
- A failed step of a pre-fence abort is recorded as a resumable error, never
handed to the post-fence abort, which refuses a pre-fence phase and failed
every tick.
- One transaction's error no longer fails run_scheduler_tick: each candidate
is resumed on its own and its fault logged, and retiring a terminal record
tries the next one when an archive fails. An unreadable control store still
fails the tick.
- serve is boot plus the loop, so a test can run the boot sequence.
Tests pin, through the Engine: the marker and no-reopen for each pre-B1
fixture; a resident terminal abort plus a live transaction plus a second
resident abort for one target; a withdrawn incarnation never re-published and
history keeping one copy after a crash between the archive steps; a wedged
abort leaving freezing alive; an unarchivable terminal leaving resume alive;
and boot demoting the residue.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cargo-mutants over the batch's diff missed the lift's gate, the unexplained
projection accounting, the refusal count and the deferral clock. Tests now
cover each: the lift owes only a continuity that issued an activation; a live
transaction accounts for its own activation and nobody else's; three refusals
in history hold the restart ceiling; the deferral lands between now and now
plus its length.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul found B1's rule sound and its transition unsafe. This makes the rule's
old inputs impossible to reintroduce a stuck state from.
- The control store's legacy lift owns the transition. A pre-B1 continuity
abort that issued an activation lifts with topology_reconciliation Pending,
so the one resolution demotes its own activation. A record already terminal
under the old rule is reopened at Starting, since a terminal record cannot
owe work. History lifts records as written. Before this, one resident
record of that shape failed validation and ControlSnapshot::read refused the
whole store, so Idunn could not boot.
- Supervision owns the Expected-only precondition. When its pre-restart
demotion fails it mints no continuity and writes next_restart_at_unix_millis
on the generation's ContinuityBackoff (attempts and window untouched), so it
neither restarts over an undemotable projection nor retries every tick.
- Boot reconciles once: a projected activation that exactly matches the
activation a failed continuity in history (or a resident terminal record)
issued is demoted. Nothing is adopted; an activation no admitted generation,
live transaction or failed issuer accounts for is reported and left alone.
New read-only CultCacheTopologyDriver::projected_activation.
- Unreadable history.cc fails closed. The continuity restart ceiling and the
consumed-command check no longer read an empty history; they stop, and the
fault is reported once while it lasts (ReportOnce). The undecodable-record
report is a pure function, pinned.
- docs/deployment-authority.md no longer says the projection never drifts.
Tests: legacy fixtures written by the 9001b58 encoder for the three abort
shapes (a second throwaway generator, see the fixtures README), driven through
the Engine; the seeded-transaction helper now takes a command kind and an
incumbent, and a Deploy carries a frozen-source receipt and a signed brake
release, so Deploy commit, Deploy pre- and post-fence aborts and a continuity
abort over an incumbent run through the real phase machine.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The generation moves to v3 and the transaction to v4. They carry
ReadinessEvidence (the class is derived from Expected by ReadinessClass::of),
phase deadlines owned by one writer (enter_phase), RouteSupervisionState,
ContinuityBackoff, LeaseAdoptionEvidence and TerminalRecovery. The plan
moves to v3 with frozen deadline durations. A boot migration lifts every
legacy record with stored_at preserved. No decision reads the new fields
yet, so behaviour does not change at this cut.
Soul closed it after one fix batch. The first pass found every commit and
post-fence abort wedged by a deadline nothing cleared; Engine tests now
drive admissions and aborts to Complete. 204 tests. Filed forward: B3 must
give Odin its own readiness class before any route-proof admission ships,
or boot wedges; an unreadable history.cc must fail closed (B1 fix batch).
Back up control.cc before deploying: an older binary refuses a v4 store.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
advance_pre_fencing_abort survived cargo-mutants as Ok(()): the earlier
pre-fence abort test ran with candidate cleanup already complete. The seeded
transaction now takes its phase, so a Warming transaction is aborted and
driven to Complete through the stub workload port, and the abort's terminal
write is checked to carry no deadline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
History: read_history_transactions dropped undecodable records with .ok(),
which undercounts continuity restarts for backoff. decode_history_transactions
now returns each failure with its key and reason, and the reader prints every
one plus a count. An unreadable history.cc is reported instead of read as
empty. The pre-c848459 IsolationEvidence shape needs no Legacy arm: that
commit made the enum untagged with a Linux variant of the same six fields, and
a test now decodes the old shape under both MessagePack encodings to prove it.
The Legacy structs' stated death condition now says history.cc is never
migrated or pruned, so they die only when history is.
Odin authority: current_odin_authority fell back to the bootstrap key when the
admitted Odin generation carried none (route-proof readiness). It now refuses.
OdinReceipts.authority, read only by tests, is deleted.
Tests through the production path for the rules cargo-mutants showed
unexercised: ControlSnapshot::read of a lifted legacy generation; the
route-proof arm of AdmittedGeneration::validate and route-proof shape checks;
Odin readiness, lease adoption, route supervision and continuity backoff
validation; the admitted half of max_odin_sequence; and the receipt-equality
comparisons in AwaitingReady and Routing, driven through the Engine.
The awaiting-ready default gets a comment that it is unsafe for stateful
targets whose migrations run in AwaitingReady; B5 owns the fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every admission commit and every post-fencing abort failed validation:
transition() stamped a phase deadline on entry to Fencing..Committing and
nothing cleared it, so the record cloned into Complete still carried a
Committing deadline and validate() refused it. The first deploy or continuity
past Fencing wedged in Committing; a post-fence abort never completed.
DeploymentTransaction::enter_phase is now the only writer of phase. It sets
the phase, the update time and the deadline together, so a record outside
Fencing..=Committing carries no deadline by construction. transition(), both
abort completions, the commit, rejected() and Starting/Warming all use it.
Wall-clock ordering rules are deleted: entered_at <= updated_at, created_at <=
updated_at and promoted_at <= updated_at each wedged a transaction after an
NTP step backwards, since same-phase writes stamp now_millis(). The deadline
is entered_at + duration, fixed at entry.
The Engine takes its systemd workload port as a trait object
(Engine::open_with_systemd_workload), which lets three Engine-level tests drive
a continuity transaction from Fencing through Leasing, AwaitingReady, Routing
and Committing to Complete, and a post-fence abort to Complete, with real
Odin-signed evidence and no systemd, nginx or network. deployment_plan's test
fixtures become pub(crate) for them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul accepted it on 2026-09-29. With an exact fragment, a stale
observation costs one challenge and zero actuations; rendering is
byte-deterministic; drift restores once. Its merge with idunn/cut1-fix5
is clean (213 lib tests). The storm paths it does not claim go to B2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture envelope's stored_at now differs from the record's own update
time, so a migration that re-stamps the envelope fails the test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One migration for the route-continuity foundation. No decision reads the new
state.
- ReadinessEvidence (OdinCorrelated | RouteProof) replaces the bare Odin Ready
receipt on both records. The class a target must collect is derived from its
Expected (ReadinessClass::of): a shared-infrastructure odin.verse-rendezvous
dependency means Odin-correlated, none means route-proof. It is not stored.
RouteProof evidence is refused for a target that declares Odin.
- Generation carries route supervision state (challenge times, failures,
degraded marker, actuation window) and continuity backoff. A new generation
inherits the actuation window and backoff and starts unobserved and
undegraded. route_repair_started_at stays readable.
- Transaction carries the current phase deadline (stamped by transition from
the plan's frozen durations) and lease adoption evidence.
- FailedAfterFencing names its recovery: RestartAdmitted, RestoreIncumbent
(the default for older records) or OperatorRequired{reason}.
- Every v2 generation lifts as Odin-correlated with its receipts unchanged;
v2 and v3 transactions lift through LegacyDeploymentTransaction. The boot
migration rewrites both, preserving stored_at, and history reads lift the
same way. The Legacy structs die when no older record can remain.
Fixtures under tests/fixtures/idunn-control-legacy are the bytes the 9001b58
encoder wrote for records built through its own constructors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An operator binding may now declare a [deadlines] table naming any of the
five post-fencing phases (fencing, leasing, awaiting_ready, routing,
committing) in seconds, 1..=86400. compile_deployment_plan resolves the
binding over Idunn's defaults and freezes the result in the plan, so a later
change to a default cannot move an in-flight plan.
The plan becomes schema v3, with the frozen durations as a trailing field.
A v2 plan keeps its nine-field encoding (the field is skipped when absent),
so every existing plan id still verifies; it runs on Idunn's defaults at read
time. Nothing reads the durations yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A continuity candidate shares the admitted Expected key. Both abort intents
marked its topology reconciliation Skipped, so a failed continuity left its
candidate activation projected under the admitted key while the admitted
receipt named the older one, and a later deploy's post-fence demotion refused
as substituted.
DeploymentTransaction::abort_topology_reconciliation is now the one rule for
both intents and pre-fence validation: a deployment owes the projection when it
published its Expected, a continuity owes it when it issued an activation.
reconcile_failed_candidate_projection is the one resolution for both abort
paths: a deployment withdraws its own key, a continuity demotes the shared key
to Expected-only with its own exact activation and never withdraws the
Expected. The post-fence incumbent demotion stays deployment-only and exact.
Tests read the persisted topology store after the production
publish/demote sequence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This reverts commit a148802cb6.
Rollback after fencing adopted whatever activation was projected under the
shared Expected key, so a continuity abort's leftover activation could be
demoted by a later deploy. The drift is closed at its source by the next
commit: a continuity abort demotes its own exact activation. Rollback demotes
the admitted generation's exact activation again.
The doc paragraph is dropped; the 9f00e7a paragraph above it stays until B4.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delete the stale-observation -> repair-intent -> restore path from
supervise_admitted_route. An exact fragment with a stale observation now
yields one route challenge and nothing else. restore_admitted_membership
returns before nginx -t, the private-mount unit, ufw and reload when the
fragment already equals the admitted membership. route_repair_started_at
stays readable but no longer decides anything.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All six were ruled as recommended, one at a time, on 2026-09-29. Q6 adds
a Soul pass on the weekend CultLib branch before it merges. The campaign
moved to the Eureka session "Codebase audit".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Body facts, identity/lifecycle/authority table, six operator questions and a
draft cut list (S1, B1, F0, B2-B5, S2, C1, S3/C2) for the route-continuity
target. Campaign handed to the "Codebase audit" Self session; operator forks
are ruled there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eureka target for the route-continuity rebuild after the 2026-09-28 StreamPixels
deployment failure: observation never actuates routing, failure never
accelerates actuation, web apps prove over their own HTTP route, every
post-fencing phase ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first v2 Ghostlight deploy sealed its release and then failed creating
the runtime bundle: /etc/gamecult/ghostlight/runtime was read-only under
ProtectSystem=full. Runtime root only, not the credential directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The version tag could not be recorded where the ruling said, because the
stored value is validated to a bare sha256 shape by a pinned dependency in
another repository. Hands implemented an equivalent implicit version read,
flagged the divergence, and asked whether proceeding was right. It was: the
ruling named a mechanism, the hazard was live, and halting would have left the
fleet exposed.
Five tests prove it by failing without the fallback, which is the point, since
the whole finding was that nothing had ever exercised the stored path.
The frozen-source digest turned out to have the same exposure through a
narrower window, and this cut's own sixth and seventh batches crossed it
twice. Same answer applied, with the prior shapes reproduced from git history
rather than from memory.
Hands also spotted a third instance and declined to chase it: the systemd
actuator compares a raw content-only digest read through procfs against a
stored value now carrying mode, gid and xattr terms. Two different algorithms
compared to each other, which is the same defect a third time. Flagged for its
own look rather than guessed at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bypass is a compile error now, proven by builds that fail on Soul's exact
spellings rather than by reading the lint config. The repository had no CI
surface at all before this, so there was nothing for "denied in CI" to attach
to until now.
Hands flagged that the single-file artifact digest format changed and reasoned
nothing compares against literals. That holds for fixtures and not for
receipts: two call sites in host.rs compare a freshly computed digest against
a stored value. So the first upgrade past this commit makes every already
installed workload fail "installed executable differs from Expected", and
every running one fail "the installed executable changed under the running
process". Idunn is the deployment and daemon-survival authority, and this
would land silently at restart across the fleet.
The ruling versions the digest and verifies in the version the receipt names,
rather than re-sealing admitted state to make new code agree with itself. It
is not closed until a test seals under the old format and verifies under the
new, because the whole finding is that nothing exercised the stored path.
Two bugs in this batch were found by running the suite rather than trusting
it, including a test that set an attribute on a root whose own metadata is
never hashed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The port can be walked around. Two spellings of the original bug restore it
with all 193 tests green, and the proof it leaks is already in the file: one
function still calls fs::read_link directly, and a probe measured sixteen
counted calls beside three uncounted syscalls. My record claimed the count was
total rather than conventional. It is still conventional; the convention moved
from remembering the counter to remembering the port. That is the second time
this record has overclaimed what a pin does.
The artifact digest's single-file path never got the ruling, and it is the
path the installed executable actually takes. Setuid, mode and a file
capability are all invisible there, immediately before launch. Every test for
it wraps the file in a directory so the other path runs, and one of their doc
comments claims to pin exactly the scenario it does not reach.
The xattr framing is sound and nothing proves it. The suite stays green with
values dropped entirely, so a capability edited from cap_setuid to
cap_sys_admin is covered by no test.
Soul named one cause with three faces: every pin here is a convention a later
edit steps around, and the fixtures use the convenient shape rather than the
one production uses. So the eighth batch makes the bypass a compile error and
moves the tests onto the production shape, instead of adding a ninth round of
point fixes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>