Idunn: static release transaction (flip in Starting) and the v5 control record

A static release is the same DeploymentTransaction in three phases: Sealing,
Starting (carry and flip, behind the deployment brake) and Complete. It has no
Expected, activation, workload, warming, isolation, fencing, leasing, ready or
routing, and creates no AdmittedGeneration.

DeploymentTransaction gains key 37 `flip` (FlipEvidence: release, previous
release, carry source, carried sha256, time). Adding a key changes the bytes of
every record, and the control store refuses a record that does not re-encode to
what is stored, so this is schema idunn.deployment_transaction.v5. The v4
layout is kept as LegacyDeploymentTransactionV4 and lifted by the existing
mechanism (read_transaction_record, history decode, and the boot migration,
which rewrites v4 records in control.cc to v5). Migration is forward-only: an
Idunn that predates v5 cannot read a migrated control.cc.

Also: is_static() and brake_subject() (the brake names the binding's runtime id
and the sealed release, read by deployment_authorization, durable-authority
validation and status); validate_static; advance_sealing skips readiness_class
and expected_projection for static; advance_starting delegates to
advance_static (intent with the previous release read from current_link,
carry_and_flip then record, observe link == release, then Complete with
Admitted{generation-<tx>} and post_commit_cleanup(SkippedNoIncumbent, source
Pending)); a failure in Starting takes the existing pre-fencing abort;
Engine.static_release Arc<dyn StaticReleasePort>. Docs: a Static release
section in deployment-authority.md and a scope line in
deploy-provenance-target.md.

Tests: control_plane::tests::static_release_ (3), a pinned v4 record lifts and
re-encodes as the pinned v5, a v4 control store copy in a temp dir migrates to
v5 and an already-v5 store is untouched byte for byte, and the pinned-bytes
test now pins v5 with the v4 bytes kept as a legacy fixture. The test runner is
a stand-in over the real seal_artifact behind a cfg(test) Engine seam, because
DockerRunnerDriver workspace ownership requires root.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-13 16:32:06 +00:00
commit d5ea779096
3 changed files with 836 additions and 128 deletions

View file

@ -53,6 +53,11 @@ In:
(`heimdall-discord-launch.md`, `idunn-host-raven.md`, the StreamPixels
runbook).
- A static release (a sealed tree published by repointing a link, with no
workload): its transaction records `flip` (release, previous release, the
sha256 of the carried per-site file) beside the brake bytes. See
`deployment-authority.md`, "Static release".
Out:
- `idunn-reincarnation`. This campaign is mapped after that campaign's cut 2

View file

@ -171,6 +171,49 @@ different semantic capability.
`gamecult-ops` publishes these fleet bindings and provisions host substrate. It
does not own application build or deployment programs.
## Static release
A target whose product is a sealed tree, with no process to run (a built web
front end), declares `[static_release] artifact = "<id>"` in place of
`[service]`, and its binding declares `driver = "static-release"` with a
`release_root`, a `current_link` and one `[workload.carry]`. It is the same
transaction as any deployment, in three phases: Sealing (source, build, seal,
install: unchanged), Starting (the one external change, behind the deployment
brake) and Complete. It never enters Warming through Committing, has no
Expected projection, no activation and no workload, publishes nothing to
Odin, and its Complete creates no admitted generation, so continuity never
supervises it. It declares no state, capabilities, dependencies, conflicts,
route or write lease, and its rollout is `candidate-then-promote` with at least
two retained releases.
What Starting does, in three durable steps recorded as `flip` on the
transaction: the release to name and the release the link names now are
written first; then Idunn copies the operator's carry source into
`<release_root>/<sealed release id>/<carry.destination>` (mode 0444) and
renames a fresh symlink over `current_link`; then it observes the link naming
the release and completes. A failure before the rename ends the transaction
through the ordinary pre-fencing abort with `current_link` untouched. A crash
resumes at the recorded intent and repeats the flip, which is harmless.
The carry is a copy of one per-site file (for example `config.json`) taken at
flip time. It sits beside the artifacts, never inside one, so it is outside
every sealed digest: changing the site file changes no release id, and the
sha256 of the bytes carried is provenance on the transaction
(`flip.carried_sha256`), beside the brake bytes and the requester. The
destination is a single leaf name that no artifact uses. The web server reads
`current_link/<artifact destination>` for the tree and `current_link/<carry
destination>` for the carried file (a one-time root edit of the vhost, an
`alias` for the carried file beside the root for the tree). A missing or
non-regular carry source fails the deployment before anything moves.
What `current_link` names is observed from the symlink and confirmed by the
latest Admitted static transaction's `flip.release_id`; it is stored nowhere
else. A rollback is a flip back to `flip.previous_release_id`: release
directories stay, and Idunn prunes none. The brake is read exactly as for a
service, naming the binding's runtime id and the sealed release id. A new
upstream tarball enters as a recipe `external_input` with a pinned url and
sha256, so each release is a recipe commit whose pin is its provenance.
## Typed deployment state
Mutable control-plane state will be CultCache. Runtime integration must use

File diff suppressed because it is too large Load diff