Skip to content

v0.30.0: two live defects a five-way review found, and the gates that stop them returning - #114

Merged
avrabe merged 14 commits into
mainfrom
plan/v0.30.0
Aug 28, 2026
Merged

v0.30.0: two live defects a five-way review found, and the gates that stop them returning#114
avrabe merged 14 commits into
mainfrom
plan/v0.30.0

Conversation

@avrabe

@avrabe avrabe commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

A concept document went to five reviewers — a security architect, an STPA-Sec analysis, a ceremony operator, an adopting engineer, and an independent certification assessor. All five dissented. Two findings were live defects on the production path rather than opinions about a future design. Those are fixed here; the design argument continues in #113.

1. varve which returned a value no script could use

It printed the path and the provenance to stdout, so M=$(varve which meld) yielded a two-line string that is not an executable path.

The code carried a comment claiming "STDOUT is the dispatched path, unchanged, so scripts that capture it keep working", and a second saying "the first two lines are what scripts capture". A script captures all of them. The comments asserted the property the code violated.

Not cosmetic: this is what made a consumer's build script fall through to an ambient meld 0.41.3 and die naming a version nobody pinned (#102) — the mixed-toolchain failure varve exists to close, produced by the command whose job is closing it, still reproducing on v0.29.0 against a layer published the same day.

The test that should have caught it asserted stdout.contains(path) and stdout.contains(layer_id) — which passes just as happily on two lines. The defect survived a test named for the behaviour it broke. The new test asserts the shell contract: what a caller captures must be one line and an executable absolute path.

Verified end-to-end against published layer 2026.08.4 — the captured value now runs and reports meld 0.52.0.

2. The realm's signing key was written to disk on a shared runner

docs ci names echo "$SECRET" > key.tmp as the thing adopters wrongly invent. docs root-ceremony says the key must reach varve "through a file descriptor, never a workspace file".

varve's own deposit workflow wrote it to /tmp/rolling.key — a predictable path, not even the mktemp+chmod 600 fallback the docs allow — for every layer it has ever published. release.yml did the same for the release sums.

The assessor found this by reading the repository rather than the documentation, and was right about why it matters: a published procedure the publisher does not follow invalidates every other procedural claim by induction.

Both now use the documented process-substitution form. And the rule stops depending on review: tools/no-key-on-disk.sh refuses any workflow redirecting key-shaped material into a file, with --self-test proving it goes red against four shapes — including the exact line this repo shipped — and stays green on the two documented forms. Both run in CI.

The gate earned its self-test immediately: on first run it flagged its own documentation comment, because grep -rIn prefixes hits with file:line: and a naive leading-# filter never sees a commented line.

What is deliberately NOT fixed here

There is still no GitHub Environment with required reviewers on the signing job, so the two-person rule remains a convention presented as a control (assessor finding (b)4). That needs a repository settings change, not a commit.

avrabe and others added 14 commits August 27, 2026 07:13
…w turned up

A concept document went to five reviewers -- a security architect, an STPA-Sec
analysis, a ceremony operator, an adopting engineer, and an independent
certification assessor. All five dissented. Two findings were live defects on
the production path rather than opinions about a future design, and they are
fixed here.

1. `varve which` returned a value no script could use.

It printed the resolved path AND the provenance to stdout, so
`M=$(varve which meld)` yielded a two-line string that is not an executable
path. The code carried a comment claiming "STDOUT is the dispatched path,
unchanged, so scripts that capture it keep working", and a second saying "the
first two lines are what scripts capture". A script captures all of them. The
comments asserted the property the code violated.

