Skip to content

DIVE-3512: cut buzz-cli releases, a verified install path, and two defects found on the way - #3

Merged
5dive-bot merged 11 commits into
mainfrom
dive-3512-buzz-cli-release
Aug 17, 2026
Merged

DIVE-3512: cut buzz-cli releases, a verified install path, and two defects found on the way#3
5dive-bot merged 11 commits into
mainfrom
dive-3512-buzz-cli-release

Conversation

@5dive-bot

Copy link
Copy Markdown
Collaborator

DIVE-3512: a release rail for buzz-cli, a verified install path, and two defects found on the way

Customer boxes are configured with "buzz_path": "/usr/local/bin/buzz" and no
box has ever had that file — 5dive agent buzz status <name> returns rc 3 on
every correctly-wired seat. This adds the build and distribution path that
closes that, on top of current upstream rather than on our 63-commit-stale
fork.

What the branch is

Current upstream block/buzz main (f956e6f) plus our one pairing commit
cherry-picked onto it, plus this work. The cherry-pick was the question main
asked to answer before building anything, and the answer is clean: zero
conflicts, and git diff <picked> ourfork/main -- crates/buzz-pairing-cli
is empty, so the rebased crate is byte-identical to the fork's. Building from
upstream+patch loses nothing and picks up 63 commits of upstream movement.

Two defects this turned up, both worth more than the artifact

1. Our pairing patch cannot build reproducibly, and never could.
28e125f added rustls as a direct dependency of buzz-pairing-cli to pin
the CryptoProvider, and never updated Cargo.lock. rustls was already in the
lock as a transitive dependency, so a plain cargo build resolves it and the
omission is invisible. Only --locked surfaces it:

error: cannot update the lock file ... because --locked was passed to prevent this

This is live on 5dive-ai/buzz main today, not only on this branch, and
upstream CI would reject the patch on exactly these grounds if it were ever
sent there. The fix here is the missing half of that commit, not build
scaffolding: rustls is added to buzz-pairing-cli's dependency list in
Cargo.lock, and the release build keeps --locked rather than dropping the
flag to buy a green tick.

