Commit graph

261 commits

Author SHA1 Message Date
Meta Vens
99d3e86d03 Record the ninth batch: the frozen-source digest was exposed too
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>
2026-09-23 05:54:43 +02:00
Meta Vens
b029cd29b7 Record the eighth batch, and a migration hazard it did not raise
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>
2026-09-23 05:27:33 +02:00
Meta Vens
098febb996 Record Soul pass 8, and the cause under three passes of symptoms
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>
2026-09-23 04:51:18 +02:00
Meta Vens
bcf9de3837 Record the seventh batch: the pin now catches its own regression
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>
2026-09-23 04:37:09 +02:00
Meta Vens
9d86ca1f60 Record Soul pass 7: three pieces of verification that do not verify
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>
2026-09-23 04:14:54 +02:00
Meta Vens
7de13c8c1d Record the sixth batch, and withdraw the nosuid ruling I had not checked
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>
2026-09-22 23:39:58 +02:00
Meta Vens
a79d6282a3 Record Soul pass 6: the timing pin is a coin toss, and the digest misses setuid
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>
2026-09-22 22:52:10 +02:00
Meta Vens
38502d1c21 Record the fifth batch, and the containment escape my own ruling would have shipped
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>
2026-09-22 22:32:31 +02:00
Meta Vens
8da8e99a0b Record Soul pass 5: Cut 1 closes on mechanism, and the bill it leaves
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>
2026-09-22 21:42:40 +02:00
Meta Vens
12a7f16a61 Record the fourth Cut 1 fix batch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:47:45 +02:00
Meta Vens
9475dfa742 Land the fourth Cut 1 fix batch (S4-1 through S4-6)
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>
2026-09-22 20:30:12 +02:00
Meta Vens
d562a12238 Record Soul's fourth pass: the new symlink resolver is exponential
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:07:24 +02:00
Meta Vens
4c28727434 Record the third Cut 1 fix batch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:34:46 +02:00
Meta Vens
f540d50835 Fix S1 test: use a safe.directory-scoped git for post-chown commit-tree/update-ref
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>
2026-09-22 19:28:22 +02:00
Meta Vens
ad31f51273 Fix S1's fetch-guard test order: establish the checkout on the good commit first
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>
2026-09-22 19:27:04 +02:00
Meta Vens
748e9f3dfd Fix the fix batch: S6 test against the new symlink split, S1 test around the HTTPS-only binding
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>
2026-09-22 19:25:32 +02:00
Meta Vens
57ecd43d6b Land the third Cut 1 fix batch: fsck on every fetch, published-location symlink checks, streamed hashing
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>
2026-09-22 19:21:37 +02:00
Meta Vens
04afdedcf0 Record Soul's third pass on Cut 1 and rule the third fix batch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:56:38 +02:00
Meta Vens
071bf19da6 Delete the Cut 1 mutation suite
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>
2026-09-22 18:42:12 +02:00
Meta Vens
81d1009aa2 Record the second Cut 1 fix batch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:33:24 +02:00
Meta Vens
82e0edfaf7 Land the second Cut 1 fix batch: F2/F3 write-outside-root regression, F4 hardening, F5 chain-aware symlink check
Soul's second pass on the Cut 1 fix batch found a new regression (a
duplicate/aliased tree name lets the raw freeze write outside its root) and
three pre-existing gaps: two redundant hardening passes with no pin, a
lexical (not chain-resolving) symlink escape check, and unpinned Docker argv
combinations. F1's perf finding (40-80x slower than the old git archive path)
is addressed here too.

F2/F3 (write-outside-root, two required layers):
- git_tree_entries now calls refuse_conflicting_tree_entries, which refuses
  a tree where two leaf paths collide case-insensitively, or where one leaf
  path is a case-folded ancestor of another (the exact shape
  `git hash-object --literally` lets through: a symlink and a subtree
  sharing one name). The exact-revision fetch and both clone paths
  (ensure_checkout, materialize_gitlink_raw) enable transfer.fsckObjects.
- The writer itself never trusts that check alone. ensure_frozen_directory
  is the only way materialize_tree_raw obtains a directory to write into: a
  path must already be a directory this freeze created (tracked in
  created_dirs, shared across the superproject and every Gitlink), or must
  not exist at all; anything else is refused rather than traversed.
  Symlinks are written in a dedicated last pass, after every directory and
  regular file the tree needs already exists.

F4: freeze_exact now calls harden_frozen_source once, last, instead of
harden then a separate validate_frozen_source pass over the same tree.
validate_frozen_source stays as the read-only re-check observe_frozen uses
on an already-hardened tree it did not just write.

F5: validate_frozen_source_symlink resolves the whole symlink chain with
fs::canonicalize instead of lexically counting ".." components, which
undercounts a chain that loops back through another symlink (D -> .,
L -> D/D/.../../../../etc/passwd). copy_artifact (the one place besides
freeze that reads through a possibly-attacker-controlled tree) gets the same
whole-chain resolution: it now takes the artifact's containment root and
refuses a source that resolves outside it, closing the gap where an
intermediate symlink in a runner's own build output could be followed.

F1: materialize_tree_raw bulk-fetches every blob a tree needs in one
`git fetch --stdin` round trip (Git 2.36+, no argv-length limit) instead of
letting a blob:none partial clone lazily fetch each one as `cat-file --batch`
reads it. Blobs stream straight to their destination file instead of being
held in a BTreeMap for the whole tree. read_blobs is replaced by
stream_blobs, which drains stderr on a dedicated thread and waits on the
child on every return path, including an error from the per-object
callback (F9).

F6: pins four previously-unpinned Docker argv/environment combinations
(N1: --read-only survives a Named network plus a cache mount; N2: a secret
mount stays read-only alongside a plain environment entry; N3: the source
stamp is never dropped alongside a secret and a plain environment entry;
N5: an LFS pointer file sets contains_lfs_pointers).

Tests: 142 to 152. cut1.psd1 grows by 12 entries (5 for F1-F5, 4 for F6's
N1/N2/N3/N5, and one recording the F2 fsck flag as not yet reached -- see
below); all 28 non-comment entries in the suite now kill.

Discrepancy: cut1-fix-f2-no-fsck-on-fetch could not be pinned. Every tree
this crate's own tools can build that fsck's duplicateEntries check rejects
also has a literal duplicate path or a case-folded ancestor collision that
refuse_conflicting_tree_entries rejects first, unconditionally, before the
fetch's fsck result can matter. The flag stays as defense in depth (fsck's
other structural checks are broader than this crate's own parser), recorded
as not yet reached in cut1.psd1 rather than removed or falsely claimed
unreachable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:29:36 +02:00
Meta Vens
f7be933de3 Record Q-V9 and Q-V10 ruled A by Self; every verify question is now settled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:59:21 +02:00
Meta Vens
e26a0992ea debug: four-entry isolation 2026-09-22 17:58:51 +02:00
Meta Vens
02a81fa1ae debug: isolate f1 mutant 2026-09-22 17:57:10 +02:00
Meta Vens
3f6d690f92 wip6 2026-09-22 17:51:05 +02:00
Meta Vens
43c7a6e46d wip5 fix spy log perms 2026-09-22 17:47:29 +02:00
Meta Vens
47912bc6b5 wip4 remove scratch mutator 2026-09-22 17:46:08 +02:00
Meta Vens
f8baa93dac wip3 tests+psd1 2026-09-22 17:46:02 +02:00
Meta Vens
2ee7c225e2 wip2 2026-09-22 17:41:06 +02:00
Meta Vens
4aa8d3b3b7 Record Epiphany's measured build memory under Q-V6
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:34:00 +02:00
Meta Vens
d2a98edbaa Record Soul on the Cut 1 fix batch: the raw freeze is slow and can write outside its root; rule the fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:26:23 +02:00
Meta Vens
9fb6c1a615 Record Q-V6: 2 jobs, 4 CPU and 12 GiB each, from measured build memory
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:12:02 +02:00
Meta Vens
686f3d0839 Record the Cut 1 fix batch and the eol=lf ruling
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:51:06 +02:00
Meta Vens
95016e817d Land F2: freeze writes blobs raw, per Self's ruling on the Eve survey finding
Self reviewed the F2 survey finding (Eve's gitlink under Ghostlight carries
`packages/*/dist/** text eol=lf`) and ruled it is not a real dependency on an
archive transform: Git stores text blobs as LF, `eol=lf` checks them out as
LF, so no conversion happens. Self verified this directly at Ghostlight's
pinned Eve gitlink revision `672c0c1e9fc828b63752345385727b06c50491d0` — all
26 files under `packages/*/dist/` hash identically between `git archive` and
`git hash-object --no-filters`, and none contains a CR. Stopping for the
survey was correct; the ruling is to proceed.