This is not cosmetic and it is not hypothetical: it is what made a consumer's
build script fall through to an ambient meld 0.41.3 and die with an
`unexpected argument` error naming a version nobody pinned (#102) -- the
mixed-toolchain failure varve exists to close, produced by the command whose
job is closing it, still reproducing on v0.29.0 against a layer published the
same day.

The path stays on stdout; provenance moves to stderr, where a human at a
terminal still reads it and command substitution does not.

The test that was supposed to cover this asserted `stdout.contains(path)` AND
`stdout.contains(layer_id)`, which passes just as happily on a two-line
stdout -- so the defect survived a test named for the behaviour it broke. The
new test asserts the shell contract itself: what a caller captures must be one
line and must be an executable absolute path. Verified end to end against
published layer 2026.08.4: the captured value now runs, and reports meld
0.52.0.

2. The realm's signing key was written to disk on a shared runner.

`docs ci` tells adopters that "every adopter therefore invents
`echo "$SECRET" > key.tmp`, which leaves the realm's one secret on disk", and
documents two forms that avoid it. `docs root-ceremony` states the rule
outright: in CI the key reaches varve "through a file descriptor, never a
workspace file".

varve's own deposit workflow wrote it to /tmp/rolling.key -- a predictable
path, not the mktemp-and-chmod fallback the docs allow -- for every layer it
has ever published. release.yml did the same for the release sums. The
assessor found this by reading the repository rather than the documentation,
and was right about why it matters: a published procedure the publisher does
not follow invalidates every other procedural claim by induction. Every other
self-declaration in the repo now has to be checked rather than sampled.

Both now use the documented process-substitution form, with `shell: bash` made
explicit because that is a bash feature.

And the rule stops depending on review. tools/no-key-on-disk.sh refuses any
workflow that redirects key-shaped material into a file, and carries
`--self-test`, which proves the gate goes RED against four shapes -- including
the exact line this repository shipped -- and stays green on the two documented
forms. Both run in CI.

The gate earned that self-test immediately: on its first run it flagged its own
documentation comment, because `grep -rIn` prefixes hits with `file:line:` and
a naive leading-`#` filter never sees a commented line.

What is NOT fixed here, and is the assessor's finding (b)4: there is still no
GitHub Environment with required reviewers on the signing job, so the
two-person rule remains a convention presented as a control. That needs a
repository settings change, not a commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…ent varve won PATH

Reported from a real install, and reproduced on the maintainer's machine:

    ~/.varve/bin/varve   ->  varve 0.25.0     what install.sh installed
    ~/.cargo/bin/varve   ->  varve 0.29.0     what `varve` actually runs

The user followed the documented install, was told it succeeded, and then ran a
binary the installer had not put there. Nothing in the output said so. Worse,
the PATH message actively reassured: "$INSTALL_DIR is already on PATH" -- true,
and useless, because being ON PATH is not being FIRST on it.

The installer also overwrote whatever was at the destination with `mv` and
reported "Installed varve X", never mentioning that it had replaced a different
version.

This is REQ-SHADOW-001 turned on varve itself. varve REFUSES to claim success
when PATH shadows a pinned tool -- "your shell will run the first one; varve
dispatches the second" -- and its own installer did not apply that reasoning to
the binary it had just installed. A bootstrap that can leave you running a build
it did not install has not bootstrapped anything, and this one verified a
signature over bytes the user then did not execute.

Now:

  * it reports what it replaced ("replaced varve 0.25.0"), and says
    "Reinstalled (unchanged)" when nothing moved, rather than implying change;
  * it resolves `varve` the way the shell will -- `command -v`, not a guess
    about where things live -- and when the winner is not what it just
    installed, it prints both paths with both versions and how to fix it,
    including `cargo uninstall varve` for the common cause;
  * "already on PATH" is now only printed when it is also what runs.

tools/systest/install-shadow.sh gates it, and carries its own negative control:
the warning must FIRE against a shadowing PATH, must stay quiet when the
install wins and when nothing competes, and install.sh must still contain the
check. Verified the gate goes red -- deleting the warning text from install.sh
fails it.

That control is the point. This defect survived because nothing ever exercised
the shadowed case; an installer test that only walks the happy path cannot see
the failure the installer exists to prevent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…r did we

Reported from a real session. A user pinned a project to `realm = "linc"`,
layer 2026.08.26, ran `varve inspect`, and pointed out that the output never
says the realm:

    layer 2026.08.26 (rolling) sha256:a371ba81...
    2 payload(s): 2 DISPATCHED, 0 HELD  (platform x86_64-unknown-linux-gnu)

Layer, channel, digest, platform, and a LAYER column -- and not one mention of
the realm the whole trust model hangs on.

That is not cosmetic. A layer identifier is YYYY.MM.P and is unique only WITHIN
a realm; two realms can each publish 2026.08.26. varve is built for precisely
that world -- REQ-REALM2-001 stood up a second realm and added a pin qualifier
to settle name collisions between them -- so "layer 2026.08.26" does not say
which 2026.08.26.

The realm was available all along, on ComposedLayer.realm, and was printed only
inside the `composition` block, which is skipped when a layer composes nothing.
That is the ordinary case, so in ordinary use the realm was invisible.

`varve inspect` now names it in the header and carries a top-level `realm` in
--json, where a consumer diffing two reports needs it most.

The same gap from the other side: a shim declining a tool named the layer and
what it exposes, but not which varve.toml chose that layer, nor the realm it
selects. With one shim directory serving every realm and `cd` switching
toolchains, the user's real question is not "what is in this layer" but "why
did this tool stop working HERE" -- and the answer is a file they can open.
The refusal now names both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…ibing two

An assessor's finding was that the two-person rule on the deposit path is a
convention presented as a control: no GitHub Environment, no required
reviewers, no CODEOWNERS. The obvious fix was to configure required reviewers.

The operator's answer is that there is nobody to review. That is correct and
checkable: 105 of 105 commits in this repository are by one person. A
required-reviewer gate here produces either a self-approval -- a control in
name only, and one that reads as an attempt to appear compliant -- or a
permanently blocked pipeline.

So the requirement now asks for the control the organisation can actually
staff, and for the documentation to claim exactly that and no more.

For a single operator that means trading prevention for DETECTION. Prevention
is what a second person buys. Detection is available alone: a wait timer that
opens a cancel window, an environment restricted to the one ref a deposit may
run from, and a durable record of what was signed and when, written by the
pipeline rather than by the operator. An unauthorised signature you can
discover afterwards is enormously better than one you cannot.

`docs root-ceremony` prescribed "two people present" and "an access log with
two-person rule" as though varve's own realm had them. It does not. The topic
now says so directly, in a "When you are one person" section, because a
document prescribing controls its own author cannot staff is a document an
assessor will use to discount everything else in it -- and this project has
already collected that exact finding once, over a key written to /tmp against
its own published rule.

The section also carries the two things a solo operator should do that the rest
of the topic underweights: prefer a custody share held by an INSTITUTION, which
survives moving house and dying in a way a friend's desk drawer does not; and
write down now what happens to the realm if the operator stops, because a realm
whose root is unreachable while consumers still pin it decays into a
supply-chain hazard quietly, and that decision is unmakeable later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…ot signed

Ports the step that locates the tool inside an extracted release archive:

    bin="$(find "extract/$tool-$platform" -type f -name "$binname" | head -1)"

Two defects in one line, and both decide which BYTES GET SIGNED into the layer.

`head -1` takes the first result in filesystem enumeration order. An archive
carrying `bin/rivet` and `share/doc/examples/rivet` deposits whichever the
kernel handed back first, and the same archive can resolve differently on the
next runner. Nothing downstream notices: the wrong file is hashed, recorded and
signed exactly as carefully as the right one would have been.

`-type f` never checks executability, so a README named `rivet` is an equally
valid candidate -- it would be deposited as a dispatched tool and fail only
when somebody tried to run it.

The port refuses ambiguity rather than resolving it by luck. One executable of
that name is taken. Several are narrowed by `bin/`, which is the only
tie-break real layouts justify -- every archive that ships two files of one
name puts the tool under bin/ and the copy elsewhere. If that still leaves more
than one, it is an ERROR naming both, because which one is deposited decides
what gets signed and that is not a guess worth making. A name that matches only
non-executable files is refused separately, saying so, since "found nothing"
and "found only documentation" send an operator to different places.

Candidate lists in errors are sorted, so a refusal reads identically on every
machine -- the message is the only record of why a deposit stopped.

cargo mutants: 17 mutants, zero survivors. Two survived the first pass, both
`>` widened to `>=`, and both were real coverage gaps rather than noise:

  * every "single executable" test happened to place it under `bin/`, so
    nothing could tell "exactly one candidate" from "several, one under bin".
    A flat archive is a real layout and now has a test.
  * the preview elision was only exercised well past its cut, so a message
    claiming "and 0 more" would have passed. Tested at exactly the boundary
    and one past it.

extract.rs joins the trust-critical mutation gate on the same terms as the rest
of the crate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
… that they work

Everything the producer verifies answers one question: are these the bytes
upstream published? A cosign-signed sums file or a build attestation
establishes that, and the digest recorded in the layer transcribes it.

Nothing establishes that the bytes are a WORKING TOOL for the platform they are
filed under. The deposit's own "sanity" step installs the layer and runs `varve
verify`, which checks signatures and digests -- the same property again, more
carefully. No deposited binary is ever executed, and no systest runs one.

So an upstream that ships an x86_64 binary inside its aarch64 tarball produces
a layer that assembles, signs, publishes and installs perfectly. The digest is
correct: it faithfully records the wrong file, which is precisely why nothing
in the pipeline notices. The failure surfaces on a consumer's machine as
"cannot execute binary file" -- the one place nobody can fix it.

This reads the executable header instead. Reading, not executing, because a
deposit runs on ONE machine and ships four platforms; a check that only covered
the runner's own architecture would miss three quarters of the layer. ELF
(honouring EI_DATA rather than assuming little-endian) and Mach-O, mapped
against the target triple.

The distinctions matter as much as the check. A truncated file is refused --
it hashes perfectly and is not a program. A `#!` script and a universal Mach-O
are REPORTED, not refused: both are legitimate payloads that carry no single
architecture. An unrecognised format is surfaced rather than silently accepted,
because "varve cannot identify this" is a fact an operator should see before
signing.

Validated against the real binaries in published layer 2026.08.4, not only
synthetic headers -- a fixture that speaks a shape the tool never produces is
how this project has been burned before. All three identify as MachO(Aarch64),
and the same files filed under x86_64-unknown-linux-gnu are refused, exit 1.

cargo mutants: 26 mutants, zero survivors. Four survived the first pass and all
four were real:

  * `Arch::as_str` could return "" or "xyzzy" undetected -- no test asserted
    that the mismatch message NAMES both architectures, which is the entire
    output of this check;
  * `&&` widened to `||` in the ELF guard walks off the end of a TRUNCATED ELF
    header, a plausible real input that nothing exercised. Now tested at every
    length from 4 to 19;
  * the length guard was only tested well inside its boundary, so a four-byte
    file could have been refused as truncated.

Clause 4 -- smoke-running host-platform payloads -- is not implemented here.
The architecture check covers all four platforms and is the higher-value half;
execution only ever covers the runner's own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Measured first. Layer 2026.08.4 is 421 MiB across 43 blobs and four platforms.
A consumer installs 99 MiB of it, because install.rs filters on
platform::entry_matches BEFORE fetch_blob -- the `continue` skips the download
entirely. So the 421 MiB is a PRODUCER cost, not a consumer one, and daily
scanning (DD-024) multiplies it by the cadence: a full re-download to produce a
layer that usually differs by one binary.

The previous layer's signed manifest already records each payload's repo,
release, asset and sha256. When layer.toml still pins that release the bytes
are already in the registry under that digest.

The trap, and the reason this module is not a version-string cache: a release
asset can be DELETED AND RE-UPLOADED under the same tag. "rivet v0.34.0" today
is not necessarily the bytes it was yesterday. Carrying a digest forward
because the version matched would make varve blind to exactly the substitution
it exists to catch -- and blind silently, since every later check would agree
with the carried digest.

So the saving comes from skipping the DOWNLOAD, never the PROOF. The ingestion
proof is re-established unconditionally; a sums file is kilobytes while the
binary it describes is tens of megabytes. Only once upstream's CURRENT digest
is in hand, and equal, do the bytes go unfetched.

When they disagree it is not a cache miss. It is an upstream that re-published
a release under the same tag, the deposit stops, and the message names both
digests -- varve cannot tell a re-release from a substitution, which is
precisely why it will not choose. That is a detection varve does not have
today, arriving as a side effect of trying to save bandwidth.

Reuse also requires the blob to still be PRESENT in the destination registry: a
manifest entry is a record, not a guarantee of storage, and registries garbage
collect. A republished upstream aborts even when the blob is absent, because
the substitution is the finding and a missing blob does not demote it to a
routine fetch.

Digest comparison normalises the `sha256:` prefix and case, because the OCI
form and the sums-file form are the same digest -- and normalises nothing
further. An empty digest equals nothing, including another empty one: "we have
no digest" must never satisfy a comparison. A prefix is not a match.

cargo mutants: 15 mutants, zero survivors, first pass.

Not yet wired into an orchestrator -- that arrives with the gh seam. The gate
clause 6 names is depositing an unchanged layer.toml twice and fetching no
payload bytes the second time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
… org

The second realm has existed only inside tools/systest/compose-realms.sh. The
shipped varve-realms.toml defines exactly one realm, and all five published
tags are pulseengine's, so "varve composes two realms" has been true in the
gate and untrue in the registry.

Two different things get called "the bytecodealliance layer", and only one of
them is buildable today.

A separate bytecodealliance REALM needs its own root key. Minting one now would
repeat 2026-08-07 precisely -- the STPA-Sec analysis named that failure
UCA-MINT-d1, "ceremony stopped before custody", and observed that the current
realm exists because it already occurred once. A second unbackupable CI key
while #110 is open is not a trade worth making.

wasm-tools carried IN the pulseengine realm needs no new key, and every part of
it is already proven. Verified against the real release rather than assumed:

  * all four platforms resolve through the %U upstream tag --
    wasm-tools-1.257.1-{aarch64,x86_64}-{macos,linux}.tar.gz -- using the
    mapping ported into varve-producer;
  * `gh attestation verify` succeeds on the downloaded aarch64-macos tarball
    (exit 0), and fails on tampered bytes (exit 1). The rung works and the
    negative control bites.

This is what REQ-INGEST-001 was built for. bytecodealliance publishes no
cosign-signed sums file; it publishes build attestations, which bind the
artifact to the workflow, repository and source commit -- strictly more than a
sums file asserts. The mechanism that vouched is recorded INSIDE the signed
layer, so `varve inspect` will show this payload arriving on build-provenance
while the rest arrive on cosign-sums. A consumer can see how each tool got in
rather than inferring it.

The honest tradeoff: the pulseengine root signs a layer containing bytes it did
not build. That is exactly what the ingestion proof records and makes visible,
and it is forward-compatible -- if a bytecodealliance realm is ever stood up
with its own root, both realms would expose `wasm-tools` and the pin's realm
qualifier settles it (REQ-REALM2-001 clause 4c).

The same entry lands in pulseengine-layers/layer.toml in a companion PR;
`varve layer-spec` confirms the two are byte-identical, which is the check #106
exists to make routine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…eremony

Reverts carrying wasm-tools inside the pulseengine realm, and records why.

The shortcut worked. All four platforms resolve through the %U upstream tag,
`gh attestation verify` succeeds on the real bytecodealliance release and fails
on tampered bytes, and it needs no new key. I proposed it because it was
buildable today.

It was rejected for a better reason than it was proposed. A realm is a trust
universe defined by one root. Letting the pulseengine root sign bytes this
organisation did not build makes that boundary decorative -- and recording an
ingestion proof documents WHOSE bytes they are, not who vouched for them. The
shortcut's entire appeal was avoiding the key problem, which is an argument for
solving the key problem rather than routing around it.

So bytecodealliance gets its own root, produced by the ceremony
REQ-CEREMONY-001 defines rather than generated into CI, which is how the
pulseengine root came to be unbackupable and unmovable (#110).

The part worth keeping from this detour: THIS is the realm to run the first
ceremony on. It is the only realm whose ceremony can go wrong cheaply -- no
consumers, no published root anyone pins, nothing frozen if it has to be
redone. The exact opposite of the pulseengine realm, where a mistake is
unrecoverable because varve has neither rotation nor revocation.

An assessor's single strongest recommendation was to stop writing about the
ceremony and run one, this month, and publish the transcript. Doing it on this
realm gives that run a real deliverable instead of a discarded key -- and gives
the ceremony a deadline, which "at v1.0" has never supplied.

Sequencing that matters: a rehearsal with a genuinely throwaway key comes
first, its transcript IS the runbook the real ceremony is executed from, and
the two do not happen on the same day. A real ceremony run at hour eight of a
rehearsal is where custody defects are born.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…-for

Setting up the bytecodealliance realm exposed that layer.toml could not express
the one thing that realm needs. Four of the six tools rules_wasm_component
requires -- wac, wit-bindgen, wrpc and wkg -- publish no SHA256SUMS, no cosign
bundle and no build attestation. Checked twice, against two different assets
each. Only wasm-tools and wasmtime are attested.

REQ-INGEST-001 refuses such a release unless an operator states why, and the
reason is signed into the layer where `varve inspect` shows it beside the
payload. But the reason lived only in the assembler's UNVERIFIED_INGEST
environment variable, so the layer's definition would have been split between
layer.toml and a workflow file -- which is exactly the drift #106 is about.

`unverified-reason` now sits on the tool it excuses. Emission is per REPOSITORY,
because the opt-in is per release: two tools from one repo must agree, or one
reason would be recorded and the other silently dropped.

The rendering needed care. UNVERIFIED_INGEST is LINE-separated -- the assembler
chose that deliberately, because a reason is prose and any punctuation
separator can occur inside it -- and a `KEY=value` line cannot carry newlines.
It uses $GITHUB_ENV's heredoc form, with the delimiter chosen AGAINST the
content: a reason that happened to contain the delimiter would close the block
early and let whatever followed be read as further environment, which is an
injection rather than a typo. There is a test that plants the delimiter plus a
`PATH=/evil` line and requires both that the block holds and that the reason
survives verbatim.

TWO REAL BUGS, both found by actually trying to assemble the manifest rather
than by reasoning about it:

The assembler's single raw-per-platform slot is not generic -- it fetches `wsc`
from `pulseengine/sigil`. Putting any other raw tool in it emitted
WSC_VERSION = that tool's version, so `wac v0.10.1` would have downloaded wsc
from pulseengine/sigil at v0.10.1: wrong tool, wrong repository, wrong version,
deposited under the wrong name, silently. Now refused, naming all three.

And the guard for that was `name != wsc || owner != pulseengine || repo !=
sigil`, tested only with a tool where all three differ -- which cannot
distinguish it from a much weaker guard. cargo-mutants narrowed it to `&&` and
killed nothing. Now tested with each field wrong on its own.

The bytecodealliance realm still cannot be assembled: it needs THREE
raw-per-platform tools and the shell assembler has one slot. That is a
prerequisite the Rust producer removes by construction, since layout is a
property of a tool there rather than a hardcoded variable -- which makes
finishing the port a dependency of the second realm, not a refactor beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…comes buildable

This is where the port stops being a translation.

The shell could not read layer.toml. It read three environment variables --
TARBALL_TOOLS, WSC_VERSION, VSIX_PACKAGES -- encoded as space-separated entries
of colon-separated fields. That encoding is why layerspec must refuse a version
containing a space, why the opt-in reasons need a heredoc delimiter chosen
against their own content, and why a whole class of tests exists. plan.rs reads
the manifest directly: there is no encoding, so there is nothing for a
separator to corrupt.

It also removes a hard limit instead of guarding against one. The shell carried
exactly ONE raw-per-platform tool, because that layout lived in a variable
named WSC_VERSION that named a specific tool in a specific repository. The
bytecodealliance realm needs three -- wac, wkg and wrpc -- and could not be
assembled at all. Here layout is a property of a tool, so three is not a
special case; it is three.

Verified by planning the real bytecodealliance manifest: 12 payloads from 6
releases, three raw-per-platform tools, four releases flagged as carrying no
proof of origin.

THEN CHECKED, WHICH IS THE PART THAT MATTERED. Every planned asset name was
compared against the actual release listings. Four of twelve did not exist, and
each exposed a real gap in the template language rather than a typo:

  * wasmtime names assets `wasmtime-v48.0.1-aarch64-macos.tar.xz` -- the
    version WITH its `v`. %V strips it and nothing carried the tag as written,
    so a manifest would have had to hardcode the version inside the template,
    making a version bump edit two places. Added %R, expanded before %V so a
    template using both does not leave a stray `v`.

  * wac and wrpc ship only a musl Linux build:
    `wac-cli-x86_64-unknown-linux-musl`. A static musl binary is the correct
    payload for a gnu platform, but no template can derive that name from
    `x86_64-unknown-linux-gnu`. Inventing a %MUSL placeholder would guess at a
    convention; `[tool.asset-for]` names the file, which is wrong-by-typo
    rather than wrong-by-inference.

With both, the same check reports 0 missing of 12.

A first attempt at that check reported 12 of 12 missing, because `declare -A`
is not portable to zsh and the reference list was silently empty. It was one
step from being reported as a finding about upstream. A checker that cannot
distinguish "nothing matched" from "nothing to match against" is the same
vacuous-gate shape this project keeps finding, and it appeared in the tool
built to catch it.

plan.rs joins the trust-critical mutation gate. plan + asset: 54 mutants, zero
survivors. layerspec after the schema change: 29 mutants, zero survivors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…uild

MSRV failed with "the lock file needs to be updated but --locked was passed".
`tempfile` was added when extract.rs was expected to walk a directory; it ended
up a pure function over a listing, so nothing ever used it -- but Cargo.lock
carried the entry and was never committed.

Removing it restores the lockfile exactly, so `cargo build --workspace
--locked` is clean again.

Worth naming why it slipped: every local run was `cargo test`, which updates
the lock silently. Only `--locked` refuses, and only CI passes it. The gate
caught what my habit did not, which is what it is for.
`inspect` was changed to name the realm and NOTHING tested it. Added the test
that would fail if it regressed -- header carries the realm, and --json carries
it as a field so a pipeline gets it without scraping.

Statuses advanced from draft. Two reach verified because a Rust test would fail
if the behaviour went away: REQ-WHICHSTDOUT-001 and REQ-NAMETHEREALM-001.

The rest stay `implemented`, each for a stated reason rather than for lack of
attention:

  * REQ-PRODUCER-002 -- the port is partial; the gh seam and orchestrator are
    not written.
  * REQ-CARRYFORWARD-001 -- clause 6's gate is "deposit an unchanged layer.toml
    twice and fetch nothing", which needs the orchestrator to exist.
  * REQ-PAYLOADSMOKE-001 -- clause 4, smoke-running host-platform payloads, is
    deliberately unimplemented; the architecture check covers all four
    platforms and is the higher-value half.
  * REQ-NOKEYDISK-001 and REQ-INSTALLSHADOW-001 -- verified by shell gates that
    each carry a negative control, and `rivet coverage` does not read markers
    from shell files. Established by reproduction: the same marker in the same
    directory is found in a .rs file and missed in a .sh file, so it is the
    extension rather than --scan-paths. Filed as rivet#870.

That last one is worth not papering over. Leaving a gate-verified requirement
at `implemented` makes "verified by an executable check with a negative
control" look identical to "not verified", and that difference is exactly what
an assessor asks about. The markers are in the scripts already, waiting for the
scanner to read them.
@avrabe
avrabe merged commit 1ea7669 into main Aug 28, 2026
21 checks passed
@avrabe
avrabe deleted the plan/v0.30.0 branch August 28, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant