fix(security): bump nostr crates for RUSTSEC-2026-0225..0232 + default sprig image to published digest (#4392)

## 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>
This commit is contained in:
Tyler
2026-08-02 16:17:55 -04:00
committed by GitHub
co-authored by npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d
parent f86cfc7369
commit 318fbf896e
6 changed files with 80 additions and 24 deletions
Generated
+4 -4
View File
@@ -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",
+24 -6
View File
@@ -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
+30 -3
View File
@@ -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:<digest>. 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:<digest>. 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]
+10 -7
View File
@@ -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<ImageRef, String> {
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() {
@@ -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
+4 -4
View File
@@ -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",