v0.29.0: multi-realm distribution, and an ingestion premise corrected twice - #101
Open
avrabe wants to merge 16 commits into
Open
v0.29.0: multi-realm distribution, and an ingestion premise corrected twice#101avrabe wants to merge 16 commits into
avrabe wants to merge 16 commits into
Conversation
… a human
A manually-cut layer cannot keep up. Days after 2026.08.3 was published, rivet
had moved v0.33.1 -> v0.34.0, spar v0.36.0 -> v0.40.0 (four minors), synth
v0.55.0 -> v0.58.0, witness v0.39.0 -> v0.43.0, ordeal v0.18.0 -> v0.19.0. A
consumer pinned to `rolling` was not getting a rolling toolchain; they were
getting whatever was current the last time somebody remembered.
The obvious automation is the wrong one (DD-024). `channel = "rolling"` is not
a trust boundary in varve — a rolling layer is signed by the SAME realm root as
a qualified one, and the realm is the boundary. Publishing on a timer means
that root signing unattended dozens of times a week, against everything `varve
docs root-ceremony` says about it, in a system with no revocation and no
rotation, where a layer published in error cannot be withdrawn.
So this automates the toil and leaves the judgement:
tools/scan-upstream.sh notice what moved; check who vouches for each
new artifact; rewrite the pins; write the diff
tools/upstream-mechanism.sh the ingestion-mechanism table. The mechanism is
a VALUE the proposal reports, not the
assumption that it is cosign: cosign-signed
sums and GitHub build provenance are two
entries, and UPSTREAM_MECHANISMS is
configuration
tools/next-layer-id.sh the layer id and per-line counter, read out of
the published registry rather than typed
.github/workflows/scan-upstream.yml
daily scan -> assembly gate on the PROPOSED
pins -> one pull request; and, separately, a
merge dispatches the signed deposit
Nothing here deposits, signs or publishes. Clause by clause: the scan opens a
PR carrying the version diff (1); the existing REQ-SYSTEST-002 assembly gate
runs against the proposed env block before anyone is asked to look at it (2) —
in the scan run itself, because a PR opened by GITHUB_TOKEN does not trigger
`pull_request` workflows and a gate that silently never ran would be worse than
none; the merge, and only the merge, dispatches the deposit (3); the PR names
each tool, how far it moved, how many releases went by, and which mechanism
vouched for the new artifact (4); an upstream nothing vouches for holds ITS pin
and lets the rest of the layer move (5); and a scan where nothing moved writes
nothing and opens nothing (6).
Daily, not hourly. Over the 30 days to 2026-08-21 these nine repos published 56
non-draft releases on 15 distinct days — half the days had nothing, and the
busy days came in bursts of seven to nine. Hourly would scan 24x to find the
same answer and, on a burst day, rewrite the same proposal nine times, each
rewrite invalidating the assembly gate that had just run on it.
tools/systest/scan-upstream.sh is the gate, with fixture-backed `gh` and
`cosign`: a repo named in both env lists, an upstream with no attestation, one
whose bundle does not cover its sums, a prerelease and a draft that must not be
proposed, a pin ahead of upstream, both mechanisms in one proposal, the
silent cases, and a negative control that reintroduces a per-list rewrite and
requires the gate to go red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
The layer assembler accepted exactly one proof that an upstream artifact was genuine — a cosign-signed SHA256SUMS.txt — and aborted the whole run otherwise. Every PulseEngine repo publishes one, so nobody noticed the constraint until a second realm was attempted. bytecodealliance/wasm-tools v1.257.1 publishes no sums and no cosign bundle (measured 2026-08-21), and it was therefore uningestible despite carrying something STRONGER: a GitHub build attestation binding each artifact to publish.yml@refs/heads/main and a source commit. Clause 1. `verify_release` now asks what a release publishes and picks a mechanism: a cosign-signed sums file, or a build attestation whose in-toto statement supplies the digest of every asset (which is why provenance REPLACES the sums file rather than accompanying it). The ladder does not downgrade — a sums file whose signature is REJECTED aborts, because "no cosign proof" and "a bad cosign proof" are different facts. Clause 2. Three annotations inside the DSSE-signed payload, beside the kind and the source digests: eu.pulseengine.source.proof (the mechanism), .proof-signer (the identity that vouched), .proof-asserts (what it asserted). A consumer can tell a cosign-signed tool from an attested one without leaving the layer. Absent reads as `unrecorded`, never as verified: every layer already published carries no annotation, and defaulting would upgrade them all to a claim nobody made. Clauses 3+4. A release with neither mechanism is refused, and the refusal names the fork through this org's signed pipeline — for wit-bindgen that IS the answer. Shipping one anyway needs UNVERIFIED_INGEST="repo=reason"; the reason is mandatory and is signed into the layer. `varve deposit` independently refuses `unverified` with no reason, or naming a signer that vouched for nothing. TARBALL_TOOLS entries gain an optional owner and asset template, without which no foreign realm can be named at all. Existing entries parse unchanged. The producer gate now carries a cosign-signed realm, an attested one and one with neither, checks the mechanism in the signed payload rather than in the spec, and gains a second negative control: with the refusal deleted the unproven tool must sail through, which is what proves the refusal is what stopped it. Not satisfied: clause 5. The data is in the layer and reachable via ManifestEntry::ingest_proof(); wiring it into `varve inspect` is CLI code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
REQ-INGEST-001 and REQ-ROLLING-001 landed from two agents with disjoint
file ownership. Clean merge, no conflicts.
The mechanism id diverged, exactly as the hand-off anticipated: ingest
minted `build-provenance` for the signed annotation, the scanner reported
`gh-provenance` in its proposal. Reconciled toward the PAYLOAD's
vocabulary, not the scanner's — the annotation is a compatibility promise
inside a signature that consumers will policy on, and the PR body is
prose that can change freely. What the proposal says vouched for a tool
is now the same string the layer records.
Wired clause 5 (`varve inspect`), which the ingest agent specified but
could not apply — CLI is not its ownership. Per payload: the mechanism
and the identity credited with vouching, kept as separate fields because
the signer is the thing a consumer might FILTER on ("refuse anything not
signed under pulseengine/") while the assertion is prose. In the summary:
`unverified` and `unrecorded` counts, so a CI gate can assert "nothing
here went unverified" without walking the list.
`unrecorded` is deliberately not `unverified`. Every already-published
layer carries no annotation, and defaulting either way would restate a
hundred payloads as a claim nobody made.
Then the part that matters: the producer gate CHECKED clause 5 with a
NOTE that printed "NOT satisfied" while the gate passed. A clause a
passing gate reports as unmet is a clause nothing enforces — the exact
shape this release exists to remove. It is an assertion now, and it
demands the real mechanisms this layer mixes rather than the mere
presence of a key, plus the summary counts. Verified it bites by deleting
the wiring:
FAIL: clause 5 — the mechanism is in the signed payload but
`varve inspect --json` does not surface it (no "ingest_proof" key)
Two findings from the agents worth keeping visible. Provenance REPLACES
the sums file rather than accompanying it — the in-toto statement carries
the sha256 of every asset in the release, so verifying one artifact
authenticates the whole subject list. And `verify_release` now decides
from what a release PUBLISHES, not from a download that failed, so "no
cosign proof" stays distinct from "a REJECTED cosign proof": a bad
signature aborts rather than falling through to the weaker path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Both advanced via `rivet verify`. INGEST on 7 source markers; ROLLING needed a VER- artifact because its evidence is shell-based and carries no cargo-test markers — `rivet verify` refused it outright, correctly, and that refusal is the reason the record exists rather than a status flipped by hand. VER-ROLLING-001 records the constraint, not just the result: automating the rolling channel end to end would have the root key signing unattended in a system with no revocation, one release after publishing a ceremony document that says otherwise. It also records the clause met in substance but not in letter — a GITHUB_TOKEN PR does not trigger `pull_request` workflows, so the assembly gate runs BEFORE the PR and its verdict goes in the body. Stated rather than hidden. Both gates run here: the scanner systest passes with its negative control red (a per-list rather than per-repo rewrite is caught), and all four non-cargo-test evidence steps execute green — checked directly, because `rivet check verification-evidence` reads cargo-test filters and cannot see a `run:` grep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
The collision error's own suggested fix is not unimplemented — it is structurally incapable. `tools` filters by NAME; the collision is two layers exposing the SAME name. No value of it has ever been able to disambiguate, which is why the persona who tested it twice found it did nothing. The other suggestion, remove the duplicate from one layer, is unavailable to a consumer composing a realm they do not control. So the command refuses correctly and then offers one impossible fix and one unavailable one. This stops being an edge case in v0.29.0. Two realms shipping `wasm-tools` is the ORDINARY case the moment a fork exists beside its upstream — exactly the topology this release builds, where the fork exists precisely because upstream does not attest that tool. 4a gives `tools` an optional realm qualifier, so the filter can express the choice its own error demands. 4b keeps the unselected layer installed and verified and addressable qualified, because comparing a fork against its upstream is a real workflow and losing the binary is worse than refusing. 4c keeps exactly one shim per name, so a bare name is decided by the pin and never by install order — realm PRECEDENCE was considered and rejected for that reason: it would let adding a tool to a high-priority realm silently change which binary a build runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Two decisions changed what these clauses should demand, and a clause that outlives its decision is how a graph starts lying — the same correction DD-023 forced on REQ-ADVISORY-002 two releases ago. REALM2 clause 1 required a PUBLISHED bytecodealliance realm. It now requires the realm to be constructible and composable, proven by a system gate over real upstream artifacts and real roots; publishing is operational and tracked separately. Generating a realm root is a ceremony `docs root-ceremony` says should be air-gapped, two-person and unhurried — blocking a code release on it would either delay the release or rush the ceremony, and rushing it is the worse outcome in a system with no rotation. LAYERREPO clause 4 said the pulseengine realm migrates first. It now says exactly ONE repo is stood up first and must publish successfully before a second exists, so a migration failure lands on the realm we can fix rather than on a new realm's first impression. The rejected alternative is recorded: one repo holding both realms puts two roots' secrets in one repo's settings, and unpicking that later means rotating keys varve cannot rotate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
`union_tools` refused a tool exposed by two layers and then offered two
fixes. One — "Restrict the pin's `tools`" — is STRUCTURALLY incapable of
working: `tools` filters by NAME and the collision is two layers exposing
the SAME name, so no value of it has ever been able to disambiguate. A
persona tested it twice and reported it doing nothing; they were right.
The other — "remove the duplicate from one layer" — is unavailable to a
consumer composing a realm they do not control. So varve refused
correctly and then offered one impossible fix and one unavailable one.
That stops being an edge case in v0.29.0. `pulseengine/wasm-tools` is a
fork of `bytecodealliance/wasm-tools`, and the fork exists precisely
where upstream does not attest, so two realms shipping `wasm-tools` is
the ORDINARY case the moment a fork is cut.
4a — `tools` accepts an optional realm qualifier:
tools = ["bytecodealliance/wasm-tools", "rivet"]
A bare name is unchanged where nothing collides; the pin stays a
compatibility surface. A `tools` entry is now either a plain name or one
realm qualifier ahead of a plain name, and everything else — a path, a
deeper path, an empty half — is still refused, with a message that shows
both accepted forms rather than sending a reader back to the one that
cannot express their fix.
4c — the choice is made in exactly one place. `compose::select_tools`
reads the pin and nothing else: not install order, not partition order,
not a precedence list. Realm PRECEDENCE was considered and rejected for
that reason, since it would let adding a tool to a high-priority realm
silently change which binary a build runs. `Resolved.tools` still holds
one entry per NAME, so the shim directory stays one file per name.
4b — the unselected layer stays installed and verified and stays
addressable: `varve run pulseengine/wasm-tools`, `varve which
bytecodealliance/wasm-tools`. Comparing a fork against its upstream is a
real workflow and losing the other binary is worse than refusing. A
qualified `which` also prints the layer that OWNS the binary, which the
existing `layer …` line cannot (it names the layer the pin resolves to).
4d — where the pin has not chosen, the refusal names both providers WITH
their realms and shows the qualified line to paste. Where a qualifier
cannot help — two layers of one realm, or a layer with no realm at all —
it says so instead of printing a form that would not work.
Clause 3, the gate: tools/systest/compose-realms.sh builds BOTH realms
with the production assembler (the same tools/build-deposit-spec.sh
deposit-layer.yml runs), signs each under its own generated root,
composes them through a signed `[[include]]`, installs one at a time and
verifies each against its own realm's root — then settles the name they
share, asking the RUNNING BINARY which realm it came from rather than
reading a path. It ends by rebuilding varve with the pin's choice
deleted from `select_tools` and requiring the run to go red.
Three things the gate found that no unit test could:
* the assembler could only build a PulseEngine-shaped layer — WSC_VERSION
and VSIX_PACKAGES were required non-empty, and both name pulseengine
repos, so "a second realm is constructible" was a claim nothing could
execute. Empty is now a legal answer for both.
* it could not produce a COMPOSING layer at all; composition could only
be hand-authored. `COMPOSES` emits the `[[include]]`, so the gate tests
the artifact rather than its own TOML.
* a tarball tool whose asset template matches nothing on ANY platform was
silently omitted with four notices — the vsix loop has always refused
that shape. It now refuses too, with a scenario in deposit-layer.sh.
And one regression this found in the change itself: offering every name
the SIGNED manifest declares exposed `loom` on a host whose platform that
release skips, so an unrestricted pin stopped resolving. Offers are now
manifest ∪ `bin/`, minus any name no layer has on disk — which keeps a
half-installed root from silently yielding its name to an included layer
(dispatch decided by install state is exactly what 4c forbids) without
over-reporting a tool this host never received.
Gates, by exit code: cargo fmt --all --check, cargo clippy --workspace
--all-targets -D warnings, cargo test --workspace,
tools/systest/deposit-layer.sh and tools/systest/compose-realms.sh all 0.
cargo mutants over pin.rs and resolve.rs: 107 mutants, 0 survivors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
`select_tools` now decides which binary a bare tool name dispatches to across two realms (REQ-REALM2-001 clause 4c). That is a trust decision, and it sat in a file only the advisory nightly run covered. It had three survivors while it was advisory: two off-by-ones in the depth bound (`>` mutated to `>=` and to `==`, both invisible because no test walked a chain exactly MAX_DEPTH long) and one refusal branch. All three are killed by tests added with the qualifier work — a file admitted to a zero-survivor gate with known survivors is not a gate. Measured locally: 37 mutants over compose.rs, 32 caught, 5 unviable, 0 missed; 107 over pin.rs and resolve.rs, 79 caught, 28 unviable, 0 missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…o real realms
varve#91's suggested fix was structurally incapable — `tools` filters by
NAME and the collision is two layers exposing the SAME name. `tools` now
accepts an optional realm qualifier, so the filter can express the choice
its own error demands. A bare name still parses to no realm and resolves
exactly as before.
`compose::select_tools` is the ONLY thing that decides what a bare name
dispatches to: it reads the pin's map and nothing else — not install
order, not partition order, not precedence. That is 4c, and precedence
was rejected for exactly this reason. The unchosen layer stays installed
and verified and is reachable qualified; `which` gained a `provided by
realm …` line, because the existing `layer …` line names the layer the
PIN resolved, which for a composed tool is the wrong answer to "which
fork is this".
The gate is the point. `tools/systest/compose-realms.sh` builds BOTH
realms with the production assembler, signs each under its own generated
root, composes them through a signed `[[include]]`, and verifies each
against its own root. Its negative control rebuilds varve with the pin's
choice ignored — a real `cargo build`, not a stub — and requires the
collision to become inescapable again. I ran it: PASS, control red.
Three defects it found that no unit test could:
* the assembler could only build a PULSEENGINE-shaped layer —
WSC_VERSION and VSIX_PACKAGES were required non-empty and both name
pulseengine repos. "A second realm is constructible" was a claim
nothing could execute.
* the assembler could not produce a COMPOSING layer at all, so the
gate would have had to hand-write the TOML it was meant to test.
* a tarball tool whose asset template matched nothing on any platform
was silently omitted with a notice. It bit the agent directly — a
%V mismatch dropped the fork and the run went green.
It also killed three pre-existing `compose.rs` mutants (two off-by-ones
in the depth bound, one refusal branch) and promoted that file into the
zero-survivor gate, since `select_tools` is now a trust decision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Maintainer decision, recorded as a DIRECTION with its open questions named — the design is not finished and pretending otherwise would be the overclaim this project keeps removing. Today a realm IS a long-lived ed25519 secret. threat-model and root-ceremony both state the consequences as permanent: no rotation, no revocation, no expiry, no transparency log. That is not an implementation gap, it is what pinning a raw public key means. Keyless removes the ASSET rather than mitigating the risk — nothing to generate, split, paper-back, store, rotate or lose. What made it credible is that sigil's wsc, which varve already depends on, has the pieces: signature/keyless/ (Fulcio, Rekor, OIDC, cert pinning, Merkle) and airgapped/ doing OFFLINE verification against a provisioned Trust Bundle, with a real tuf.rs — 471 lines, zero stubs. The air-gap objection was the one that nearly killed this and it does not survive contact with the source. Two findings decide feasibility. airgapped::verify_signature takes a 32-BYTE HASH, not a WASM module — a layer manifest digest is exactly that, so offline verification is already general enough. Signing is not: sign_module is WASM-shaped, so wsc needs a sign-an-arbitrary-digest entry point, which is a small addition to sigil rather than a subsystem. Also worth recording: the bundle verifier handles expiry with a grace period AND an explicit unreliable-time fallback for devices with no clock, which is exactly varve's air-gapped consumer. And a short Fulcio cert does not rot old layers — the Rekor entry is the durable evidence that the signature existed while the cert was valid. Five open questions are written into the decision rather than left implicit, the sharpest being whether provisioning a Trust Bundle is actually easier for a consumer than pinning 64 hex characters. If it is not, we have moved the problem rather than solved it, and I would rather that be answered by a spike than assumed by me. The hardware alternative was investigated and rejected on evidence, not preference: the available token is a YubiKey NEO (firmware 3.0.0) whose PIV and OpenPGP applets cap at RSA 2048 and cannot sign ed25519 at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…emove it
A spike read the source instead of the module names and refuted three
premises I had written into DD-025 hours earlier.
The deciding fact: wsc's air-gapped trust bundle is ITSELF signed with a
long-lived offline key that the consumer must pin to use the bundle at
all. Keyless does not delete the asset — it moves it from the realm root
to whoever signs the bundle, and charges a 4.5 KB expiring artifact and
roughly 120x per-layer overhead for the move. The maintainer's motivation
was unease with holding this class of secret. The mechanism does not
address it.
For an air-gapped consumer the properties get WORSE. Today's pinned root
has no expiry and no update obligation; a trust bundle hard-fails at day
396, and a clockless device fails immediately.
My three false premises, all from names rather than code:
* wsc 0.10.0 — the version varve PINS — does not do offline keyless
verification. verify_crypto says 'in a full implementation we would'
and does neither, and parses SPKI DER as ed25519 while Fulcio issues
ECDSA P-256, so it cannot succeed on a real cert. The fix is closed
upstream but unpublished.
* tuf.rs performs no TUF. It is an HTTP GET against a hardcoded URL
plus a JSON converter. I cited '471 lines, zero todo!' as evidence of
completeness. It is evidence of neither.
* the clockless fallback is in a DIAGNOSTIC function; the verification
path fails closed. varve's air-gapped consumer is exactly the case I
claimed it covered.
Also corrected: Rekor inclusion proofs are verified on neither path and
do not travel — varve's own v0.28.0 bundle carries a SET but no inclusion
proof. The SET is timestamping, not inclusion. Any doc implying otherwise
would be a fresh false claim.
Keyless is deferred, not dead. It buys a real custody reduction — a
ten-minute cert window instead of a decade-long key — and becomes
decidable when wsc 0.11 ships, sigil#256 lands, and the payload-type
binding is designed out rather than lost. Signing a bare digest would
drop a property varve has and TESTS.
Next move is deliberately not a realm-model change: adopt the trust
bundle for varve's own release artifacts first, where a hard expiry
failure is recoverable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
DD-026's next move, scoped deliberately small. The question that decides whether keyless is ever worth taking is empirical — can a consumer keep a trust bundle fresh? — and it gets answered on the cheapest surface rather than the most expensive. `varve self-verify` already verifies varve's own release sums out of band. Adding the trust bundle as an ADDITIONAL verifier there exercises provisioning, expiry and refresh against a real consumer on a path where a hard failure is recoverable: the user downloads varve again. Putting the same failure on the realm model first would mean recovery is re-pinning a realm. Clause 4 is the one I care about. varve must NOT claim inclusion proof. What verifies offline is the Rekor SET — a countersignature proving the log saw an entry at a time. That is timestamping, and it does not detect an equivocating log. threat-model currently says varve has no transparency log and no inclusion proofs; adopting a bundle must not turn that into a softer sentence, or we replace an honest limit with a false comfort. Clause 5 explicitly forbids touching varve-realms.toml or Realm::fingerprint(). Whether a realm can ever be keyless is decided by what this teaches, not by this. BLOCKED on sigil#257: wsc 0.11.0 is unpublished and varve pins 0.10.0, whose airgapped verifier fails OPEN. Building against it would produce a gate that verifies nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
varve layer-spec reads a realm's layer.toml and prints the environment the
assembler already consumes, so a realm's contents can live in that realm's own
repository. Bumping rivet stops being a commit to the tool that signs the
layer.
The adapter lives here, beside the assembler it feeds, and not in each realm's
repo: the assembler is system-tested in varve (REQ-SYSTEST-002) and a per-realm
copy inherits none of that testing.
It refuses far more than it accepts, for one reason. The assembler's inputs are
space-separated entries of colon-separated fields, an encoding that cannot
represent a value containing either, and the shell does not complain -- it
splits or truncates, and the resulting layer signs and verifies while carrying
the wrong bytes. So a mistyped key is refused rather than ignored, a second
raw-per-platform tool is refused rather than dropped, and an extension under a
foreign owner is refused rather than silently refetched from pulseengine.
Two of those refusals exist because cargo-mutants found them, not because I
reasoned my way to them. The tests and the implementation were written
together, so they agreed with each other; mutation testing is what proved they
could catch a WRONG implementation. It found two survivors, both real:
- deleting the "tarball" match arm killed nothing, because every fixture
either omitted layout or used raw-per-platform -- the spelling the docs
invite was untested;
- flipping && to || killed nothing, which exposed the serious one. The
assembler derives a tarball tool's identity from the REPOSITORY basename,
then names the payload, the extract dir and the default asset template
after it. A tool declared name = "wsc" with repo = "pulseengine/sigil"
would have deposited a payload called sigil, and a consumer asking for wsc
would find nothing in a layer that deposited, signed and verified cleanly.
That second one is the exact defect class this module was written to prevent,
sitting inside the module written to prevent it. Now refused, with an error
naming the basename it would have used. A foreign OWNER stays legal --
bytecodealliance/wasm-tools is the point of the second realm -- and a test
pins that distinction.
The && survivor's right fix was deleting the condition, not testing it: once
the mismatch is refused, repo_name == t.name holds by construction, so the
second conjunct was a condition no input could vary. The mutant proved it dead
rather than my guessing.
layerspec.rs joins the trust-critical mutation gate at zero survivors (21
mutants, 18 caught, 3 unviable), on the same terms compose.rs did earlier this
release. That job's timeout goes 45 -> 60 minutes: the v0.29.0 run used ~40 of
45 with twelve files, and a gate that times out is indistinguishable from one
that fails -- the wrong lesson to draw from a red timeout is to drop a file.
Two existing gates caught this change and both were right. The CI-tagged
command contract (REQ-CIGATE-001 clause 2) demanded --json, which it now has.
And the documented-example test parsed my layer.toml with the deposit-spec
parser, because it classifies on [[tool]] -- a layer manifest has one too. It
now classifies on [varve], and it TRANSLATES the block rather than merely
parsing it, since a documented manifest that parses but cannot be translated
is a broken example.
REQ-LAYERREPO-001 is deliberately NOT marked verified and moves to v0.30.0.
Its clauses 4 and 5 are not code: they need pulseengine-layers to publish a
real layer with a key its custodian must provision, and varve to drop that
secret only after the publish succeeds. Neither can be discharged by the party
writing the code, and claiming them on the strength of the adapter would
report a migration that has not happened. The layers repo's deposit workflow
still refuses to run rather than pretending.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
avrabe
marked this pull request as ready for review
August 21, 2026 19:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — 3 of 4 requirements verified. Opened so CI runs against the real branch.
What lands
REQ-INGEST-001✅REQ-ROLLING-001✅REQ-REALM2-001✅REQ-LAYERREPO-001⬜pulseengine-layersexists and is bootstrapped; thelayer.toml→ deposit-spec adapter does notTwo premises I got wrong, both corrected by evidence
bytecodealliance provenance. I reported they publish nothing verifiable. Wrong — I listed release assets and filtered for cosign/sums filenames, and GitHub build attestations aren't release assets. Measured with negative controls:
wasm-toolsverifies,wit-bindgenhas none,cargo-componentships no assets. That correction produced a better design than either option I'd offered — provenance is stronger than a sums file, binding artifact to workflow, repo and commit, which is what[tool.source]always wanted to record and couldn't.Keyless signing. Recorded as
DD-025on premises drawn from module names and agrep -c todo!. A spike read the source: wsc 0.10.0's airgapped verifier is a stub that fails open,tuf.rsperforms no TUF, and the clockless fallback is a diagnostic — the verify path fails closed. Most decisively, the trust bundle is itself signed with a long-lived key the consumer must pin, so keyless relocates the secret rather than removing it.DD-026supersedes it; varve keeps ed25519. Five issues filed upstream (sigil#256–#260).varve#91 — the fix its own error message couldn't express
The collision advice — "restrict the pin's
tools" — wasn't unimplemented, it was structurally incapable:toolsfilters by name, and the collision is two layers exposing the same name.toolsnow takes a realm qualifier (tools = ["bytecodealliance/wasm-tools"]), the unchosen layer stays installed and reachable qualified, and exactly one shim per name means the pin decides dispatch — never install order. Realm precedence was rejected for that reason.What the gates found that review didn't
tools/systest/compose-realms.shbuilds both realms with the production assembler, signs each under its own root, composes them through a signed include, and its negative control rebuilds varve with the pin's choice ignored. It found three defects:It also killed three pre-existing
compose.rsmutants and promoted that file into the zero-survivor gate.Gates
fmt · clippy · test · docs-coverage · rivet validate · claim-check · verification-evidence — all exit 0. Plus
deposit-layer.sh,scan-upstream.shandcompose-realms.sh, each with its own negative control confirmed red.REQ-LAYERADAPT-001— a realm's manifest, translated exactly or not at allvarve layer-specreads a realm'slayer.tomland prints the environment the assembler already consumes, so a realm's contents live in that realm's repository. Bumpingrivetstops being a commit to the tool that signs the layer. The adapter lives beside the assembler it feeds, not in each realm's repo, because the assembler is system-tested here and a per-realm copy inherits none of that.It refuses far more than it accepts, for one reason: the assembler's inputs are space-separated entries of colon-separated fields, and a shell does not complain when a value contains either — it splits or truncates, and the layer signs and verifies while carrying the wrong bytes.
Two of those refusals exist because
cargo mutantsfound them. The tests and implementation were written together, so they agreed with each other; mutation testing is what proved they could catch a wrong implementation. Two survivors, both real:"tarball"match arm killed nothing — every fixture omittedlayoutor usedraw-per-platform, so the spelling the docs invite was untested;&&to||killed nothing, which exposed the serious one. The assembler derives a tarball tool's identity from the repository basename, then names the payload, extract dir and asset template after it.name = "wsc"withrepo = "pulseengine/sigil"would have deposited a payload calledsigil— invisible to anyone asking forwsc, in a layer that deposited, signed and verified cleanly.That second one is the exact defect class this module was written to prevent, sitting inside the module written to prevent it. A foreign owner stays legal (
bytecodealliance/wasm-toolsis the point of the second realm) and a test pins the distinction. The&&survivor's right fix was deleting the condition: once the mismatch is refused, it was a condition no input could vary.layerspec.rsjoins the trust-critical gate at zero survivors (21 mutants, 18 caught, 3 unviable), on the termscompose.rsset. That job's timeout goes 45 → 60 min; a gate that times out is indistinguishable from one that fails.Two existing gates caught this change and both were right: the CI-tagged-command contract demanded
--json, and the documented-example test parsed the newlayer.tomlwith the deposit-spec parser because it classified on[[tool]]. It now classifies on[varve]and translates the block rather than merely parsing it.What is deliberately NOT claimed
REQ-LAYERREPO-001is not marked verified and moves to v0.30.0. Its clauses 4 and 5 are not code: they needpulseengine-layersto publish a real layer with a key its custodian must provision, and varve to drop that secret only after that publish succeeds. Neither can be discharged by the party writing the code. The layers repo's deposit workflow still refuses to run rather than pretending, andpulseengine-layerspins varve v0.28.0 — so it cannot calllayer-specuntil this release ships.