2. This fork's inherited workflows publish into Block's namespace.
Syncing this fork's main to upstream was the first ever push to main here,
and it fired six inherited workflows. IMAGE_NAME falls through to
ghcr.io/block/* whenever GHCR_IMAGE / GHCR_SPRIG_IMAGE are unset, and
actions/variables on this repo is total_count = 0, so "Sprig image"
attempted a write to ghcr.io/block/buzz-sprig as 5dive-bot. It failed on a
missing credential — which is a missing credential, not a control.

The fix is ported verbatim from the sibling fork's 5dive-chat PR #3
(DIVE-3531, merged 3e871d9c); it applied here with no conflicts. Every
registry write in docker.yml and sprig-image.yml now fires only on a
release tag push or a deliberate workflow_dispatch — the same shape
helm-chart.yml already used in this repo. Builds on main and on PRs still
run, so the compile signal is intact; they simply cannot write. sprig.yml
additionally learns to create its rolling release rather than only edit it,
because a fork inherits no releases and gh release edit exits 1 the first
time.

The two ghcr writers that port does not touch were audited rather than
assumed: push-gateway-helm-chart.yml triggers on push-chart-v* tags only,
so a main push cannot fire it, and helm-chart.yml already gates its publish
job. Neither needs a change.

The release rail

.github/workflows/buzz-cli-release.yml builds buzz and buzz-pair for
Linux x86-64. It is a new workflow rather than a change to release.yml
because upstream's release rail is the desktop app: it triggers on desktop-v*
tags, bundles on macOS, and every job is guarded to block/buzz. It neither
fires here nor emits a server-side CLI. This one carries the same guard pointed
at 5dive-ai/buzz, so a re-fork cannot fire it either.

  • ubuntu-22.04, not ubuntu-latest. The artifact is dynamically linked;
    built on 24.04 (glibc 2.39) it will not start on a 22.04 box. Built on 22.04
    (glibc 2.35) it runs on both.
  • Both binaries. DONE MEANS names only buzz-cli, but the pairing patch is
    entirely inside buzz-pairing-cli and buzz-pair is how a handset joins.
    Shipping only half leaves the mobile client with no counterpart.
  • Provenance by construction. Every run records the commit, the run id, the
    toolchain, and each binary's size, sha256 and BuildID into a PROVENANCE.txt
    that ships with the release. An artifact that cannot say what commit produced
    it must not reach a customer box.
  • A release takes a deliberate dispatch naming a tag. A branch push builds
    and uploads the run artifact and stops. cli-v* was checked against all five
    tag prefixes this repo triggers on (relay-v*, sprig-v*, chart-v*,
    push-chart-v*, desktop-v*) and collides with none; this workflow carries
    no tag trigger at all.

The install path

scripts/install-buzz-cli.sh downloads a named release, verifies both binaries
against the release's SHA256SUMS before anything is copied into place,
prints the provenance it is about to install, and records it at
/var/lib/buzz/installed-provenance.txt so the box itself knows what it is
running.

It refuses two things on purpose. It will not accept "latest" — a tag is
required, because an installed build nobody can name is the defect this whole
row exists to fix. And it will not overwrite an existing binary without
--force, printing that binary's BuildID instead: there is an unattributed
buzz of unknown provenance on the control-plane host today, and the script
should make a human look at it rather than silently replace it.

Not covered here

The last step of DONE MEANS — 5dive agent buzz status <name> going rc 3 to
rc 0 on a customer box — is not demonstrated by this PR. It needs shell on
sure-redwood, and the ssh key for that exists on two seats on the control
plane, neither of which is the one that wrote this. It is handed back as its
own row naming that capability.

lodar and others added 11 commits August 17, 2026 08:09
…nicking on wss://

Two defects that together made it impossible to pair a handset with the Buzz
mobile app against an HTTPS relay. Both measured against a live relay.

1. THE ENVELOPE

resolve_payload returned a bare bech32 nsec as PayloadType::Nsec in both arms.
Mobile _processPayload begins with `jsonDecode(payload) as Map<String, dynamic>`
and nothing in mobile/lib branches on payload_type, so a bare nsec dies on the
leading `n` with `FormatException: Unexpected character (at character 1)`.
That is verbatim what a real store build produced on a real handset.

--envelope-relay <https url> now emits the same shape the DESKTOP client sends
(desktop/src-tauri/src/commands/pairing.rs:145-148, PayloadType::Custom at :215):
{"relayUrl","pubkey","nsec"}. pubkey is DERIVED from the transferred nsec so the
two cannot drift. Without the flag the payload is unchanged, so CLI-to-CLI
interop testing keeps working exactly as before.

2. wss:// PANICKED BEFORE ANY PAIRING COULD START

    thread 'main' panicked at rustls-0.23.42/src/crypto/mod.rs:249:14
    Could not automatically determine the process-level CryptoProvider

Both ring and aws-lc-rs are reachable in the workspace, so rustls refuses to
choose. Plain ws:// never reaches that code path — which is why previous interop
testing, all of it against a plaintext relay, was structurally blind to it: the
defect exists only past the TLS an HTTPS deployment adds. Pin the ring provider
and install it at the top of main().

VERIFIED END TO END, not by inspection:
- cargo test -p buzz-pairing-cli: 7 passed, 0 failed, including an arm that
  pins the bare-nsec failure and one that proves the envelope decodes.
- A full NIP-AB pairing driven over wss:// against a real relay: SAS matched on
  both sides, target logged "Received custom payload!" carrying the three-field
  envelope.
- Those exact transferred bytes then replayed through the mobile app's own
  sequence: jsonDecode -> map; _validateRelayUrl accepts (https, public);
  _validateCredentials NIP-42 handshake accepted the transferred identity in
  46ms against the app's 8s budget.
- A real handset paired and got into the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 28e125f)
Adds a release rail for the server-side CLI. Upstream's release.yml is the
desktop app: it triggers on desktop-v* tags, bundles on macOS, and is guarded
to block/buzz, so it neither runs here nor produces /usr/local/bin/buzz. This
is a small separate workflow with the same guard pointed at 5dive-ai/buzz.

Builds on ubuntu-22.04 rather than ubuntu-latest so the glibc the artifact
links against is old enough to run on 22.04 and 24.04 boxes alike, and records
the commit, toolchain, sha256 and BuildID of every binary it emits. The
install script verifies the download against the release's SHA256SUMS before
touching /usr/local/bin, refuses to overwrite an existing binary without
--force, and leaves the provenance on the box.

The tree is current upstream main plus our one buzz-pairing-cli commit, so the
release also carries buzz-pair with the JSON pairing envelope.
workflow_dispatch resolves only against the default branch, so as written the
recipe could not be run until it had already been merged. A path-scoped push
trigger on this branch builds and uploads the run artifact without cutting a
release; cutting one still takes a deliberate dispatch that names a tag.
Our pairing commit (28e125f) added rustls as a direct dependency of
buzz-pairing-cli to pin the CryptoProvider, but never updated Cargo.lock. The
crate was already in the lock as a transitive dependency, so the omission is
invisible to a plain `cargo build` and only surfaces under --locked, which is
what a release build should use:

    error: cannot update the lock file ... because --locked was passed

That means no tree carrying this patch can be built reproducibly, including
5dive-ai/buzz's own main, and upstream CI would reject the patch on the same
grounds if it were ever sent there.
Lifted verbatim from 5dive-ai/5dive-chat PR #3 (merged 3e871d9), which fixed
the identical defect on the sibling fork. It applied to this tree with no
conflicts.

A fork inherits upstream's publish workflows, and their IMAGE_NAME falls
through to `ghcr.io/block/*` whenever the GHCR_IMAGE / GHCR_SPRIG_IMAGE
override variables are unset — which they are here (actions/variables
total_count = 0). So the first ever push to this fork's main fired six
inherited workflows, and Sprig image attempted a write to
ghcr.io/block/buzz-sprig as 5dive-bot. It failed on a missing credential.
That is a missing credential, not a control: the boundary held by accident.

Every registry write in docker.yml and sprig-image.yml now fires only on a
release tag push or a deliberate workflow_dispatch, the same shape
helm-chart.yml already used here. Builds on main and PRs are untouched, so the
compile signal survives. sprig.yml additionally learns to CREATE its rolling
release rather than only edit it, because a fork inherits no releases and
`gh release edit` exits 1 the first time.

Audited the rest: push-gateway-helm-chart.yml triggers on push-chart-v* tags
only and helm-chart.yml already gates its publish job on chart-v*, so those
two need no change.
The paths filter meant a commit that changed Cargo.lock or a crate did not
rebuild, which is precisely the change most worth rebuilding on. The branch is
short-lived; every push to it should be exercised.
…ldID

The build went green and the smoke failed, on my own step: `buzz --version`
is a usage error, because buzz-cli's clap command sets no `version`
attribute. Fixing that upstream-side would be a one-word divergence for a
string the workspace stamps 0.1.0 on every crate, so it is not worth carrying;
the binary's real identity is its BuildID, which readelf reads out of the file
and which the release already records.

So the install script now prints BuildID rather than a version that would
always have read 'version unavailable', and the smoke is stronger than the one
that failed: it requires the CLI to DISPATCH, not merely print help, by running
a subcommand against an unreachable relay and demanding exit 2 — the CLI's own
documented 'relay/network error' code — and it requires both binaries to carry
a readable BuildID at all.
…exit code

My previous smoke demanded exit 2 from `buzz channel list` against an
unreachable relay and got 1. The binary was right and the assertion was wrong:
with no BUZZ_PRIVATE_KEY the call is rejected as bad input and never reaches
the network, which is exactly what exit 1 means in the CLI's own table.

So assert the contract the CLI actually documents — errors are
{"error","message"} JSON on stderr — across two cases, one without an
identity and one with a throwaway key against a dead port, and print both
observed payloads rather than asserting blind again.

The panic check is the part worth keeping: the defect our pairing patch fixes
is a rustls CryptoProvider PANIC on wss://, and a panic is precisely what does
not produce that JSON. This makes the release smoke a regression guard for the
class of bug the patch exists to fix.
…the class

The run went green and proved nothing. Both error-contract checks invoked
`buzz channel list`; the subcommand is `channels`, so both died in clap with
"unrecognized subcommand 'channel'". A usage error is still {error,message}
JSON on stderr, so every assertion I wrote was satisfied without the CLI
dispatching anything or touching the network.

Fixed the subcommand, and added the guard that matters more than the fix: a
check whose stderr is a clap usage error now FAILS as a broken smoke rather
than passing as a good binary. The unreachable-relay case additionally
requires an error category other than user_error, so it cannot be satisfied by
anything that stops at argument validation.

The artifacts from the vacuous run were otherwise sound — buzz 16788888 bytes
sha256 01b478d6..., buzz-pair 6675408 bytes sha256 b68699ad... — but a release
should not be cut off a smoke that graded nothing.
With the subcommand fixed the check finally reached real code and reported
rc=3 auth_error — {"error":"auth_error","message":"BUZZ_PRIVATE_KEY is
required..."} — where I had asserted 1. The CLI classifies an absent key as an
auth failure rather than as bad input, which is not what its own exit-code
table reads like at a glance.

That is the guard working: two runs ago this same case 'passed' at rc=1 while
dying in clap. Pinning the observed 3 rather than relaxing the assertion,
because a caller branching on this code would care if it moved.
Both contract checks now reach real code and report what they should:

  no private key:    rc=3 {"error":"auth_error",...}
  unreachable relay: rc=2 {"error":"network_error","message":"tcp connect
                     error: Connection refused (os error 111)","retryable":true}

The second one opens a socket to a dead port and fails there, which is the
dispatch evidence the earlier vacuous run never had.

The step still died, on the follow-up category check: it piped the CLI into jq
under `set -o pipefail`, so the pipeline inherited the CLI's exit 2 — the very
outcome being measured — and `set -e` killed the step. Capture into a variable
first, parse second.
@5dive-bot
5dive-bot merged commit 484f884 into main Aug 17, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants