v0.28.0: the release that makes varve safe to operate - #98
Merged
Conversation
REQ-VERIFYALL-001, varve#84. `verify --all --help` has always said "Verify
every installed layer instead of only the pinned one". It walked only the
PINNED project's realm partition. A security auditor in the ten-persona
audit planted a backdoored binary in a second realm's installed layer, ran
`verify --all`, got exit 0, and then executed the backdoor.
`docs recovery` sends readers here as THE store-wide integrity check, so
the docs, the help text and the operator all agreed and only the code
dissented. This is the failure this release is named for: not a missing
feature, but a check that reports success while structurally unable to
see the thing it is checking.
Two bugs in one function. `ctx.store` is the pinned realm's partition, so
`store.list()` could not see another realm; and `ctx_verifier(&ctx)` then
applied the pinned realm's root to whatever it did find. The second would
have been trust WIDENING across a realm boundary if the first had ever
let it reach a foreign layer.
`verify --all` now walks every partition and checks each layer against the
root of the realm that OWNS that partition. The realm boundary is
preserved in WHICH KEY verifies WHAT, not in what gets looked at — which
is the distinction the original code collapsed.
Also, per clauses 2-5, because "it exits non-zero now" is not the whole
requirement:
* every failure is reported in ONE run. It was fail-fast, and in one
observed run the only layer id on screen belonged to a different,
HEALTHY layer.
* each failure names the layer id AND its store path — "which one" is
the first question an operator asks.
* the scope covered is always printed (`checked N of M layer(s) across
P partition(s)`), so a future regression that narrows `--all` shows
up in the output rather than passing quietly. That line is asserted
by the test, so the tell cannot be removed silently.
* a partition whose realm varve-realms.toml no longer defines is
REPORTED, not skipped. Nothing can vouch for it, and an unverifiable
layer in the store is exactly what a store-wide check is for.
`Store::partitions()` exposes the enumeration `find_anywhere` already had
inline and private — a caller that needed to WALK the store rather than
look one digest up had no way to do it, which is how the scoping bug
became possible in the first place.
Three CLI tests, covering all five clauses; the headline one reproduces
the auditor's scenario end to end. Advanced to `verified` via
`rivet verify`, which refuses without evidence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…where
v0.27.0 shipped six requirements and only one reached `verified`. That is
trace debt, and it compounds: the release after next inherits a graph
whose left side grew and whose right side did not.
Advanced via `rivet verify`, which refuses without evidence:
* REQ-CRATEIDX-001 — 6 source markers + 1 incoming link. The real
oracle is stronger than the markers: varve's own 250 packages
exported through export-cargo and built offline in an empty
CARGO_HOME, in CI, with a negative control that must fail.
* REQ-COMPOSEEXPORT-001 — 9 markers. Clause 1b was dead code under
test until clean-room review caught it (every fixture emitted
realm-less includes, so swapping in the includer's verifier left the
suite green); that gap is now closed by an actual cross-realm test.
Deliberately NOT advanced, each with the reason recorded next to its
status so the next reader does not have to rediscover it:
* REQ-REPRO-001 stays `implemented` — clause 3's gate cannot catch a
second-granularity timestamp, because two exports in one test land in
the same second (varve#93). It samples rather than proves.
* REQ-SYSTEST-001 stays `implemented` — clause 1 is genuinely green,
clause 2 is partial (no Bazel --distdir gate, varve#27), clause 4 is
policy with no enforcement. Its evidence is shell-based so it carries
no cargo-test markers and `rivet verify` would refuse; that refusal
is CORRECT, not an obstacle to route around.
* REQ-EXPORTDECL-001 stays `approved` — clause 2's `select` is parsed,
validated, and consumed by nothing. Advancing it would claim a
capability the code does not have.
The distinction being drawn: `rivet verify` checks that evidence EXISTS,
not that it is sufficient. Three of these have markers and would have
advanced if I had asked. Whether the clauses are actually discharged is
a judgement the tool cannot make, and blanket-advancing everything with
a marker is how a graph becomes decorative.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
… deliver
REQ-CUSTODY-001.
`docs own-realm` shipped a realm that verifies perfectly and can NEVER be
yanked. Following it verbatim never signed or attached a baseline
line-status, so every consumer's `varve status` failed forever:
error: no line-status document cached for line 2026.08.
Verified end to end against the binary. The topic also taught
install-immediately-after-deposit, which is the ordering that walks an
operator into re-depositing over their attachments. It now has six steps
with the producer side (deposit → sign/attach-status → sign/attach-index)
completed before any install, states the REASON each attach exists, and
cross-links `docs ci`, which it previously referenced from nowhere.
New topic `root-ceremony`: air-gapped generation, split custody, paper
backup and restore, long-term storage. It states plainly that there is no
rotation and no revocation (matching `docs threat-model`), that the
compromise plan is manual and belongs in the ceremony record, and that
HSM/PKCS#11/KMS are ABSENT rather than out of scope — REQ-CEREMONY-001 at
v1.0.0 is where they would land. The half that people get wrong is
documented with proof: splitting the 128-hex key file down the middle is
not split custody, because the first 64 characters are the seed and the
public half is derived from it.
`[tool.source].sha256` is signed and never verified by anything — not at
deposit, not at verify, not at install — while `export-bazel` emits it as
the checksum Bazel enforces under a header reading "digests transcribed
from the signed layer manifest". True, and not what a reader takes from
it. Documented in `config-reference` and `payload-kinds`.
README addressed the reader only as a PulseEngine consumer and contained
zero occurrences of `keygen` or `own-realm`; running your own trust
universe was undiscoverable to anyone evaluating varve. It now has a
section near the top.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…-002) varve's consumer path has selfhost.sh. The producer path — the layer assembly in deposit-layer.yml, which builds the official signed layer — had no test at all. It was exercised only by dispatching it against the real GHCR registry, and that is how the 2026.08.3 deposit was found to be broken: rivet and spar each appear in BOTH TARBALL_TOOLS and VSIX_PACKAGES, so verify_release ran twice for one repo, `gh release download` refused to overwrite the sums it had already fetched, and `set -e` ended the run. A 17-check CI gate including a 29-minute mutation run could not have caught it, because nothing in CI executed that workflow. The dry-run that preceded that deposit is the other half of the lesson. It exercised the VSIX selection against both releases' real asset lists and got it right — but it STUBBED verify_release and asset_sha, so it tested everything except the download that broke. So: the assembly logic moves out of the inline `run:` block into tools/build-deposit-spec.sh, which the workflow now invokes. The gate runs THAT script, not a copy of it — a test of a copy is a test of the copy. What is replaced is the SERVICE, not the component: fixture-backed `gh` and `cosign` on PATH, driven by a recorded release inventory, so the gate is hermetic and does not depend on someone's release still existing. The `gh` double reproduces the one behaviour that matters — the refusal to overwrite — because a double that quietly clobbered would hide the very regression this exists for. Covered, on a layer with both dual-listed repos: a per-platform .vsix set (four in, and a consumer resolves exactly its own), a portable one, a tool whose binary is named differently from its repo, raw per-platform binaries, and a platform with no asset (a notice and an omission, never a failure). Then five refusals, each checked for its own message rather than merely for a non-zero exit — above all one repo requested at TWO versions, which the fixture makes reachable by carrying rivet v0.33.0 complete with its own vsix, so the guard is the only thing that can stop it and not a missing asset. Otherwise one release's assets would be checked against another release's sums: verification that passes while proving nothing. The spec is then parsed as TOML, asserted payload by payload, deposited, signed, given a baseline line-status, installed, verified and exported — and STOPS before `oras push`. Publishing is not what needs testing. Two things keep it from rotting. The live TARBALL_TOOLS/VSIX_PACKAGES/ WSC_VERSION are read out of the workflow and checked for version skew on every PR, since that failure mode is silent until someone bumps one list and not the other. And the gate ends the way selfhost.sh does, with a negative control: it deletes both guards from a copy of the assembler — its exact pre-871cfb3 state — and requires the run to go red on the real `already exists (use --clobber ...)` message. Removing only the first guard trips the second and fails for a different reason, which would be a control that goes red for the wrong cause; the control refuses to run at all if the guards move or are reworded. Run locally: PASS in ~12s, then with the bug reintroduced in the real script: FAIL, on `downloads/rivet/SHA256SUMS.txt already exists`. Extraction is faithful: diffing the dedented inline block against the new script leaves only the argument/env plumbing, a workdir so the staging tree stops being written into the checked-out repo, a shasum fallback so the gate runs on macOS, and the removal of the heredoc-dedent hack the inline form needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
The producer pipeline had no test. It was exercised only by dispatching it at the real registry, which is where the 2026.08.3 deposit failed on a bug a seventeen-check CI gate — including a twenty-nine-minute mutation run — could not have caught, because nothing in CI ran that workflow. The change that makes this a real gate rather than a second implementation: the assembler was EXTRACTED out of deposit-layer.yml's inline run-block into tools/build-deposit-spec.sh, and the workflow now calls it. A gate that re-implements the logic tests a COPY, which is the same lie as stubbing it. Covered: a repo in BOTH lists (the bug that shipped), a per-platform vsix set, a portable one, a binary rename, raw per-platform binaries, a tool missing an asset for one platform, and — the dangerous one — a repo requested at two versions, which must be refused because otherwise one release's assets are checked against another release's sums. Verification that passes while proving nothing is the failure mode of this release. Clause 4 is why the record exists at all. My pre-flight before the failed deposit stubbed `verify_release` and `asset_sha` — precisely the function that broke — and reported success. Here the seam is the external SERVICE (gh, cosign), not the component: every line of the assembler runs, from a recorded asset inventory, with no network. The gate carries its own mutation. It deletes the guards from a copy of the assembler and requires red on the real "already exists" message, and refuses to run at all if the guards move or are reworded. I ran it: PASS, with the control confirming red. A gate nobody has watched fail is not a gate. Also authored VER-SYSTEST-002 rather than advancing the requirement on a bare assertion. Its evidence is shell-based, so it carries no cargo-test markers and `rivet verify` would otherwise refuse — the same situation as REQ-SYSTEST-001, resolved the honest way instead of by leaving the requirement stranded. I executed all six non-gate steps directly, because `rivet check verification-evidence` resolves cargo-test filters and cannot see a `run:` grep — the exact blindness that hid two red steps in v0.27.0. Scope is declared in the fixtures: the asset bytes are stand-ins, and the cosign double checks bundle-to-blob binding, identity and issuer — the assembler's responsibilities — not sigstore signatures, which would need the network this gate must not depend on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
`git add -A` swept four live agent worktrees into a commit — the same class of mistake that committed a reviewer's in-flight mutation earlier in this round, from the same cause. Amended out; ignoring the path so it cannot recur through inattention rather than through discipline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Two producer-side failures that both look like success.
REQ-NODESTROY-001 — `deposit --out DIR` into a directory carrying
referrers dropped the baseline line-status, the signed line-index and
every attestation, exited 0, and printed a message byte-identical to a
clean run. Three docs topics warned about it and no code guarded it; an
own-realm operator hit it by accident, and for a realm declaring
`signed-index = true` every consumer install afterwards fails closed.
The guard lives in `archive::write_oci_layout` — the ONLY code in varve
that writes a layout — so clause 4 ("the same guard applies to any other
command that writes a layout in place") is met structurally rather than
by repetition: `archive` inherits it, and so does whatever writes a
layout next. `referrers::scan` takes the inventory and the refusal names
what it found and carries the re-attach sequence, so the operator
recovers from the message rather than from the docs. It runs before the
first mkdir, so a refused layout is left byte-identical (clause 5).
`DepositOptions`/`ArchiveOptions` carry `force`.
An artifactType the guard does not recognise counts as work. Enumerating
only today's three referrer kinds would silently stop protecting the
fourth — the same silent loss the guard exists to prevent.
REQ-ADVISORY-002 — a typo in an `affected` id signs cleanly and the
advisory then fires for nobody. Shape validation already existed
(varve#61); what was missing is EXISTENCE. `KnownLayers` is the honest
distinction: the realm's signed line-index is a listing of the line, a
deposit layout holds one layer and is not. Where a listing is in reach,
an id naming no known layer is refused with the ids that do exist ("it
exposes: …", the shape varve already uses for tools); where it is not,
`RefCheck` states the check that did NOT run rather than implying a
complete one. `--force` allows pre-signing for a layer not yet
deposited, and never bypasses the shape check — an id that is not a
layer identifier cannot become correct later.
Also: `export-bazel`'s `_generated_by` header claimed more than varve
does. `[tool.source].sha256` is transcribed from the deposit spec into
the signed payload and never checked against the asset it names — varve
never fetches it. Being inside a signature makes a value attributable,
not true. The header now says so, and the unit test pins it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…tach REQ-NODESTROY-001 clause 3 names `sign-status`/`attach-status` and `sign-index`/`attach-index`. An operator who lost the envelope needs the step that produces one, not only the step that attaches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…ing you can see into REQ-CIGATE-001 and REQ-INSPECT-001, both found by the ten-persona documentation audit — two personas independently failed to write the consumer CI gate that three separate docs topics tell them to write. BREAKING: `varve status` exits 3 when the pinned layer is YANKED. It printed YANKED and exited 0, so the only way to act on a signed yank was to grep stdout. The point of signing a yank is to stop a build. The report itself is unchanged and still goes to stdout, so a script that reads output keeps working; a script that checks $? now gets the answer. Code 3, not 1: varve ANSWERED, and stderr stays empty — a pipeline must be able to tell "your toolchain is yanked" from "your evidence is bad", which stays code 1. An exit-code contract that cannot drift from the binary. `varve docs --grep "exit code"` returned nothing across all fifty topics, so there was no contract to write a gate against. `exit::Outcome` is now the one place a code is decided, and three surfaces RENDER from it rather than restating it: main()'s exit status, `varve exit-codes [--json]`, and the `exit-codes` docs topic, which is GENERATED — a hand-written markdown table of exit codes is exactly the artefact that goes stale the first time a code is added and that nothing notices. Then the numbers themselves are checked against the real process status of a real invocation, one scenario per code: a documented code no scenario produces fails the build, which a negative control confirmed. `--json` on every (CI)-marked command. A pipeline scraped a layer digest out of an English sentence, so every wording change was a silent breaking change to somebody's CI. All seven now report through one emitter, and clap itself is enumerated to assert the set is complete — a command that gains the (CI) tag and not the flag fails a test. `docs --grep` searches slugs and titles, not only bodies (the doc comment had claimed "bodies + titles" for four releases), and exits 4 on no match so a docs check can gate too. `varve inspect` — what is actually in a layer. Nothing reported a payload's name, version, kind or platform; `list` prints layer ids and `sbom` collapses every non-tool kind to a CycloneDX `library`. It reports all four per payload, distinguishes DISPATCHED from HELD, and follows the COMPOSITION, which `sbom` does not. Offline by construction, and it verifies before it describes. …and the message that was simply false: `varve which` on a held `wit` payload said "is not part of layer". The layer holds it. Both `which` and `run` now name it as held, with its kind, version and the layer it is in — found one layer down through the composition, which the first version of the fix missed and a CLI test caught. Also: register `concept-root-ceremony.md`, which shipped as a file no topic pointed at — `varve docs root-ceremony` said "unknown topic" while two other topics linked to it, and every gate stayed green. Closed from the other direction too: the docs DIRECTORY is now the source of truth, and a .md that no topic registers fails the build. The file here is a placeholder for the docs branch's authored text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
# Conflicts: # crates/varve/docs/concept-root-ceremony.md
The four parallel branches are merged. This is the CLI wiring the
varve-core agent specified and could not apply (it does not own main.rs)
and the CLI agent correctly refused to apply (the core APIs did not exist
in its worktree, so it would not have compiled).
It closes a real, small lie that was live on the merge branch: the
REQ-NODESTROY-001 refusal ends with "Re-run with --force to overwrite the
layout and drop them deliberately" — and `--force` existed on no command.
For a release whose theme is that an error carries its fix, shipping a
fix the tool rejects is the worst form of the bug. `deposit`, `archive`,
`attach-status` and `sign-status` now all have it.
`sign-status` gains `--index <ENVELOPE>`, which is REQ-ADVISORY-002
clauses 2 and 3 at the cheapest seam. A typo in an `affected` layer id
used to sign cleanly and the advisory then fired for nobody: the producer
saw success, the consumer saw nothing, and the yank silently did not
exist. Shape is now always checked. EXISTENCE is checked only where a
listing of the line is in reach — and where it is not, the tool says so:
advisory references were checked for SHAPE only — NOT against the
layers line 2026.08 actually has: no line-index was supplied … An id
naming a layer that does not exist still signs cleanly here and fires
for nobody.
That is clause 3, and it is the part worth having. A partial check that
reads as a complete one is how the original defect survived; the fix is
not only to check more, but to be explicit about the boundary of what
was checked. `attach-status` prints the same note, and both surface it in
`--json` as `existence_checked` + `note` so a pipeline can gate on it.
Verified end to end against the built binary: deposit over a layout with
a line-status refuses; the same command with --force succeeds; sign-status
with no index prints the boundary; all four commands list --force in
--help.
fmt 0 · clippy 0 · test 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
REQ-CIGATE-001 clause 4 fell between two agents — the CLI agent owned
docs.rs and not the .md, the docs agent had finished. It was reported as
unmet rather than quietly dropped, which is the only reason it got
written at all. `docs ci` was producer-only while THREE other topics told
the reader to run a consumer job: `docs verify` says "run it in CI" and
`verify --export --help` says "run it in CI", and neither showed one.
The new section gives the four-command gate and, more usefully, says what
each command answers that the others cannot — install enforces
anti-rollback, `verify --all` now reports the scope it covered, the
lockfile check is an asserted agreement because varve cannot intercept a
build, and `status` is the only one that is not offline. Plus the
`case $?` form, now that there are exit codes to branch on.
The docs gate caught my own broken example: a bare `[[export]]` fragment
reached no parser, and the test refused it — "a silent skip is how a
broken example survives the gate". It was right; the example is now a
complete pin. Worth recording that the gate bit its author on the same
day it was praised.
Statuses, and the one that does not advance:
* REQ-NODESTROY-001, REQ-CIGATE-001, REQ-INSPECT-001 -> verified via
`rivet verify`, on 9, 17 and 6 markers respectively.
* REQ-ADVISORY-002 stays `implemented`. Clause 2 is PARTIAL: the
signed-line-index path is done, the registry path is not, because
`sign-status` has no network concept and wiring a fetch into a
producer signing command is a design decision rather than an
oversight. The reason is recorded next to the status. Clause 3 is
what makes this honest rather than merely incomplete — where no
listing is in reach the tool states which check it did not perform.
* REQ-CUSTODY-001 -> `implemented`, not verified: its evidence is prose
and a coverage gate, and it carries no test markers, so `rivet verify`
would refuse. That refusal is correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
REQ-ADVISORY-002 clause 2 said an advisory's `affected` ids should be
checked for existence "where the signer can see the line's layers (a
layout or a REGISTRY is reachable)". The registry half was left
unimplemented by the agent that built this, which flagged it as a design
decision rather than an oversight. On examination it should stay
unimplemented, and the requirement text was wrong.
Checking `affected` ids against the registry would import the exact
weakness REQ-INDEXAUTH-001 was added in v0.26.0 to remove. That
requirement exists because the listing of which layers a line has is the
registry's raw `/tags/list`, unauthenticated — "a compromised or merely
stale index host can HIDE a layer, and every artifact it does serve still
verifies, so nothing detects it."
So a `sign-status` that refused an id the registry does not list would
hand an adversary a DENIAL-OF-YANK primitive: hide the layer, and the
advisory about that very layer cannot be signed. The attack is cheapest
exactly when the yank matters most. varve would have spent a release
learning not to trust that listing, then trusted it in the one place
where being wrong is unrecoverable.
It also contradicts the custody model written earlier today.
`docs root-ceremony` says to use the root key as rarely as possible and
ideally air-gapped. A signing command requiring the network cannot run
where the key ought to live, and when guidance and tool disagree the
guidance loses.
Decided, in two parts:
* SPLIT. `sign-status` stays offline — shape always, existence against
a listing it is GIVEN. Checking a signed advisory against a live
source becomes a separate KEYLESS command, which may use the registry
precisely BECAUSE it holds no key and its verdict is advisory: a
hiding registry then makes that check fail loudly instead of silently
blocking a signature. The key-bearing command never needs the
network; the network-bearing command never needs the key.
* CLOSE THE REACH GAP LOCALLY. `signed-index` is false by default, so
most realms publish no index and an index-only check would rarely
have anything to check against — opt-in safety, which is how this
defect survived. The producer already HOLDS the layers on disk;
deriving the listing from their own layouts is the most trustworthy
source available and the only one that works for a realm that never
publishes an index.
Clause 2 now says "is GIVEN a listing" rather than naming a registry, and
clauses 5 and 6 record both halves. The requirement no longer describes
an approach we rejected — a requirement that outlives its own analysis is
how a graph starts lying.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
`signed-index` is false by default, so the index-only existence check
added earlier would rarely have had anything to check against — opt-in
safety, which is how a typo'd `affected` id came to sign cleanly and fire
for nobody in the first place. The producer already HOLDS the layers on
disk, and `sign-status --layouts <DIR>` now reads them: no network, no
published index, and it works for a realm that never publishes one.
That listing is also more trustworthy than the registry's. DD-023 keeps
the network out of the signing command because a compromised registry can
HIDE a layer and thereby block the yank of the very layer it hides; the
producer's own output tree cannot be tampered with by that adversary.
Working, through the real binary, on a spec with a deliberate typo:
error: the yank entry names layer '2026.08.10', which line 2026.08
does not contain — it exposes: 2026.08.0, 2026.08.1. `varve status`
matches layer ids EXACTLY, so this entry would fire for nobody: you
would see success, every consumer would see nothing, and the advisory
would silently not exist.
I shipped this defect first and caught it only by running the CLI. My
unit test built its layouts with `DirSource::put`, which writes the bare
manifests/+blobs/ SOURCE shape — not the OCI layout (`index.json`,
`blobs/sha256/`) that `varve deposit --out` actually produces. The test
passed. The command then found nothing at all and reported "no oci-layout
was found", which was true and useless.
That is precisely the fixture-realism defect this release is named for,
committed by me, hours after writing the requirement clause about it. The
fixture is now produced by the real `deposit` writer, and the scanner
reads both shapes. Verified the assertions bite by removing the line
filter: the 2026.09.0 layer leaked into a 2026.08 listing and the test
caught it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Clean-room review of v0.28.0: nine mutations applied, ZERO survived. The
code is sound — every clause of the four `verified` requirements that
reduces to code behaviour is genuinely discharged, including the one I
would have bet against (a refused layout left byte-identical, checked by
recursive sha256 snapshot before and after). What was wrong is the
statements about it.
The worst finding is this release's own theme pointed back at us: THREE
docs still asserted the behaviour v0.28.0 fixed —
cmd-deposit.md: "succeeds — and silently drops every attached referrer"
concept-ci.md: "succeeds — and silently drops every referrer"
concept-own-realm.md: "silently drops every attachment"
varve now REFUSES that, exit 1. Fixing the code without fixing the
sentences about the code produced exactly the "topic states something the
tool does not do" defect I deleted two of yesterday. All three now
describe the refusal, and say what the behaviour used to be so a reader
on an older version is not misled either.
`the_boundary_sections…` failed on that edit, because it pins
"silently drops every referrer" as a REQUIRED statement. The gate was
right to fire and its needle was stale: a gate that pins a sentence must
move when the sentence stops being true, or it starts protecting a
falsehood. It now pins "varve REFUSES that (exit 1)".
A cry-wolf defect I introduced yesterday and the review caught: the
"checked for SHAPE only" note fired unconditionally with no listing —
including for a BASELINE status with no yanks and no known problems, the
one `docs own-realm` tells every new operator to sign. It warned about a
check that had nothing to check, and printed twice on the documented
happy path. REQ-SHADOW-001's own lesson is that a check firing on correct
setups is one people switch off. A document naming no layer now says so.
Two stale statuses in the trace itself:
* REQ-ADVISORY-002's recorded reason said the gap was clause 2's
"registry half". DD-023 rewrote clause 2 to remove the registry ON
PURPOSE and `--layouts` implemented it, two commits before. The real
gap is clause 6. A reason that decays is worse than none, because it
is believed.
* REQ-CUSTODY-001 had no reason at all — the only held-back
requirement on the branch without one.
Also corrected: `payload-kinds` quoted an export header the tool no
longer emits and told the reader to "read that precisely"; and
`root-ceremony` said `pubkey` "re-derives the public half from the seed"
when it signs, verifies, and prints the CARRIED bytes. Security-
equivalent, and still not what the code does.
Not yet addressed, tracked for the next commits: four `verified`
requirements have no VER- artifact (rivet trace reports MISSING as a
WARN, so `rivet validate` exits 0 and the gate is not potent);
REQ-SYSTEST-002 clause 5 is unenforced while the requirement reads
verified; and CIGATE clause 1b's "shall be stated as a breaking change"
has no CHANGELOG to state it in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Clean-room review: REQ-VERIFYALL-001, REQ-NODESTROY-001, REQ-CIGATE-001
and REQ-INSPECT-001 were all `verified` with NO verification artifact.
`rivet trace` reported it — as a WARNING — so `rivet validate` exits 0
and the gate never bit. Two commits flipped `status: draft` to
`status: verified` on a single line with nothing added for
`rivet check verification-evidence` to re-run.
The clauses genuinely are discharged; the review proved it with nine
mutations, none of which survived. But the graph could not know that, and
"the reviewer checked it once" is not a record anyone can re-execute. A
requirement whose evidence exists only in a mutation someone ran by hand
is verified in the same sense a hand-written table is a contract.
This is the gap I filed upstream as rivet#838 two hours ago — `rivet
verify` advances on the EXISTENCE of a marker, not on whether the marker
discharges the clause — showing up in my own graph, in the same session.
The gate then caught me. `cargo test -p varve-core referrers` matched
NOTHING and exited 0, which is precisely the vacuity
`verification-evidence` exists to refuse:
✗ no test matching `referrers` found
A `cargo test <filter>` that matches nothing exits 0 with "0 passed",
so this would otherwise keep the requirement silently `verified`.
A module-path filter did not resolve either; the gate matches test NAMES,
so it now names two concrete functions. 373 named-test steps resolve, and
I executed every step of all four new records directly rather than
trusting the gate, because it 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
Clean-room review: the requirement read `verified` while clause 5 — "no
adapter ships without a system test" — had NO enforcement at all. Six
export adapters ship; three have a gate. That is the overclaim this
release exists to remove, sitting inside the release.
Downgraded to `implemented`, with the reason recorded next to the status
rather than in this message where nobody will look for it.
Enforcement is a RATCHET, not a wall. A hard gate would turn CI red for
three adapters' worth of work unrelated to whatever change trips it, and
a gate people switch off protects nothing — REQ-SHADOW-001's own lesson,
and the reason the cry-wolf note got fixed two commits ago.
`no_new_export_adapter_ships_without_a_system_test` enumerates adapters
from clap, reads what the gates actually exercise, and compares against a
written-down known-uncovered set. It fails in BOTH directions:
* a NEW adapter shipping uncovered — verified by shrinking the recorded
set: "export adapter(s) [\"export-sdk\"] have no system test and are
not in the recorded gap"
* an adapter that GAINS coverage while still listed — verified by
adding export-vsix to the set: "now HAS a system test but is still
listed as a known gap … so the list keeps meaning what it says"
The second direction is the one that keeps the list honest. Without it
the set decays into a permanent excuse, which is how a waiver becomes
invisible.
Two things the test refuses to do quietly: it asserts the adapter
enumeration found at least six (if clap's shape changes it is measuring
nothing), and it asserts the gate sources were actually readable (an
unreadable path would report every adapter uncovered — a different lie).
Longest-name-first matching, so `export-bazel` does not match inside
`export-bazel-distdir` and report it covered.
The gap is varve#99. REQ-SYSTEST-002 advances to `verified` when the list
is empty. Worth noting there: the two adapters that HAVE gates are the
two whose defects those gates found. The three without are the three we
are taking on trust.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
…tcome varve#88, the last of the trust-correctness set. CLAUSE 4 — the include check at install was DIRECT-ONLY. A chain root -> mid -> leaf with `leaf` missing PASSED, because root's own includes (just `mid`) were all present. `verify` walks the whole graph and rejects it at depth 2, so install and verify disagreed while `docs verify` promises "the CI gate and the install agree". Reproduced before fixing: `install` exited 0 printing "composes 1 installed layer(s)" with the layer two hops down deleted. The walk is now transitive, deduplicated and depth-bounded, and the refusal says the list is transitive so a layer named there is not necessarily one you composed directly. CLAUSE 3 — the success line printed BEFORE that check ran, so even when it fired the operator saw "installed layer X" and then an error, leaving a layer in `varve list` no other command would touch. The check now precedes the claim. CLAUSE 1 — `archive` was verbose about omitted PLATFORM payloads and silent about a missing baseline line-status, the inconsistency that hid it. The artifact it produces has a permanently broken `varve status` for every consumer, and no yank can ever reach them. I first made this REFUSE, and four legitimate tests went red. That was the useful signal, not an inconvenience: `archive` is most often run by the CONSUMER exporting their own core, and they cannot retroactively add a baseline the producer never published. A gate that fires on a correct setup nobody can fix is one people learn to pass --allow-no-status to reflexively, and then it protects nothing — the same cry-wolf lesson as REQ-SHADOW-001, and as the note I fixed two commits ago. So it warns loudly, names the CONSEQUENCE rather than the condition, and --allow-no-status silences it. Silence is something you ask for. CLAUSE 2 — `export-cargo` closed by telling you to run the very `cargo build --offline` that could exit 0 having compiled everything featureless. #73 fixed the index, but an index is only as good as what each .crate declares and cannot express what a vendor tree can, so every run now says which of the two is stronger. Advanced to `verified` via `rivet verify`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
Six of eight requirements verified. The two that are not are dispositioned
in the trace with their reasons recorded beside the status, and both
appear in the CHANGELOG's known-limitations section rather than being
left for someone to discover:
* REQ-ADVISORY-002 — clause 6's keyless `check-status` is deferred to
v0.29.0 per DD-023. Validating an advisory against a registry would
let a registry that HIDES a layer block the yank of that layer, on
the same unauthenticated listing REQ-INDEXAUTH-001 exists to
distrust, and would put the root key and the network in one command.
* REQ-CUSTODY-001 — evidence is prose and a coverage gate, so it
carries no test markers and `rivet verify` refuses. That refusal is
correct.
* REQ-SYSTEST-002 — clause 5 is a ratchet, not a wall; it advances when
varve#99's list is empty.
Two BREAKING exit-code changes are stated first, before the feature list,
because a pipeline that ignores them fails silently — which is the exact
defect class this release is named for:
* `varve status` exits 3 on YANKED (was 0)
* `varve docs --grep` exits 4 on no match (was 0)
Tag-match preconditions verified locally before pushing: workspace
version 0.28.0, the internal varve-core pin bumped with it, Cargo.lock in
sync, the built binary reporting 0.28.0, and the CHANGELOG heading
matching the tag. The release workflow asserts the first and last of
those and I would rather find a mismatch here than after the tag exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu
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 — 5 of 8 requirements verified, two still open. Opened early so CI runs against the real branch while the rest lands.
The theme
Not missing features. The sharpest open issues were all one defect class: varve reporting success over a bad outcome.
verify --alldepositstatussign-statusaffectedid signed cleanly and fired for nobody (#61)Verified
REQ-VERIFYALL-001·REQ-NODESTROY-001·REQ-CIGATE-001·REQ-INSPECT-001·REQ-SYSTEST-002Still open in this PR
implemented; clause 6's keylesscheck-statusdeferred to v0.29.0 per DD-023implemented; evidence is prose, sorivet verifycorrectly refusesBreaking
varve statusexits 3 when the pinned layer is yanked (was 0)varve docs --grepexits 4 on no match (was 0)sdk-prefixrequired onkind = "sdk"(v0.27.0, restated)How this was built
Four agents in isolated worktrees with disjoint file ownership — docs / tools+workflows / varve-core / varve CLI — plus hand-written merge wiring. One conflict, flagged in advance by the agent that caused it.
Clean-room review: DISSENT
Nine mutations applied, zero survived — the code is sound. The review's framing: "the code is stronger than I expected; the statements about it are not." Six false or stale statements, all fixed in this PR:
docs own-realmtells every operator to signpubkeymechanism claimThe review also independently confirmed the
root-ceremonysecurity claim by reconstructing a working key from the first 64 hex characters alone.Gates
fmtclippytestdocs-coveragerivet validateclaim-checkverification-evidence— all exit 0 locally. 373 named-test steps resolve. CI has not yet run on this branch, which is why this PR exists now rather than at the end.