From 318fbf896ec335bc7bcb40edafde0b6ebca53428 Mon Sep 17 00:00:00 2001 From: Tyler <109685178+tlongwell-block@users.noreply.github.com> Date: Sun, 2 Aug 2026 16:17:55 -0400 Subject: [PATCH] fix(security): bump nostr crates for RUSTSEC-2026-0225..0232 + default sprig image to published digest (#4392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Two changes, both fallout/follow-up from #4289 landing: ### 1. Fix the Security job failing on main (lockfile-only) Eight RUSTSEC advisories published today against the nostr stack turned `cargo-deny check` advisories red on main ([failing run](https://github.com/block/buzz/actions/runs/30761611723/job/91533106673)). Not introduced by #4289 — the advisories landed upstream and any push to main today would have tripped them. - **RUSTSEC-2026-0225..0230** → `nostr` 0.44.6 → **0.44.7** (Debug output exposing NIP-46/NIP-60 credentials; wallet parsers accepting unauthenticated events; NIP-44/NIP-04/NIP-98 resource exhaustion; NIP-50 empty-filter panic) - **RUSTSEC-2026-0231..0232** → `nostr-relay-pool` 0.44.2 (root) / 0.44.1 (tauri) → **0.44.3** (auth-challenge memory exhaustion; processing of unverified relay events) Both workspace lockfiles bumped (`Cargo.lock`, `desktop/src-tauri/Cargo.lock`). No manifest changes. ### 2. Default the desktop GUI's sprig image to the published `ghcr.io/block/buzz-sprig` The first main-push after #4289 published the image publicly (package created 18:44Z, visibility `public`). The `config_schema()`'s `image` property now carries a `default`: ``` ghcr.io/block/buzz-sprig:sha-6530b58@sha256:17facfc7608d8ddb33bc056c9aaba1098f4ef6abe5655702fbfd7584d1f74d76 ``` **Why tag+digest, not tag:** the backend deliberately rejects tag-only references — the pod runs with the agent's nsec and tags are mutable pointers (`image.rs` §Image). The tag+digest form keeps the human-traceable `sha-6530b58` while the digest does the pinning; `image::parse` already normalizes it to the tagless canonical form, so create-intent fingerprints are identical to the bare-digest spelling. The digest is the **multi-arch manifest-list digest** (amd64+arm64), resolved via `docker buildx imagetools inspect`. **This is a UI prefill, not a baked fallback:** `image` stays in the schema's `required` list, an empty value still fails closed with a named field, and the desktop submits the value explicitly in `provider_config` (the `WhereToRunSection` probe seeds `providerConfig` from schema defaults) — so deploy fingerprints never depend on compiled-in provider state, and the spec's §K8s pod-reconciliation concern about baked-default divergence is not engaged. Module prose that said "no published image exists yet" is updated to match reality. No desktop code changes needed: the form already prefills from `properties[*].default` and submits seeded defaults. ## Testing - `cargo-deny check` at head: **advisories ok, bans ok, licenses ok, sources ok** (was: advisories FAILED) - `cargo test -p buzz-backend-kubernetes`: **158 passed** (154 lib + 4 wire), including new `schema_default_image_round_trips_through_parse` pinning the constant + its normalization, and the wire `info` test now asserting the default is present in the provider's real stdout response - Live provider probe: `{"op":"info"}` against the built binary returns the default in `config_schema.properties.image.default` with `required` unchanged (`["namespace","image"]`) - Full workspace test suite via pre-push hook: green (earlier direct `cargo test --workspace` run: sole failure was `api::mesh_demo::demo_join_forwarded_arm_round_trips_echo`, the documented pre-existing main flake — unrelated, fails on base) - Image existence verified against GHCR: `docker buildx imagetools inspect ghcr.io/block/buzz-sprig:sha-6530b58` resolves to the pinned manifest-list digest with linux/amd64 + linux/arm64 manifests --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> --- Cargo.lock | 8 ++--- Justfile | 30 +++++++++++++---- crates/buzz-backend-kubernetes/src/config.rs | 33 +++++++++++++++++-- crates/buzz-backend-kubernetes/src/image.rs | 17 ++++++---- .../tests/wire_fixtures.rs | 8 +++++ desktop/src-tauri/Cargo.lock | 8 ++--- 6 files changed, 80 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62bcea0ca..937ead564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5809,9 +5809,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.44.6" +version = "0.44.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e826dd648489de2c5b293920e20b92932ef820302007c1987c758d4d06eeb2cf" +checksum = "c7d3d987ea7078dc36947cde532637c472a229426702e4331dd7667325378bd9" dependencies = [ "base64 0.22.1", "bech32", @@ -5853,9 +5853,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.44.2" +version = "0.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb94d61a467a869a6790b907838a9bea82c813d567d9fcbac995f207be8cee4b" +checksum = "c85c54d6ca9aae4ae2bf19a7663ba9db5f45f783f1d24aff55f006386b8b99a1" dependencies = [ "async-utility", "async-wsocket", diff --git a/Justfile b/Justfile index 8dbe125a7..d6e86c8d0 100644 --- a/Justfile +++ b/Justfile @@ -526,11 +526,20 @@ staging *ARGS: bootstrap _ensure-sidecar-stubs FEATURES=(--features mesh-llm) export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)" fi - # Replace the 0-byte sidecar stub with the real CLI binary so tauri dev picks it up. + # Replace 0-byte sidecar stubs with real binaries so tauri dev picks them up. + # buzz: the CLI sidecar. buzz-backend-kubernetes: provider discovery scans the + # exe dir for executable buzz-backend-* files, so the non-executable stub that + # tauri dev copies next to the exe would hide the provider from "Run on". TARGET=$(rustc -vV | sed -n 's|host: ||p') TARGET_DIR=$(cargo metadata --format-version 1 --no-deps | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).target_directory") - cp "${TARGET_DIR}/release/buzz" "desktop/src-tauri/binaries/buzz-${TARGET}" - chmod +x "desktop/src-tauri/binaries/buzz-${TARGET}" + STAGING_SIDECARS=(buzz) + if [[ "$TARGET" != *windows* ]]; then + STAGING_SIDECARS+=(buzz-backend-kubernetes) + fi + for bin in "${STAGING_SIDECARS[@]}"; do + cp "${TARGET_DIR}/release/${bin}" "desktop/src-tauri/binaries/${bin}-${TARGET}" + chmod +x "desktop/src-tauri/binaries/${bin}-${TARGET}" + done cd {{desktop_dir}} export BUZZ_RELAY_URL="wss://sprout-oss.stage.blox.sqprod.co" source ../scripts/instance-env.sh @@ -553,11 +562,20 @@ production *ARGS: bootstrap _ensure-sidecar-stubs FEATURES=(--features mesh-llm) export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)" fi - # Replace the 0-byte sidecar stub with the real CLI binary so tauri dev picks it up. + # Replace 0-byte sidecar stubs with real binaries so tauri dev picks them up. + # buzz: the CLI sidecar. buzz-backend-kubernetes: provider discovery scans the + # exe dir for executable buzz-backend-* files, so the non-executable stub that + # tauri dev copies next to the exe would hide the provider from "Run on". TARGET=$(rustc -vV | sed -n 's|host: ||p') TARGET_DIR=$(cargo metadata --format-version 1 --no-deps | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).target_directory") - cp "${TARGET_DIR}/release/buzz" "desktop/src-tauri/binaries/buzz-${TARGET}" - chmod +x "desktop/src-tauri/binaries/buzz-${TARGET}" + PRODUCTION_SIDECARS=(buzz) + if [[ "$TARGET" != *windows* ]]; then + PRODUCTION_SIDECARS+=(buzz-backend-kubernetes) + fi + for bin in "${PRODUCTION_SIDECARS[@]}"; do + cp "${TARGET_DIR}/release/${bin}" "desktop/src-tauri/binaries/${bin}-${TARGET}" + chmod +x "desktop/src-tauri/binaries/${bin}-${TARGET}" + done cd {{desktop_dir}} export BUZZ_RELAY_URL="wss://buzz.block.builderlab.xyz" source ../scripts/instance-env.sh diff --git a/crates/buzz-backend-kubernetes/src/config.rs b/crates/buzz-backend-kubernetes/src/config.rs index 39b68ce0f..4d96735b7 100644 --- a/crates/buzz-backend-kubernetes/src/config.rs +++ b/crates/buzz-backend-kubernetes/src/config.rs @@ -1,8 +1,9 @@ //! `provider_config` parsing and the `info` config schema //! (spec §`provider_config` v1 fields, `docs/remote-agents.md:1384-1389`). //! -//! Nine fields, all optional except `image` (v1 ships no baked default — -//! §Image). No credential field exists, by I2: cluster auth comes from ambient +//! Nine fields, all optional except `image` (required at parse time; the +//! schema offers the published sprig image as a prefill default — §Image). +//! No credential field exists, by I2: cluster auth comes from ambient //! kubeconfig resolution and nothing else (`:196-198`). use crate::image::{self, ImageRef}; @@ -33,6 +34,15 @@ impl Default for Resources { /// `BUZZ_ACP_EXIT_AFTER_INACTIVITY` are one knob, not two. pub const DEFAULT_INACTIVITY_SECONDS: u64 = 7200; +/// Default `image` schema prefill: the published sprig image, in tag+digest +/// form so the tag stays human-traceable to its git SHA while the digest does +/// the pinning (§Image — tag-only refs are rejected; `image::parse` drops the +/// tag on normalization). This is a UI prefill, not a baked fallback: `image` +/// stays required, an empty value still fails closed, and the value always +/// arrives explicitly in `provider_config`, so create-intent fingerprints are +/// unaffected by provider upgrades. +pub const DEFAULT_IMAGE: &str = "ghcr.io/block/buzz-sprig:sha-6530b58@sha256:17facfc7608d8ddb33bc056c9aaba1098f4ef6abe5655702fbfd7584d1f74d76"; + /// Fixed nonzero UID/GID for the agent container (§Pod shape hardening). pub const RUN_AS_UID: i64 = 10001; pub const RUN_AS_GID: i64 = 10001; @@ -201,7 +211,8 @@ pub fn config_schema() -> serde_json::Value { "image": { "type": "string", "title": "Agent image", - "description": "Digest-pinned image containing the buzz-acp runtime ABI, e.g. ghcr.io/block/buzz-sprig@sha256:. Tags are not accepted: this pod holds the agent's private key." + "description": "Digest-pinned image containing the buzz-acp runtime ABI, e.g. ghcr.io/block/buzz-sprig@sha256:. Tags alone are not accepted: this pod holds the agent's private key.", + "default": DEFAULT_IMAGE }, "cpu_request": { "type": "string", "title": "CPU request", "default": defaults.cpu_request @@ -396,6 +407,22 @@ mod tests { assert_eq!(parse(&cfg).unwrap().namespace, default); } + /// Same guarantee for the image prefill: the schema's default must be a + /// value `image::parse` accepts, or the UI prefills a form that fails on + /// submit. Its tag+digest form normalizes to the tagless canonical form. + #[test] + fn schema_default_image_round_trips_through_parse() { + let schema = config_schema(); + let default = schema["properties"]["image"]["default"].as_str().unwrap(); + assert_eq!(default, DEFAULT_IMAGE); + let cfg = serde_json::json!({"namespace": "buzz-agents-abc123", "image": default}); + let parsed = parse(&cfg).unwrap(); + assert_eq!( + parsed.image.as_str(), + "ghcr.io/block/buzz-sprig@sha256:17facfc7608d8ddb33bc056c9aaba1098f4ef6abe5655702fbfd7584d1f74d76" + ); + } + /// Nine fields exactly (§`provider_config` v1 fields). The cap is 20; the /// count is pinned so a field added without a spec change is caught here. #[test] diff --git a/crates/buzz-backend-kubernetes/src/image.rs b/crates/buzz-backend-kubernetes/src/image.rs index b35e6bab1..409989fda 100644 --- a/crates/buzz-backend-kubernetes/src/image.rs +++ b/crates/buzz-backend-kubernetes/src/image.rs @@ -5,9 +5,11 @@ //! distinguishes them from digests for exactly this reason — so a tag-only //! reference is rejected, not just `:latest`. //! -//! v1 ships no baked default (there is no published `ghcr.io/block/buzz-sprig` -//! image yet, so a compile-time digest would be a placeholder). `image` is -//! therefore required, and its absence fails closed with a named field. +//! There is no parse-time fallback: `image` is required, and its absence +//! fails closed with a named field. The published `ghcr.io/block/buzz-sprig` +//! digest is offered only as a schema `default` (a UI prefill the desktop +//! submits explicitly — see `config::DEFAULT_IMAGE`), so the create-intent +//! fingerprint never depends on compiled-in provider state. /// A validated, digest-qualified image reference. /// @@ -35,9 +37,9 @@ impl std::fmt::Display for ImageRef { pub fn parse(raw: &str) -> Result { let reference = raw.trim(); if reference.is_empty() { - return Err("provider_config.image is required: v1 ships no default \ - image, so the digest-pinned image to run must be given \ - explicitly" + return Err("provider_config.image is required: no image is assumed \ + at parse time, so the digest-pinned image to run must be \ + given explicitly" .to_string()); } @@ -169,7 +171,8 @@ mod tests { } } - /// v1 has no baked default, so an absent image is an error that names the + /// Parsing has no fallback (the schema default is a UI prefill, not a + /// parse-time substitute), so an absent image is an error that names the /// field rather than a silent fallback. #[test] fn empty_reference_names_the_field() { diff --git a/crates/buzz-backend-kubernetes/tests/wire_fixtures.rs b/crates/buzz-backend-kubernetes/tests/wire_fixtures.rs index 2c3af82b1..b3049a98e 100644 --- a/crates/buzz-backend-kubernetes/tests/wire_fixtures.rs +++ b/crates/buzz-backend-kubernetes/tests/wire_fixtures.rs @@ -114,6 +114,14 @@ fn info_response_carries_the_contract_fields() { default.starts_with("buzz-agents-"), "unexpected namespace default: {default}" ); + let image_default = schema["properties"]["image"]["default"] + .as_str() + .expect("no image default"); + assert!( + image_default.starts_with("ghcr.io/block/buzz-sprig:") + && image_default.contains("@sha256:"), + "unexpected image default: {image_default}" + ); } /// The desktop's richest payload must parse. No response fixture: this one diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 1ca7075f1..9feecbee0 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -6124,9 +6124,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.44.6" +version = "0.44.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e826dd648489de2c5b293920e20b92932ef820302007c1987c758d4d06eeb2cf" +checksum = "c7d3d987ea7078dc36947cde532637c472a229426702e4331dd7667325378bd9" dependencies = [ "base64 0.22.1", "bech32", @@ -6168,9 +6168,9 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.44.1" +version = "0.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91b2c039df4f96c4bf7dae52a74fd5516ad6dda83a11c0c69dea91b5255a4f37" +checksum = "c85c54d6ca9aae4ae2bf19a7663ba9db5f45f783f1d24aff55f006386b8b99a1" dependencies = [ "async-utility", "async-wsocket",