Release ids are per transaction, so redeploying an unchanged tree with a changed
site file yields a new release (the test asserts distinct ids, the first release
keeping its carry, current carrying the new file); previous_release_id can never
equal release_id and no refusal is needed. A flip whose current link was replaced
by a directory after the intent retries visibly: last_error is shown by idunn
status as 'waiting on', cancel is refused, and the docs name the by-hand fix.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
carry_and_flip returns FlipRefused only before the rename (the last fallible
step); the parent-directory sync after it is best-effort. Once a flip intent is
recorded, any error that is not FlipRefused leaves the transaction resumable
instead of taking the pre-fencing abort, and a resumed step that finds current
already naming the release records the carry already in it (new port method
carried_sha256) instead of flipping again. validate refuses a pre-fencing abort
beside a landed flip.
Tests: post-rename port fault and post-landing observe fault both end Admitted
with current on the new release; validation refuses FailedBeforeFencing with a
landed flip; a refused flip over a real directory writes no carry; a populated
v4 record lifts to the exact v5 field by field.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
d5ea779 named this work but did not contain it: the working-tree edits were never
staged. LegacyDeploymentTransactionV4, the v5 schema constant, the v4 lift in
read_transaction_record and history decode, the boot-migration case, and the
pinned v5 / legacy v4 tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
install_release and validate_installed_release read the release root through
WorkloadBinding::release_root(), so a static release installs through the
same copy, digest and harden path as a systemd one. StaticReleasePort
{ observe_current, carry_and_flip } with FilesystemStaticRelease: reads the
operator's site file (regular file, not a link, 1 MiB cap) and hashes it
before anything moves; writes <release>/<destination> by temp + rename at mode
0444 outside every artifact digest; then renames a fresh symlink over
current_link (a stale flip link a crash left is replaced; a real directory in
the link's place is never replaced). observe_current refuses a link that names
anything but a release under the release root.
Tests on a tempdir stand-in site (fake vite dist with .br/.gz twins, fake site
config): cargo test -p idunn --lib drivers::tests::static_release_ (5 passed).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A target may declare [static_release] artifact = "<id>" instead of
[service], and bind driver = "static-release" (release_root, current_link,
one carry source/destination). TargetDeclaration.service becomes Option with
a service() accessor; admission refuses both-or-neither, state/provides/
dependencies/conflicts/route/write lease on a static target, stop-then-start,
retain_releases < 2, a carry destination that is not a single leaf or collides
with an artifact destination, a carry source inside the release root, and a
current link overlapping the release root or an authority path.
Read-only audit of TransactionCompletion::Admitted / admitted_for readers
(cut spec step 2); a static Admitted transaction has no AdmittedGeneration:
- control_plane.rs:1781 (deadline_for_current_activity, Complete): reads the
variant and drains_incumbent only. Tolerates.
- :1828 (carries_a_deadline): variant only. Tolerates.
- :2086 (is_terminal): variant plus post_commit_cleanup. Tolerates.
- :2550 (validate, Complete): requires Admitted and generation-<tx>; the
generation record is never read. Tolerates; static gets its own branch.
- :5085 (derived_command_status): variant only. Tolerates.
- :6567 region (command admission; the admitted_for read is at :6587): passes
an Option incumbent, None for a static target. Tolerates. Anchor drifted by
~20 lines; the code matches.
- :8371 (advance_committing): the only writer of Admitted with a generation;
a static transaction never enters Committing. Unchanged.
Other admitted_for readers (3517 fenced-incumbent check, 4963 render, 5482
incident close, 9571 provider rehydrate, 9654/9712/9777 rebase, continuity,
take_target): all handle None; a static target has no fencing, no incident,
no capability to be a provider.
Fallout: runner_for/workload_for gain a StaticRelease arm (docker runner; the
systemd driver serves install only), and the service-only readers in host.rs,
drivers.rs and deployment_plan.rs go through service()?.
Tests: cargo test -p idunn --lib deployment::tests::static_ (6 passed).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A unit with Upholds= is refused for start and restart (systemd would start the
upheld unit in a full transaction). A wrapper with ConsistsOf= is refused for
stop, restart and reload with a typed Refused::Wrapper naming the first member,
so it cannot report ran while touching nothing. Tests pin empty BoundBy, a
single show per refusal, no show for a refused name and each posted reason.
README: the one-job rule's residual full transactions. Removes the unused
BTreeMap import and the never_loop in the host actuator test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete the transaction model (Edge, EDGES, action_target, Transaction, the
early-boot rule). Every unit verb runs systemctl <verb>
--job-mode=ignore-requirements -- <unit>; refusal_for reads one systemctl show
of the ordered unit: its names, the host-ending names, what fires when it
ends, and who is bound to it. Targets are no longer orderable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refusal_for now reads SuccessAction, FailureAction, StartLimitAction and
JobTimeoutAction (each mapped to the target it runs, so reboot, poweroff,
halt, kexec, soft-reboot and exit, with -force and -immediate) and OnFailure
and OnSuccess as units a touched unit fires, followed through what they pull
in and what those fire. An action it does not know fails the order closed.
A stopped DefaultDependencies=no unit in the transaction is refused (Via::
EarlyBoot): rescue.service ends by isolating default.target, which no edge
describes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
execute refuses, with a record and a post, an order whose principal or issuing
uid is not the steward bound now, before grammar, scope and brake; the brake
is asked for the bound principal only. idunn-orders.service drops Group= and
UMask=: bind_socket gives the socket to the steward uid at 0600 and serve sets
Idunn's own umask, so a deploy order writes control.cc 0640. Root may show and
list, never order. The issuer unit is the first unit under system.slice, never
a name the peer chose below it. Synapse waits for a reply within one deadline.
due_posts takes the deploy-state reader so a finished deploy's post is tested.
Tests: the gate both ways, root, per-order posts, deploy result, intake and
Synapse timeouts, parse_serve, user_uid, socket and store modes, and the whole
service as root with a fake systemctl, systemd-run and homeserver.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An order is refused when the closure systemd would build for it reaches an
always-refused unit: a start pulls Requires/Requisite/BindsTo/Wants/Upholds and
stops Conflicts/ConflictedBy, a stop takes down RequiredBy/BoundBy/RequisiteOf/
ConsistsOf/PropagatesStopTo/StopPropagatedFrom, a restart does both. A refused
unit already running is left alone by a start. Names are unquoted from
systemctl show; a unit systemd cannot show fails the order.
Package installs run apt-get --no-remove with ?exact-name(<name>) per package
and a final dpkg -s step that fails unless every named package is installed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
idunn orders serve takes orders over a root-only socket from the steward uid, records
order/start/outcome/post in its own orders.cc, and posts acceptance and result to
#operator as a retried projection. Unit orders may act on any unit except the compiled
always-refused set (idunn-*, mind-self@*, mind-persona@*), matched on a unit's Id and every
name, and for stop and restart on the units systemd stops with it. A refusal names a field
and a code, never the value. A started order is never run twice. Deploy orders commit
through the same primitive as idunn up.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cut idunn-deploy-provenance:cut_spec:cut-runner-failure-evidence.r1,
reports h1-h3, Soul s1-s3. The docker runner kept only stderr, so a step
reporting on stdout failed with an empty message (Vili's node --test). A
failed step's name and bounded stdout/stderr tails go to a root-only typed
store read by idunn status; errors and the journal carry no output; one
allowlist escape at the status sink.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Status writes everything it prints from the evidence store, including the
unreadable-store line, through one function (emit): printable ASCII and tab
pass, every other code point is a \u{..} escape and every invalid UTF-8 byte
a \x.. escape. The bidi and C1 class lists are deleted.
The writer reads the store through a seam. An I/O error leaves the store in
place, keeps no evidence and names its kind; the store is set aside only when
bytes that were read still read the same on a second read and do not decode.
A set-aside that displaces an earlier one says so on the journal (last-written
time, no content). Tests pin a failed set-aside, its no-echo, the I/O seam and
the replacement.
Not yet built or run; verified in the next job.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The writer moves a store it cannot decode (corrupt, or a newer schema) to
runner-evidence.unreadable.cc, root-only, one slot replaced by the next, and
opens a fresh store; the journal names the file and none of its content. A
store that cannot be read at all is left in place and the write fails as before.
Status escapes the bidi controls (U+061C, U+200E/F, U+202A-E, U+2066-9) in
tails, subject and runner, and a test pins U+0080-U+009F.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An interrupted read is retried with no byte lost, a persistent read error ends
the drain instead of spinning, and a non-interrupted error after data is
returned. Kills the three Interrupted-guard mutants (false, !=, true).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The vili failure: node --test wrote only to stdout, stderr was empty, the step
exited 1. Pin the cap at the literal 32 and the Debug impls that keep tails out
of text.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Drive the real DockerRunnerDriver over a fake docker program, through the
engine's Sealing path: the second step is named with its exit status, both
tails are kept in a 0600 store capped at 32, no error, control.cc or journal
line carries the output, status escapes control bytes, an unwritable store
does not stop the abort, and control.cc's record bytes equal those captured at
base 804ed75.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
DockerRunnerDriver::docker no longer buffers whole streams or interpolates
stderr into the error. It pipes both streams through an 8 KiB tail and returns
RunnerStepFailure (subject, runner, status, tails); the engine's pre-fence
path is the one writer of runner-evidence.cc (0600, newest 32) and journals a
line that names the store and no output. idunn status --command renders the
record with control bytes escaped.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Imagination for cut runner-failure-evidence: the docker runner discards
stdout, so a step that reports on stdout fails with an empty message; the
tail moves to a root-only typed evidence store read by idunn status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut idunn-deploy-provenance:cut_spec:cut-cut-isolation-credential-evidence.r1,
report h1, Soul s1 (all claims hold). systemd 257 prints LoadCredential as
[unprintable]; the workload's credential directory, read from its MainPID's
root, must hold exactly the delivered names. Unblocks Vili's promotion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cargo-mutants showed that treating every read_dir error as the empty set
survived. Test the non-NotFound case (the unit path is a file).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
systemd 257 displays LoadCredential as [unprintable] for every unit, so the
conjunct was always false for a unit with only the presence identity and
vacuous for one with credentials. Delete the show argument, the conjunct and
its twin clause; the isolation conjunction becomes
require_systemd_isolation_contract. The credential rule is now
require_delivered_credentials: entry names of /run/credentials/<unit> in the
MainPID's root equal Idunn's launch names. load_credential stays a retired
wire slot (written empty, out of the drift list) so persisted records decode.
Not yet built or verified.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Real systemctl show output (Vili presence-only, Odin with one service
credential) and the two MessagePack encodings of a pre-cut observation, as
inputs for the credential-evidence cut. Nothing reads them yet.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Imagination for cut isolation-credential-evidence: systemd 257 prints
LoadCredential as [unprintable] for every unit, so observe_unit's comparison
never passes a target with no extra credential and proves nothing otherwise;
the rule moves to the delivered credential directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
systemd refuses to start a unit whose ReadWritePaths names a missing path,
and none of Vili's three roots exist on Yggdrasil yet. Idunn's startup must
not depend on a managed target, so the Vili line takes the "-" prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>