Sync fork main with upstream Buzz - #3
Merged
Merged
Conversation
## Summary - require an exact-head trusted approval before desktop auto-tagging - remove rule-suite authorization that `GITHUB_TOKEN` cannot access - pin review pagination to `page=1` and test the deployed `gh` control flow ## Why The previous verifier unconditionally queried repository rule-suite endpoints with `github.token`. Those endpoints require Administration: read, which Actions `GITHUB_TOKEN` cannot receive. Its paginated list request also duplicated page one when no explicit page was supplied. This deliberately removes admin-bypass authorization rather than introducing a second credential during release recovery. Desktop release PRs must now have GitHub's overall `APPROVED` decision and a MEMBER/OWNER/COLLABORATOR approval attached to the exact candidate SHA. ## Validation - `scripts/test-desktop-release-authorization.sh` - `scripts/test-release-ref-contract.sh` - `bash -n scripts/verify-desktop-release-merge.sh scripts/verify-desktop-release-authorization.sh scripts/test-desktop-release-authorization.sh scripts/test-release-ref-contract.sh` - `git diff --check origin/main...HEAD` The new flow test uses a stub `gh` executable, asserts the exact `page=1` request, fails any rule-suite API call, and rejects stale-SHA, untrusted-author, changes-requested review, and non-approved aggregate-decision cases. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.3 - **Frozen main:** `54c8ef30a9bb9c59a4415a8a7ee84c7c5454b48a` - **Reviewed candidate:** `d0c06978bbf494ded6fe1a55d69d810ae9b65863` - **Previous desktop release:** `v0.5.2` - **Proposed immutable tag:** `desktop-v0.5.3` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current ; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary - escape the Markdown backticks around `main` in the desktop release PR body - prevent the shell from executing `main` as command substitution - lock the heredoc contract into the release-ref test ## Verification - `scripts/test-release-ref-contract.sh` - `bash -n scripts/prepare-desktop-release.sh scripts/test-release-ref-contract.sh` - `git diff --check origin/main...HEAD` This is a follow-up to the cosmetic PR-body issue observed on block#3972. It does not modify that frozen release candidate. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
) Buzz renders one card per `kind:30617`, so a project spanning several repositories has no representation. [NIP-MP](block#3163) defines `kind:30621` as an addressable container holding a group's name, description, channel binding, and member coordinates. This adds the kind to `buzz-core` and its structural validation to the relay ingest path. ## Event shape ```json { "kind": 30621, "tags": [ ["d", "platform"], ["name", "Platform"], ["description", "Relay, desktop, and mobile."], ["a", "30617:<owner-a-hex>:buzz"], ["a", "30617:<owner-b-hex>:buzz-infra"], ["buzz-channel", "<channel-uuid>"], ["buzz-visibility", "listed"] ] } ``` ## Validation at ingest | Rule | Behavior | |------|----------| | `d` tag | exactly one, non-empty (length already bounded by the generic `D_TAG_MAX_LEN` check) | | member `a` tag arity | exactly 2 or 3 elements per NIP-01's `a` tag grammar; a 4th element has no defined meaning and is rejected | | member `a` tag coordinate | must parse as `30617:<lowercase-64-hex-owner>:<non-empty-d>` | | duplicate members | rejected on exact string match of the canonical coordinate | | member cap | 64, counted over raw `a` tags | | metadata cardinality | at most one each of `name`, `description`, `buzz-channel`, `buzz-visibility` | | metadata length | `name` ≤ 256 bytes, `description` ≤ 2048 bytes, `buzz-channel` ≤ 256 bytes, `buzz-visibility` ≤ 256 bytes | | zero members | valid | | unknown tags | ignored | Rejection order is normative so a client can predict which rule fires: `d`-cardinality → `d`-empty → member-cap → member-arity → coordinate parse → member-duplicate → metadata cardinality → metadata length. ## Design notes **No membership authorization.** Members are `a` tags, so one project may name repositories owned by different pubkeys — the entire point of the kind. That is safe because membership grants nothing: push policy reads a repository's own `kind:30617` (`api/git/policy.rs`) and never a project. `buzz-channel` is a metadata reference, not a routing directive, so projects are classified global-only. **Owner-only editing is free.** NIP-33 addressing keys replacement on `(pubkey, kind, d)`, so one signer can never overwrite another's project. No relay-side permission check exists or is needed, and `test_project_same_d_under_two_authors_are_independent` pins it. **Duplicates are rejected, not deduped.** A relay cannot rewrite tags inside a signed event without invalidating its id and signature, so the alternative to rejection is a stored duplicate-member head that every consumer must apply a first-wins rule to. **The cap is checked before the duplicate set is built.** Counting raw `a` tags rather than distinct coordinates means an event naming one coordinate thousands of times is refused on count, instead of being bounded only by the relay frame limit. **No side-effect handler.** Generic NIP-33 replacement and generic NIP-09 coordinate soft-delete already cover replacement and deletion; `kind:30621` needs no entry in `is_side_effect_kind`. ## Generic NIP-09 fix carried along `soft_delete_by_coordinate` (`crates/buzz-db/src/event.rs`) previously deleted the live coordinate head regardless of the tombstone's own `created_at`, so a delayed or replayed `a`-tag deletion signed between two versions destroyed the newer replacement. NIP-09 scopes an `a`-tag deletion to versions at or before the deletion request, so the `UPDATE` now carries `created_at <= $5` and `handle_a_tag_deletion` threads the deletion event's `created_at` through. The bug predates `kind:30621` and affected every parameterized-replaceable kind on the generic path — `kind:30617` repository announcements included — so the fix lands there rather than as a project special case. `events.created_at` is immutable per row, so the predicate guarantees a tombstone can never erase a version newer than itself; the UPDATE re-evaluates its WHERE clause after any lock wait. Under READ COMMITTED, a same-coordinate replacement racing the deletion may cause the deletion to evaluate before the new head lands, returning `Ok(false)` — but that outcome is state-identical to the deletion having arrived first, a valid Nostr ordering Nostr never fixes. The return value feeds only a debug log. No coordinate-level lock is needed. ## Coverage 32 unit tests in `crates/buzz-relay/src/handlers/ingest.rs` pin the envelope contract (accept: minimal, cross-owner, zero-member, same repo `d` under two owners, colon-bearing repo `d`, cap boundary, unknown tags, relay hint on member `a` tag, max-length metadata, stranger-owned member, uninterpreted metadata values, non-empty content; reject: every rule above plus valueless `d`/`a` tags). A fixture-driven test (`project_envelope_validates_all_shared_fixtures`) runs every case in the shared `NIP-MP.fixtures.json` oracle (11 accept + 20 reject) against `validate_project_envelope`, so any future change that breaks a case turns the test suite red. 6 `#[ignore]`d e2e tests in `crates/buzz-test-client/tests/e2e_project.rs` cover behavior that only exists past storage — coordinate round-trip, newer-wins replacement, two authors sharing a `d`, an `a`-tag tombstone that removes the project while leaving referenced `kind:30617`s intact, and a tombstone timestamped between V1 and V2 that must leave V2 live. The negative e2e case asserts on the rejection message so a refusal for an unrelated reason cannot satisfy it; that is what proves the validator is reachable from the live write path rather than merely correct in isolation. The new e2e binary is wired into the Relay E2E job. The timestamp predicate is additionally pinned at the storage layer by `coordinate_delete_spares_head_newer_than_the_deletion` in `crates/buzz-db/src/lib.rs`, which asserts both directions: a stale tombstone deletes nothing and leaves the newer head readable, and a tombstone at the head's own timestamp still deletes it. This test is wired into the Backend Integration job. Related: block#3163 (the NIP-MP spec and shared conformance fixtures). Independent — either can merge first. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…block#3999) ## Problem `buzz-agent` measures and sends `accumulatedCachedInputTokens` on the wire (`usage.rs:93`). `buzz-acp` deserializes it correctly — but then drops it: `TurnUsage` had no cache field, and `build_turn_metric_counts` hardcoded `cache_read_tokens: None` and `cache_write_tokens: None` into both `turn` and `cumulative` `TokenCounts`. Every kind:44200 event published permanently lacked data the harness measured. The archive is append-only — this is unrecoverable data loss per turn, every turn, until fixed. NIP-AM already specifies the fields (`cacheReadTokens` / `cacheWriteTokens` inside `turn` and `cumulative`). This is a pure threading fix. ## Changes **`crates/buzz-acp/src/usage.rs`** - `SessionState` gains `last_cached_input: u64` to track the committed cache-read baseline. - `TurnUsage` gains `turn_cache_read_tokens: Option<u64>` (field-local; `None` when no baseline or counter decreased) and `cumulative_cache_read_tokens: u64` (always present; zero when no cache hits reported). - `record()` computes the cache-read delta with field-local taint semantics: a decrease in the cumulative counter nulls only `turn_cache_read_tokens` — it does not flip `delta_reliable` or invalidate `turn_input_tokens`/`turn_output_tokens`. Identical to the `accumulatedTotalTokens` pattern already present. - `take()` and the setup-notification branch both advance `last_cached_input` in the committed baseline. **`crates/buzz-acp/src/pool.rs`** - `build_turn_metric_counts` wires `turn_cache_read_tokens` into `turn.cache_read_tokens` (when `delta_reliable`) and `Some(cumulative_cache_read_tokens)` into `cumulative.cache_read_tokens`. - `cache_write_tokens` remains `None` on both counts with an explanatory comment: buzz-agent does not emit a write-side count on the wire today. - Six existing `TurnUsage` struct literals in tests updated with the two new fields. ## Tests **`usage.rs` — new cache-read section (5 tests):** - `cache_read_first_turn_produces_none_turn_delta_and_passes_cumulative_through` — no baseline → delta None, cumulative passes through - `cache_read_second_turn_delta_computed_correctly` — delta = current − previous - `cache_read_decrease_nulls_turn_cache_but_leaves_delta_reliable` — field-local taint: decrease nulls cache delta only, input/output stay reliable - `cache_read_zero_payload_after_baseline_produces_zero_delta` — zero on both sides → `Some(0)`, not `None` - `cache_read_threads_through_setup_notification_baseline` — setup notification baseline correctly seeds the cache counter **`pool.rs` — new acceptance test (1 test):** - `test_build_turn_metric_counts_cache_read_tokens_thread_through` — wire-parses a buzz-agent payload with nonzero `accumulatedCachedInputTokens`, runs two turns through the tracker and `build_turn_metric_counts`, and asserts nonzero `cacheReadTokens` in cumulative + correct per-turn delta in `turn`; also asserts `cache_write_tokens` is `None` throughout ## Quality gates at tip `c6405eb43f532572e3b7775e0dee826dc9cb3f82` | Gate | Result | |---|---| | `cargo test -p buzz-acp` | **655/655**, 0 failed | | `cargo clippy -p buzz-acp --all-targets -- -D warnings` | clean | | `cargo fmt --check` | clean | Note: the pre-push hook `mobile-test` gate fails on `origin/main` before this branch (Flutter test in `channels_page_test.dart` / `compose_bar_test.dart` — verified independently). My changes touch only `crates/buzz-acp/src/`; the mobile failure is unrelated and pre-existing. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
…s with optional NIP-44 lock (block#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>
## 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>
…ck#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 block#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>
## 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 block#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>
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 block#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>
…#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>
…Reading (block#2613) ## Problem Three small documentation defects, each verified against the code at 06e3d82: 1. **ARCHITECTURE.md (Event Kinds section)** says `buzz-core` defines "all 81 kinds". The registry has grown: `ALL_KINDS` in `crates/buzz-core/src/kind.rs` now has **127** entries (all unique values). The sentence also says every kind is `pub const KIND_*`, but registry entries such as `RELAY_ADMIN_ADD_MEMBER` do not use that prefix. 2. **NOSTR.md Quick Start** numbers its steps 1, 2, 3, 5 — there is no step 4. PR block#797 (2a03851) collapsed the old steps 1-4 (dropping the separate "Start infrastructure" step) into 1-3, but the final "Connect any NIP-29 + NIP-42 client" comment kept its old number 5. 3. **NOSTR.md "Further Reading"** is an empty heading — the section's only content (a link to `crates/buzz-proxy/README.md`) was removed in PR block#1321 (14fba21) along with the proxy crate itself, leaving a dangling header as the last line of the file. ## Fix 1. Reworded the ARCHITECTURE.md sentence to defer to `crates/buzz-core/src/kind.rs` as the source of truth, with the current count (127) as an explicit "at the time of writing" snapshot, so the sentence stays truthful as kinds are added. Also removed the incorrect `KIND_*`-naming claim. 2. Renumbered the final quick-start step 5 → 4. 3. Populated Further Reading with three durable links: the upstream nostr-protocol/nips repo, this repo's `docs/nips/` extension documents, and `ARCHITECTURE.md`. Docs-only; no code changes, no build impact. ## Verification (each claim ~30 seconds) - Kind count: `python3 -c "import re; s=open('crates/buzz-core/src/kind.rs').read(); m=re.search(r'ALL_KINDS: &\[u32\] = &\[(.*?)\];', s, re.S); print(len([e for e in m.group(1).split(',') if e.strip()]))"` → 127. All 127 values are distinct. Non-`KIND_*` entry example: `RELAY_ADMIN_ADD_MEMBER` (kind.rs, in `ALL_KINDS`). - Missing step: `grep -n '^# [0-9]' NOSTR.md` on main shows `# 1.`, `# 2.`, `# 3.`, `# 5.` in the Quick Start block; `git show 2a03851 -- NOSTR.md` shows the renumbering that orphaned step 5. - Empty section: `tail -1 NOSTR.md` on main is `## Further Reading` with nothing after it; `git log -S'buzz-proxy/README' --oneline -- NOSTR.md` shows the content removal in 14fba21 (block#1321). ## Links - `crates/buzz-core/src/kind.rs` — `ALL_KINDS` registry (source of truth for the count) - PR block#797 / 2a03851 — introduced the step-numbering gap - PR block#1321 / 14fba21 — emptied the Further Reading section Signed-off-by: Sean Gearin <sgearin@gmail.com> Co-authored-by: Sean Gearin <sgearin@gmail.com>
) 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 block#4023. Four lines, no behaviour change. Signed-off-by: Szymon Tanski <szymontanski8@gmail.com>
Signed-off-by: Cvv9 <Varun.cumbamangalam@oralens.com>
…d:9033) (block#3998) ## Problem The desktop deliberately shows the workspace icon editor on open relays (block#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>
…lock#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>
… repoURL + path) (block#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>
Cvv9
force-pushed
the
agent/sync-upstream-20260801
branch
3 times, most recently
from
August 1, 2026 16:31
28b8add to
1fdbabd
Compare
…lock#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>
Cvv9
force-pushed
the
agent/sync-upstream-20260801
branch
4 times, most recently
from
August 1, 2026 17:03
d7a1136 to
09aa4f2
Compare
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>
Cvv9
force-pushed
the
agent/sync-upstream-20260801
branch
from
August 1, 2026 17:17
09aa4f2 to
f2c03f5
Compare
Signed-off-by: Cvv9 <Varun.cumbamangalam@oralens.com>
Cvv9
force-pushed
the
agent/sync-upstream-20260801
branch
from
August 1, 2026 17:30
f2c03f5 to
5321959
Compare
…ck#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 block#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 block#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 block#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>
Signed-off-by: Cvv9 <Varun.cumbamangalam@oralens.com>
Signed-off-by: Cvv9 <Varun.cumbamangalam@oralens.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
block/buzzupstream/maininto the maintained Cvv9 forkWhy
The fork intentionally carries downstream changes and is synchronized through merge commits. Resetting it to upstream would discard those changes; merging upstream preserves both histories.
Validation
mainrequires pull requestsThis PR contains only the upstream synchronization merge.