diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f88c2..14d4216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,95 @@ # Changelog +## v0.37.0 — 2026-09-18 + +*A realm can say what it composes, and what it ingests without proof.* + +Cut early, with two finished items and a waiting consumer, because a second +realm could not deposit its first layer without them. REQ-SDKTARGET-001, key +roles and parallel hashing move to v0.38.0 — deferred, not descoped. + +| | before | now | +|---|---|---| +| a realm declaring a composition | impossible — `[[include]]` existed only in a hand-written deposit spec | `layer.toml` declares it, and the digest is signed into the layer | +| `unverified-reason` in a manifest | read by `plan`, ignored by `deposit` | an opt-in wherever it decides anything | +| varve's own upstream scanner | 533 lines, a daily cron, publishing nothing | deleted; the realms scan for themselves | + +### Composition had no worked example, and the reason was not documentation + +varve has claimed layer composition since early on — one pin, two trust +universes, each layer keeping its own root and cadence. It was implemented, +tested and documented. And **a realm's `layer.toml` could not express it at +all**: `[[include]]` lived only in a hand-written deposit spec, so no layers +repository could produce a composed layer. The only demonstration was a system +test that generated two throwaway roots in a temporary directory and deleted +them. + +```toml +[[include]] +digest = "sha256:001480e7…" # the included layer's SIGNED MANIFEST digest +realm = "pulseengine" # whose root verifies it +layer = "2026.09.4" # for messages before it is fetched +``` + +Two refusals are enforced where the manifest is read, because by deposit time an +error costs a published layer id that cannot be reused: an include with **no +`realm`** (verify would fall back to the *pinning project's* root — installing +cleanly, failing verification afterwards, or silently widening trust if the two +roots happen to match), and a **digest that is not one** (a tag would let the +composition drift after signing, which is the single thing an include prevents). + +### A stated reason is an opt-in, not decoration + +Depositing the first layer of a new realm, varve refused an upstream that +publishes no proof — correctly. But `layer.toml` already carried +`unverified-reason` for it, and `varve-producer plan` had just printed *"3 +release(s) carry NO proof of origin (opt-in recorded)"*. + +`plan` read the manifest; `deposit` read only the `UNVERIFIED_INGEST` +environment variable. The field parsed, displayed, and was **inert at the one +moment it decides anything** — and a realm that stated its reason in a reviewed, +committed file, where it gets signed into the layer, was told to set an ambient +environment variable instead. + +`ingest::optins_in_force` is now the single answer. The manifest wins a conflict +(it is the reviewed record; the environment is unreviewable), and the +environment can still name a repository the manifest does not. + +### The migration finished + +`pulseengine-layers` holds the manifest and the key, consumes a released +assembler, and since 2026-09-18 scans and deposits unattended. So varve's own +`scan-upstream.sh` (533 lines), `upstream-mechanism.sh`, `scan-upstream.yml` and +their system test were not a smaller version of that pipeline but an unused copy +of it, inside the tool the realm consumes. Deleted. + +Two rivet artifacts pointed at them and were deprecated rather than dropped: +**REQ-ROLLING-001**, whose second clause — *"publishing it stays a deliberate +act"* — was **reversed** by DD-030 rather than met, and **VER-ROLLING-001**, +every step of which ran a file this release deletes. + +### Falsification + +Each of these would refute a claim above: + +- Put `[[include]]` in a `layer.toml`, deposit it, and find no `layer`-kind + entry in the signed manifest. +- Deposit an include with no `realm` and watch it succeed. +- Put `unverified-reason` in a manifest, deposit without `UNVERIFIED_INGEST` + set, and watch varve still refuse. +- Find any file in this release that still scans upstreams from varve's own + repository. + +### Requirement status + +`REQ-LAYERREPO-001` is **verified**, on evidence that is deliberately an +inspection rather than a test: no CI job in this repository can demonstrate that +two *other* repositories assemble and publish layers. The record names both — +`pulseengine-layers` (layers 2026.09.4 and 2026.09.5, the second deposited with +no person in the loop) and `wasm-layers`, which stood up from the same tooling in +a day and whose deposit refused twice for reasons varve is supposed to refuse +for. + ## v0.36.0 — 2026-09-18 *A layer can carry the crate and its documentation; verify says where its time goes.* diff --git a/Cargo.lock b/Cargo.lock index c8bcf2f..b9ea15f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1962,7 +1962,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "varve" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "assert_cmd", @@ -1980,7 +1980,7 @@ dependencies = [ [[package]] name = "varve-core" -version = "0.36.0" +version = "0.37.0" dependencies = [ "criterion", "flate2", @@ -2000,7 +2000,7 @@ dependencies = [ [[package]] name = "varve-producer" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "base64 0.23.1", @@ -2018,7 +2018,7 @@ dependencies = [ [[package]] name = "varve-serve" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 8fd9f4f..3d5615c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/varve", "crates/varve-core", "crates/varve-producer", "crates/varve-serve"] [workspace.package] -version = "0.36.0" +version = "0.37.0" edition = "2024" license = "Apache-2.0" repository = "https://github.com/pulseengine/varve" @@ -14,4 +14,4 @@ authors = ["PulseEngine"] # dependency cannot be published (REQ-BOOTSTRAP-001). It must be bumped in # lockstep with workspace.package.version above — `publish-crates.yml` # asserts they agree before it publishes anything. -varve-core = { path = "crates/varve-core", version = "0.36.0" } +varve-core = { path = "crates/varve-core", version = "0.37.0" } diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 3511f11..fd71f71 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -3486,7 +3486,7 @@ artifacts: - id: REQ-LAYERREPO-001 type: requirement title: A realm's layers are assembled outside varve, in that realm's own repository - status: approved + status: verified release: v0.37.0 description: "`deposit-layer.yml` lives inside varve and hard-codes one realm's tool list, so bumping a tool version is a commit to the tool that installs tools, and a second realm's signing key would live in varve's repository settings. That couples varve's release cadence to every tool bump and puts two realms' custody in one place — which `docs root-ceremony` argues against in the same breath as it argues for split custody. . Clauses: (1) Layer assembly shall live in a repository per realm, each holding its own tool manifest and its own signing secret. (2) The assembler itself shall stay in varve and be CONSUMED by those repos as a released artifact — it is system-tested here (REQ-SYSTEST-002) and a copy per realm is a copy that drifts, which is the same defect as a gate testing a re-implementation. (3) A layers repo shall need no varve source checkout: it takes a released varve and a released assembler. (4) EXACTLY ONE layers repo shall be stood up first — `pulseengine-layers`, the realm that already has consumers and an existing key — and it shall publish a layer successfully before a second repo is created. The two-realm topology is proven in the gate (REQ-REALM2-001 clause 1) rather than by standing up two repos at once, so a migration failure lands on the realm we can fix rather than on a new realm's first impression. A single repo holding BOTH realms was rejected: it puts two roots' secrets in one repo's settings, and unpicking that later means rotating keys varve cannot rotate. (5) varve's own repository shall stop carrying any realm's signing secret once the migration completes. . Moved v0.29.0 -> v0.30.0 deliberately. The ADAPTER this depends on shipped in v0.29.0 and is verified separately (REQ-LAYERADAPT-001), but clauses 4 and 5 are not code: clause 4 requires `pulseengine-layers` to publish a real layer, which needs `VARVE_ROLLING_KEY` provisioned in that repository by its custodian, and clause 5 requires removing the secret from varve only AFTER that publish succeeds. Neither can be discharged by the party writing the code, and marking this verified on the strength of the adapter alone would claim a migration that has not happened. The layers repository's deposit workflow therefore still refuses to run rather than pretending, and the ordering is deliberate: varve keeps the secret until the new repository has proven it can publish without it. . STATE AS OF 2026-09-02, recorded because it drifted from what was claimed. `pulseengine-layers` exists and holds the manifest, and has NEVER RUN ANYTHING — zero workflow runs, and its deposit still exits 1. Layers 2026.08.4 and 2026.09.0 were both deposited from `pulseengine/varve`. Clause 4's gate is therefore NOT met, and no second layers repository may be created: not for `bytecodealliance` (REQ-REALM2-002), not for `covalent` (REQ-COVALENT-001). Both were described as blocked only on a root key; that was wrong, and they are blocked on this clause first. . The present arrangement is a HALF-MIGRATION and should be named as such: the manifest lives in the layers repository while signing lives in varve, so a cut edits both and keeps them byte-identical by a check. That check is better than drift but it is not clause 1 — it is two sources kept in step, which is the state the migration exists to end. . What blocks clause 4 has also changed. It is no longer the adapter — `varve layer-spec` shipped in v0.29.0, and the layers repository's workflow still carries a stale TODO saying otherwise. It is that no ASSEMBLER is released for a layers repo to consume (clause 3), plus the key and the GHCR write permission. The Rust producer changes the answer to the first: rather than packaging shell scripts as a signed release asset, the released binary IS the assembler. So finishing REQ-PRODUCER-002 is now a prerequisite of this clause, not work beside it. . THE MIGRATION IS NOW COMPLETE EXCEPT FOR THE RETIREMENT, recorded 2026-09-18. pulseengine/pulseengine-layers holds the manifest, holds the rolling key as its own secret, consumes a RELEASED assembler (varve-producer, pinned in its own layer.toml and riding in the layer it builds), deposits and publishes on its own authority, and since 2026-09-18 scans upstream and deposits every 15 minutes with no person in the loop (DD-030). Layer 2026.09.4 was deposited that way. Clause 4's gate is therefore met for this realm: varve's repository no longer assembles, signs or publishes it. . WHAT REMAINS IN varve IS A SECOND IMPLEMENTATION OF WHAT MOVED (varve#142): tools/scan-upstream.sh (533 lines), tools/upstream-mechanism.sh, .github/workflows/scan-upstream.yml on a daily cron, and their system test. They are not a smaller version of the realm's pipeline; they are an unused copy of it inside the tool the realm consumes, and every one of them is a place a future reader could take for the live path. Retiring them is the last step of this requirement, not cleanup beside it — assigned to v0.37.0. The systest and the verification artifact that cite the shell scanner must be re-pointed or removed with it, or the gate they belong to goes vacuous while still reporting green." @@ -4316,8 +4316,10 @@ artifacts: type: requirement title: A payload can name the target it builds for, not only the host it runs on status: approved - description: "varve's platform model is host-only: a payload is filed under a Rust triple describing the machine that RUNS it. That is right for a compiler you invoke and wrong for a cross-toolchain, which is identified by a PAIR. zephyrproject-rtos/sdk-ng v1.0.1 ships 140 assets named toolchain_gnu__.tar.xz — toolchain_gnu_linux-x86_64_arm-zephyr-eabi.tar.xz, toolchain_gnu_macos-aarch64_riscv64-zephyr-elf.tar.xz and so on. A realm wants a few targets across four hosts, not one of 140. . Without this, the four arm-zephyr-eabi assets would have to be deposited under four payload names invented by hand, and nothing would connect them or stop a fifth being added under a name that means something else. . The consumer side needs it too: gale asks for 'the arm-zephyr-eabi toolchain', not 'the x86_64-linux payload'. Today it answers that question with `west sdk install` and a glob across /opt, /root and /mnt. . Clauses: (1) A payload may declare a TARGET alongside its platform, and the pair shall identify it. (2) The target shall be recorded in the signed manifest, so a consumer resolves by target rather than by a name a producer happened to choose. (3) Payloads differing only by target shall not collide in the store, and a manifest that would deposit two payloads under one (name, platform, target) shall be refused — the same rule the platform dimension already follows. (4) A payload with no target keeps behaving exactly as today, so every existing manifest is unchanged. (5) `varve inspect` shall show the target, because a layer that carries three cross-toolchains and cannot say which is which is not inspectable. . MOVED TO v0.36.0 on 2026-09-11. This is my call rather than the maintainer's and is easily reversed: v0.35.0 grew a new payload kind, a new manifest section, a new export command and a new shipped binary (varve-serve), which is a full release on its own. Adding a second payload-model change beside it would make one release carry two independent reasons to fail. . MOVED TO v0.37.0 on 2026-09-16 — THE SECOND DEFERRAL, and recorded as such because a requirement deferred twice without comment is how backlog becomes permanent. The reason is unchanged and still correct: this is an independent change to the payload model, and v0.36.0 already carries one (REQ-CRATEPAYLOAD-001 with the rustdoc half of REQ-LAYERDOCS-001). v0.35.0 showed the cost of two such changes riding together concretely — a mutation shard killed twice, three survivors, and an OOM-killed CI job — each of which cost a full CI cycle to find. . THE SAFEGUARD, so the reason cannot be reused a third time: this is the SOLE scope of v0.37.0. Nothing else is assigned there, and anything proposed for v0.37.0 lands in v0.38.0 instead unless it is a security fix. If v0.37.0 is ever cut without this requirement verified, that is a decision someone made and must be recorded, not a slip. . This is my call rather than the maintainer's and is easily reversed." - release: v0.37.0 + description: |- + varve's platform model is host-only: a payload is filed under a Rust triple describing the machine that RUNS it. That is right for a compiler you invoke and wrong for a cross-toolchain, which is identified by a PAIR. zephyrproject-rtos/sdk-ng v1.0.1 ships 140 assets named toolchain_gnu__.tar.xz — toolchain_gnu_linux-x86_64_arm-zephyr-eabi.tar.xz, toolchain_gnu_macos-aarch64_riscv64-zephyr-elf.tar.xz and so on. A realm wants a few targets across four hosts, not one of 140. . Without this, the four arm-zephyr-eabi assets would have to be deposited under four payload names invented by hand, and nothing would connect them or stop a fifth being added under a name that means something else. . The consumer side needs it too: gale asks for 'the arm-zephyr-eabi toolchain', not 'the x86_64-linux payload'. Today it answers that question with `west sdk install` and a glob across /opt, /root and /mnt. . Clauses: (1) A payload may declare a TARGET alongside its platform, and the pair shall identify it. (2) The target shall be recorded in the signed manifest, so a consumer resolves by target rather than by a name a producer happened to choose. (3) Payloads differing only by target shall not collide in the store, and a manifest that would deposit two payloads under one (name, platform, target) shall be refused — the same rule the platform dimension already follows. (4) A payload with no target keeps behaving exactly as today, so every existing manifest is unchanged. (5) `varve inspect` shall show the target, because a layer that carries three cross-toolchains and cannot say which is which is not inspectable. . MOVED TO v0.36.0 on 2026-09-11. This is my call rather than the maintainer's and is easily reversed: v0.35.0 grew a new payload kind, a new manifest section, a new export command and a new shipped binary (varve-serve), which is a full release on its own. Adding a second payload-model change beside it would make one release carry two independent reasons to fail. . MOVED TO v0.37.0 on 2026-09-16 — THE SECOND DEFERRAL, and recorded as such because a requirement deferred twice without comment is how backlog becomes permanent. The reason is unchanged and still correct: this is an independent change to the payload model, and v0.36.0 already carries one (REQ-CRATEPAYLOAD-001 with the rustdoc half of REQ-LAYERDOCS-001). v0.35.0 showed the cost of two such changes riding together concretely — a mutation shard killed twice, three survivors, and an OOM-killed CI job — each of which cost a full CI cycle to find. . THE SAFEGUARD, so the reason cannot be reused a third time: this is the SOLE scope of v0.37.0. Nothing else is assigned there, and anything proposed for v0.37.0 lands in v0.38.0 instead unless it is a security fix. If v0.37.0 is ever cut without this requirement verified, that is a decision someone made and must be recorded, not a slip. . This is my call rather than the maintainer's and is easily reversed. + . MOVED TO v0.38.0 on 2026-09-18, by the maintainer's decision, and the reason is not deprioritisation. v0.37.0 is being cut EARLY because two of its items are finished and both have a waiting consumer: layer.toml can declare a composition, and a realm's stated `unverified-reason` is now an opt-in at deposit rather than only in `plan`. The pulseengine-wasm realm cannot deposit its first layer until a varve RELEASE carries the second of those, so holding the release until this requirement is done would block a realm on work it does not need. Nothing here is descoped; it is the first scope of v0.38.0. + release: v0.38.0 fields: root-cause-2026-09-06: "The target dimension is a SYMPTOM. Tested: two entries from one repo are refused — `error: two tool entries are both named \"sdk-ng\"` — because a tool's `name` must equal its repository basename (RepoNameMismatch) and two entries may not share a name (Duplicate). Together those mean ONE REPOSITORY MAY CONTRIBUTE AT MOST ONE PAYLOAD. sdk-ng publishes 140 host×target toolchains, so a layer wanting arm-zephyr-eabi and riscv64-zephyr-elf cannot express it, and no target field fixes that. The correction is that a payload's identity is its own: `name` is what it is called in the layer, `repo` is where it came from, and uniqueness is on the deposited name. Same correction the fifth field already made for versions. Do this first; the target dimension sits on top of it." @@ -4418,7 +4420,9 @@ artifacts: type: requirement title: Verify hashes payloads concurrently, once a cold measurement says hashing is the cost status: proposed - description: "MEASURED FIRST, then written — the reverse of how this question was answered the first time. REQ-VERIFYSTREAM-001 clause 2 attributed `varve verify` by stage on the real path: a 6-payload layer carrying one 2.00 GiB payload verifies in 0.89 s, of which hashing is 0.74 s at 2.70 GiB/s (hardware SHA-256) and reading is 0.136 s. Signature and manifest are under a millisecond each. So 83% of the time is SHA-256 and the disk is not the wall. . That is what makes this requirement legitimate now and illegitimate before. varve#141 was first read as \"verify is single-threaded, thread it\", and the arithmetic said the hash could not be the cost — 2 GB in 9 s is 227 MB/s against a 1-2 GB/s hash. Streaming the digest removed a 2 GB allocation and brought the same work to 0.89 s, and only THEN did the profile say hashing dominates. A week spent threading before that measurement would have been a week spent on the wrong half. . WHAT IS STILL UNKNOWN, and it bounds this requirement: no genuinely cold-cache figure was obtained (macOS `purge` needs root on the machine measured, and evicting with 20 GiB of unrelated reads on a 16 GiB machine did not work — the \"cold\" run still read at 10.9 GiB/s). On a cold read the balance may shift toward I/O, and a parallel hash would then buy less. The first task here is therefore a cold measurement on a CI runner, which is cold by construction, NOT a thread pool. . Clauses: (1) The per-stage attribution shall be taken on a runner with a cold page cache before any concurrency is written, and the result recorded — if reading dominates there, this requirement is withdrawn rather than implemented. (2) Where hashing dominates, payloads shall be digested concurrently, bounded by available parallelism, with memory still bounded per payload: streaming is not to be given back to gain threads, because the 2 GB allocation is the defect that started this. (3) Verify shall not check less to go faster. Every payload is still digested in full; a failure still names which payload failed and why; the exit code is unchanged; and the verdict must not depend on scheduling order — a layer with two bad payloads shall report the same one every time, or the report is not reproducible evidence. (4) The small case shall not regress: a layer of a few small payloads verifies in ~0 s today, and spawning threads to hash 64 KiB is slower than not. (5) The speed-up shall be MEASURED on both shapes and recorded here, as clause 2 of REQ-VERIFYSTREAM-001 was, so the next person inherits a number rather than a claim." + description: |- + MEASURED FIRST, then written — the reverse of how this question was answered the first time. REQ-VERIFYSTREAM-001 clause 2 attributed `varve verify` by stage on the real path: a 6-payload layer carrying one 2.00 GiB payload verifies in 0.89 s, of which hashing is 0.74 s at 2.70 GiB/s (hardware SHA-256) and reading is 0.136 s. Signature and manifest are under a millisecond each. So 83% of the time is SHA-256 and the disk is not the wall. . That is what makes this requirement legitimate now and illegitimate before. varve#141 was first read as "verify is single-threaded, thread it", and the arithmetic said the hash could not be the cost — 2 GB in 9 s is 227 MB/s against a 1-2 GB/s hash. Streaming the digest removed a 2 GB allocation and brought the same work to 0.89 s, and only THEN did the profile say hashing dominates. A week spent threading before that measurement would have been a week spent on the wrong half. . WHAT IS STILL UNKNOWN, and it bounds this requirement: no genuinely cold-cache figure was obtained (macOS `purge` needs root on the machine measured, and evicting with 20 GiB of unrelated reads on a 16 GiB machine did not work — the "cold" run still read at 10.9 GiB/s). On a cold read the balance may shift toward I/O, and a parallel hash would then buy less. The first task here is therefore a cold measurement on a CI runner, which is cold by construction, NOT a thread pool. . Clauses: (1) The per-stage attribution shall be taken on a runner with a cold page cache before any concurrency is written, and the result recorded — if reading dominates there, this requirement is withdrawn rather than implemented. (2) Where hashing dominates, payloads shall be digested concurrently, bounded by available parallelism, with memory still bounded per payload: streaming is not to be given back to gain threads, because the 2 GB allocation is the defect that started this. (3) Verify shall not check less to go faster. Every payload is still digested in full; a failure still names which payload failed and why; the exit code is unchanged; and the verdict must not depend on scheduling order — a layer with two bad payloads shall report the same one every time, or the report is not reproducible evidence. (4) The small case shall not regress: a layer of a few small payloads verifies in ~0 s today, and spawning threads to hash 64 KiB is slower than not. (5) The speed-up shall be MEASURED on both shapes and recorded here, as clause 2 of REQ-VERIFYSTREAM-001 was, so the next person inherits a number rather than a claim. + . MOVED TO v0.38.0 on 2026-09-18, by the maintainer's decision, and the reason is not deprioritisation. v0.37.0 is being cut EARLY because two of its items are finished and both have a waiting consumer: layer.toml can declare a composition, and a realm's stated `unverified-reason` is now an opt-in at deposit rather than only in `plan`. The pulseengine-wasm realm cannot deposit its first layer until a varve RELEASE carries the second of those, so holding the release until this requirement is done would block a realm on work it does not need. Nothing here is descoped; it is the first scope of v0.38.0. links: - type: derives-from target: REQ-VERIFYSTREAM-001 @@ -4426,13 +4430,15 @@ artifacts: created-by: ai model: claude-opus-5 timestamp: 2026-09-18T06:54:05Z - release: v0.37.0 + release: v0.38.0 - id: REQ-KEYROLES-001 type: requirement title: A realm's keys are scoped by role, so a signature that is valid can still be out of scope status: proposed - description: "REPORTED as varve#148 and sharpened by comparison with criticaltrust, Ferrocene's trust format, which scopes keys BY ROLE — a key that may sign a release manifest is not thereby a key that may sign the index of what exists. varve has one flat root per realm: the same key signs layer manifests, line-status documents, line-index documents and attestations, and nothing in the format says which of those a given key was ever meant to do. . WHY IT MATTERS MORE NOW THAN WHEN IT WAS FILED. The pulseengine realm's key is used by an unattended depositor every 15 minutes (DD-030). One key, one secret, one blast radius: a compromise of the deposit credential is a compromise of everything a consumer trusts about that realm, including the documents that say which layers are yanked and which line-index is current — the very statements a consumer would use to react to the compromise. Role separation is what keeps the reaction channel out of the blast radius. . WHAT THIS REQUIREMENT MUST NOT DO. It must not invent a key hierarchy on its own authority. Custody, succession and what a root may delegate are the maintainer's policy, drafted in varve#113, and that PR's artifacts are deliberately proposed rather than merged. So this requirement's first clause is a DECISION to be taken, not a design to be implemented. . Clauses: (1) The maintainer decides which roles exist — at minimum whether the document-signing role (line-status, line-index) is separable from the layer-signing role — and that decision is recorded before any format change. (2) Where roles exist, the signed artifact shall name the role its signature claims, so a verifier can refuse a signature that is valid but out of scope. A signature that verifies and should not have been made is exactly the failure a flat key cannot express. (3) A realm's published trust material shall be able to carry more than one key, with each key's role stated, or roles are unenforceable at the consumer. (4) The existing single-key realms shall keep working unchanged: a realm that declares no roles behaves as today, because a format change that invalidates every published layer is not a security improvement. (5) It shall be settled BEFORE the v1.0 ceremony (REQ-CEREMONY-001): the provisional rolling root is replaced there, and replacing one flat key with another flat key would spend the one ceremony this project gets on the arrangement it has already outgrown." + description: |- + REPORTED as varve#148 and sharpened by comparison with criticaltrust, Ferrocene's trust format, which scopes keys BY ROLE — a key that may sign a release manifest is not thereby a key that may sign the index of what exists. varve has one flat root per realm: the same key signs layer manifests, line-status documents, line-index documents and attestations, and nothing in the format says which of those a given key was ever meant to do. . WHY IT MATTERS MORE NOW THAN WHEN IT WAS FILED. The pulseengine realm's key is used by an unattended depositor every 15 minutes (DD-030). One key, one secret, one blast radius: a compromise of the deposit credential is a compromise of everything a consumer trusts about that realm, including the documents that say which layers are yanked and which line-index is current — the very statements a consumer would use to react to the compromise. Role separation is what keeps the reaction channel out of the blast radius. . WHAT THIS REQUIREMENT MUST NOT DO. It must not invent a key hierarchy on its own authority. Custody, succession and what a root may delegate are the maintainer's policy, drafted in varve#113, and that PR's artifacts are deliberately proposed rather than merged. So this requirement's first clause is a DECISION to be taken, not a design to be implemented. . Clauses: (1) The maintainer decides which roles exist — at minimum whether the document-signing role (line-status, line-index) is separable from the layer-signing role — and that decision is recorded before any format change. (2) Where roles exist, the signed artifact shall name the role its signature claims, so a verifier can refuse a signature that is valid but out of scope. A signature that verifies and should not have been made is exactly the failure a flat key cannot express. (3) A realm's published trust material shall be able to carry more than one key, with each key's role stated, or roles are unenforceable at the consumer. (4) The existing single-key realms shall keep working unchanged: a realm that declares no roles behaves as today, because a format change that invalidates every published layer is not a security improvement. (5) It shall be settled BEFORE the v1.0 ceremony (REQ-CEREMONY-001): the provisional rolling root is replaced there, and replacing one flat key with another flat key would spend the one ceremony this project gets on the arrangement it has already outgrown. + . MOVED TO v0.38.0 on 2026-09-18, by the maintainer's decision, and the reason is not deprioritisation. v0.37.0 is being cut EARLY because two of its items are finished and both have a waiting consumer: layer.toml can declare a composition, and a realm's stated `unverified-reason` is now an opt-in at deposit rather than only in `plan`. The pulseengine-wasm realm cannot deposit its first layer until a varve RELEASE carries the second of those, so holding the release until this requirement is done would block a realm on work it does not need. Nothing here is descoped; it is the first scope of v0.38.0. links: - type: derives-from target: REQ-CEREMONY-001 @@ -4440,4 +4446,4 @@ artifacts: created-by: ai model: claude-opus-5 timestamp: 2026-09-18T06:54:44Z - release: v0.37.0 + release: v0.38.0 diff --git a/artifacts/verification.yaml b/artifacts/verification.yaml index dac652d..469532d 100644 --- a/artifacts/verification.yaml +++ b/artifacts/verification.yaml @@ -3173,3 +3173,28 @@ artifacts: model: claude-opus-5 timestamp: 2026-09-18T05:46:32Z release: v0.36.0 + + - id: VER-LAYERREPO-001 + type: verification + title: Two realms assemble, sign and publish outside varve + status: accepted + description: |- + Clause 4 asks for something varve's own repository cannot demonstrate from inside itself: that a realm's layers are assembled, signed and published OUTSIDE varve, in that realm's own repository. This records the check made against the two realms that now do it. + + THE FIRST REALM. pulseengine/pulseengine-layers holds layer.toml and the rolling key as its own secret, consumes a RELEASED assembler (varve-producer, pinned in its own manifest and riding in the layer it builds), and deposits on its own authority. Layer 2026.09.4 (counter 5, 15 payloads) was deposited from a dispatch and 2026.09.5 (counter 6) by that repository's own scanner with no person in the loop — the first autonomous deposit, recorded in DD-030 with the control it gives up. + + THE SECOND REALM, which is the stronger evidence, because a pattern one repository follows may be an accident of that repository. pulseengine/wasm-layers stood up from the same tooling in a day: its own manifest, its own root (published as realm-root.pub, since varve's release carries the pulseengine realm's root and not this one), its own key as its own secret. Its deposit refused twice for reasons varve is supposed to refuse for — an upstream that vouches for nothing, and then a producer defect — which is the migration working rather than failing: the realm's own pipeline enforced the ladder without varve's repository being involved at all. + + WHAT WAS RETIRED, and why the retirement is the clause rather than tidying. varve carried tools/scan-upstream.sh (533 lines), tools/upstream-mechanism.sh, .github/workflows/scan-upstream.yml on a daily cron, and a 22 KB system test with fixtures. None of it published anything any more; it was a second implementation of the live pipeline, inside the tool the realm consumes, and every file of it was a place a reader could mistake for the live path. Deleted in varve#168 with the two rivet artifacts that pointed at it (REQ-ROLLING-001 and VER-ROLLING-001, both deprecated rather than quietly dropped — the first because its second clause was reversed by DD-030, not met). + + WHAT THIS DOES NOT CLAIM. The other two realms REQ-REALM2-002 and REQ-COVALENT-001 describe do not exist yet; the bytecodealliance-sourced realm here is pulseengine-wasm, named for who holds the key rather than whose software it carries. Clause 4's gate is met for the realms that exist, not for every realm the roadmap names. + fields: + method: inspection + links: + - type: verifies + target: REQ-LAYERREPO-001 + provenance: + created-by: ai + model: claude-opus-5 + timestamp: 2026-09-18T17:43:05Z + release: v0.37.0