Survey table (repo, attributes found, verdict), recorded here as ruled:

| Repo (deploy target) | Binding source | .gitattributes findings | Verdict |
|---|---|---|---|
| Ghostlight | idunn/yggdrasil/bindings/ghostlight.toml.in | none | clean |
| Ghostlight's gitlink vendor/eve -> Eve | same binding | packages/*/dist/** text eol=lf | transform present, but a no-op on stored LF content (Self's evidence above) |
| Odin | idunn/yggdrasil/bindings/odin.toml.in | none | clean |
| CodexConnector | idunn/yggdrasil/bindings/codex-connector.toml.in | none | clean |
| Muninn (raven-muninn) | idunn/yggdrasil/bindings/raven-muninn.toml.in | none | clean |
| Heimdall | deployment/idunn-bindings/heimdall.toml.template (a second, previously unlisted binding directory, installed live 2026-09-06) | none | clean |
| Heimdall's gitlink -> CultLib | same binding | * text=auto only (F12) | clean |

`freeze_exact` now writes every tree entry raw from the object store:
`git ls-tree -r -z` for the manifest, `git cat-file --batch` for content, with
no `git archive` anywhere in the path and so no attribute-driven transform of
any kind. Modes and symlinks are preserved exactly as the tree records them.
Gitlinks are frozen the same raw way, through the same primitive, at their
own path. LFS content is never fetched; `freeze_exact` now also returns
whether any materialized file is an LFS pointer (recorded in the frozen
result's own return value — not yet plumbed into `FrozenSourceReceipt`, which
has no field for it; that is follow-up scope, not this fix).

Deleted: `git_archive_into`, `materialize_gitlink_archive`, and the
now-unused `tar_program` field — the archive-and-extract path they built has
no remaining caller. `GitSourceDriver::new` no longer takes a tar program.

Tests:
- `freeze_exact_is_byte_exact_across_every_attribute_transform`: a fixture
  with `eol=crlf`, `export-ignore`, `export-subst`, `ident`, a symlink, the
  executable bit, and a Gitlink. Every frozen file's bytes are asserted equal
  to `git cat-file blob` at the selected revision.
- `freeze_exact_matches_archive_when_untransformed_and_diverges_when_transformed`:
  the deploy-equivalence test Self's ruling asked for. An `eol=lf` file over
  already-LF content matches today's independently-reconstructed `git archive`
  tree exactly; a second file with `eol=crlf` over the same LF content
  diverges from it, so the equivalence check has teeth rather than trivially
  passing for every fixture.
- `exact_git_blobs_become_root_owned_immutable_source_without_git_metadata`
  (renamed from the git-archive-era test) now exercises `materialize_tree_raw`.
- `tools/verify-mutations/cut1.psd1`: `cut1-fix-freeze-skips-gitlinks`
  retargeted at the new call, and a new `cut1-fix-f2-revert-to-git-archive`
  entry that reverts `materialize_tree_raw` to a `git archive | tar` pipeline;
  it must die against the byte-exact test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:48:17 +02:00
