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>
193 tests, and the mutation that passed in 0.39 seconds on pass 7 now fails.
Putting canonicalize and symlink_metadata behind a port that charges the
counter inside the same method as the syscall is what turned an honour system
into a measurement; the hand-placed call sites are gone.
The validator mask tests are precise rather than merely red: narrowing the
file mask fails exactly the setgid test and narrowing the directory mask fails
exactly the setuid one, with the other five staying green. That is the
evidence they pin single bits.
The unreachable test is deleted rather than repaired, with a note saying why
the defence it claimed never existed. The xattr tests close Soul's file
capability finding and state in their own doc comments that the probe runs as
root and proves nothing about an unprivileged attacker.
Hands shipped a compile bug in the first commit and its own Yggdrasil run
caught it before the report. That is the order these should happen in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul restored the exact bug this line of rulings exists to prevent, without
instrumenting it, and the new call-count pin passed in 0.39 s. The counter
only counts calls someone remembered to annotate, so it is an honour system
rather than a measurement. The code said so in a doc comment four lines below
the assertion; my record claimed the opposite. That record is corrected here.
The fixture I recorded as deleted for being unable to fail is in the tree,
still unable to fail. Hands' reasoning about why the collision class is
unreachable was sound and Soul retested it; what landed on top of that
reasoning was a test that cannot go red.
A file capability is invisible to both the validator and the digest, which is
the grant setuid used to be with no mode bit to see it. no-new-privileges
prevents it on our own deployment path, but the digest is the detection and
Cut 4's verdict claims the tree is the one we froze. The artifact digest also
got a single executable boolean where the ruling said full mode and gid, so a
post-install setuid re-verifies clean.
Six rulings for the seventh batch. The first moves filesystem access behind a
narrow injected port so the count is total rather than conventional, which is
the shape doctrine already asks for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
183 tests green. The timing pin is now a call counter asserting an exact
formula, which is what it should have been before a clamp made a 1.63x
separation look like 33x.
I ruled the bind mount should carry nosuid. Docker exposes no such
per-bind-mount flag through either syntax, and Hands verified that against
the real CLI rather than arguing from memory. The container already runs with
no-new-privileges, which is process-wide and stronger than what I asked for.
The ruling is withdrawn: the prevention was already there and I ordered a
second one without looking.
Two fixtures in this batch would have lied and were caught by mutating rather
than reading. The container's chown clears a freshly-set setuid bit, so a
fixture that sets then chowns tests nothing. And the first observe_frozen test
tampered with the recipe file, which a different check inside observe_frozen
catches, so it stayed green with the digest check deleted.
The artifact digest's symlink terminator is deliberately left unpinned:
symlink(2) cannot put a NUL in a target, so no honest test exists. Hands built
one, confirmed it did not go red, and deleted it instead of keeping it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five of seven rulings hold. The containment argument, where I asked Soul to be
most suspicious after S5-8, survived twenty escape shapes it built itself.
R-I1's pin is not a ratio test. Its clamp fires on every run, so it reads as
"under 200 ms", and clean code has already measured 198.9 ms under the
three-slot load the operator just authorised. The reported 33x separation was
an artifact of that clamp; the real separation is 1.63x, which a slightly
faster host erases. It fails in both directions, and under Cut 4 a false red
is a false verify verdict on unattended work.
R-I3 is not met. The validator masks the mode to 0o777 and checks uid but not
gid, so setuid, setgid, sticky and gid are all permitted to vary and none are
hashed. A frozen binary made setuid-root re-observes clean against its
receipt, and the tree is bind-mounted into runner containers with no nosuid.
The ruling adds the mount flag as well as the digest terms: the digest detects
and the flag prevents, and the flag costs a mount option.
Soul also found that the artifact digest still has the blindness the frozen
source digest just lost, and that observe_frozen, which is the tamper check
itself, has no test at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All seven rulings landed, 175 tests green, and every new test dies under its
own mutation. The two-link cycle fixture hangs rather than asserting, which is
the correct falsification: nothing but the budget bounds a cycle, so a timeout
is what a broken budget produces.
The batch's real finding runs the other way. Soul called three containment
checks redundant and I ruled delete them. Hands deleted two, hand-traced the
third, and found that with all three gone a symlink whose whole target is ".."
at the root resolves to the parent of the root and returns Ok, because no
later Normal step ever runs to trip the other arm. That is a containment
escape, the exact class this resolver exists to prevent. Soul was wrong, my
ruling repeated it, and a hand-trace caught it.
Soul's HTTPS rig had also evaporated: no session, no work directory, no
scratch trace. Hands rebuilt it from this map's description, which worked only
because the description was detailed. A probe that is going to be committed
has to be handed over while it still exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul could not falsify any claim the fourth batch made. What it found instead
is that the S4-1 fix reached the exponent and not the per-component cost, and
that the line responsible is dead code: the value is already canonical, so
deleting four lines removes a cubic term and leaves the suite green.
The other half of the bill is coverage. The traversal budget is the only thing
that terminates a symlink cycle and nothing tests it; cargo-mutants missed two
mutants on that single line. The digest that serves as the tamper check is
blind to symlink targets and to the executable bit, both of which the
validator permits to vary. S4-6 was reported landed and has no test at all.
Seven rulings for the fifth batch. R-I5 departs narrowly from my earlier
"keep S6's behaviour exactly" and says so rather than letting it pass as
drift: the 40-traversal ceiling matches the kernel's own ELOOP limit and is
kept, but it must be declared and the error must name the link.
FU-Heimdall-Gitlink cleared upstream on its own. Heimdall froze end to end,
945 entries, zero mismatches. The ops follow-up closes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul's fourth pass found the symlink resolver newly exponential
(resolve_frozen_source_symlink recursed per path component and
re-resolved every shared prefix from scratch, so an n-link
self-doubling chain cost 2^n real filesystem resolutions), the
streamed hash unpinned, three of the five S5 guard tests not killing
their own guard, all three S1 fsck guards unpinned against
already-local objects, no test proving a legitimate in-root ".."
freezes, and the symlink writer still buffering a whole blob for a
120000 entry.
S4-1: resolve_frozen_source_symlink is now an explicit iterative walk
(a work-stack of frames standing in for the old call stack) with
per-symlink memoisation and one shared traversal budget charged per
distinct symlink read, not per nesting depth. Soul's n=20 doubling
chain (a0 -> ., ak -> a{k-1}/a{k-1}) now resolves in milliseconds
instead of 43.7 s, and freezes rather than refusing: nothing in the
chain dangles or escapes the root, matching S6.
S4-2: two trees differing only past the first 64 KiB chunk now assert
different digests, killing the zero-length-buffer and first-chunk-only
mutants cargo-mutants found.
S4-3: the duplicate-path, ensure_frozen_directory containment, and
create_dir guards each get a fixture that only that guard refuses --
calling git_tree_entries directly (before any fsck runs) for the
duplicate path, and pre-seeding created_dirs with state a second,
independent bookkeeping pass could produce for the other two.
S4-4: Soul's eleven already-local hostile fixtures (dup-trees, six
.git look-alikes, an unsorted tree, mode 120000 on a tree, mode 040000
on a blob, a zero-padded mode) land as tests against freeze_exact's
own explicit fsck. A second test proves freeze_exact's own fetch fsck
flag is what refuses a hostile revision that was never local, by
asserting which context wraps the error.
resolve()'s own fetch fsck flag is still unpinned: exercising it needs
a real HTTPS origin (OperatorBinding::validate requires one), and
url.insteadOf does not work around that -- `git remote get-url` expands
the rewrite, so it can never satisfy ensure_checkout's own
origin-equality check. Confirmed by hand against a local git; a real
smart-HTTPS harness like Soul's S1 rig is what closing this needs.
Recorded as a gap, not faked with a broken test.
S4-5: a legitimate in-root ".." link (two hops up from a nested
directory, back to a file at the root) now has a test proving it
resolves and freezes, not just that an escaping "..' refuses.
S4-6: materialize_tree_raw refuses a 120000 entry whose blob exceeds
the platform's PATH_MAX before reading it, for both the streamed-blob
and the shares-content-with-a-regular-file cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
git_at runs plain `git` with no `-c safe.directory=*`. By the time the
hostile commit is built, the origin repo is already chowned to the
unprivileged fixture identity, so root's plain git refuses it as dubious
ownership. Use the same safe.directory-scoped helper the duplicate-tree
fixture above already uses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The test was moving `main` to the hostile commit before the first checkout
existed, so the initial (also fsck-guarded) clone refused it, never
exercising the fetch line S1 actually changed. Establish the checkout on the
good root commit first, then advance `main`, matching the real scenario: an
already-fetched deploy target whose admitted ref later moves to a hostile
commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
frozen_source_rejects_a_symlink_that_escapes_the_transaction_tree asserted
against harden_frozen_source, which no longer resolves symlink chains (S6
moved that to validate_frozen_source_symlinks, against the tree's final
published path). Point it at the right function.
resolve_then_freeze_refuses_hostile_trees_through_the_production_path could
not go through the literal SourcePort::resolve()/freeze() trait methods:
OperatorBinding::validate() requires an https:// origin unconditionally, and
this suite has no HTTPS Git fixture. Replaced with a fetch that reproduces
resolve()'s exact invocation (this fix's -c transfer.fsckObjects=true
included) against the checkout, the same approximation Soul's own probe used,
followed by the real freeze_exact as defense in depth.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
S1: guard resolve()'s admitted-ref fetch with transfer.fsckObjects=true (it
had none), and have freeze_exact additionally run an explicit `git fsck`
against the selected revision so objects that were already local -- not only
freshly transferred ones -- get checked too. Refusal now happens through the
production path (resolve() then freeze()), not only a hand-built fetch.
S5: behavioural tests for six guards Soul's third pass found unpinned: root
ownership in harden_frozen_source, an exact duplicate leaf path, the
ensure_frozen_directory root-escape check on its own, the writer's pass-1
create_dir semantics, and copy_artifact's containment check. (The sixth,
case-sensitive ancestor alias, goes away under S7.)
S6: symlinks are now judged against the tree's real, final published path,
not `.partial`, the temporary name the tree is built under before the atomic
rename that publishes it. A dangling link whose target stays inside the root,
resolving every component that exists, is accepted; one that escapes is
refused, dangling or not. harden_frozen_source_tree defers symlink chain
resolution; the new validate_frozen_source_symlinks walk runs once, right
after the rename, and validate_frozen_source_symlink (used by observe_frozen
too) now shares the same dangling-tolerant resolver, so freeze-time and
observe-time agree.
S7: delete the case-folding collision/alias refusal in
refuse_conflicting_tree_entries. The host is case-sensitive ext4, the check
was inconsistent (leaf paths only, plain to_lowercase), and .git look-alikes
are now refused by git fsck itself (S1), independent of case. The exact-name
alias case (a leaf sharing a root with a tree recursing into it) stays
refused at write time by ensure_frozen_directory, which never writes through
an existing entry.
S9: hash_frozen_source_tree streams each file through the hasher in 64 KiB
chunks instead of fs::read-ing it whole, so peak RSS no longer scales with
the largest file in the tree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Better to have nothing than a harness that punishes refactoring
(operator ruling, 2026-09-22). Remove tools/verify-mutations/cut1.psd1
(428 lines), the only file in that directory. The harness that ran it,
eureka-mutations.ps1, was already deleted from the Eureka skill
(37c80e4).
docs/verify-transaction-cut.md still names the suite and its entry
ids; that file is Self's campaign record and is left untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>