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>
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>
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>
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>
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>
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>
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>
A scheduler tick that blocks on a build (odin's docker build took a
minute) left every attached host unserviced; the actuator timed out,
redialled, and three continuity restarts of raven-muninn failed with
'host actuator raven is not attached' inside one minute. The hub now
lives behind a mutex with a service thread; a request holds the lock and
services the socket itself for its whole exchange.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The host actuator re-parses a Linux binding on Windows to reach its own
section; the Idunn host's /var paths must not fail there. This also makes
the Linux-shaped test fixtures pass on a Windows checkout.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
provider-health-identity-id and host-actuator-identity-id are read-only.
The id is what a binding names as expected_signer_identity_id and what
a host is keyed by; deriving it here closes the gap where an operator
had to decode an anchor by hand to fill a binding.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A managed host runs idunn-host in the user session. It dials Idunn's hub
over CultNet RUDP on the mesh, proves itself with a host actuator service
identity, and executes the runner and workload ports for its host: fetch
the exact frozen revision, build with the host's cargo under the binding's
allowed programs, install the sealed release, start the workload as a
child, report pid, creation time and executable digest, stop on request.
It keeps no transaction state and decides nothing.
On the Idunn side the engine chooses the runner and workload driver per
plan from the binding's driver tag; the yggdrasil daemon is still the
single deployment and continuity authority, and a binding of the old kind
takes the same path it always did. Every request is signed by Idunn's
identity and every report by the host's; both ends refuse the other's
message unless signature, session nonce and sequence hold. The hub admits
its own firewall allow the way the route driver does.
idunn-provision learns to enroll and export the host actuator identity.
The unit gains --host-actuator-bind on the mesh address.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RunnerBinding and WorkloadBinding become enums tagged by driver, so a
host-native runner and a host-actuator workload can be declared without
counterfeiting docker images or systemd units. The single-variant
RunnerDriver and WorkloadDriver enums are gone; the driver is the tag.
WorkloadObservation is now Systemd or Host, untagged on the wire so every
transaction the previous Idunn persisted decodes unchanged (proved under
both MessagePack encodings CultCache uses). IsolationEvidence follows:
Linux proves uid, pid and mount namespaces; a host proves two live
processes distinct by pid and creation time. Mixed kinds are refused.
The build machine-id seal is unix-only so the library compiles on the
Windows host that will run the actuator.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Odin re-stamps a correlation only when its facts change, and a live presence
changes them every heartbeat, so a correlation older than the trusted window
means nothing about that incarnation has arrived lately. Admission and
supervise now read that one refusal as nothing-new. It had failed every
Heimdall deployment and the first observed Odin deployment at Warming.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 118 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Admitting the latest topology persists the sequence cursor; asking twice
with the same record failed the second time as "transaction changed before
topology admission". Only reachable now that an admitted Odin can observe a
candidate Odin under incarnation keys; it failed the first fold-back
deployment on yggdrasil at Warming.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 118 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A route that is admitted must be reachable without an operator opening a
port by hand. NginxRouteDriver admits one ufw allow for the binding's stable
address, port and transport, tagged with the route id, before it reloads a
fragment, and withdraws that one rule when the last membership is removed.
A rule already gone reads as the state asked for. Odin on yggdrasil is
admitted on 10.77.0.1:17971 with ufw still admitting only the previous
generation's 17871; this closes that class.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 118 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A replaced incumbent, an aborted candidate whose reconciliation was skipped,
and a record left by the projection migration all leave an Expected-only
incarnation in the projection that nothing owns. Supervise now withdraws one
per tick for each admitted target: any projected incarnation that is neither
the admitted generation nor a live transaction's. One owner for all three
sources, instead of a per-commit field.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 117 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Transactions that finished before finished transactions travelled to history
are still resident with their commands, gating nothing but growing the store
every decision reads. They leave the same way a transaction finishing today
does, one per tick.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 116 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Commands whose transactions were retired to history before commands travelled
with them were still resident and read as queued; freeze took the oldest
first, so one stale odin command was re-frozen hundreds of times today and
the newest command never got a turn. freeze_one_queued_command now reads
history, retires such commands on sight, and skips a busy target instead of
returning. status reports a live transaction as running rather than an older
attempt's failure, and prints completion and cleanup evidence for --command.
README carries the Linux build recipe; Windows does not compile.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 114 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Failed restarts retire to history.cc as soon as they finish, so the give-up
counter that read only the live set found zero every time and restarted a
release that could not start forever. Seen on yggdrasil during the
projection-key migration.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 114 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A candidate that died while its correlation was pending was never observed
again: each phase returned on no-evidence-yet every tick, the error that
triggers the post-fencing abort never happened, and the transaction owned the
target forever behind a process that was gone. Seen on yggdrasil with a
continuity transaction parked in Committing across the projection-key
migration.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 114 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The projection kept one Expected/activation/lease slot per target. A
deployment's Sealing overwrote that slot with the candidate's Expected, the
incumbent failed activation-expected-projection on its next heartbeat and
exited, the deployment was aborted for having killed its incumbent, and the
command -- left resident after its transaction was retired to history --
read as queued and was frozen again. Odin cycled on that every few minutes.
- Topology driver records are keyed {target}@{expected sha}; the incumbent and
the sealed candidate are projected side by side. One CAS mutation replaces
the six bespoke loops. withdraw_incarnation removes only one incarnation;
demote_to_expected_only replaces restore_admitted_expected_only and is used
only for an incarnation whose process is gone. Legacy target-keyed records
are retired on the first publish for that target.
- receive() takes the incarnation; the read-as-absence compensator for a
correlation about another incarnation is gone.
- Candidate Expected is published as Starting's first step, after the brake
admitted the transaction, not in Sealing before it.
- A pre-fencing abort withdraws only the candidate; a post-fencing abort also
demotes the stopped incumbent.
- A consumed command is archived with its last transaction; status still
answers for it from history.
- An engaged lifecycle brake stops continuity being minted instead of parking
a transaction on the target.
- idunn cancel <command-id> retires a queued command as an operator refusal.
Verified with cargo test --lib in rust:1.95-bookworm on yggdrasil: 114 passed,
2 ignored.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The control store held every transaction Idunn had ever run -- 260 on
yggdrasil, all terminal, 2.6 MB re-read and byte-exact checked on every
scheduler tick. A finished transaction gates nothing; it only makes the
live set slower to read and gives history more ways to refuse one.
replace_transaction now moves a transaction out of control.cc the moment
it becomes terminal. History first, then the live copy: a crash between
the two leaves the record in both, which reads as still-resident and is
archived again next time. The other order can lose it, and a completion
nobody can observe is what R11 refused to trade away for a smaller live
set. insert_entry_if_absent makes the repeat a no-op and the delete is a
compare-and-swap on the exact envelope archived, so a transaction that
changed underneath is left alone rather than dropped.
history.cc is read leniently and for display only; control.cc keeps the
byte-exact canonical check because its records still gate decisions. That
asymmetry is the whole reason the two files are separate. status merges
both, so a completed command still reports what it did rather than
appearing never to have run.
No sweep. The earlier attempt archived resident terminal records from
inside the scheduler tick's propagating prefix, so an unwritable history
starved every live transaction on every other target. One primitive, one
caller, at the moment of completion.
Verified against a copy of the live store: status output is byte-identical
between the installed binary and this one, so the 260 existing records are
unaffected. They stay resident -- moving them is a separate explicit
operation, not something to bolt onto a scheduler.
cargo test --locked -> test result: ok. 113 passed; 0 failed; 2 ignored
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
validate_durable_authority read the deployment-brake operator anchor
unconditionally and then re-proved every transaction in the store. Two
consequences, both live until now:
An absent brake artifact refused startup against an empty store. That is
a deployment brake gating Idunn itself, which F:\Projects\CLAUDE.md
forbids in as many words -- a target brake "may never gate Idunn itself
or unrelated service lifecycle". The anchor is now read on demand, when
a record actually carries an authorization to verify.
Terminal transactions were re-proved on every boot. They describe what
already happened and authorize nothing further, so the only thing that
re-proving them can do is invent new ways for history to refuse a boot --
a rotated signer on a record from last week making the daemon
unstartable. They are skipped. On yggdrasil that is 259 of 260 records,
and this is what R4 meant by terminal history not being live authority.
Live records keep every check unchanged.
Known gap, deliberately not closed here: a live record whose signed
evidence no longer verifies still refuses startup. The right answer is to
abort that transaction rather than refuse the boot, which needs the
post-fencing abort path and is its own cut.
cargo test --locked -> test result: ok. 110 passed; 0 failed; 2 ignored
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
serve's loop propagated every scheduler-body error with `?`. Idunn runs
under Restart=always, so a single unreadable record turned into a
crashloop that took every unrelated target's continuity down with it --
the daemon-survival organ killed by exactly the thing it exists to
survive. This is the second half of the store-history work; the first
half landed as the admission gates.
run_scheduler_tick extracts one pass with the loop's ordering preserved
exactly, including that Ok(true) means "go round again immediately",
which is what the caller's `continue` did. The loop logs a faulting tick
and keeps polling. The fault recurs every poll until someone fixes it,
which is louder than a restart loop and cheaper than an outage.
Adds EngineFixture, the first Engine-level test harness this crate has
had -- enrolled Idunn and Odin identities, an exported anchor, tempdir
roots. Its absence is why "does one bad record kill the daemon" was
arguable rather than measurable.
The test proves the tick is a clean unit of failure: a 0xc1 payload (the
one byte msgpack never emits) makes three consecutive ticks report an
error and leaves the control store byte-identical, so a tick that cannot
read does not rewrite, truncate or silently repair. The loop's own match
arm is not covered -- serve never returns, so no unit test reaches it --
and that part rests on inspection.
cargo test --locked -> test result: ok. 109 passed; 0 failed; 2 ignored
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
is_permanently_stopped is called on the incumbent as well as the
candidate since 8448f83, so a message naming only the candidate would
misdirect the first person who hits it. Unreachable today -- observe_unit
guarantees every recorded workload is Restart=no -- which is exactly why
the wording would be read cold.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Present correlation with no activation is refused by the schema before
authentication, and a foreign Odin signature only names a different
identity when the record carries that identity's id.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul pass on the five gate changes. Negative checks only:
- another incarnation's perfect correlation, another activation of the same
Expected, and a foreign target beside our projection are all refused by
authentication alone (the deleted identity gate was a subset of it)
- the signing fixture refuses at every link: activation/Expected binding,
Idunn anchor, provider key, foreign Odin signer, forged signature
- a durable receipt replays at its own admitted_at and not at now
- is_permanently_stopped is true only for failed or not-found units; every
transitional ActiveState reads as running, and a systemctl failure errors
- an ignored live-store inventory probe
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>