mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
d82afe9012efd8d86a1facbf677efea362aa2abe
2091
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d82afe9012 |
test(auth): expand provider negative coverage
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> (cherry picked from commit 12f6d2ec639ece2a669f7ea76d74ea4e72912209) Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
33b11db767 |
fix(auth): preserve provider decision scope
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> (cherry picked from commit 26f64a507c3fe1cd4db3352998b2804b0cb80644) Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
edf28cca46 |
feat(auth): add authorization provider contract
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> (cherry picked from commit 93428020c84f477186f400952d403e33e6f2ab04) Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
6db9d336fb |
fix(auth): validate embedded domains before authority
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
4affe89a4d |
fix(auth): expose sealed finalizer evidence
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
76f9b9f4cd |
fix(auth): enable sealed owner admission
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
5a014bda25 |
fix(auth): seal authoritative adapter boundary
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
8ea116a5d0 |
fix(auth): bind authoritative policy and binding evidence
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
c3c17cf850 |
fix(auth): enforce binding expiry contract
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
f5115171a3 |
feat(auth): add versioned authorization context
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> (cherry picked from commit 49af92663ec3aefd9552b1bd6decbdec876e4761) Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
bde90293c9 |
ci(identity): run auth boundary regressions
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
a28957d79b |
fix(audio): release failed admission lease
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
af657ea23b |
fix(identity): make private huddle admission atomic
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
8921f6d7a8 |
fix(identity): restore CI gates
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
a11293a646 |
fix(identity): harden corporate identity lifecycle
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
bc2a16ff39 |
fix(identity): close auth boundary gaps
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
cb61ed5dbe |
chore(desktop): satisfy file size ratchet
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
e82d72d5c3 |
ci(identity): run lifecycle tests
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
7b925279fd |
fix(identity): make verification provider-neutral
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
c52ae39085 |
fix(identity): authorize agents through owners
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
923a592c3f |
fix(identity): refine verified badge
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
3e761a7431 |
feat(identity): show binding in profile panel
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
948cec8395 |
fix(identity): clarify verified profile labels
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
d4eb40fcb4 |
fix(identity): advertise stable dev relay key
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
e900decf6f |
feat(identity): show relay-verified profiles
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
b53bcfdd6d |
feat(relay): add corporate identity binding
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> |
||
|
|
2c0ac24674 |
fix(desktop): stop the create-agent provider config probe from erasing keystrokes (#4411)
## What
Fixes the create-agent dialog's "Run on" provider config fields eating
keystrokes — reported by Tyler in buzz-remote-agents (channel
`29414326`, thread `db76677a`): the Kubernetes **Kubeconfig context**
field would not accept typing.
## Why it happened (the Typewriter Eraser, shipped in #4289)
`WhereToRunSection`'s probe `useEffect` depended on the whole `draft`:
1. every keystroke changed the draft → effect re-fired → provider binary
re-probed;
2. each probe result is a fresh object written into the draft → the
effect re-triggered **itself**, respawning the provider binary in a loop
for as long as the dialog sat on a provider;
3. every probe resolution reset `providerConfig` to schema defaults —
erasing whatever was typed. A field with no schema default (`context`)
snapped back to empty, i.e. "won't let me type". Unrelated to how many
kubeconfig contexts you have.
## Fix
- **Probe once per provider selection**, keyed on the provider's stable
`binaryPath` — not the draft, not the provider object (a
`useBackendProvidersQuery` refresh must not reprobe an unchanged
selection).
- **Latest-state resolution** via `React.useEffectEvent` + a new pure
`applyProbeResult` helper: schema defaults merge **beneath** the current
`providerConfig`, so a probe landing after the user typed can never
clobber in-flight input (per Wren's pre-patch red-team: changing deps
alone leaves a stale closure).
Existing `cancelled` cleanup keeps provider-switch/unmount safe;
selection reset (`emptyWhereToRunDraft`) and the fail-closed probe-error
path are unchanged.
## Tests
- **Unit** (`whereToRunIntent.test.mjs`): `applyProbeResult` merge
semantics — defaults under typed values, user-cleared fields stay
cleared, schema-less results, unrelated fields preserved.
- **E2E** (new `where-to-run-config.spec.ts`, added to the smoke
project, **red-first verified**: all 3 fail against the unfixed
component):
- typing into a defaultless provider field sticks, and
`probe_backend_provider` fires exactly once per selection;
- the config form is gated on probe resolution (slow probe: no
half-rendered form, defaults prefill once);
- provider → local → provider re-probes and resets cleanly.
- Mock bridge gains `backendProviders` / `backendProviderProbeResult` /
`backendProviderProbeDelayMs` seams (defaults preserve prior behavior).
## Verification at
|
||
|
|
a5dbdf5e61 |
fix(mobile): recover and pace live subscriptions (#3053)
### What changed? Mobile now recovers live subscriptions after retryable or rate-limited relay `CLOSED` responses. It ports the existing desktop model: classify terminal versus retryable closures, honor retry hints through a session-owned rate-limit gate, retry with bounded backoff, and replay visible-channel subscriptions first in bounded batches. Channel refreshes also retain unchanged live subscriptions instead of clearing and recreating them. This is desktop parity, not a new relay policy. ### Why? On reconnect or resume, mobile replayed its retained live subscriptions while `channelsProvider` independently cleared and recreated roughly the same set, alongside unread catch-up and open-channel requests. The relay allows 50 REQs per 5 seconds, so users in many channels could predictably exceed the budget. In live reproduction, 55 subscriptions produced 9 rate-limit closures, 60 produced 18, and 80 produced 36. Mobile then treated every live `CLOSED` as terminal, removed the affected subscription, and never restored it. Channel updates could remain dead until a later session reconstruction. This is the primary causal chain behind [BOT-1449](https://linear.app/squareup/issue/BOT-1449/buzz-mobile-posted-messages-dont-appear-until-leavingre-entering-the). Desktop already handles this as normal transient pressure by classifying closures, gating and backing off retries, pacing reconnect replay, and retaining unchanged subscriptions. This change brings mobile to the same recovery model while removing the avoidable request burst. ### How is it tested? Full mobile suite: 721 passed, 1 skipped. Analyzer and formatting checks pass. Required CI checks pass. Added and updated tests cover `CLOSED` classification, retry hints, rate-limit gating, bounded retry and reset behavior, terminal failures, timer cleanup, history gating, visible-first batched replay, and retention of unchanged subscriptions. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: npub1tu6ed4gf70jg7pvk8uhttlprexznhzpg74am2d3seqd3ececzgusy8hzac <5f3596d509f3e48f05963f2eb5fc23c9853b8828f57bb53630c81b1ce3381239@buzz.block.builderlab.xyz> Co-authored-by: npub1w85l93z2dyetvaev42kvmgv3r5qsgc7rutrvgpqshqefj4sydqqskwstfm <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> |
||
|
|
7ff5fc3189 |
feat(acp): deliver system prompt via _meta.systemPrompt for claude-agent-acp (#4395)
`claude-agent-acp` (since v0.6.0 / PR #91) accepts `_meta.systemPrompt: {append: text}` on `session/new` to append to the adapter's native preset while keeping its tool-use prompt intact — the same non-standard extension pattern as `_session/steering` was before it was standardised. ## What changes **Rust (`crates/buzz-acp/`)** - Adds `SystemPromptTransport` enum to `acp.rs`: `Field(&str)` (ACP protocol v2, unchanged) vs `ClaudeMeta(&str)` (new `_meta.systemPrompt: {append: text}`). When both `ClaudeMeta` and `session_title` are present the two `_meta` members are merged into one object so neither clobbers the other. - Gates on exact adapter identity `@agentclientprotocol/claude-agent-acp` in `pool.rs`: `session_new_system_prompt()` routes that name to `ClaudeMeta` regardless of reported `protocolVersion` (CC declares v1). `has_system_prompt_support()` gains the same name check so user-message `[Base]`/`[System]` framing is suppressed for CC sessions. - All other paths — goose post-hoc method, protocol-v2 `Field`, legacy user-message framing — are byte-identical to before. **Desktop (`desktop/src/features/agents/ui/`)** - `agentSessionTranscript.ts`: the `session/new` extractor now checks `params._meta.systemPrompt.append` as a fallback when bare `params.systemPrompt` is absent. Bare field takes precedence. Net line count stays at 1173 (ratchet limit). - `agentSessionTranscript.test.mjs`: two new tests — one verifying the `_meta` transport produces the identical standalone card (same five sections, same `turnId: null`, same placement before the first turn) as the bare-field transport; one proving bare field wins when both transports are present. ## Gate claim `@agentclientprotocol/claude-agent-acp` implies `_meta.systemPrompt` support because the feature landed in v0.6.0 (Oct 2025, commit `ea796f3`) before the `@zed-industries/claude-code-acp` → `@agentclientprotocol/claude-agent-acp` package rename (Mar 2026, commit `b409782`). The new name is therefore a reliable capability gate; the old name falls through to the protocol-version gate (status quo, no regression). ## Tests - Rust: Claude append serialization; `_meta` coexistence with `sessionTitle`; protocol-v2 bare field byte-identical; codex/old-zed omission; claude-name support/suppression gate; old `@zed-industries` name falls through to protocol-version gate. - Desktop: `_meta` transport → identical standalone card; bare field wins over `_meta` when both present. ## Pre-existing failures `just mobile-check` and `just mobile-test` fail identically on clean `origin/main` (5 `compose_bar` / `channels_page` tests + 3 Flutter lint warnings) — not caused by this change. All other `just ci` jobs are green. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
318fbf896e |
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> |
||
|
|
f86cfc7369 |
fix(desktop): back/forward via keyboard chords, mouse X1/X2 buttons, and swipe gestures (#3778)
## Problem Two related gaps in global back/forward navigation. Fixes #3775. 1. The keyboard shortcuts almost never fire in real use — users fall back to clicking the toolbar chevrons and assume the shortcuts don't exist. 2. On macOS, mouse back/forward buttons (X1/X2) and horizontal swipe gestures do nothing, although they navigate in every browser and in Slack. **Duplicate check:** searched open PRs and issues — none found beyond #3775 (filed alongside this fix). #3078 / #3377 are next/previous-*channel* navigation, a different feature. ## Root causes **Keyboard:** `useBackForwardControls`'s keydown handler bailed whenever the event target was editable — but `useComposerAutofocus` deliberately focuses the message composer (a ProseMirror contenteditable) on mount and on every channel switch. In steady state focus almost always lives in the composer, so the chords were silently swallowed. Invisible to CI because `navigation.spec.ts` only ever clicked the `global-back` / `global-forward` buttons, never pressed the keys. **Mouse/swipe:** on macOS, WKWebView never delivers X1/X2 button events or swipe gestures to the page (Safari handles them natively in the app layer, not in page JS), and Buzz had no native handler. ## Fix ### Keyboard chords (web layer) Match the existing platform chord regardless of the event target and drop the editable-target guard: - `⌘[` / `⌘]` have no text-editing semantics in macOS text fields, and the TipTap/StarterKit editor config binds no `Mod-[` / `Mod-]` shortcuts (checked `useRichTextEditor.ts` — list indentation is Tab/Shift-Tab). - `preventDefault()` keeps the chord out of the editor — asserted in the e2e test. This matches browsers and Slack, where back/forward chords work while a text field is focused. Chord matching is extracted into a pure helper, `app/navigation/backForwardChords.ts`, so it can be unit tested; behavior (bindings, modifier exclusivity, `code`-based matching for non-US layouts) is unchanged. ### macOS mouse buttons and swipe gestures (native layer) An NSEvent local monitor in `mouse_nav.rs` catches what the webview can't see and emits a `mouse-nav` Tauri event to the main window (`emit_to`, so navigation stays scoped if multi-window ever lands) that the frontend acts on. Two AppKit event shapes map to navigation: - `otherMouseUp` with button 3/4 — mice whose X1/X2 buttons arrive as plain button events. These are swallowed after emitting so nothing downstream double-handles them. - `swipe` with a horizontal delta — AppKit's page-swipe gesture (`swipeWithEvent:`): `deltaX > 0` back, `deltaX < 0` forward. Sent by mouse drivers that synthesize a page-swipe gesture for the back/forward buttons instead of button-3/4 events (the hardware this was verified on). Stock Apple trackpad and Magic Mouse swipes arrive as phased scroll-wheel events instead, which this PR does not handle — that path (`ScrollWheel` + `trackSwipeEventWithOptions:`, which also needs scroll-edge detection) is deferred to a follow-up. Swipes are passed through (swallowing mid-gesture events could confuse AppKit gesture tracking). The swipe path was verified end to end on hardware whose back/forward buttons emit only swipe gestures, never button-3/4 events — an instrumented event monitor confirmed the events arrive as `NSEventType::Swipe` with `deltaX ±1`, and navigation worked after mapping them. ## Tests - **13 unit tests** for the web-side chord matcher (`backForwardChords.test.mjs`): supported chords, modifier exclusivity, `code` fallback, and preservation of line-editing shortcuts. - **6 Rust unit tests** for the native mapping helpers (`mouse_nav.rs`): button 3/4 directions, other buttons ignored, swipe delta sign → direction, zero-delta (gesture-begin) ignored. - **e2e regression case** in `navigation.spec.ts`: presses the platform chord *while the composer is focused* — the missing coverage. Verified it fails against the pre-fix implementation and passes with the fix. - Full desktop unit suite: 3832/3832 pass. Full Rust suite (`cargo test`, buzz-desktop): 1888 passed / 0 failed. `pnpm typecheck`, `biome check`, `pnpm check`, `cargo fmt --check`, `cargo clippy`: clean (no new warnings). - Full Playwright e2e: 958 passed; 6 failures are relay-infrastructure tests (live relay seeding / relay state seam) that fail identically without this change — `navigation.spec.ts` is fully green. ## Manual test 1. Open a channel, then another (composer autofocuses on each switch). 2. `⌘[` — returns to the previous channel; `⌘]` — forward again. Typing `[` / `]` in the composer inserts normally. 3. Mouse back/forward buttons navigate the same way, from anywhere in the window (verified on macOS on hardware using both event shapes). ## Update — 2026-07-31 Removed the redundant DOM mouse-button handler after verifying it was unnecessary. The native macOS path remains unchanged and was revalidated manually. --------- Signed-off-by: npub1yvnq5equak5errqpku8stskushny9wsvt0fc2ywcpwt79yslwaqswe7tse <23260a641ceda9918c01b70f05c2dc85e642ba0c5bd38511d80b97e2921f7741@buzz.block.builderlab.xyz> Signed-off-by: Matheus Iser <matheusiser@squareup.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1yvnq5equak5errqpku8stskushny9wsvt0fc2ywcpwt79yslwaqswe7tse <23260a641ceda9918c01b70f05c2dc85e642ba0c5bd38511d80b97e2921f7741@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
6530b58a61 |
feat(k8s): Kubernetes backend plugin + desktop deploy path (#4289)
# Kubernetes backend plugin (crates/buzz-backend-kubernetes) + desktop deploy path Implements docs/remote-agents.md (merged @ |
||
|
|
fc598f5f8d |
fix(git): allow deleting the default branch (#4297)
Tal here, human. Trying to help. This bug bugged me... ## Summary A repository's first branch becomes its symbolic `HEAD`, and Git's bare-repository default rejects deleting that branch even when another branch survives. This change: - sets `receive.denyDeleteCurrent=ignore` only for the ephemeral `git receive-pack` process - preserves the existing server-side `core.hooksPath` override and authorization hook - lets the existing CAS publication logic select a surviving branch as the next manifest `HEAD` - adds regression coverage using a real stateless `git receive-pack` request and a manifest HEAD-selection test This lets users replace an accidental default branch without deleting the object-storage manifest pointer. ### Related issue Fixes #3572 ### Testing - `cargo test -p buzz-relay api::git::` (128 passed, 5 ignored) - `just ci` - live E2E roundtrip against a release relay with PostgreSQL, Redis, and MinIO: - created a repository through signed Nostr events - verified authorized pushes and rejected unauthorized clone/push - pushed a surviving `master` branch - deleted the active `main` branch over authenticated Smart HTTP - freshly cloned the repository and verified `master` became HEAD, `origin/main` was absent, and repository content remained intact Signed-off-by: Tal Weiss <major.tal@gmail.com> |
||
|
|
b7bb15122e |
feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) (#4020)
Implements the `buzz projects` command group — the NIP-MP Phase 2 write path for kind:30621 multi-repo projects. The relay accepted kind:30621 in #3171; this adds the two-layer Rust builder in `buzz-sdk` and the seven CLI commands. ## What this adds ### `crates/buzz-sdk/src/builders.rs` — two-layer builder **Layer A (protocol):** - `validate_project_envelope(tags, content)` — 8 NIP-MP rules in relay order: `d`-cardinality, `d`-empty/length, member-cap (≤64 `a` tags, checked before per-tag parse), member-tag-arity (2–3 elements), member-coordinate grammar (first-two-colons split, literal `30617`, lowercase 64-hex owner, non-empty remainder), member-duplicate (coordinate only, hint ignored), singleton metadata cardinality, byte bounds (`name` ≤256 / `description` ≤2048 / `buzz-channel` ≤256 / `buzz-visibility` ≤256). - `build_project_with_tags(content, tags)` — raw Layer A builder; RMW mutations path. - `ProjectMemberCoord` — `30617:<owner-hex>:<repo-d>` + optional opaque relay hint; equality/Hash by coordinate only. **Layer B (writer policy):** - `build_project(slug, name, description, members, channel, visibility)` — constructs `d` tag, enforces UUID channel and `listed|unlisted` visibility, forces empty content; composes onto Layer A. This is the `create` path. **Shared:** - `build_delete_addressable(kind, pubkey, d)` — generic NIP-09 kind:5 coordinate delete; `build_workflow_delete` now delegates to this. - All 31 `NIP-MP.fixtures.json` cases exercised through `build_project_with_tags`; count assertion guards against omissions. ### `crates/buzz-cli/` — seven commands ``` buzz projects create <slug> --repo <coord> [--name] [--description] [--channel <uuid>] [--visibility listed|unlisted] buzz projects get <slug> [--owner <pubkey>] buzz projects list [--owner <pubkey>] [--limit <n>] buzz projects add-repo <slug> --repo <coord> [--repo <coord>]... buzz projects remove-repo <slug> --repo <coord> [--repo <coord>]... buzz projects update <slug> [--name|--clear-name] [--description|--clear-description] [--channel <uuid>|--clear-channel] [--visibility listed|unlisted|--clear-visibility] buzz projects delete <slug> ``` Command semantics: - **`create`**: all local validation (slug, repos, channel, visibility, name length) fires before the collision preflight — invalid input returns `Usage` without a network call. Routes through Layer B (`build_project`). - **`update`**: at least one setter/clearer required — enforced by a clap `ArgGroup` with `required(true).multiple(true)`, with a runtime backstop for programmatic callers; setter + own clearer are mutually exclusive per clap conflicts. - **`add-repo`/`remove-repo`**: coordinate expansion and dedup fire before head fetch — malformed or duplicate `--repo` values return `Usage` without touching the relay. - **`delete`**: head-based tombstone at `created_at = head + 1`; post-submit re-query verifies tombstone landed. - All mutations: strip `auth`, re-validate full envelope through Layer A; `created_at` advances from observed head, never wall-clock. - Relay hints on existing member tags preserved verbatim through RMW. ## Limitations (recorded, not in scope) - **No relay-hint authoring**: `--repo` carries a coordinate only; existing hinted `a` tags survive RMW unchanged. - **Signer-self delete only**: NIP-OA owner-delete extension not exposed; `delete` targets the signer's own coordinate. - **Deletion durability**: watermark carry-over applies; `delete` is best-effort against a later-arriving replacement. ## Live round-trip 21-step transcript executed against a relay built from `origin/main` `b1b283cd4`, covering create, get, multi-field update (name + description + channel in one call), channel set/clear, add-repo, remove-repo, delete (tombstone verified at `head+1`, repeated delete → `NotFound`). Delta transcript confirmed multi-field update, channel set/clear, no-op add-repo → `Conflict` exit 5, empty update and setter+own-clearer both rejected at parse time. Duplicate create → `Conflict`. Cross-owner `add-repo` with full coordinate exercised. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
28ae6cd217 |
docs: formal spec for remote agents and their management (#3748)
## What A formal specification for remote agents and their management — `docs/remote-agents.md` — in the style of `docs/git-on-object-storage.md`: stated system model, named invariants, explicit trust boundaries, provider conformance checklist, and an implementation-correspondence table. Requested by Tyler in the buzz-remote-agents design thread; co-designed with Dawn and Wren (review pending). ## Structure - **System model** — five principals (Desktop / Provider / Substrate / Agent / Relay) and the design axiom **M1: no management channel** — everything the desktop knows about a live remote agent flows through the relay. - **Five invariants** with enforcement mechanism and stated boundary: - I1 identity fail-closed, I2 no secrets in configuration, I3 presence-is-status, I4 at-most-one-live-instance, I5 bounded lifetime. - **Provider protocol** — discovery, `info`/`deploy` wire contract, untrusted-output rules, the reserved-key rule, and the **deploy state machine** (Running → no-op). - **Auto-stop** — `--exit-after-inactivity` / `BUZZ_ACP_EXIT_AFTER_INACTIVITY`, default off, definition of "inactive", and why it must not share a name with the three existing timeout concepts. - **The Kubernetes binding** — `buzz-backend-kubernetes`: kubeconfig-only auth, random-default namespace via schema `default`, the sprig image, pod shape (bare Pod, `terminationGracePeriodSeconds: 60`, 32-hex label / full-pubkey annotation), secrets, GC, config budget. - **Known defects** at `c1bca1b56` (Windows `.exe` id pollution; provider env inheritance vs kubeconfig exec plugins). - **Open decisions A–E** marked inline and consolidated, awaiting owner ruling. ## Notes for review Docs-only. Every code claim was verified against the tree (correspondence table maps each spec concept to its file/function). The spec deliberately documents two desktop bugs as Known Defects rather than fixing them here — fixes are follow-up PRs. --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
89bf03c05d |
fix(nip-oa): accept raw Nostr tag form in parse_json_array (#4203)
## What `BUZZ_AUTH_TAG` stored in the **raw Nostr tag form** `[auth,hex,,hex]` (unquoted, comma-delimited — how an `auth` tag serializes inside a Nostr event and how `.env` files commonly store it) was rejected by the CLI: ``` BUZZ_AUTH_TAG is malformed: invalid JSON: expected value at line 1 column 2 ``` …and even when the CLI *could* parse it, it forwarded the raw string as the `x-auth-tag` header, so the relay's `verify_auth_tag` (which expects JSON) rejected it with `403 relay_membership_required`. Two commits close both gaps. ## Commits ### 1. `fix(nip-oa): accept raw Nostr tag form in parse_json_array` `parse_json_array` (`crates/buzz-sdk/src/nip_oa.rs`) only accepted well-formed JSON arrays. Added a fallback: when strict JSON parsing fails *and* the trimmed input is bracket-delimited, split on `,` and treat each field as a string (empty field `,,` → empty string, matching `["auth","hex","","hex"]`). All consumers (`parse_auth_tag`, `verify_auth_tag`, the CLI, `buzz-acp`) benefit from one change at the lowest layer. ### 2. `fix(cli): canonicalize BUZZ_AUTH_TAG to JSON before sending x-auth-tag header` The CLI stored the raw input string and sent it verbatim as the `x-auth-tag` header (`client.rs:618`). Added `canonicalize_auth_tag` in `buzz-sdk`: parse either form, re-serialize to canonical JSON. The CLI now canonicalizes before storing as `auth_tag_json`, so the header is always valid JSON regardless of input form. Together: local parse + wire canonicalization means the raw form works end-to-end. ## Why The raw form `[auth,hex,,hex]` is exactly how an `auth` tag serializes inside a Nostr event. That shape leaks into `.env` files and shell variables because there's no canonical "stored form" outside an event. The SDK + CLI should accept it rather than push quoting/conversion logic onto every consumer (harnesses, agent shells, external tools). ## Security Both changes are purely syntactic — they only change how a 4-element string array is extracted and containerized. All downstream validation is unchanged: - `parse_auth_tag`: still checks exactly 4 elements, `"auth"` label, 64-char lowercase-hex pubkey, 128-char signature. - `verify_auth_tag`: still reconstructs the preimage and verifies the BIP-340 Schnorr signature against the owner pubkey. No new attack surface — a malformed or forged tag is still rejected at the same validation points. ## Tests 4 new tests in `nip_oa::tests`: - `test_parse_auth_tag_raw_nostr_form` — raw form with conditions + empty conditions - `test_parse_auth_tag_raw_form_with_whitespace` — raw form with surrounding whitespace - `test_canonicalize_auth_tag_raw_to_json` — raw→JSON and JSON→JSON normalization All 25 `nip_oa` tests pass (21 existing + 4 new). `cargo fmt --check` and `cargo clippy -p buzz-sdk -p buzz-cli` clean. ## Verification Confirmed end-to-end against a live community relay (`wss://hermesagent.communities.buzz.xyz`): - **Before:** raw `BUZZ_AUTH_TAG` → CLI parse error, or `403 relay_membership_required` if somehow parsed. - **After:** raw `BUZZ_AUTH_TAG` → CLI parses it, canonicalizes to JSON for the header, relay accepts via NIP-OA owner delegation, `buzz channels members` returns the full roster. ## Context Originated from a community investigation where agent-side relay access was failing because the harness-exported `BUZZ_AUTH_TAG` (raw Nostr form) was rejected by the CLI (expecting JSON). This removes the impedance mismatch at the source. --------- Signed-off-by: amanning3390 <adam.manning@pro-serveinc.com> Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
ac4fa13b8e |
perf(relay): serve relay-membership checks from the read replica (#4124)
## Summary Route `Db::is_relay_member` — the membership check that runs on every authenticated HTTP request and WS AUTH — through the standard `route_read` machinery on the bounded arm, instead of adding a bespoke cache (replaces #3844). - `crates/buzz-db/src/relay_members.rs`: add `is_relay_member_on(&mut PgConnection, ...)` executor seam; the pool version delegates to it. - `crates/buzz-db/src/lib.rs`: `Db::is_relay_member` now routes via `route_read("relay_membership", RoutePredicate::Bounded)` — replica only on a proved fresh session, writer on any route rejection, writer re-run on replica query error. Exactly the shape of every other routed read. This is the one permission read served from the replica, by explicit product decision (Tyler accepted ≤1s bounded staleness on reads we choose): the fleet-wide fence guarantee (`BUZZ_REPLICA_READ_MAX_AGE_MS`, deploy target 1s) is an order of magnitude tighter than the 10s TTL proposed in #3844 and needs no invalidation machinery. Staleness is symmetric for admits and revokes. `BUZZ_REPLICA_READ_MAX_AGE_MS` unset = writer-only = kill switch. It is not precedent for routing other permission reads. ## Validation At this exact commit (`git rev-parse HEAD` confirmed in the same shell, rustc 1.95): - `cargo test -p buzz-db` — 94 passed, 0 failed - PG-gated suite single-threaded — **151 passed, 2 failed**; the 2 failures are the per-owner-limit tests broken on main by #3829 (limit 3→5, tests still seed 3) — they fail identically at base `19d57b0d4` in a pristine control checkout; separate trivial fix to follow - New PG-gated test `is_relay_member_is_bounded_routed_and_fails_closed` — divergent writer/replica fixtures prove: budget unset ⇒ writer; budget set + fresh proof ⇒ replica; over-budget entry ⇒ writer - clippy `-D warnings` + fmt clean; pre-push hooks green (desktop check/test, rust tests, tauri checks) - **Live-local pass** (TESTING.md, release binary, `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`, fresh DB): - writer-only (no `READ_DATABASE_URL`): member accepted, outsider 403 `relay_membership_required`; metrics `route_decision{path="relay_membership",decision="writer",reason="disabled"}` - replica configured + `BUZZ_REPLICA_READ_MAX_AGE_MS=1000`: member accepted / outsider denied via `decision="replica",reason="fresh"`; admit visible to the routed check within ~1.2s; revoke enforced within ~1.2s - reader outage mid-flight (TCP proxy killed): member send still succeeds in <200ms via `decision="writer",reason="reader_acquire_timeout"`; outsider still denied — fails closed, no availability loss Reviewed by Wren: 9/10 minimalness, 9/10 elegance, 9.5/10 correctness at this SHA. Supersedes the 10s-cache approach in PR 3844, which should be closed unmerged once this lands. Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
9d6726e5b3 |
chore(deps): bump nostr-relay-pool for RUSTSEC-2026-0224 (#4139)
Bump `nostr-relay-pool` from 0.44.1 to 0.44.2 to clear [RUSTSEC-2026-0224](https://rustsec.org/advisories/RUSTSEC-2026-0224), which addresses verification-cache poisoning that could let forged Nostr events bypass signature validation on redelivery. The dependency is transitive through `nostr-sdk`; this PR updates only the corresponding package version and checksum in `Cargo.lock`. The advisory currently marks every open PR red until this fix merges. - `cargo test -p buzz-sdk -p buzz-cli` passes: 271 + 241 tests - `cargo deny check advisories` passes - `just fmt-check` passes Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> |
||
|
|
756dd7f65d |
docs(nostr): document #h requirement for live reaction subscriptions (#3487)
## What this fixes
`fan_out_scoped` (`crates/buzz-relay/src/subscription.rs:278-394`)
enforces a deliberate, symmetric scoping invariant — documented in the
code itself:
> Global subscriptions (channel_id = None) do NOT receive channel-scoped
events. Channel-scoped subscriptions do NOT receive global events.
The relay derives a reaction's stored channel from its `#e` target at
ingest — client-supplied `#h` is ignored for channel determination
(`NOSTR.md:50` documents this for *writing*). The consequence for
*reading* is that every reaction is a channel-scoped event, so a live
subscription `{"kinds":[7]}` without `#h` is a global subscription and
**silently receives no reactions at all** — no error, no CLOSED, just
nothing. The working form is `{"kinds":[7],"#h":["<channel-uuid>"]}`,
and it works regardless of how the reaction was signed: explicit `h`
tags on the event are matched directly, and tagless reactions match via
the stored channel fallback (`crates/buzz-core/src/filter.rs:78-91` —
fallback applies only when the event has no `h` tags; explicit tags are
authoritative).
`NOSTR.md` already documents this exact pitfall for group-metadata
events:
> **Note:** Channel-scoped storage means live global subscriptions
(`{kinds:[39000]}`) won't receive these via fan-out.
(`NOSTR.md:124-126`)
…but has no equivalent note for reactions, which is the case a
bot/integration author is far more likely to hit: any client that wants
to observe approvals/reactions live (workflow reaction-triggers make
this a first-class pattern in Buzz) will naturally try a kinds-only REQ
first and conclude reactions are broken. We lost real debugging time to
exactly this while building a headless integration
(https://github.com/OriginTrail/buzz-dkg-integration); the behavior is
by design, only the docs are missing.
## What this PR changes
Docs only (`NOSTR.md`): a subscribe-to-reactions example in "Sending
Messages", plus one note mirroring the existing 39000 note. No code
changes.
## How to verify
- Behavior: with the relay running, open a live REQ `{"kinds":[7]}` (no
`#h`) and react to a channel message from another client → nothing is
delivered; re-subscribe with `{"kinds":[7],"#h":["<channel-uuid>"]}` →
the reaction arrives.
- Claims against code (verified at `485d03a`): scoping invariant
`crates/buzz-relay/src/subscription.rs:386-393`; channel derivation
`derive_reaction_channel()` in
`crates/buzz-relay/src/handlers/ingest.rs`; `#h` fallback
`crates/buzz-core/src/filter.rs:78-91` and its test
`h_tag_fallback_uses_stored_channel_id`.
Duplicate search: no existing issue/PR found for `reactions
subscription`, `fan-out kinds` (searched 2026-07-29). DCO signed-off.
---------
Signed-off-by: Žiga Drev <ziga.drev@gmail.com>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: Žiga Drev <ziga.drev@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
|
||
|
|
36cf932ff0 |
docs(chart): fix ArgoCD example for native OCI sources (full artifact repoURL + path) (#3426)
## Problem `examples/argocd-app.yaml` uses the split form: ```yaml repoURL: oci://ghcr.io/block/buzz/charts chart: buzz targetRevision: 0.1.0 ``` On ArgoCD >= 3.0 (native OCI sources), the `chart` field is **ignored** for `oci://` repoURLs, so ArgoCD tries to pull the `charts` path itself and fails with `403 … repository:block/buzz/charts:pull denied` — a misleading error that reads like an auth problem. Additionally, spec validation rejects the Application without a `path` (`spec.source.repoURL and either spec.source.path or spec.source.chart are required`), since `chart` isn't recognized for OCI. Hit both on ArgoCD 3.4.4 following the example verbatim. ## Fix Use the full chart artifact path as `repoURL`, add `path: "."`, bump the pinned example version to the latest published chart (0.1.6), and leave a comment explaining both traps: ```yaml repoURL: oci://ghcr.io/block/buzz/charts/buzz path: . targetRevision: 0.1.6 ``` Verified working in production (ArgoCD 3.4.4, anonymous GHCR pull, chart 0.1.6). Related open PRs/issues: none found. --------- Signed-off-by: Kampe <blindside328@gmail.com> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Kampe <blindside328@gmail.com> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> |
||
|
|
8d5afb6067 |
docs(readme): clarify which release asset to download per platform (#3481)
## Summary The "I just want to try the app" section names platforms generically (macOS `.dmg`, Linux `.AppImage` / `.deb`, Windows `.exe`), but the release publishes five assets, including two separate macOS builds. A first-time user on a Mac has no way to tell whether they need `aarch64` or `x64`, and nothing sets expectations for the SmartScreen warning on the unsigned Windows build. This replaces that sentence with a platform-to-filename table, a one-line note on how to check which Mac you have, and a note that the Windows build is unsigned and what the warning looks like. Filenames use `<version>` rather than `0.5.0` so the table doesn't go stale each release. ### Related issue None found. Searched open issues and PRs for README/download/install topics. ### Testing Docs-only change, no code paths touched. Verified the table and paragraph breaks render correctly in GitHub's markdown preview. --------- Signed-off-by: Dan Sheehan <dannysheehan90@gmail.com> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |
||
|
|
5765fc74b7 |
fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) (#3998)
## Problem The desktop deliberately shows the workspace icon editor on open relays (#2640, gate: `canEditIcon` in `desktop/src/features/communities/ui/EditCommunityDialog.tsx`) and defers to the relay-side kind:9033 check — which required an admin/owner row in `relay_members`. For a community with **no admin/owner row at all** (the `ensure_configured_community` path, which never writes an owner), every 9033 was refused and the icon was permanently unsettable. **Correction from review (thanks @Dawn):** the original version of this PR claimed nobody holds a role on an open relay. That's false — `main.rs` bootstraps `RELAY_OWNER_PUBKEY` as owner regardless of `BUZZ_REQUIRE_RELAY_MEMBERSHIP`, so a production open relay like bb-block *does* have an owner row, and the old gate was refusing everyone except that owner. The first revision of this diff would have silently widened that owner-only control to any NIP-42-authenticated sender. ## Fix — steward-wins `may_set_workspace_profile(sender_role, membership_enforced, community_has_steward)`: | Relay mode | Community has admin/owner row? | Who may set the icon | |---|---|---| | Closed (`require_relay_membership=true`) | any | admin or owner (unchanged) | | Open | yes (e.g. bb-block) | admin or owner (unchanged posture) | | Open | no (genuinely rosterless) | any NIP-42-authenticated sender | - New DB helper `has_admin_or_owner(community)` (`crates/buzz-db/src/relay_members.rs`); the call site only queries it on open relays. - The rosterless admit logs a `warn!` with the sender pubkey — 9033 writes no audit row and publishes no announcement event (unlike 9030/9031), so this is the only durable attribution. - Kinds 9030–9032, NIP-42 auth, `AdminUsers` scope, ban gate, and icon validation are all untouched. - Doc comment fixed: cited nonexistent `canEditCommunityProfile`; real symbol is `canEditIcon`. ## Test coverage — closing the mutation gap Dawn's mutation testing showed the original unit tests pinned only the helper's truth table: inverting the flag at the call site or deleting the gate entirely survived the full suite. - Unit tests now cover the 3-arg truth table (closed steward-independent, open-with-steward stays steward-only, rosterless-open admits). - Two `#[ignore]`d Postgres integration tests drive `handle_relay_admin_event` with a real `AppState` (open rosterless admit → steward appears → roleless refused again; closed relay member refused). Wired into the Backend Integration CI job as a dedicated nextest step. - **Both of Dawn's mutants verified killed** at this head: flag inversion fails 1 unit test; gate deletion fails both integration tests (`Ok(())` where `Rejected` expected). ## CI wrinkle found and fixed: pre-existing schema drift The first Backend Integration run of the new 9033 tests failed with `column "icon" of relation "communities" does not exist` — migration `0003_community_icon.sql` added the column, but `schema/schema.sql` (the desired-state file that CI job applies via pgschema) was never updated. Pre-existing drift, invisible until a test in that job actually wrote the column. Fixed in `297148f62` (3-line addition to `schema/schema.sql`). ## Receipts (at `1b4b52db8` code / `297148f62` head) - `cargo test -p buzz-relay`: 835 pass, 1 fail — `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo`, pre-existing (fails identically at the old base and on clean main); `telemetry::trace_context_lookup_does_not_enable_callsites` is a known order-dependent flake, passes in isolation. - `cargo test -p buzz-db`: 94 pass. - Both ignored integration tests pass live against local Postgres. - `cargo fmt --all -- --check`: clean. - Live-local pass per TESTING.md at this head (release build, relay on :3199, real WS + NIP-42 via nak): - open rosterless: roleless key sets icon → NIP-11 serves it; `warn!` with sender pubkey in the relay log - open + owner row inserted: fresh roleless key refused ("must be admin or owner"); owner sets icon - closed relay (owner bootstrapped, `BUZZ_RELAY_PRIVATE_KEY` set): plain member refused, owner sets icon, `javascript:` URL rejected, empty icon clears (NIP-11 → null) --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
3d7712cc36 |
docs: note that addressable channel events scope by d, not h (#4103)
The channel scoping note in `AGENTS.md` reads as universal: > **Channel scoping**: Channels use `h` tags (NIP-29 group tag), not `e` tags. > Filters and queries must scope to `h` tags when operating within a channel. It holds for events inside a channel, but not for the addressable events that describe one. kind:39000, kind:39001 and kind:39002 carry the channel id in their `d` tag, which is what `get_channels` already reads. Taking the existing wording at face value while working on kind:39002 produces an empty result rather than an error, since those events do carry `h` tags in other flows, so the mistake is quiet and costs a debugging cycle. Came up while working on #4023. Four lines, no behaviour change. Signed-off-by: Szymon Tanski <szymontanski8@gmail.com> |
||
|
|
909a3b2c31 |
docs: fix stale kind count, quick-start numbering, and empty Further Reading (#2613)
## Problem Three small documentation defects, each verified against the code at |
||
|
|
8e81afa431 |
fix(desktop): keep thread-open affordance in archived channels (#4012)
## Problem
Threaded replies "disappeared" from archived Buzz channels: the **"N
replies →"** summary row and the huddle-started **"View thread"** button
vanished, so existing threads were unreachable from the channel
timeline. The thread data was intact — this was a UI gate, not data
loss.
## Root cause
A single `onReply` prop drove two distinct affordances:
- the **compose** affordances (hover "Reply" button, inline reply
target), and
- the **view** affordances ("N replies →" summary row, huddle "View
thread").
`ChannelPane` nulls `onReply` on archived channels to keep them
read-only. That correctly hid composing — but also hid the view
affordances, since they keyed off the same prop.
## Fix
Two independent props, one per concern:
- **`onReply`** drives the compose affordances and is gated on
`archivedAt` — nulled on archived channels, so no new replies can be
started.
- **`onOpenThread`** drives the view affordances and is passed
regardless of archived state, threaded `ChannelPane → MessageTimeline →
TimelineMessageList → MessageRow`.
Opening a thread on an archived channel is read-only: the thread panel's
composer is independently gated via `isComposerDisabled` (includes
`archivedAt !== null`, `ChannelPane.tsx:318`).
### Before
<img width="811" height="794" alt="Screenshot 2026-07-31 at 20 26 00"
src="https://github.com/user-attachments/assets/670d9db4-30da-4c6d-97dc-275b5dbebca8"
/>
### After
<img width="873" height="791" alt="Screenshot 2026-07-31 at 20 28 04"
src="https://github.com/user-attachments/assets/88525231-2539-4eb3-8117-8e58a0cb3855"
/>
## Validation
- `pnpm typecheck` clean
- biome lint clean on touched files
- full `pnpm test` suite green (3885 tests)
- pre-push `branch-skew` / `desktop-check` / `desktop-test` hooks passed
Signed-off-by: Trey Wood <treyw@squareup.com>
Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e <addeb74792f4b7ef0126c3a7ae6d50a5e94fbedf1ad4e2c0620c771232587ec3@buzz.block.builderlab.xyz>
|
||
|
|
19d57b0d46 |
docs: add one-click Railway deploy for a hosted relay (#2733)
Adds a **"I want my own hosted relay"** path to *Getting started* with a one-click Railway deploy button. Buzz today asks anyone who wants a real relay to take the build-from-source route. This gives non-developers a hosted option: the template provisions the relay plus Postgres, Redis, and media storage, runs migrations, and generates the owner identity on first boot — no configuration. The listing is flagged **community-maintained, not an official Block build**, so there's no implied ownership. Happy to adjust wording, placement, or drop the button and keep just a link if you'd prefer. Template deploys green end-to-end; the owner key is surfaced as a paste-ready `nsec1…` in the deploy logs, and one deployment can host multiple communities by hostname. _Note: this supersedes the stale #984 — that template modeled a since-removed Typesense service and didn't run migrations._ ### Checklist `README.md` only, +8 −0 — no source files touched, so the build/test items don't apply. - [x] `just ci` passes (fmt + clippy + unit tests + mobile) — n/a, no code changed - [x] Integration tests pass (`just test`) — n/a, no code changed - [x] New public APIs / tools / endpoints are documented — none added - [x] No new `unwrap()` in production code paths - [x] No new `unsafe` blocks ### How to verify Click the button in the rendered README. The template stands up the relay with Postgres, Redis and media storage wired, runs migrations, and prints the owner key once in the deploy logs. Walkthrough with screenshots: https://hmseeb.github.io/buzz-railway --------- Signed-off-by: Haseeb Azhar <hsbazr@gmail.com> Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
3ade48d503 |
fix(desktop): point Oh My Pi preset at omp.sh (#3516)
## Summary Points the Oh My Pi preset at the `omp.sh` installation page instead of the GitHub repository. The project serves its current installer from `omp.sh/install.sh`. ### Related issue Extracted from the maintainer request in #3111. I found no matching open pull request in a final duplicate check. ### Testing `https://omp.sh/` returned HTTP 200 with the installation page. `https://omp.sh/install.sh` resolved to the current installer and returned HTTP 200. `cargo test --manifest-path desktop/src-tauri/Cargo.toml preset_entry -- --nocapture` passed 5 tests. `just ci` passed. This changes metadata only, so screenshots do not apply. Signed-off-by: Shreyash Vengurlekar <262980978+kiranmagic7@users.noreply.github.com> Co-authored-by: Shreyash Vengurlekar <262980978+kiranmagic7@users.noreply.github.com> |
||
|
|
fa1a5b1a79 |
fix(mesh): stop restarting a busy or loading shared-compute node (#3909)
## Problem Sharing compute with a large model (e.g. `gemma-4-26B`) put the desktop app into a **restart loop**: toggle Share → app appears to "download" / stall → the whole app restarts → repeat. Small models (E4B) were unaffected, which made it look model-specific and flaky. It is not model-specific and not flaky. It is a **false-positive liveness check**. ## Root cause (proven by black-box measurement) A `serve` node's OpenAI ingress (`:9337`) serializes **all** HTTP — including the `/v1/models` liveness probe — behind the current in-flight inference. It is *also* HTTP-unresponsive during model load and package-layer download. In every one of those phases the node is alive and progressing, but it cannot answer an HTTP probe. Measured on a standalone `gemma-4-26B` node (randomized ~30k-token prompt, cache-miss): | during one ~30s inference | result | |---|---| | concurrent `GET /v1/models` | **27.0s**, then 200 | | concurrent small `/chat/completions` | **28.8s**, then 200 | | `tcp_connect(:9337)` throughout | **~0ms** | Both HTTP calls simply queued behind the turn; TCP kept accepting instantly. A probe with any timeout shorter than the turn reads the node as dead. Buzz then acted on that false "dead" reading in two places, **both restart paths added in #2823**: 1. **Ingress watchdog** — after 2 consecutive `/v1/models` timeouts, evicts the node; for a serve node eviction means `app.request_restart()`. Two dead probes landing inside a prefill window → restart loop. 2. **Start / restore paths** — on a `wait_for_mesh_inference` timeout, `stop()` the node and (fresh start) `request_restart()` the app "to guarantee cleanup" — even though the node was still loading weights or downloading layers. This is the exact line in the incident log: `started node failed inference readiness … Buzz is restarting`. ## Fix Treat a **bound TCP port as alive**. Death has exactly one unambiguous signal: a *closed* port. - **Watchdog** (`recovery.rs`): only `PortClosed` may evict. A bound-but-HTTP-unresponsive `Unhealthy` port is never evicted, at any probe streak or urgency. Closed-port eviction is unchanged. - **Start / restore** (`commands/mesh_llm.rs`): install the runtime **before** probing readiness (so it is always tracked by `AppState` and can never be orphaned — which is what the restart was guarding against), and on a readiness timeout **leave it warming up** instead of stopping/restarting. Launch-restoration stays disarmed until real inference is confirmed, so a genuinely broken start is retried next launch rather than silently disabling Share Compute. ### What this deliberately does *not* do Detecting a node that is bound-but-internally-wedged needs a liveness signal that bypasses the inference lock. There is none today, so this fix cannot distinguish "wedged" from "busy" and errs toward not restarting. That gap is a mesh-llm bug, filed upstream: **Mesh-LLM/mesh-llm#1126** (lock-free `/live`+`/ready` on the ingress). A follow-up here can consume it once it lands. ## Tests - Watchdog never evicts a bound/busy port at any probe streak or urgency (the regression). - Closed-port eviction still fires (dead listener still reclaimed). - Black-box: a listener that accepts TCP then stalls HTTP classifies as `Unhealthy`, not `PortClosed`. - **Mutation-proven**: reverting the eviction rule to the old count-based logic fails the busy-node test. `cargo test` (desktop, `--features mesh-llm`) green, fmt + clippy clean. ## Not covered here The intermittent nature means I could not force the live loop deterministically on a warm machine; the proof is the measured serialization + the mutation-proven unit/black-box tests. Live behaviour (app no longer restarts while a 26B node loads/serves) still merits a manual check before merge. --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
45314fc504 |
fix(desktop): preserve first huddle speech (#3962)
## Context
On the first huddle after launching Buzz Desktop, a live agent reply can
arrive after agent membership is known but before the initial
TTS-enabled state has loaded. The subscription previously released
buffered messages at the membership boundary, so that first reply was
evaluated while speech was still disabled and was silently skipped.
Later replies worked, and later huddles usually worked because the state
was already warm.
## Summary
Hold initial live agent replies until both authoritative agent
membership and the initial TTS state are known. This preserves the first
eligible reply after a cold app launch without changing live-only
routing, ordering, or fail-closed behavior.
## Changes
- Replace the membership-only startup gate with a two-signal readiness
gate for membership and TTS state.
- Release buffered live messages in arrival order only after both
signals resolve.
- Drop buffered messages if either initial lookup fails.
- Add a deterministic regression for the observed ordering: membership
resolves first, TTS enables second, and the first reply is spoken.
## Related issue
None found.
## Testing
Manual validation in the daily-driver build confirmed that the first
agent reply is spoken in the first huddle after a fresh app launch.
The regression scenario was also run against both revisions:
```text
main: FAIL — actual spoken replies: []; expected: ["first agent reply"]
PR: PASS — 10 passed, 0 failed
```
## Screenshots
N/A, nonvisual speech behavior.
## Reviewer-reproducible examples
1. Quit Buzz Desktop completely.
2. Reopen it with Pocket TTS enabled.
3. Start the first huddle of the session with a running agent.
4. Send a prompt that produces a spoken agent reply immediately after
the huddle starts.
5. Confirm the first reply is spoken, not only the second reply.
6. Stop the huddle, start another one, and confirm subsequent huddles
retain the same behavior.
For a deterministic red/green check, run the same
membership-before-TTS-state ordering from `desktop/`.
On `main`:
```bash
node --import ./test-loader.mjs --experimental-strip-types --input-type=module -e '
import assert from "node:assert/strict";
import { createInitialMembershipGate, createOrderedSpeaker } from "./src/features/huddle/lib/ttsLiveMessages.ts";
const spoken = [];
const speaker = createOrderedSpeaker(async text => spoken.push(text), error => { throw error; }, false);
const gate = createInitialMembershipGate(text => speaker.enqueue(text, 1));
gate.push("first agent reply");
gate.succeed();
speaker.setEnabled(true);
await new Promise(resolve => setTimeout(resolve, 0));
console.log("spoken:", JSON.stringify(spoken));
assert.deepEqual(spoken, ["first agent reply"]);
'
```
Observed failure:
```text
spoken: []
AssertionError: Expected values to be strictly deep-equal
```
On this PR branch:
```bash
node --import ./test-loader.mjs --experimental-strip-types --input-type=module -e '
import assert from "node:assert/strict";
import { createInitialTtsReadinessGate, createOrderedSpeaker } from "./src/features/huddle/lib/ttsLiveMessages.ts";
const spoken = [];
const speaker = createOrderedSpeaker(async text => spoken.push(text), error => { throw error; }, false);
const gate = createInitialTtsReadinessGate(text => speaker.enqueue(text, 1));
gate.push("first agent reply");
gate.markMembershipKnown();
speaker.setEnabled(true);
gate.markTtsStateKnown();
await new Promise(resolve => setTimeout(resolve, 0));
console.log("spoken:", JSON.stringify(spoken));
assert.deepEqual(spoken, ["first agent reply"]);
'
```
Observed output:
```text
spoken: ["first agent reply"]
```
---------
Signed-off-by: John Tennant <jtennant@squareup.com>
|
||
|
|
eb049ddf81 |
feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock (#3278)
## Agent Trading Cards "Create Agent Card" action in the agent panel that mints an AI-generated trading card PNG which **is** the agent: the card carries the `buzz_agent_snapshot` tEXt chunk and is drag-in importable like any snapshot PNG. ### What's in here - **Mint pipeline (Rust):** one OpenAI Responses call — `gpt-5.6-sol` as card designer with `gpt-image-2` via the `image_generation` tool (~2–3 min). New `mint_agent_card` / `save_agent_card` commands; preview with reroll; save or send as `.agent.png` with round-trip verification before any bytes leave the app. - **Snapshot/chunk work stays in Rust,** reusing the existing encoder/decoder seams (byte-compat golden vector proves the plain path is identical to the pre-envelope encoder for placeholder, PNG-injection, and JPEG-transcode paths). - **Locked cards (NIP-44):** optional `buzz-agent-snapshot-encrypted` envelope encrypted to the (owner, agent) pair. `parse_canonical_pubkey` performs lift-x curve validation before any API spend; wrong-key decrypt returns a fixed refusal; the plain decoder refuses locked cards. - **Guardrails:** 10 MiB ceiling on final bytes, memory structurally `none` in the snapshot, full-manifest import disclosure, API-key hygiene via env layering (record > persona > global > process), fail-early validation ordering (all key/lock/NIP-44-cap checks before Responses spend). - **Import side:** full-manifest disclosure dialog, locked-card import disclosure, bounded avatar fetch. ### Review Code reviewed by Wren across the full arc; final locked-card cross-review **APPROVED 9/9/9** at exactly this head (`64f819dc8`), with independent same-SHA verification: Rust lib 1,843/1,843, clippy `--all-targets -D warnings`, desktop file-size gate. ### Live-mint evidence (real API, shipping seams, this SHA) - **Plain (Honey):** 188s, 1500x2250, 5,101,503 bytes (< 10 MiB); decoded manifest == built manifest; memory=none. - **Locked (Fizz):** 176s, 4,670,184 bytes; owner-key and agent-key decrypt both verified via logical manifest compare; wrong-key refusal exact; plain decoder refuses. - **Live finding:** built-in agents' ~171 KB inline avatars exceed the NIP-44 65,535-byte plaintext cap and the fail-early guard fires before API spend — clean error path, noted as a UX follow-up for large-avatar agents choosing lock. Full evidence (cards + dialog screenshots) posted in the originating thread. --------- Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |