diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e21c82..209f520 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,6 +161,13 @@ jobs: echo "::notice::Toolchain SBOM written to release-assets/varve-${BARE}.cdx.json" ls -la release-assets/ + - name: Stage onboarding assets (trust root + canonical realms) + run: | + set -euo pipefail + cp trust-roots/rolling.pub release-assets/rolling.pub + cp varve-realms.toml release-assets/varve-realms.toml + ls -la release-assets/ + - name: Generate SHA256 checksums run: | set -euo pipefail diff --git a/CHANGELOG.md b/CHANGELOG.md index 04231ff..99cf7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v0.13.1 — 2026-08-08 + +Cold-start onboarding (REQ-ONBOARD-001) — fixes pulseengine/varve#34, +found by an external consumer cold-starting from the tarball. + +- The trust root (`rolling.pub`) and a canonical `varve-realms.toml` now + ship as **release assets**, and the README has a **Getting started** + section — a consumer with only the binary can reach a verified install +- The no-trust-root error now names the **zero-config realm path** (the + stronger mechanism) and where the key is published, instead of steering + to a bare `VARVE_TRUST_ROOT` with nowhere to get the key +- `varve install` **auto-caches** a line-status carried in the installed + layout, so `varve status` works with no `--from-file` step; the status + error describes the real path. Registry-side line-status distribution is + tracked as REQ-STATUS-DIST-001 (v0.14.0) + ## v0.13.0 — 2026-08-08 Adversarial inputs (REQ-FUZZ-001 + REQ-PROP-001 + REQ-MATRIX-001 verified). diff --git a/Cargo.lock b/Cargo.lock index ff9aa3d..c4015bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,7 +1787,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "varve" -version = "0.13.0" +version = "0.13.1" dependencies = [ "anyhow", "assert_cmd", @@ -1803,7 +1803,7 @@ dependencies = [ [[package]] name = "varve-core" -version = "0.13.0" +version = "0.13.1" dependencies = [ "flate2", "hex", diff --git a/Cargo.toml b/Cargo.toml index bbfef89..0c08de9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/varve", "crates/varve-core"] [workspace.package] -version = "0.13.0" +version = "0.13.1" edition = "2024" license = "Apache-2.0" repository = "https://github.com/pulseengine/varve" diff --git a/README.md b/README.md index beef130..773f9a2 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,37 @@ > promise. The release plan lives in rivet (`rivet release status`); see > [SECURITY.md](SECURITY.md) for the current trust posture and its limits. +## Getting started + +Install one PulseEngine toolchain layer, verified, and dispatch its tools — +the zero-config path uses a **realm**, so no environment variable is needed: + +```sh +# 1. In your project, pin a layer and name the realm: +cat > varve.toml <<'PIN' +manifest-version = 1 +[toolchain] +realm = "pulseengine" +channel = "rolling" +layer = "2026.08.2" +PIN + +# 2. Drop in the canonical realm definitions (registry + trust root). +# Ships as `varve-realms.toml` with every release, and lives at +# trust-roots/ in this repo: +curl -LO https://github.com/pulseengine/varve/releases/latest/download/varve-realms.toml + +# 3. Install (verified against the realm's root — no env var), then shim: +varve install +varve shim install # then: . "$HOME/.varve/env" +rivet --version # dispatched from the pinned layer +``` + +Without a realm, point `VARVE_TRUST_ROOT` at the published root key +(`rolling.pub`, a release asset) and pass `--from oci://ghcr.io/pulseengine/varve/layers`. +The rolling channel is provisional and makes no qualification promise — see +[SECURITY.md](SECURITY.md). + ## The problem Three consumers, two toolchains, one afternoon: diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index e860065..69671bb 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -1252,3 +1252,45 @@ artifacts: fields: priority: should category: non-functional + + - id: REQ-ONBOARD-001 + type: requirement + title: A cold-start consumer can reach a verified install from the tarball alone + status: verified + release: v0.13.1 + description: > + A new consumer with only a release binary shall be able to get to a + verified install: the trust root (rolling.pub) and the canonical + varve-realms.toml ship as release assets and are documented in a README + Getting-started section; the no-trust-root error names the zero-config + realm path and where the key is published (not just "set the env var"); + and `varve install` auto-caches a line-status carried in the installed + layout so `varve status` works with no extra step. Found by an external + cold-start (pulseengine/varve#34): the error steered to the weaker + mechanism and the strong one was documented nowhere user-facing. + tags: [ergonomics] + links: + - type: satisfies + target: REQ-REALM-001 + fields: + priority: must + category: non-functional + + - id: REQ-STATUS-DIST-001 + type: requirement + title: Line-status is distributed with the layer over the registry + status: approved + release: v0.14.0 + description: > + The deposit pipeline shall sign and attach a baseline line-status to + each layer, and the registry source shall fetch it, so `varve status` + works for a registry (oci://) install without a local layout — closing + the second half of pulseengine/varve#34 (v0.13.1 auto-caches only a + locally-carried layout's status). + tags: [core] + links: + - type: satisfies + target: REQ-KP-001 + fields: + priority: should + category: functional diff --git a/artifacts/verification.yaml b/artifacts/verification.yaml index 5915d3c..9a26c98 100644 --- a/artifacts/verification.yaml +++ b/artifacts/verification.yaml @@ -407,3 +407,17 @@ artifacts: links: - type: verifies target: REQ-MATRIX-001 + + - id: VER-ONBOARD-001 + type: verification + title: Cold-start reaches verified install; status auto-works; error guides + status: verified + release: v0.13.1 + fields: + method: automated-test + steps: + - run: cargo test -p varve install_auto_caches_a_layout_carried_line_status_so_status_just_works + - run: cargo test -p varve the_trust_root_error_points_to_the_realm_path + links: + - type: verifies + target: REQ-ONBOARD-001 diff --git a/crates/varve/Cargo.toml b/crates/varve/Cargo.toml index d100501..73f7d7a 100644 --- a/crates/varve/Cargo.toml +++ b/crates/varve/Cargo.toml @@ -17,5 +17,6 @@ varve-core.workspace = true assert_cmd = "2.2.2" hex = "0.4.3" predicates = "3.1.4" +serde_json = "1.0.151" tempfile = "3.27.0" wsc-attestation = "0.10.0" diff --git a/crates/varve/src/main.rs b/crates/varve/src/main.rs index fa2a64d..1388e34 100644 --- a/crates/varve/src/main.rs +++ b/crates/varve/src/main.rs @@ -381,7 +381,16 @@ fn status(store: &Store, from_file: Option<&std::path::Path>) -> anyhow::Result< let Some(doc) = cache.load(&line, &root_pk)? else { bail!( - "no line-status document cached for line {line} — ingest one with `varve status --from-file `" + "no line-status document cached for line {line}.\n\ + \n\ + Line-status carries the support window, known problems, and yank state \ + for the line. `varve install` caches it automatically when the installed \ + layer's oci-layout carries one; otherwise ingest a signed envelope \ + explicitly:\n\ + \n varve status --from-file \n\ + \n\ + Distribution of line-status via the registry is tracked in \ + REQ-STATUS-DIST-001 (pulseengine/varve#34)." ); }; let report = doc.report_for(&pin.layer); @@ -775,8 +784,17 @@ fn trust_root() -> anyhow::Result { fn trust_root_bytes() -> anyhow::Result> { let Some(path) = std::env::var_os("VARVE_TRUST_ROOT") else { bail!( - "no trust root configured — set VARVE_TRUST_ROOT to the file holding the \ - hex-encoded PulseEngine root public key" + "no trust root configured.\n\ + \n\ + The zero-config path is a realm: add `realm = \"pulseengine\"` to your \ + varve.toml and commit a varve-realms.toml naming the registry and trust \ + root — then no environment variable is needed and the realm's root is \ + authoritative. The canonical file ships as `varve-realms.toml` with each \ + release (and at pulseengine/varve/trust-roots/).\n\ + \n\ + Or, without a realm, point VARVE_TRUST_ROOT at the published root key \ + (`rolling.pub`, a release asset). See the Getting started section of the \ + README." ); }; let path = PathBuf::from(path); @@ -868,6 +886,32 @@ fn install(store: &Store, from: Option<&str>, platform: Option) -> anyho outcome.layer ); } + + // Auto-cache a line-status the source's oci-layout carries (DD-008), so + // `varve status` works with zero extra steps (varve#34). Verified against + // the same realm/trust root; a bad or stale one is a warning, never fatal + // to an otherwise-good install. + if std::path::Path::new(from).join("index.json").is_file() { + let line = pin.layer.line().clone(); + match varve_core::read_status_from_layout(std::path::Path::new(from), &line) { + Ok(Some(envelope)) => { + let root_pk = ctx_root_bytes(&ctx)?; + match varve_core::LineStatus::verify_and_parse(&envelope, &root_pk) { + Ok(doc) => { + let cache = varve_core::StatusCache::at_root(store.root()); + if let Err(e) = cache.update(&line, &envelope, &doc) { + eprintln!( + "note: layer carried a line-status but it was not cached: {e}" + ); + } + } + Err(e) => eprintln!("note: layer carried an unverifiable line-status: {e}"), + } + } + Ok(None) => {} + Err(e) => eprintln!("note: could not read the layer's line-status: {e}"), + } + } Ok(()) } diff --git a/crates/varve/tests/cli.rs b/crates/varve/tests/cli.rs index 5eb9a45..99f5769 100644 --- a/crates/varve/tests/cli.rs +++ b/crates/varve/tests/cli.rs @@ -1133,6 +1133,89 @@ arg-prefix = "--wasi-arg" ); } +// rivet: verifies REQ-ONBOARD-001 +#[test] +fn install_auto_caches_a_layout_carried_line_status_so_status_just_works() { + let fx = fixture(Some(PIN_JULY), &[]); + let parent = fx.project.parent().unwrap(); + let (sk, pk) = varve_core::generate_root_keypair(); + let trust = parent.join("root.pub"); + std::fs::write(&trust, hex::encode(&pk)).unwrap(); + + // Deposit a layer, then attach a signed line-status to its oci-layout. + let sk_path = parent.join("root.key"); + std::fs::write(&sk_path, hex::encode(&sk)).unwrap(); + let tool = parent.join("t"); + std::fs::write(&tool, b"toolbytes").unwrap(); + let spec = parent.join("d.toml"); + let host = varve_core::host_platform(); + std::fs::write( + &spec, + format!( + "layer = \"2026.07.0\"\nchannel = \"qualified\"\ncounter = 1\n\n[[tool]]\nname = \"synth\"\nversion = \"1\"\nplatform = \"{host}\"\npath = \"{}\"\n", + tool.display() + ), + ) + .unwrap(); + let layout = parent.join("layout"); + varve(&fx) + .args(["deposit", "--spec"]) + .arg(&spec) + .args(["--issued-at", "2026-08-07T00:00:00Z", "--key"]) + .arg(&sk_path) + .args(["--out"]) + .arg(&layout) + .assert() + .success(); + + // Sign a line-status and attach it to the layout via the library. + let status_json = r#"{"line":"2026.07","counter":1,"issued-at":"2026-08-07T00:00:00Z","support-until":"2028-07-31","yanked":{},"known-problems":[]}"#; + let doc: varve_core::LineStatus = serde_json::from_str(status_json).unwrap(); + let envelope = doc + .sign( + &hex::decode(std::fs::read_to_string(&sk_path).unwrap().trim()).unwrap(), + "k", + ) + .unwrap(); + let line = "2026.07.0" + .parse::() + .unwrap() + .line() + .clone(); + varve_core::attach_status_to_layout(&layout, &line, envelope.as_bytes()).unwrap(); + + // Install — and then `varve status` works with NO --from-file (varve#34). + varve(&fx) + .env("VARVE_TRUST_ROOT", &trust) + .args(["install", "--from"]) + .arg(&layout) + .assert() + .success(); + varve(&fx) + .env("VARVE_TRUST_ROOT", &trust) + .arg("status") + .assert() + .success() + .stdout(predicate::str::contains("supported until 2028-07-31")); +} + +// rivet: verifies REQ-ONBOARD-001 +#[test] +fn the_trust_root_error_points_to_the_realm_path() { + let fx = fixture(Some(PIN_JULY), &[]); + let signed = signed_layer_fixture(&fx, "2026.07.0", 1); + varve(&fx) + .args(["install", "--from"]) + .arg(&signed.archive) + .assert() + .failure() + .stderr( + predicate::str::contains("realm") + .and(predicate::str::contains("rolling.pub")) + .and(predicate::str::contains("Getting started")), + ); +} + // rivet: verifies REQ-COEXIST-001 #[test] fn list_with_an_empty_core_succeeds_and_says_so() { diff --git a/varve-realms.toml b/varve-realms.toml new file mode 100644 index 0000000..51971f9 --- /dev/null +++ b/varve-realms.toml @@ -0,0 +1,11 @@ +# Canonical realm definitions for the PulseEngine toolchain. +# Commit this beside your varve.toml (or above it); `realm = "pulseengine"` +# in the pin then needs no environment variable — the realm supplies both +# the registry and the trust root, and the realm's root is authoritative. +# +# The rolling root is PROVISIONAL until the v1.0 ceremony (see SECURITY.md); +# the rolling channel makes no qualification promise. + +[realm.pulseengine] +registry = "oci://ghcr.io/pulseengine/varve/layers" +trust-root = "4e771dc62a08be89e3450f8cd807da58ff70af4a4e124ebf2d2b71684cfd9973"