- F29. The Eureka harness is published at `github.com/GameCult/Eureka`
(`tools/eureka-mutations.ps1`). It is not in any target repo.
### 1.6 Probes (Yggdrasil, capped, cleaned up)
- P1. The Idunn baseline at `5b3f646` in `rust:1.95-bookworm`, using the
README's method (`/etc/machine-id` mounted), capped at 4 CPU and 8 GiB:
`cargo test --lib` gives **126 passed, 0 failed, 2 ignored**. The ignored
tests are `live_committing_record_is_unstuck_by_the_replacement_gate` and
`live_store_inventory`. The test profile compiled in 31.9 s with a warm
registry.
- P2. Idunn-shaped hardening (all of F5, user `65532:65532`, network `none`):
- pwsh 7.5.3 **crashes** at start with no writable `HOME` (a stack in
`GetPolicySettingFromConfigFile`).
- With `HOME=/workspace/.home` it works, and an in-place file write under
`/workspace` succeeds.
-`dotnet` 10.0.400 runs, and `dotnet new console` restores.
- node 24.14.1 runs.
- P3. A **locally built** image can be pinned. `docker image inspect` reports
`RepoDigests` equal to the image id under this Docker's image store, and
`docker run eureka-verify-rust@sha256:<id>` runs. So `require_pinned_image`
(`deployment.rs:1841-1847`) admits images built on Yggdrasil without a
registry.
---
## 2. Doctrine placement and the campaign authority map
**What verify is.** A verify transaction changes no artifact, revision,
configuration, schema, unit or authority binding of any managed target. It
installs nothing, publishes nothing to the Verse, and restarts nothing. It is
therefore **neither deployment nor continuity**, and neither brake governs it.
The doctrine says a target brake may gate only Idunn's mutation of that target,
and verify mutates no target, so **no brake may gate verify**. F16 shows that
the Body already treats build and test as unbraked: the deployment brake gates
the Verse-visible change, not the build.
**What governs it instead.** Verify spends shared host resources beside live
services, so its governor is a **resource authority**: the typed host verify
policy (Cut 2). That policy holds concurrency, per-runner ceilings, aggregate
budget, and a suspend switch (`max_concurrent = 0`). This is not a brake. It
names no target and cannot touch deployment or continuity.
**The survival invariant extends.** Idunn must be able to start, recover,
observe and report while verify is absent, broken, overloaded or wedged. F15
makes that a structural requirement: verify must never execute on the
scheduler thread. The recommended form (Q-V8) is a separate process with its
own stores, which the survival process never opens.
**Verdicts are evidence, not admission.** No Idunn decision reads a verdict.
Deployment does not consult verify; that coupling is out of scope.
### Authority map (campaign level)
- **Owner:** the Idunn verify worker (`idunn verify-serve`; see Q-V8). It alone
decides what ran, in what runner, under what limits, and what the verdict is.
- **Inputs:**
- verify request and cancel records from the request store;
- verify bindings (operator);
- the host verify policy (operator);
- exact Git objects fetched by SHA from the binding's origin;
- the verify recipe blob at that revision;
- step exit status, deadline expiry, and mutation report files.
- **Outputs:** verify transactions and verdicts in the verdict store, step logs
under the log root, and the CLI's status, log and wait output.
- **Derived state:**
- mutation counts are derived by Idunn from report entries, never supplied by
the harness;
- the `--wait` exit code is derived from the verdict;
- CLI status text is display only;
- logs are evidence, never read back to decide anything.
- **Forbidden writers:**
- Eureka agents running builds, tests or harnesses on Starfire (apart from
the Windows exception, Q-V5);
- ad hoc containers on Yggdrasil: the stopgap's `docker run`, and any
hand-launched test container;
- request-supplied argv, images, caps, network, environment or secrets;
- recipe-supplied images, network, caps, secrets or seccomp;
- the deploy scheduler, which must never read or write verify stores;
- the verify worker writing `control.cc`, the topology, brake stores,
bindings, release roots or deploy source checkouts.
- **Shared paths:**
- one container-spec lowering and one step-execution primitive, used by both
deploy materialize and verify (Cut 1);
- one exact-revision freeze primitive (Cut 1);
- CLI submission, cancel and restart recovery all go through the same
request, transaction and cancel record types.
- **Deletion line:** Cut 7 deletes the stopgap (`tools/stopgap/`, the SKILL.md
text, and `~/eureka-verify` plus its volumes and image on Yggdrasil).
**Rejected path: verify as a Deploy transaction that stops before Starting.**
It looks smaller, but it fails three ways:
1. It needs a `TargetDeclaration` with a service (F3).
2. It runs on the blocking scheduler (F15).
3. It would put verify records into `control.cc`, which every survival decision
reads.
---
## 3. Step 0b: identity, lifecycle and authority per persistent kind
| Kind | What names it | What happens to it over time | Who decides |
|---|---|---|---|
| **Verify recipe**`gamecult.idunn.verify_recipe.v1` | `(repo id, revision, recipe_path)`, content-addressed by `recipe_blob_sha256`. Step ids are unique within the recipe. | Immutable per revision. Read once at resolve. Its bytes are retained in the transaction, so the verdict names exactly what ran. Changing it means committing a new revision. | The repository authors capability: steps, argv, working dir, timeouts, required env names, report declaration, external inputs. It cannot name an image, network, caps, seccomp or secrets. |
| **Verify binding**`gamecult.idunn.verify_binding.v1` | `repo` field, which must equal the file stem of `/etc/gamecult/idunn/verify/bindings/<repo>.toml`. This namespace is **separate from deploy targets**: a different directory, schema and record types, so `odin` the verify repo and `odin` the deploy target never collide. | Edited by the operator from the gamecult-ops template. Read at each admission. Bytes and sha256 are snapshotted into the transaction, so a later edit never changes a running or finished transaction. | Operator (installed as root, from gamecult-ops). |
| **Host verify policy**`gamecult.idunn.verify_host_policy.v1` | Singleton per host at `/etc/gamecult/idunn/verify/host.toml`. Its `host` field must equal the worker's configured host name. | Re-read on each admission pass. Changes apply to new admissions only; running jobs keep the limits they were admitted under. `max_concurrent = 0` suspends admission. | Operator. |
| **Verify request**`idunn.verify_request` (v1), in the **request store** | `verify-<uuid v4>`, minted by the CLI. The same id becomes the transaction key. | Write-once. Consumed when the worker creates a transaction with the same id. Retired with that transaction to history. A request whose binding is missing is sealed `Rejected`, not left resident. | The requester (agent or operator) chooses the repo, a 40-hex revision, an optional step subset, and `requested_by`. Nothing privileged. |
| **Verify cancel**`idunn.verify_cancel` (v1), in the request store | Keyed by `verify_id`. | Write-once. The worker observes it: a queued transaction is sealed `Cancelled`, and a running one has its step container killed and is sealed `Cancelled`. The CLI refuses to cancel an unknown or terminal id. | Requester. |
| **Verify transaction / verdict**`idunn.verify_transaction` (v1), in the **verdict store** | `verify_id`. | Phases: `Admitted → Frozen → Running{step} → Terminal`. Terminal is one of `Passed`, `Failed`, `Error{kind}`, `TimedOut{step}`, `Cancelled`, `Interrupted`, `Rejected`. **Terminal is immutable.** It moves to the verify history store after `resident_terminal` newer terminals, and history is pruned to `history_limit`. **No resume:** a transaction that is not terminal when the worker starts is sealed `Interrupted`. | The verify worker is the only writer. |
| **Step outcome** (embedded in the transaction) | `(verify_id, step id)`. | Set once when the step ends: exit status, duration, deadline, log reference, optional decoded mutation report. Steps after a failed step are `NotRun`. | Worker. |
| **Step log** | `<log_root>/<verify_id>/<step>.log`. | Combined stdout and stderr, written while the step runs and capped at `log_byte_cap`. At step end its `sha256`, `bytes_kept`, `bytes_total` and `truncated` are recorded. Deleted when its transaction leaves history. | Worker. |
| **Mutation report**`gamecult.eureka.mutation_report.v1` (JSON at the xenos boundary; see Q-V4) | `(verify_id, step id)`, read from the step-declared path. | Read once after the step exits, decoded strictly, and embedded as a typed record. The source file dies with the workspace. | The harness produces entries. Idunn decodes them, derives the counts, and cross-checks them against the exit status. |
| **Frozen tree and runner workspaces** | `<workspace_root>/<verify_id>/{frozen,runner-<id>}`. | Created at `Frozen` and `Running`, deleted when the transaction goes terminal. At worker start, any directory whose id is terminal or unknown is deleted. | Worker. |
| **Step container** | name `idunn-verify-<verify_id>-<step>`, label `gamecult.idunn.verify=<verify_id>`. | Runs with `--rm`, and is killed on deadline, cancel or worker restart. Any container carrying the label with no live transaction is an orphan, and the worker kills it. | Worker. |
| **Verify source checkout** | `<verify_source_root>/<repo>`, a partial clone of the binding origin. **Separate from deploy checkouts** (F11), so fetching arbitrary branch SHAs never touches deploy refs. | A persistent cache that fetches by SHA. Recreated if its origin differs from the binding. | Worker, as the `idunn` uid. |
| **Runner images** | `name@sha256:<digest>` pinned in the verify binding (P3). | Built from a committed Dockerfile whose `FROM` is digest-pinned. Changed only by editing the binding. | Operator (Q-V10). |
| **Verify stores** | `/var/lib/gamecult/idunn-verify/requests.cc` (requests and cancels) and `/var/lib/gamecult/idunn-verify/verify.cc` plus `verify-history.cc` (transactions). | The request store is written by the CLI and read by the worker. The verdict store is written only by the worker and readable by the requester group. The deploy daemon never opens either. | Worker owns the verdict store; requesters own only request and cancel records (Q-V7). |
No cell is empty. The identity rule that the rest of the map depends on: **the
request id, transaction id, workspace directory, log directory and container
label are all one `verify_id`.** One id, so cleanup and orphan detection are
exact.
---
## 4. Operator questions
Each question gives its options, a recommendation, and what depends on it. The
**Q-V1: RULED A by the operator, 2026-09-22 ("A sounds good").** Recipes live in the repo at a binding-named path, read from the exact revision. Privilege stays in the operator verify binding.
*History, the question as asked:* **Q-V1. Where do verify recipes live?**
**Q-V2: RULED A by the operator, 2026-09-22 ("agreed").** A request carries an exact 40-hex commit that can be fetched from the binding's origin, plus an optional subset of step ids, and nothing else.
*History, the question as asked:* **Q-V2. What does a request carry?**
**Q-V3: RULED A by the operator, 2026-09-22 ("A is fine").** Each runner has an allowlist on the binding side, and a request names only step ids. The command line comes from the recipe at the verified revision and is admitted against the binding. The confinement is the container and the binding.
*History, the question as asked:* **Q-V3. How does the allowlist admit `pwsh`, `node` and `bash` without
**Q-V4: RULED B, via `Add-Type`, by the operator, 2026-09-22 ("yep").**
The harness writes its mutation report as CultCache itself. PowerShell 7 is .NET, so the harness loads `GameCult.Caching.dll`, CultLib's C# reference runtime at a pinned published version, with `Add-Type -Path`. It compiles the report document type inline with `Add-Type -TypeDefinition` and writes a `.cc` report to the step's declared path. Idunn decodes the report into its own Rust type, which is canonical, and cross-checks it against the exit status. A report that contradicts the exit status becomes `Error{ReportContradictsExit}`.
Why B over A, C or D:
- **A put load-bearing JSON between two GameCult components.** The operator's doctrine rules that out.
- **A and B's original premise was wrong.** It assumed PowerShell had no CultCache runtime, and Self's option D (an `idunn verify-report` subcommand) rested on the same assumption. The operator pointed out that PowerShell compiles and loads C#.
- **B gives one report path for every run on every host.** That includes the win32 QUIC runs that stay on Starfire outside Idunn (Q-V5). Under D those runs had no `idunn` binary and would have fallen back to text.
The cost is two declarations of the report type, one in C# and one in Rust. CultCache schema identity makes drift a loud decode refusal rather than a silent misread. Cut 5 changes accordingly: the harness side lands in Eureka and CultLib, and Idunn's side is the decoder plus the cross-check. Idunn parses no JSON and no stdout.
*History, the question as asked:* **Q-V4. What shape is the verdict, and how does mutation data reach it?**
**Q-V5: RULED A by the operator, 2026-09-22 ("A indeed").** This campaign has no Windows verify host. Win32 jobs stay on Starfire, run by hand, one at a time, with no burners. They still write the typed CultCache report (Q-V4). Cut 9 stays a placeholder until a dedicated Windows host exists.
*History, the question as asked:* **Q-V5. Is there a Windows verify host?** The QUIC win32 harness has to run on
**Q-V6: RULED by the operator, 2026-09-22 ("That's fine"), on measured data.** `max_concurrent = 2`. Each runner is capped at 400% CPU and **12288 MiB**. The aggregate budget is 800% CPU and **24576 MiB**, so live services keep half of Yggdrasil's CPU. Measurement: Ghostlight `cargo test --workspace --no-run -j 4` peaks at 5.7 GiB in 78 s, with a 3.4 GB target. Epiphany is not yet measurable, because its pinned Ghostlight rev has a submodule pointing at the deleted `GameCult/cultcache-py`. **Epiphany was measured later the same day**, after its Ghostlight pin moved past the dead submodule: 4.36 GiB, 92 s, 3.3 GiB target. The 12 GiB ceiling holds with about 2x headroom on both workspaces.
**Q-V7: RULED B by the operator, 2026-09-22 ("B sounds good to me too").** A dedicated `eureka` account on Yggdrasil holds an SSH key and is a member of group `idunn-verify`. It has no sudo. It can submit and cancel requests and can read verdicts and logs, and nothing else. The ban on ad hoc containers becomes structural rather than prose.
*History, the question as asked:* **Q-V7. How does a pipeline agent on Starfire reach Idunn?**
**Q-V8: RULED B by the operator, 2026-09-22 ("agreed").** Verify runs as a separate unit, `idunn-verify.service`, which runs `idunn verify-serve` from the same binary. It has its own stores and lock, and its `ReadWritePaths` are narrowed to its own roots and the `/srv/build` cache. It protects daemon survival and privilege isolation.
*History, the question as asked:* **Q-V8. What process topology?**
**Q-V9: RULED A by Self, 2026-09-22. The operator deferred ("I have no meaningful opinion on Q-V9").** `seccomp` is an explicit per-runner field in the operator binding. It defaults to `default`, and `unconfined` is permitted only where the binding names it. The verdict echoes it. The first user is CultLib's Linux TSan mutation.
*History, the question as asked:* **Q-V9. May a verify runner be bound with `seccomp = "unconfined"`?** CultLib's
**Q-V10: RULED A by Self, 2026-09-22.** This is a default, not a product fork, and the operator deferred the adjacent Q-V9. Runner images are built from Dockerfiles committed to gamecult-ops, on digest-pinned bases, and the binding pins each image by digest.
*History, the question as asked:* **Q-V10. Who builds runner images?**
- **2026-09-22: Soul on Cuts 0–1** (Opus, on Yggdrasil).
- **Held.**
- Behaviour is preserved. A fake-docker probe over 8 real call paths found the security flags byte-identical and in order.
- Only the declared differences appeared, plus one extra: secret environment variables are reordered (F4). That is harmless in Docker and is **accepted, now declared**.
- The numbers reproduce: tests 126 to 130, and cut1 5/5. Both needed a machine-id; see F10, fixed in the stopgap at Eureka `2fdc056`.
- **F1 (high): the argv test pins only the default runner path.** Thirteen of 14 mutants survive, including:
-`--cap-drop ALL` dropped only on `bridge`;
-`--cpus` rounded to 0.5;
- environment passthrough by prefix;
- ambient `GAMECULT_*`/`IDUNN_*` environment;
- writable secret mounts;
- explicit `none` lowered to `bridge`;
-`--read-only` dropped on a named network;
- an extra mount of the cache's parent;
- the secret and cache-root validation calls removed.
- **F2 (medium): `freeze_exact` is `git archive`, so it is not byte-exact.** Non-recipe files come through with `eol=crlf` applied, `export-ignore` dropped, `export-subst` and `ident` expanded, and LFS left as pointers. The recipe check does catch transforms on the recipe itself. The test asserts no byte-exactness at all.
- **F3:** deploy's `freeze` fetches twice.
- **F5:** the gitlink loop is untested; skipping it survives.
- **F7:** `StepPort` and `StepOutcome` are dead code, and `run_step` duplicates `docker()`.
- **F8:** the `--cap-drop` negative check also matches a test.
- **F9:**
-`docs/guide.md:11` is still stale.
- These gamecult-ops files still route through the dead `idunn redeploy` script: `runbooks/odin-yggdrasil.md:88`, `ghostlight-dungeon-yggdrasil.md:134,243`, `heimdall-discord-launch.md:125,144`, `epiphany-yggdrasil-deploy.md:156`, and the `bootstrap-ghostlight-yggdrasil.sh` and `bootstrap-codex-connector-yggdrasil.sh` scripts.
- **Pre-existing:** a binding environment variable with the source stamp's name overrides the stamp.
- **Self's rulings for the Cut 1 fix batch, 2026-09-22:**
- **F1.** The lowering is pinned on every branch: network `none`, `bridge` and named; cache present and absent; secrets present; required environment present; a range of CPU quotas (100, 150, 250, 333, 800). Every Soul mutant becomes an entry and must die. The validation calls are pinned by tests that go through `ContainerSpec::for_step`.
- **F2. The frozen tree is the commit's blobs, exactly.** Blobs are written raw from the object store (`ls-tree -r -z` plus `cat-file --batch`), with no attribute transforms. Modes and symlinks are preserved as the tree records them, and gitlinks are handled as today. LFS content is **not fetched**: a pointer is the blob, and the verdict records that the tree contains LFS pointers. Verifying an LFS repository's real content is a follow-up. **Deploy shares this freeze, so its behaviour would change.** Before changing it, Hands surveys the `.gitattributes` of every Idunn deploy target (the target repos named in the gamecult-ops bindings) for `export-ignore`, `export-subst`, `ident`, `eol`/`text`, `filter` and LFS. If any deploy target depends on an archive transform, **stop and report**: that is a fork for the operator. The test asserts byte equality against `cat-file` for every file of a fixture that has CRLF, `export-ignore`, `export-subst`, `ident`, a symlink, the executable bit and a gitlink.
- **F3:** `freeze` fetches once.
- **F5:** add a gitlink test and entry.
- **F7:** delete the dead `StepPort`/`StepOutcome`. There is one Docker spawn path; `run_step` goes through `docker()`.
- **F8:** make the negative check `rg` over `src/` excluding `#[cfg(test)]`, or state its hit count.
- **F9:** fix `guide.md:11`, and repoint every listed gamecult-ops runbook and bootstrap script at the current procedure (`idunn up` plus a brake release, as in `runbooks/idunn-host-raven.md`), or delete the steps. Delete the dead script.
- **Stamp override:** refuse a binding environment variable whose name collides with the source stamp.
- **2026-09-22: the Cut 1 fix batch landed** (Sonnet, verified on Yggdrasil).
- **Commits:** Idunn `7a5d32f` (F1, F3, F5, F7, F9, stamp override) and `95016e8` (F2); gamecult-ops `62b41b1` (runbooks and bootstraps moved off the dead `idunn redeploy`, script deleted).
- **Tests:** 130 to 142. `cut1.psd1` 17/17 killed. The revert to `git archive` dies on `freeze_exact_is_byte_exact_across_every_attribute_transform`.
- **The F2 survey found one attribute:** Eve's `packages/*/dist/** text eol=lf`, reached through Ghostlight's gitlink. **Self ruled it not a fork.**`eol=lf` transforms nothing on stored-LF blobs. At Eve rev `672c0c1e`, all 26 `dist` files hash the same through `git archive` and through `hash-object --no-filters`, and none contains a CR. Every other deploy target (Ghostlight, Odin, CodexConnector, Muninn, Heimdall and its CultLib gitlink) is clean. So the byte-exact freeze produces exactly what deploy got before.
- **Deleted:** `git_archive_into`, `materialize_gitlink_archive`, and the dead `tar_program`.
- **Not yet reached:** `cut1-freeze-exact-recipe-check`. Both sides are now the same raw blob, so it is kept as defense in depth.
- **Soul's S8 is not yet reached.** `harden_frozen_source` enforces the same invariants first.
- **Follow-up:** `freeze_exact` returns an LFS-pointer flag that `FrozenSourceReceipt` does not carry yet. It must reach the verify verdict (Cut 2/4).
- **Recorded, not this cut's:** `test-voidbot-swarm-yggdrasil.sh` fails on `add-daemon-health-trust-binding`. That failure is pre-existing.
- **2026-09-22: Soul on the Cut 1 fix batch** (Opus, on Yggdrasil). **Cut 1 does not close.**
- **Held.**
- 142 tests; cut1 17/17 killed.
- Byte-exact on a 3,026-entry fixture: 0 mismatches, against 7 on the old commit. The fixture covers CRLF, export-*, ident, LFS, the three symlink kinds, the executable bit, a gitlink carrying attributes, hostile names, and 6 MB and empty files.
- Hostile trees are refused.
- **Deploy equivalence: identical digests on all five live targets.**
- F3: one fetch.
- The stamp refusal holds.
- No live `redeploy` remains.
- **F1 (high): the raw freeze is 40 to 80 times slower on real targets.** Ghostlight takes 412 s against 5.3 s, Heimdall 457 s against 8.5 s, Odin 90 s against 2.2 s. `cat-file --batch` on the blobless checkout (`drivers.rs:788,1053,1245`) fetches missing blobs lazily in small packs. `read_blobs` also holds the whole tree in memory.
- **F2 (high, new regression): a tree with duplicate or aliased names makes root write outside the frozen root.** A symlink `a` pointing outward plus a subtree `a/pwn` gives a root-owned `pwn` outside the root, left behind after the freeze fails. `fs::write`, `symlink` and `create_dir_all` follow symlinks and run before hardening (`:1173-1203`, `:1350`). The old tar path refused this tree. The fetch does not set `transfer.fsckObjects`.
- **F3 (medium):** by the same mechanism, the recipe check *can* fail, and a single alias silently freezes the wrong content. The "not yet reached" claim is false.
- **F4 (medium):** no test pins the hardening. Removing both `harden_frozen_source` and `validate_frozen_source` from `freeze_exact` survives all 142 tests. The two calls are redundant.
- **F5 (medium, pre-existing):** a symlink chain (`D -> .`, `L -> D/D/../../../etc/passwd`) passes the lexical check (`:5516`) and resolves to `/etc/passwd`.
- **F6 (medium):** unpinned combinations. A named network plus a cache drops `--read-only` (N1). A secret mount becomes writable when a plain environment variable is present (N2). The stamp is dropped when a secret and a plain variable are both present (N3). The LFS flag is never set (N5).
- **F7 (medium):** `bootstrap-ghostlight-yggdrasil.sh` and `bootstrap-codex-connector-yggdrasil.sh` still provision the previous generation (`/srv/odin/deploy-manifests`, `daemon-health-trust.cc`, `/srv/ghostlight/current`, the old identity store). Only their last line changed, and neither has a brake release.
- **F8 (low):** `heimdall-discord-launch.md` says `idunn up heimdall`, but Heimdall has no installed binding.
- **F9 (low):** `read_blobs` never drains stderr, and its error paths leave zombie processes.
- **Self's rulings for the second Cut 1 fix batch, 2026-09-22:**
- **F1:** fetch the exact commit's whole tree in **one bulk fetch** (depth 1, no blob filter), then read blobs locally. Stream to disk rather than holding the tree in memory. Target: within twice the old times on the five live targets, measured on Yggdrasil at the host's git version if the container can match it. Otherwise say which version you measured with.
- **F2 and F3: two layers, both required.**
- Fetch with `transfer.fsckObjects=true` (or run fsck on the fetched tree) and refuse duplicate, aliased and case-folding-colliding entries before anything is written.
- The writer never follows a link: each parent component must be a real directory created by the freeze itself, and symlinks are written last.
- Fixtures: Soul's duplicate-name and alias trees. Nothing is written outside the root, and the error is refused by name.
- **F4:** keep one hardening pass, the one that runs last, and pin it. Tests assert that absolute, escaping and chained symlinks are refused and that modes are root-owned 0444/0555. Its removal must die.
- **F5:** the symlink check resolves the full chain inside the root; a lexical check is not enough. The chain fixture must refuse.
- **F6:** pin N1, N2, N3 and N5 (the flag is set and tested). Carrying the flag into `FrozenSourceReceipt` stays in Cut 2/4.
- **F7:** delete the previous generation's provisioning steps from both bootstrap scripts. Keep only what is true under the v2 bindings, and add the brake-release step. If a script has nothing true left, delete it and point to the binding-install runbook.
- **F8:** mark the Heimdall runbook step as gated on installing its binding.
- **F9:** drain stderr and wait on the child on every path.
- **2026-09-22: the second Cut 1 fix batch landed** (Sonnet, on Yggdrasil), Idunn `82e0edf` and gamecult-ops `9fc3af0`.
- **F1:** one bulk `git fetch --stdin` of every blob, plus a streaming writer.
| Target | Freeze time | Previous | Ratio |
|---|---|---|---|
| Ghostlight | 5.17 s | 5.3 s | 0.98x |
| Heimdall | 10.08 s | 8.5 s | 1.19x |
| Odin | 2.20 s | 2.2 s | 1.0x |
| Muninn | 1.93 s | 1.9 s | 1.0x |
| CodexConnector | 1.76 s | 1.8 s | 0.98x |
Measured with git 2.39 in the container, not the host's 2.47.
- **Digests are identical on all five live targets.**
- **F2 and F3:** `refuse_conflicting_tree_entries` runs before any write, and `ensure_frozen_directory` never follows a link. `transfer.fsckObjects` is kept as defense in depth. Its entry is *not yet reached*, because the refusal always fires first.
- **F4:** one hardening pass, pinned.
- **F5:** chain-aware symlink resolution.
- **F6:** N1, N2, N3 and N5 pinned.
- **F9:** stderr is drained and the child is waited on.
- **F7:** the four v1 artifacts are removed from both bootstraps, with a v2 anchor export and a brake release.
- **F8:** the Heimdall step is gated.
- Tests go from 142 to 152. cut1 is 28/28 killed.
- **Operator step, outside this campaign:** the Ghostlight and CodexConnector bindings still carry a placeholder `expected_signer_identity_id`. The scripts print the derived key.
- **2026-09-22: Soul's third pass on Cut 1** (Opus, on Yggdrasil).
- **Held:**
- 152 tests pass. cut1 killed 26/26 before its deletion; the commit message said 28.
- Speed is 1.0 to 1.3x across the five targets, and the digests are identical.
- A Forgejo origin works, and so does protocol v2 without `allowAnySHA1InWant`.
- Freezing 30k files plus a 200 MiB blob takes two fetches.
- Duplicate, alias and gitlink attacks are refused, and nothing is written outside the root.
- The chain resolver works for cycles, depth, and `copy_artifact`.
- **Cut 1 does not close.**
- **S1, medium-high:** `resolve()`'s fetch (`drivers.rs:1735`) has no fsck, so fsck never sees the main repo's trees. The claim "not yet reached" is false. Two sibling trees both named `d` freeze merged. fsck-rejected names freeze too: `.GIT`, `git~1`, `.git.` and `.git` with a zero-width non-joiner or a trailing space, `/` inside a name, and zero-padded modes.
- **S2, medium:** both bootstraps print `provider-health-public-key`, but the binding needs the identity id (`provider-health-identity-id`).
- **S3, medium:** neither bootstrap can work. The `ghostlight`, `codex-connector` and `odin` templates set `checkout = /srv/build/idunn-sources/*`, but the installed unit uses `--source-root /var/lib/gamecult/idunn/sources`, and the host's `odin.toml` already uses that. The two scripts contradict each other about `codex-connector.service`. v1 provisioning remains in both. The wiring test pins the old unit text.
- **S4, low:** Heimdall's binding *is* installed on Yggdrasil, so the runbook note is wrong about the host.
- **S5, low:** these guards are unpinned:
- root ownership (X04);
- an exact duplicate path (X06);
- case-sensitive ancestor alias (X08);
-`ensure_frozen_directory` containment (X09);
- pass 1 `create_dir_all` (X10);
-`copy_artifact` containment (X11).
- **S6, low:** a symlink is validated against `.partial` but published after the rename. Dangling in-root links are now refused, where the old lexical check accepted them.
- **S7, low:** the case-folding refusal is partial and inconsistent (files only, `to_lowercase`). The host is case-sensitive ext4.
- **S9, low, pre-existing:** `hash_frozen_source_tree` reads each file whole. A 200 MiB file costs 232 MB of RSS.
- **Self's rulings for the third Cut 1 fix batch, 2026-09-22:**
- **S1:** fsck guards every fetch that brings the main repository's objects. That is `resolve()` as well as `freeze_exact`. Independently, `freeze_exact` also runs fsck on the selected tree, so that objects already present in the local store are checked too. Refuse every `.git` look-alike by name, case-insensitively and under fsck's own rules, even though the host is case-sensitive, because a frozen tree can be copied elsewhere. Fixtures: Soul's `dup-trees` fixture and each look-alike. Each must refuse through the production path (`resolve`, then `freeze`), not a hand-built fetch.
- **S2:** fixed in the S3 deletion.
- **S3: delete both bootstrap scripts.** Nothing true is left in them, which is the F7 ruling applied. Point every caller at the binding-install procedure, which includes the `provider-health-identity-id` step. **Correct the `ghostlight`, `codex-connector` and `odin` templates to the source root the installed unit uses** (`/var/lib/gamecult/idunn/sources/*`), matching the host's installed `odin.toml`. Delete the wiring test's assertions on old unit text, or the whole test if nothing true remains. This is gamecult-ops hygiene the campaign exposed, and it lands in the same batch.
- **S4:** correct the Heimdall runbook note. The binding is installed on the host and absent from the repo.
- **S5:** a behavioural test for each guard, through `freeze_exact` wherever the guard is reachable from there. No mutation suite.
- **S6:** validate symlinks against their final published location. Allow a dangling link whose target stays inside the root (resolving every component that exists), and refuse one that escapes. Test both.
- **S7:** delete the case-folding refusal. The host is case-sensitive, and the partial check is inconsistent. The `.git` look-alikes are covered by S1.
- **2026-09-22: the third Cut 1 fix batch landed.** Idunn `57ecd43..f540d50`, gamecult-ops `2f3c1b3` and `9d0e37e`.
- **S1:** fsck guards `resolve()`'s fetch, and `freeze_exact` fscks explicitly. The dup-trees fixture and all six `.git` look-alikes are refused through the production path.
- **S3:** both bootstrap scripts are deleted (319 lines). Callers point at the binding-install procedure. The three `checkout` templates now match the installed source root. The stale wiring assertions are gone.
- **S4:** the Heimdall note is corrected.
- **S5:** six behavioural tests.
- **S6:** symlinks are judged at their published location. A dangling in-root link is accepted, and the `.partial` case refuses.
- **S7:** the case-fold refusal is deleted.
- **S9:** hashing streams. Peak RSS on a 200 MiB blob falls from 232 MB to 25.4 MB.
- Tests: 152 to 160. Digests unchanged on all five targets, with 0 mismatches over 1,975 entries. Times run 0.96x to 1.17x.
- **Discrepancies to judge:**
- No test stands up a real HTTPS origin, so the S1 test reproduces `resolve()`'s fetch invocation and then calls the real `freeze_exact`.
-`test-ghostlight-yggdrasil-wiring.sh` still pins the static units and the bespoke deploy scripts. Whether that whole generation is superseded by the binding and transient-unit path is a **follow-up**, larger than this batch.
- **2026-09-22: Soul's fourth pass on Cut 1** (Opus, on Yggdrasil). **Cut 1 does not close.**
- **Held, and proven harder than the shipped tests show.** Soul stood up a real smart-HTTPS origin in the container, with a self-signed CA and `git-http-backend`, and froze a clean repo end to end over it. Every ingress carries fsck (`:833`, `:1137`, `:1525`, `:1536`, `:1610`, `:1629`, `:1736`). Eleven hostile fixtures, brought in by an **unguarded** fetch first so the objects were already local, were all refused by `freeze_exact`'s own fsck: dup-trees, six `.git` look-alikes, and three attacks nobody had tried (an unsorted tree, a zero-padded mode, and mode 120000 on a tree). S6 holds across eight link shapes. S7 cannot collide on ext4. S9's RSS holds at 27 MB for 30k files plus a 200 MiB blob. The gamecult-ops templates match the host, and no caller invokes a deleted bootstrap. 160 tests; digests identical on four targets over 1,109 entries.
- **S4-1, medium-high, introduced by this batch: the symlink resolver is exponential.** `resolve_frozen_source_symlink` (`drivers.rs:5878-5945`) recurses per path component, so `MAX_HOPS = 40` bounds depth rather than work. Measured: n=16 2.7 s, n=18 10.7 s, n=20 43.7 s, and n=30 about 6 hours. Through production, `freeze_exact` with n=18 returned `Ok` after 15.9 s and `observe_frozen` spent another 15.8 s. Both run on the single scheduler thread with no timeout, so 40 small symlinks pushed to any bound repository wedge continuity for every target. **It is a regression:** at `82e0edf` the check was `path.canonicalize()`, which did the n=30 fixture in 2 ms and let the kernel's ELOOP refuse it.
- **S4-2, medium: the streamed hash has no behavioural pin.** Hashing zero bytes of every file, or only the first 64 KiB, leaves 160 tests green. The digest is the receipt anchor, so a content-blind digest lets two trees share one receipt.
- **S4-3, medium: three of the five S5 tests do not kill their guard.** The duplicate-path check, `ensure_frozen_directory`'s root containment and `create_dir` against `create_dir_all` all survive deletion, because another refusal fires first. The batch note said six tests; five landed.
- **S4-4, low: all three fsck guards survive deletion**, because the shipped test hand-builds the argv and every fixture is refused at the fetch, so the arm that exercises the mechanism never runs.
- **S4-5, low:** nothing proves a legitimate in-root `..` link freezes.
- **S4-6, low, pre-existing:** the writer buffers a whole blob for a symlink. A 64 MiB blob with mode 120000 takes RSS to 206 MB before failing closed.
- **Could not run, and not this batch's:** Heimdall's digest fails at every revision, including `82e0edf`. Its `vendor/CultLib` gitlink names `dfe07704`, which GitHub no longer serves. **So `idunn up heimdall` would fail on the host today.** Recorded as an ops follow-up.
- **Self's rulings for the fourth Cut 1 fix batch, 2026-09-22:**
- **S4-1: bound the work, not the depth.** Resolve iteratively rather than recursively: keep one resolved prefix, and count **every link traversal** against one budget, so a chain costs O(n). Memoise resolved prefixes. Keep S6's behaviour exactly: a dangling in-root link freezes, an escape refuses. Add a test with Soul's `a0 -> .`, `ak -> a{k-1}/a{k-1}` fixture at n=20 that must complete in **under a second** and refuse or accept as S6 says. Say which.
- **S4-2:** pin the hash with two trees that differ only in file content past 64 KiB, and assert their digests differ. The zero-length-buffer and first-chunk-only mutants must die.
- **S4-3:** each S5 test isolates its own guard. Build a fixture that **only** that guard refuses, so deleting the guard makes the test fail rather than another refusal covering it. Do that for the duplicate path, root containment and `create_dir`.
- **S4-4:** commit Soul's already-local fixtures as tests. Establish the checkout on a clean commit, bring the hostile objects in with an unguarded fetch, then prove `freeze_exact`'s own fsck refuses. Deleting each of the three guards must fail a test.
- **S4-5:** test that a legitimate in-root `..` link freezes.
- **S4-6:** read a symlink target without buffering a whole blob, and refuse a target longer than the platform's limit.
- **Heimdall:** record as an ops follow-up, **FU-Heimdall-Gitlink**. The binding is installed, and its gitlink names a commit GitHub does not serve. That is not this campaign's to fix, but the deploy is broken today and the operator should know.
- **2026-09-22: the fourth Cut 1 fix batch landed** at `9475dfa` (Sonnet, on Yggdrasil). One commit, `src/drivers.rs` only, +940/-35.
- **S4-1:** the resolver is iterative and memoised, and counts link traversals against one budget. Soul's n=20 chain goes from **43,700 ms to effectively instant**, and it freezes, which matches S6: every hop resolves inside the root and nothing escapes.
- **S4-2:** the hash is pinned by two trees differing only past the first chunk. Both the zero-length-buffer and first-chunk-only mutants die.
- **S4-3:** each of the three guards has a test that isolates it, and all three mutants die.
- **S4-4:** Soul's 11 already-local hostile fixtures are committed as tests. Deleting the explicit fsck, or the freeze's own fetch flag, each fails a test.
- **S4-5 and S4-6:** landed.
- Tests 160 to 168. Digests identical on all five reachable targets over 1,109 entries. Heimdall is still unreachable for the unrelated gitlink reason.
- **Gap, honestly reported: `resolve()`'s own fsck flag has no test.** Reaching the real `resolve()` needs a validated binding, which needs an HTTPS origin. Rewriting the origin with `insteadOf` fails `ensure_checkout`'s own origin-equality check first, so the shortcut cannot work. **Soul has a working smart-HTTPS rig from its first pass; that is where this closes.**
- A note on the fsck fixtures: some hostile objects are refused by the git server's own `pack-objects` before any Idunn code runs, so they cannot serve as already-local fixtures. The test asserts that at least one fixture reaches the explicit fsck, so it cannot pass vacuously.
- **Soul's fifth pass dispatched**, including a judgement on the batch's size.