Meta Vens
7a5d32f02d Land the Cut 1 fix batch: pin every argv branch, one fetch, dead StepPort gone
Soul's pass on Cuts 0-1 found the argv-lowering test pinned only the
default runner path (13 of 14 mutants survived), freeze fetching the
same revision twice, an untested Gitlink loop, dead StepPort/StepOutcome
code, and a stale guide.md line. This lands Self's rulings for all of
it except F2 (the byte-exact freeze): the F2 survey found that Eve, a
Gitlink of the Ghostlight deploy target, depends on an archive
transform (`.gitattributes` eol=lf under packages/*/dist/**), so per
ruling 1 the byte-exact rewrite is withheld and reported instead.

- F1: `docker_run_args`/`ContainerSpec::for_step` are now pinned across
  every network branch (none/bridge/named), the full CPU quota range
  (100/150/250/333/800), cache root present/absent, and secret mounts,
  with dedicated fixtures for the cache-root and secret-file validation
  calls so a mutant that skips either is caught.
- F3: `freeze` fetches the exact revision once, through `freeze_exact`,
  instead of fetching before it and again inside it.
- F5: `freeze_exact_materializes_gitlinks` pins that the Gitlink loop
  actually runs.
- F7: deletes the dead `StepPort`/`StepOutcome` types; `run_step` never
  had a caller, and `docker()` is the one Docker spawn path.
- Stamp override: `ContainerSpec::for_step` now refuses a step
  environment name that collides with the Idunn source stamp, so the
  invariant holds at the one place every runner's environment is built,
  not only at deploy's separate `admit` check.
- docs/guide.md:11 no longer claims the retired generation still runs
  on Yggdrasil.

F8 (the `--cap-drop` negative check) is a reporting fix, not a code
change: `rg -n '"--cap-drop"' src/` now returns 4 hits (1 real site
plus 3 test fixtures, one of which this batch adds), never the "1 hit"
the map's verification text describes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:15:38 +02:00
Meta Vens
09ccfcb3e0 Record Soul on verify Cuts 0-1 and rule the fix: a byte-exact freeze, every argv branch pinned
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:03:42 +02:00
Meta Vens
31f72a5449 Record Idunn verify Cuts 0 and 1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:48:19 +02:00
Meta Vens
95e15efca3 Cut 1: one step primitive and one freeze primitive (behaviour-preserving)
Deletes the driver-private Docker argv assembly in run_in_workspace and
base_run_args and the OperatorBinding/CompiledDeploymentPlan coupling inside
GitSourceDriver::freeze's fetch-and-archive core. Both become one shared
primitive apiece, the shape Cut 4's verify worker will reuse instead of
growing a second copy:

- ContainerSpec + docker_run_args(spec, workspace, working_directory, argv):
  a plain, pure lowering to the exact `docker run` argv. ContainerSpec::for_step
  is the one constructor (a DockerRunnerBinding, a step's required
  environment, and the source stamp); it does the cache-root and secret
  validation against the filesystem before docker_run_args ever runs.
  run_in_workspace and materialize_external_input both build a ContainerSpec
  and call docker_run_args now; base_run_args is gone (rg '"--cap-drop"'
  src/ has exactly one hit).
- ExactSource {origin, checkout, gitlinks, recipe_path} plus
  GitSourceDriver::freeze_exact(&ExactSource, revision, transaction_id, root),
  which fetches, archives and hardens an exact revision with no
  OperatorBinding or compiled plan. freeze() now derives an ExactSource from
  the binding, keeps its own verify_exact_source pre-check against the
  durable resolution, and delegates the archive to freeze_exact.
- A StepPort trait (DockerRunnerDriver::run_step) that runs a lowered step
  and returns exit status plus a bounded stderr tail instead of bailing.
  Not yet reached from materialize (Cut 4 wires it into the verify worker);
  deploy keeps DockerRunnerDriver::docker's existing bail-on-failure error
  text unchanged.

materialize_external_input's curl step now goes through the same lowering,
which adds a harmless --workdir /workspace and one inert IDUNN_EXTERNAL_INPUT_ID
env var to its argv; curl's behavior (an absolute --output path) is
unaffected. materialize_gitlink_archive's cache-directory name changed from
the operator binding's target label to a hash of the checkout path, since
ExactSource carries no target field; this is bookkeeping only; a UUID
already made every gitlink checkout unique.

Adds three unit tests: container_spec_lowers_to_exact_docker_argv pins the
full docker run argv; required_environment_is_the_only_environment pins F6
(a binding environment entry the step does not require never leaks in);
freeze_exact_is_byte_exact_and_recipe_checked and its negative twin
freeze_exact_rejects_a_recipe_the_archive_transforms (an export-subst
recipe, which git archive substitutes and git cat-file does not) exercise
freeze_exact directly, with no OperatorBinding in sight.

tools/verify-mutations/cut1.psd1 covers all five rules with a revert or
loosening mutant apiece, run through the Eureka harness in the stopgap
container. Verified on Yggdrasil at this commit: cargo test --lib is 130
passed, 0 failed, 2 ignored (the P1 baseline of 126/2 plus these four new
tests), and every mutation entry was killed by its own test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:46:57 +02:00
Meta Vens
4845e989aa Record Q-V5 ruled A: no Windows verify host in this campaign
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:31:35 +02:00
Meta Vens
effe40b7a4 Record Q-V3 ruled A: requests name step ids, the binding admits programs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:29:03 +02:00
Meta Vens
e01ce88e61 Record Q-V8 ruled B: verify runs as its own unit; Cut 2 is unblocked
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:25:38 +02:00
Meta Vens
e0cfd0dcf5 Record Q-V7 ruled B: a no-sudo eureka account that can only request and read
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:24:30 +02:00
Meta Vens
c65a88500c Record Q-V4 ruled B via Add-Type: the harness writes its report as CultCache
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:23:22 +02:00
Meta Vens
2eb25b6a85 Cut 0: kill the stale docs, verify transaction campaign
Deletes the two stale "Current implementation boundary" sections in
docs/deployment-authority.md and docs/authority-map.md; both described a
pre-drivers.rs era and were wrong (drivers.rs has driven Git, runners,
systemd, nginx, brakes, CultCache and CultMesh for weeks). The still-true
second paragraph of the deployment-authority.md section (private plan state
is never published as Odin topology) moves to the end of "Typed deployment
state", reworded present tense.

Fixes README.md's stale claims: the CLI has five commands (cancel and
validate were missing), and docs/guide.md plus deploy/legacy/ describe a
generation that no longer runs anywhere, not "still what runs on yggdrasil".

Documents present truth in "Candidate promotion": advance_sealing resolves,
freezes, runs every recipe step and installs before the deployment brake is
consulted at all. The brake only gates the move into Starting, the first
Verse-visible change.

Docs only, no build required.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:21:11 +02:00
Meta Vens
37f1a1dfd9 Record Q-V2 ruled A: a request is an exact pushed commit and step ids
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:18:59 +02:00
Meta Vens
3020c7c56f Record Q-V1 ruled A: verify recipes live in the repo
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:18:05 +02:00
Meta Vens
52620ac699 Map the verify transaction: Idunn owns verification
Operator ruling 2026-09-22 after Starfire froze under agent build load. A
verify transaction takes an exact revision and declared steps, runs them in
capped runners in its own worker, returns a typed verdict, seals nothing and
sits outside the deployment brake. Imagination's map; ten operator questions
open, five of them blocking Cut 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:14:53 +02:00
Meta Vens
5b3f646bf1 Bump CultLib pin to 61549aa: anchor-export permission fix
Idunn's cultmesh-rs/cultcache-rs/cultnet-rs pin moves from c13b6ba to
CultLib origin/main 61549aa, picking up e9b91c1 ("Preserve public
trust-anchor parent permissions"). That fix matters here: Idunn calls
export_service_identity_trust_anchor from provisioning.rs:129 and
control_plane.rs:6633 to publish a public anchor into a directory another
service reads from. Before the fix, export routed through the same
prepare_parent helper as private enrollment and chmod'd that directory
0700 on every export, which would lock the legitimate reader out instead
of protecting anything. CultLib now splits ensure_parent (export, no
narrowing) from prepare_private_parent (enrollment only).

Encoding audit (c13b6ba..61549aa in cultcache-rs/cultnet-rs/cultmesh-rs/
contracts, 21 commits): none touch the eight record types Idunn persists
or verifies (GameCultServiceTrustAnchorRecord, IdunnDaemonHealthTrust
BindingRecord, IdunnDeploymentBrakeRecord, IdunnExpectedIncarnationRecord,
IdunnLifecycleBrakeRecord, IdunnProcessWriteLeaseRecord, IdunnRuntime
ActivationRecord, OdinRuntimeTopologyCorrelationRecord).
- 9e6027e adds only a test (byte_slot_migration.rs); no production flip
  lands in this range.
- 2ba404c/96138ff/a3e6877/etc. touch MediaStreamContracts and
  media_stream_wire record types (Muninn/Ratatoskr's media path) that
  Idunn never derives, persists or reads.
- c2a9a6e changes cultcache-rs's Windows-only atomic-replace retry
  (cfg(windows) MoveFileExW), not record encoding, and Idunn targets
  Linux.
- 47b9fc4/d3bc6e6/66f05e1 are cultnet-rs rudp/transport changes with no
  record-shape effect.
No re-encode guard in Idunn (drivers.rs, control_plane.rs, provisioning.rs
canonical-bytes checks) is at risk; this is a pin bump, not a store
migration.

Schema-id audit: fcc49cf ("Consolidate CultNet schema contracts") moves
JSON Schema files and changes their $id to
https://github.com/GameCult/CultLib/contracts/cultnet/... inside
packages/cultnet-ts and the C# reference implementation. Idunn's Rust
source and tests contain no reference to cultnet-ts/contracts,
schema_catalog, content_hash, CultNetSchemaRegistry or
builtin_schema_registry. Not a consumer.

Live-branch audit: origin/claude/windows-host-actuator (the branch the
live yggdrasil binary was built from, last commit 9dc80f8) is fully
contained in origin/main (git merge-base --is-ancestor confirms it) and
still carries the old c13b6ba pin. This bump is based on origin/main only;
the live branch is not touched here and would need its own follow-up.

Cargo.lock: cargo update -p cultmesh-rs -p cultcache-rs -p cultnet-rs
moved exactly four git-sourced packages (cultmesh-rs, cultcache-rs,
cultcache-rs-derive, cultnet-rs) to the new rev; the lockfile diff is only
their `source = git+...?rev=...` lines, no checksums, versions, or
unrelated packages changed.

No Idunn source changes were needed: CultLib made no API-breaking rename
or reshape Idunn depends on. cargo check --locked (Windows, compile gate
only) and cargo check --locked --tests both pass clean.

Verification ran on yggdrasil (Linux, the deploy target), not the
Windows workstation. Built a throwaway unprivileged toolchain (no sudo,
no docker) under ~/idunn-pin-61549aa since /srv/build is root:idunn
0755 and gamecultadmin has no docker-group access; rustc/cargo 1.95.0
matches the recipe's rust:1.95-bookworm image exactly. `cargo test --lib`:
117 passed, 9 failed, 2 ignored. All 9 failures require root privilege or
ufw (frozen-source root ownership, sealed-release root ownership, runner
cache/secret chown, service-credential 0400 root inode, write-lease
actuation, host-actuator-hub ufw allow) -- capabilities this unprivileged
non-containerized session does not have, not CultLib regressions. Proved
by re-running the identical suite at the old c13b6ba pin in the same
unprivileged environment: identical 117 passed / 9 failed, same 9 test
names, same errors. All 16 provisioning:: tests pass at the new pin,
including identity_enrollment_and_export_are_immutable_and_paths_cannot_alias
and provider_and_odin_anchor_exports_are_profile_exact_immutable_and_non_aliasing,
which cover the trust-anchor export path this bump fixes. `cargo build
--release --locked` also succeeds. Both throwaway directories
(~/idunn-pin-61549aa, ~/idunn-baseline-c13b6ba) were removed from
yggdrasil afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 19:25:50 +02:00
Meta Vens
2500529a08 Doc: what the Raven cut taught, measured
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:59:54 +02:00
Meta Vens
9dc80f8f5e An unobservable host is silence, not a death; stop-then-start for fixed-resource hosts
Continuity counted 'host actuator raven is not attached' as a workload
death and burned all three restart attempts inside the reattach window
after an Idunn restart, demoting a healthy Muninn. The hub now returns a
typed HostUnobservable for not-attached, detached, reconnected and
unanswered, and supervision skips the tick instead of restarting.

A route-less host workload that owns fixed host resources (one desktop,
one capture, fixed advertised ports) cannot overlap two incarnations. A
binding may now declare rollout strategy stop-then-start: the admitted
incumbent is stopped at Starting, before the candidate starts, and
supervision leaves the target alone from Starting on. A systemd-transient
workload still rolls out candidate-then-promote only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:42:35 +02:00