Merge Buzz Desktop v0.5.4 - #4
Merged
Merged
Conversation
## Summary - Keep the Agents header full width while cards reflow independently. - Collapse header actions into an overflow menu at the compact layout threshold. - Apply the same responsive grid rules to Agent Teams. ## Validation - `pnpm -C desktop build:e2e` - Focused Agents Playwright coverage - Pre-push desktop checks and unit tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
Microphone/camera capture works on macOS (WKWebView) and Windows (WebView2) but fails on Linux with `NotAllowedError`. WebKitGTK ships with `enable-media-stream` off and a default `permission-request` handler that denies every request. This reaches the underlying `webkit2gtk::WebView` from `on_webview_ready` and enables `enable-media-stream`, then installs a **deny-by-default** `permission-request` handler: a `UserMedia` request is allowed only from a trusted app origin (`tauri://localhost` in prod, the Vite dev origin in debug) **and** when it targets an audio/video device — everything else is denied. No-op on macOS/Windows. - `webkit2gtk` is pinned to the version wry already uses (`=2.0.2`) so there's a single shared copy of the native binding. --------- Signed-off-by: Beckley <mattcbeckley@gmail.com>
## Summary - Refine the agent share dialog around recipient sharing, link copying, catalog sharing, and export. - Show memory settings only when a linked agent has memories to include. - Use a catalog toggle for custom agents and keep built-in agents out of the catalog flow. ## Validation - `pnpm typecheck` - Focused Playwright share and catalog flows --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-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 — the relay, desktop app, and mobile app look like three unrelated things. This adds the spec for the container event that fixes that, plus the two shared fixture files that make it machine-checkable. Docs only; no code changes. Membership cannot live in the repository announcements themselves. A project spanning Alice's and Bob's repositories would need *both* of them to publish a tag naming the group, and Alice cannot sign for Bob's key. A project's own name, description, and channel binding likewise have no single writer when scattered across per-repository tags, and no deletion story. That is why multi-repo grouping is the one forge concept in Buzz that warrants a custom kind. ## `docs/nips/NIP-MP.md` `kind:30621`, an addressable event per NIP-01, addressed by `(pubkey, 30621, d)`. Members are `a` tags holding canonical `30617:<lowercase-64-hex-owner>:<repo-d>` coordinates, following NIP-01's 2-or-3-element grammar where the optional third element is a relay hint clients MAY use and whose content ingest does not parse. Metadata is `name`, `description`, `buzz-channel`, `buzz-visibility`. - **Authority stops at the container.** The signer can replace their own project and nothing else — no edit, delete, push, or admin over any member. Deletion additionally admits the signer's registered NIP-OA owner, because `validate_standard_deletion_event` (`crates/buzz-relay/src/handlers/side_effects.rs`) grants that platform-wide so a human can clean up events published by an agent they own; the spec documents it as a Buzz extension to NIP-09 rather than carving `kind:30621` out of it. `buzz-channel` on a project is metadata only; git push policy reads the repository's own `kind:30617` (`crates/buzz-relay/src/api/git/policy.rs`) and a project never becomes an input to it. - **Ingest validation contract**, with named rules the fixtures reference: `d-cardinality`, `d-empty`, `member-cap` (64, counting every `a` tag), `member-tag-arity`, `member-coordinate-malformed`, `member-duplicate`, `metadata-cardinality`, `metadata-length`. Arity is its own rule rather than part of coordinate parsing, because a four-element member tag can carry a valid coordinate — the tag's shape is what is wrong, and ignoring elements past the relay hint would admit unvalidated data no consumer reads. Duplicates are rejected rather than normalized — a relay cannot rewrite tags inside a signed event without invalidating its id and signature. - **Metadata interpretation is normative, not left to the reader.** Ingest bounds cardinality and length and interprets nothing; clients resolve absent `name` to the `d` value, any unrecognized `buzz-visibility` token to `listed` (a typo is not a privacy signal), and an unresolvable `buzz-channel` to a project rendered without a channel rather than dropped. `content` carries no meaning: writers SHOULD emit `""`, and readers and relays MUST ignore any value rather than reject it. - **Claim authority.** A project suppresses a member's standalone card only when it is listing eligible *and* its signer is that repository's owner or appears in the repository's own `maintainers` tag. Without this, anyone could publish a project naming your repository and pull it out of the collection into a container you never consented to. An unauthorized project still renders, and still renders its members — it just cannot remove a repository from where its owner expects to find it. - **Deterministic client fold**, seven steps, with a table of required cases: exhaustive enumeration (a fixed `limit: 200` makes repository 201 vanish), multiple membership, fallback to a standalone card, unresolvable members marked unavailable rather than dropped, and local hide of a container never hiding repositories. On a relay that provides no exhaustive mode, the conformant behavior is a persistently marked possibly-incomplete collection — not a violation of the enumeration requirement. - **Pagination is specified in two modes**, because exhaustive enumeration is not universally achievable. Both modes share an explicit three-condition relay contract: a relay must (1) apply the complete filter before enforcing any limit, (2) expose the exact effective page limit it enforces, and (3) saturate pages — return `min(effective limit, remaining matches)`, so a short page proves all remaining matches were returned. A relay satisfying any proper subset does not provide the guarantee, and absent it a client MUST mark the collection possibly incomplete. On a relay exposing a composite `(created_at, event id)` keyset cursor — Buzz does on its authenticated HTTP bridge endpoint, via `until` + `before_id`; the NIP-01 websocket REQ path silently discards `before_id`, so a websocket client against Buzz is in mode 2 — clients MUST page by it; within the relay contract the cursor's uniqueness means no skips or re-reads and a short page is an unambiguous end signal, but cursor uniqueness alone does not substitute for the relay contract. A vanilla NIP-01 filter has no id tiebreak, so `until` alone either skips a second's unread events or never advances; there a client MUST drain the boundary second explicitly. The spec also adds normative guidance on query shapes: a client MUST use only query shapes the relay applies completely before limiting, and where a needed constraint (such as `#a`) is post-applied, MUST widen to a pushable shape and match the rest client-side. - **Kind allocation** recorded with the checks performed: `30621` is unassigned in the upstream nostr NIPs kind table and has no nostrbook.dev entry, and it is the one free number between `30620` and `30622` locally. ## `docs/nips/NIP-MP.fixtures.json` The ingest contract: 31 cases — 11 accept, 20 reject — as unsigned templates consumers sign with their own test key. Coverage includes minimal and full projects, zero members, the 64-member boundary from both sides, cross-owner and same-`d`-different-owner members, colon-bearing repository `d` values, relay hints, non-empty `content`, and every rejection rule. Each of the two 256-byte `buzz-` bounds gets its own reject case so neither can hide behind the other's rejection, and duplicate detection is pinned to the coordinate alone by a case whose two identical coordinates carry different relay hints. A four-element member tag carrying an otherwise valid coordinate pins arity separately from coordinate parsing. Every rejection case names the rules that may fire, so an implementation cannot pass by rejecting a bad event for an unrelated reason. ## `docs/nips/NIP-MP.fold-fixtures.json` The fold oracle: 12 cases covering every row of the required-fold-cases table, including the discriminating case where one authorized and one unauthorized project list the same repository — an implementation that requires every listing project to be authorized emits a spurious implicit card, and one that lets any listing project suppress drops a card it owes the owner. Inputs are semantic rather than signed envelopes: a repository or project is named by its coordinate plus only what the fold reads — signer, members, `maintainers`, visibility, viewer-hidden, deletion. Every collection in `expect` is compared as a set, including each container's `members`, since the fold fixes placement and not order. Signing would re-test the ingest contract and obscure what is under test. The fold is where claim authority lives, so without a shared oracle two clients could each satisfy the prose and still render different collections from identical heads. ## `VISION_PROJECTS.md` Line 41's "zero custom kinds" now reads "no custom kind for the repo itself", with a new "One Project, Many Repos" section recording why the one exception is warranted. `30621` rows added to the kind and status tables. Related: block#3171 (the `KIND_PROJECT` constant, relay ingest validation of this contract, and the inclusive `created_at <= tombstone` bound this spec's coordinate-deletion rule cites). Independent — either can merge first. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…(split 1/2 of block#3467) (block#3741) ## Summary This is **part 1 of 2** split out from block#3467 (per Tyler's request), carrying only the mesh-scoped changes. The agent/ACP response-behavior changes and the new `send_message` tool stay in block#3467 as part 2. All commits are @michaelneale's work, cherry-picked with authorship preserved. - Upgrade embedded Mesh to v0.74.0 (tag-pinned instead of commit rev) and use canonical Gemma model IDs. - Keep shared compute serving through member joins, roster changes, app recovery, and community switching. - Wait for actual model readiness and avoid resuming incomplete downloads after quit. - Leave `BUZZ_AGENT_THINKING_EFFORT` unset by default so each model's chat template picks its own thinking default (`none` suppressed Gemma tool-calling entirely; pinning `low` made Qwen3 burn ~4x output budget). Explicit agent/persona/global values still win. ## Relationship to block#3467 Contains the mesh commits from block#3467 (`2cd640b23`, `0ad81c341`, `ad13ed841`) rebased onto current main, with one deliberate exclusion: the `crates/buzz-agent/src/llm.rs` reasoning→text parser change from `2cd640b23` is **not** here. That change unconditionally affects every OpenAI-compat/Responses provider, so it belongs with the reply-behavior work in part 2, where it can be reviewed as what it is. Not included (remaining in block#3467 / part 2): - typed `send_message` tool in dev-mcp + `BUZZ_ACP_SEND_MESSAGE_TOOL` gating - plain-reply delivery fallback in buzz-acp (`BUZZ_ACP_DELIVER_PLAIN_REPLIES`) - the mesh_agent_e2e P5/P6 rewrite (exists to prove the reply path) - the two `env.insert` preset opt-ins in `relay_mesh.rs` for the flags above - the llm.rs parser change This PR is independently mergeable; part 2's flags are all off by default so it can land before or after. ## Testing - `cargo test -p buzz-relay --locked` — 780 passed (one telemetry test is order-sensitive under parallel default settings; passes in the pre-push suite and standalone, unrelated to this diff — files untouched here). - `just desktop-tauri-test` (default features) — 1877 passed. - `cargo test --locked --features mesh-llm` in `desktop/src-tauri` — 1961 passed, including the new relay-mesh preset and coordinator/recovery tests. - Both `Cargo.lock`s resolve with `--locked` against the v0.74.0 tag. - Full pre-push hook suite green (rust-tests, desktop-check/test, tauri checks). Live validation of the mesh v0.74 upgrade itself is documented on block#3467 (two-Mac cross-version test). --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
…lock#3640) The catalog detail pane hardcoded "Community member" for every non-own catalog entry. The publisher pubkey (`catalogSource.ownerPubkey`) was already on every entry — it just was not being resolved to a name. ## What changed **`desktop/src/features/agents/ui/PersonaCatalogDialog.tsx`** `PersonaCatalogDetail` now calls `useUsersBatchQuery([ownerPubkey])` when the selected entry is a community (non-own) catalog agent. The label derivation is extracted into the exported pure function `resolveCatalogOwnerLabel` and uses truthy fallbacks to handle empty or whitespace-only kind:0 fields: - Own entry → `"You"` (unchanged) - `displayName` present and non-blank → the display name - `displayName` absent/blank but `name` present and non-blank → the name - Loading, unresolvable, or both candidates blank → `"Community member"` (fallback preserved) The batch query is disabled (`enabled: false`) when the entry is not a community entry, so there is no extra network call for own entries or built-in agents. **`desktop/src/features/agents/ui/personaCatalogOwnerLabel.test.mjs`** Unit tests for `resolveCatalogOwnerLabel` covering: populated `displayName` wins; whitespace-only `displayName` falls through to `name`; both candidates empty/whitespace/null/undefined all fall through to `"Community member"`. **`desktop/tests/e2e/agents.spec.ts`** - Updated the existing assertion — it previously checked for the hardcoded fallback; now asserts the resolved mock display name `"alice"`. - Added "catalog detail shows Community member when the publisher profile cannot be resolved" — installs a catalog event from an unknown pubkey and asserts the fallback still renders. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
## Why Long Buzz threads were rendered as `[Thread Context (13 of 13 messages)]` because the harness counted only the already-limited query result. That hid older context and could also hide the agent's own prior reply in busy threads. ## What - Fetch one extra thread reply as a sentinel so truncated context is labeled correctly. - Use a best-effort `/count` call for improved truncated totals when available, clamped to the sentinel-proven minimum so racy counts cannot render impossible labels. - Keep the `/count` path single-attempt with a short timeout and only add the root to exact totals when the root was actually fetched. - Fetch and preserve the agent's newest prior reply when it falls outside the recent window, with exact event-id matching for the pin/dedup boundary. - Add parser and fetch-boundary tests for truncation, exact count, missing root, count-below-minimum clamping, count failure fallback, distinct fetched-reply lower bounds, agent-reply dedup/pinning, and serialized query/count filter semantics. ## Risk Assessment Low-to-medium — limited to buzz-acp prompt context fetching and a small RestClient helper. If `/count` fails or times out, the code falls back to the sentinel-derived minimum total rather than failing the prompt. The synchronous `/count` happens only for truncated thread contexts and is bounded to one short best-effort attempt. ## References - Buzz thread: chotchkies-buzz-bombing-flakes / `7ef71407f1c7a642382c7e48e0c80fb6ca66948890e04d1eb6f1408c3b7278b1` - Validation at `c1cfd1b16a04a3ac1d1d0d3cf43e1a08508f3532`: - `cargo fmt -p buzz-acp` ✅ - `cargo test -p buzz-acp test_fetch_thread_context -- --nocapture` ✅ (6 tests) - `cargo test -p buzz-acp parse_nostr_thread_response` ✅ - `cargo test -p buzz-acp` ✅ (649 unit + 9 lifecycle tests) - `git diff --check` ✅ - Push was completed with `--no-verify` after pre-push hooks reached non-code local environment failures: `flutter` missing for `mobile-test`; Node.js v20.20.2 too old for pnpm/node:sqlite in `desktop-check` and `desktop-test`. Earlier hook stages passed: `check-push-org`, `branch-skew`, `rust-tests`, `test`, `desktop-tauri-checks`. - Earlier full `./bin/just ci` at `622ed7eb8807d64e06209101569b1013414af091`⚠️ passed Rust/desktop/web stages, then failed in `mobile-test` on unrelated existing mobile test `ChannelDetailPage keeps follow mode off while a tall newest message stays visible`; rerunning that single mobile test reproduced the same failure without touching mobile code. Generated with Codex Signed-off-by: npub1m0vvn9qm5md0a080p27qzkm9uaw49e699ukwfq7fc0756xq0y5zqhzhdk2 <dbd8c9941ba6dafebcef0abc015b65e75d52e7452f2ce483c9c3fd4d180f2504@buzz.block.builderlab.xyz> Co-authored-by: npub1m0vvn9qm5md0a080p27qzkm9uaw49e699ukwfq7fc0756xq0y5zqhzhdk2 <dbd8c9941ba6dafebcef0abc015b65e75d52e7452f2ce483c9c3fd4d180f2504@buzz.block.builderlab.xyz>
## Summary - replace Amp's outdated Sourcegraph attribution in the runtime catalog - describe Amp neutrally as a coding agent for the terminal and editor ## Verification - `pnpm test` (desktop: 3,819 passed) - `pnpm typecheck` - pre-push `desktop-check`, `desktop-test`, and `branch-skew` hooks Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - Render selected agent mentions as visible bot chips in the mobile composer. - Recognize agent profiles consistently when rendering message-body mentions. <img width="630" height="1368" alt="Screenshot 2026-07-30 at 07 54 16" src="https://github.com/user-attachments/assets/035b46bf-ee78-4ee5-82fc-84591415ed7c" /> ## Validation - `flutter test test/features/channels/compose_bar_test.dart test/features/channels/message_content_test.dart` - `flutter analyze` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Why People joining a community with an existing relay profile should not be asked to recreate their name and avatar. ## What - Check the active identity's relay profile after the joined community becomes active - Skip directly to the starter-team step when a kind-0 profile event exists - Preserve the profile setup path when no event exists or discovery fails - Cover both new-profile and existing-profile join paths in E2E tests ## Risk Assessment Low — the lookup is scoped to the community onboarding profile stage, runs once per transaction, and fails open to the existing flow. ## References - `pnpm build:e2e && pnpm exec playwright test --project=integration tests/e2e/onboarding.spec.ts --grep 'first-community direct join reaches profile|community onboarding reuses an existing relay profile'` (2 passed) Generated with Codex Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
**Category:** new-feature **User Impact:** Users can create, download, and verify a password-protected backup of their private identity from desktop Settings. **Problem:** Buzz does not currently give signed-in users a Settings-based path to protect or validate their private identity independently of onboarding. **Solution:** Add a focused backup menu to the private-key row, keep encryption and verification local in Rust, and preserve completed encrypted backups briefly so native saves can be retried without repeating encryption. <details> <summary>File changes</summary> **desktop/src/features/settings/** Adds the background backup lifecycle, create and test dialogs, private-key menu integration, password handling, and focused unit coverage. **desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx** Extends the masked private-key display with reusable overflow-menu actions used by Settings. **desktop/src/app/App.tsx** Mounts the backup provider at app scope so encryption and save work survive closing Settings or the modal. **desktop/src/shared/api/tauriIdentity.ts** Adds typed desktop bindings for local backup creation, save, selection, and verification. **desktop/src-tauri/src/key_backup.rs and desktop/src-tauri/src/commands/identity.rs** Implements local NIP-49 encryption, password generation, file handling, and public-identity-only verification results. **desktop/src-tauri/src/egress_guard.rs and guarded call sites** Blocks encrypted secret material from relay, websocket, snapshot, sharing, and huddle egress paths. **desktop/src-tauri tests and fixtures** Covers encryption, verification, file behavior, and fail-closed no-egress protections. **desktop/src/testing/e2eBridge.ts, desktop/tests/, and desktop/playwright.config.ts** Expands the mock native bridge and browser coverage across create, retry, expiry, and current/different-identity verification states. **desktop/src-tauri/Cargo.toml, Cargo.lock, and assets** Adds the local cryptography/password-generation dependencies and embedded short-word list. </details> ## Reproduction steps 1. Run the desktop app and open **Settings → Profile → Identity**. 2. Open the private-key overflow menu and choose **Create backup**. 3. Enter or generate a valid password, submit, and confirm progress continues if the dialog or Settings is closed. 4. Save the resulting `.ncryptsec` file; cancel and retry to confirm the temporary download remains available. 5. Choose **Test backup**, select the file, enter a wrong password, then retry with the correct password. 6. Confirm success identifies whether the backup matches the current identity and displays only the public `npub`. ## Screenshots | Settings identity | Private-key menu | Create backup | |---|---|---| | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/981e391b-6829-4081-95ca-ca75a369de71" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/7972c68e-7635-47d8-b0ad-9639390d3e6c" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/4709c8f7-cf02-46f1-bec9-b3f98fe56fb2" /> | | Encrypting | Download available | Test success | |---|---|---| | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/1ac3e934-2b4b-4135-bae6-126c715c8c59" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/cb6f07ee-a16f-44a5-b9a0-6b9fe0e4d40d" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/ea58b1b1-966c-46aa-8d59-92c9f06a25bd" /> | Visual review and additional states: [Buzz thread](buzz://message?channel=50ca7ef1-201e-4159-9499-40de3964b7c3&id=87eceb5f0f82fd50c32e560de3d35be48e293760f6620718aafdcef289d475fe) --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary - make the relay reconnect coordinator authoritative during outages so query, publish, and subscription traffic waits for the scheduled attempt instead of cancelling backoff - release waiting operations after the coordinated AUTH + live-subscription replay attempt, while preserving one explicit manual reconnect fast path - suppress duplicate notification side effects when reconnect replay overlaps previously delivered events ## Root cause `resetConnection()` scheduled exponential backoff, but `ensureConnected()` cleared any pending reconnect timer. Operation-level retry paths immediately called `ensureConnected()`, so ordinary app traffic could repeatedly bypass the reconnect policy during an outage. The resulting churn also replayed overlapping live events into notification side effects without a shared event-ID guard. ## Validation - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 3,823 passed - pre-push: `desktop-check`, `desktop-test`, and `branch-skew` passed - file-size, px-text, and pubkey-truncation ratchets passed --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - add a manual desktop release preparer that regenerates one version-only candidate from current `origin/main` - validate deterministic complete changelog accounting, candidate authorship, allowed files, exact-head approval, required checks, and two-parent merge topology before tagging the reviewed candidate - move desktop tags/releases from `v*` to `desktop-v*` while preserving relay, chart, push-chart, and mobile behavior - stage all four platform outputs in Actions artifacts and grant GitHub release write access only to one final all-platform-gated publisher - publish the versioned release only after complete artifact assembly; update stable `latest.json` last; never promote prereleases or published rebuild outputs ## Safety properties - desktop tags point to the reviewed candidate SHA, not the merge commit - release builds remain tag-bound and reverify tag == checked-out HEAD - one final writer fails closed on artifact basename collisions - per-tag concurrency serializes publication without cancellation - published reruns do not replace immutable versioned assets or promote signatures from a rebuild - candidate branches use an explicit remote OID lease when regenerated ## Validation - `scripts/test-desktop-release-candidate.sh` - `scripts/test-release-ref-contract.sh` - `scripts/test-mobile-release-contract.sh` - changed workflow YAML parsing (Ruby Psych) - changed shell syntax (`bash -n`) - `git diff --check` - push hooks: branch-skew, Rust workspace tests (1,853 passed), desktop Tauri tests (3 passed) ## Coordinated companion - squareup/buzz-releases#79 updates the manually entered desktop source-tag contract to stable-only `desktop-v*` - merge the private contract companion before the first namespaced desktop release ## Rollout blockers (no settings changed here) Before the first candidate/release: 1. enable merge commits in repository settings 2. allow `merge` in ruleset `13596885` 3. require approval after the last push in ruleset `13596885` 4. include `refs/tags/desktop-v*` explicitly in release ruleset `14378754` 5. prove the non-publishing candidate/merge/tag/artifact validation path before any production release Do not test the old workflow with a prerelease: it can still mutate the production rolling updater release. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - Show video review comments when a video is opened from a thread reply. - Reuse review-context construction across timeline and thread views. ## Validation - `pnpm run build:e2e && pnpm exec playwright test tests/e2e/video-attachment.spec.ts --project smoke --grep "video replies in threads open the review comments view"` - `pnpm test` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary - use uniform 4px top and bottom padding for continuation rows - keep continuation timestamps top-aligned and remove the thread-only minimum-height gutter - raise continuation hover actions by 12px - align virtualized row estimates with the compact layout ## Validation - `pnpm test` (3,782 tests via pre-push) - `pnpm check` - desktop snapshots ## Screenshots ### Mention-chip continuation  ### Emoji continuation  --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary - send desktop presence heartbeats every 60 seconds instead of every 30 seconds - extend presence TTL from 90 to 180 seconds to preserve the existing three-heartbeat expiry window - add mutation-sensitive tests that pin the one-minute / three-window timing contract - update presence documentation to match This halves steady-state **desktop** presence `SET` + `PUBLISH` traffic while retaining tolerance for two missed heartbeats. Mobile already uses a 60-second heartbeat, so the fleet-wide reduction depends on desktop's share of connected clients. ## Rollout order Deploy the relay TTL increase before shipping the desktop heartbeat change. Old desktop + new relay is safe; new desktop + old relay leaves only a 90-second TTL on a 60-second cadence and can flap after one missed heartbeat. ## Verification At initial live-test commit `00816e233b187bc5ba12c667d675ed050a8cc1c9`: - isolated clean-room relay built from the exact SHA against fresh Postgres, Redis, and MinIO - live Redis `MONITOR` observed kind-20001 writes as `SET ... EX 180`, global `PUBLISH`, and clean-disconnect / explicit-offline `DEL` - normal workflows passed: channel create/update/archive/unarchive; message send/get/reply/thread/search; archived-channel write rejection and resumed write after unarchive At follow-up commit `bf38a8c5c96f196ff8ee46e48d4141ee7811f186`: - `pnpm -C desktop test` — 3829 passed - `pnpm -C desktop typecheck` - `cargo test -p buzz-pubsub` — 24 passed, 11 Redis-dependent tests ignored - mutation probes fail when the server TTL changes to `999999` or the desktop heartbeat changes back to 30 seconds - `git diff --check` The pre-push suite's relevant checks passed, but its unrelated Tauri clippy step fails on current `origin/main`: `desktop/src-tauri/src/linux_media.rs` has three dead-code warnings on macOS. This PR does not modify that file, so the branch was pushed after independently running the suites above. ## Buzz context Originating channel: `buzz-redis-cluster-mode` (`f4e36d32-afdb-447f-8c87-ab003e069d18`) --------- Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
**Category:** improvement **User Impact:** Activity feeds now clearly identify the agent and keep update recency visible even when channel names are long. **Problem:** The activity header led with a generic label, making it hard to tell which agent was in view, while channel scope and recency competed for limited horizontal space. Long channel names could hide the update timestamp entirely. **Solution:** Lead with the resolved agent avatar and name, then place mode and scope in a truncating metadata region with recency pinned at the right edge. This preserves the compact two-line header while keeping the most important identity and freshness signals legible. <details> <summary>File changes</summary> **desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx** Reorganizes the activity header around the agent identity, reuses the existing resolved profile avatar and label helpers, and separates scope truncation from the always-visible recency label. **desktop/tests/e2e/activity-scope-label-screenshots.spec.ts** Expands activity-header coverage across channel-scoped, all-channel, raw, long-name, and narrow layouts, including measured truncation and recency visibility. </details> ## Reproduction steps 1. Open an agent's activity feed from a channel. 2. Confirm the agent avatar and name lead the header. 3. Open a feed scoped to a channel with a long name and resize the panel narrowly. 4. Confirm the mode and channel scope truncate while the recency label remains visible at the right edge. 5. Toggle Raw mode and open an all-channel feed to confirm the same hierarchy and truncation behavior. ## Screenshots | Long channel | Narrow layout | |---|---| | <img width="380" height="671" alt="image" src="https://github.com/user-attachments/assets/19682aac-9938-41ed-8c27-fe59bf8b7535" /> | <img width="371" height="771" alt="image" src="https://github.com/user-attachments/assets/92a6fc05-c9ba-4c11-a18c-22b5225d8b9a" /> | | Raw mode | All channels | |---|---| | <img width="380" height="671" alt="image" src="https://github.com/user-attachments/assets/c8135600-e3c9-4644-8350-fa5f6b2d3aaa" /> | <img width="380" height="671" alt="image" src="https://github.com/user-attachments/assets/bac73a6a-4ed5-42d6-98cc-039a75c48ef3" /> | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary - Add Devin to the built-in preset harness catalog using the official native ACP invocation: `devin acp`. - Link setup guidance to Cognition's official Devin CLI documentation. - Render a bundled, attributed Devin mark on a white canvas through Buzz's existing runtime-icon system. - Keep preset capability metadata in the Rust catalog; no duplicate TypeScript runtime table or React runtime checks. - Move the existing preset catalog and its focused tests into a Rust submodule without changing existing preset behavior, keeping the touched files within the repository's file-size limit. ### Related issue Follow-up to the generic BYOH harness work in block#2773. ### Scope This is the small preset/data-entry follow-up described in the block#2773 discussion. It uses the generic preset readiness contract and does not add Devin-specific authentication probing, permission bypasses, model switching, cloud handoff, or cloud Devin capability claims. The preset supplies: - ID: `devin` - Executable: `devin` - Arguments: `acp` - Installation guidance: https://docs.devin.ai/cli ### Testing Local verification was rerun at the final PR head, `7bb9aa6e862a47a5062b5b8234fdb5ce2aae6c1d`. - Focused Rust preset tests: 7 passed - Desktop JavaScript tests: 3,768 passed - Desktop lint, formatting, file-size, and text guards: passed - Full Tauri test suite: 1,851 passed, 14 ignored - Root Rust unit-test groups: passed - Web production build: passed - Mobile format, analyze, and test suites: passed - Full repository `just ci`: passed The branch also merges cleanly with the current Block `main`. The upstream fork-triggered CI workflow is awaiting maintainer approval; DCO, Semgrep OSS, and zizmor are passing. The bundled SVG was rendered and visually inspected in both its source dimensions and a 512px preview. The cross-language preset-logo guard verifies that the Devin mapping exists and the asset is present on disk. Signed-off-by: Mark Fenner <markfenner57@yahoo.com>
…k#3670) ## What Problem This Solves `test_usage_metrics_lock_has_single_owner_and_releases_on_drop` hardcodes the **production** advisory lock key (`0x4255_5A5A_4D45_5452`) on the shared `TEST_DATABASE_URL`. Postgres advisory locks are per-database, so any live `buzz-relay` pointed at the same DB holds that key and the test fails (or races the relay tick). Diagnosis time was burned during block#3268 verification, including near-misses on live dev relays. Fixes block#3619. ## Why This Change Was Made Preferred fix from the issue: run the test on a private scratch DB via existing `create_scratch_db` / `drop_scratch_db` (same pattern as replica-routing fixtures). Keep the production lock key so the test still documents the real constant, without colliding with a running relay. ## User Impact - Local `cargo test -p buzz-db -- --ignored` no longer fails when a dev relay is running against the shared test DB - Safer: no temptation to `pg_terminate_backend` a live relay to "fix" the test ## Evidence - Code review of fixture isolation - Pattern matches existing `create_scratch_db` usage in this file - Test remains `#[ignore = "requires Postgres"]` (same as before) ## Related - Issue: block#3619 - None found among open PRs for this exact fix Signed-off-by: NanoRisk6 <aidashtherapy@gmail.com>
…block#3368) Windows installs of Goose and other harnesses failed at exactly five minutes with an empty error (block#2401). The 300s ceiling was killing installs that were working, just slowly — the Goose step pulls a ~79MB release asset, and Windows Defender scans every file npm extracts. When the ceiling fired it discarded the output it had already read, so the user got a bare timeout string and no way to tell a hang from a large download. ## The ceiling `INSTALL_TIMEOUT` is 900s, and the error names the limit: `install command exceeded the 15m ceiling and was terminated`. It stays a pure wall-clock ceiling with no inactivity kill — nothing observable distinguishes a hung installer from one silently transferring a large artifact, so silence alone never kills an install. A ceiling kill remains non-retryable; re-running a command that already burned 15 minutes costs the user more time with no plausible path to success. The child's exit and both stream drains fold into one resumable settle governed by a single deadline. Waiting on the drains outside that deadline would let a descendant that outlived the install shell hold the output pipes — and the per-runtime install guard behind them — open with no bound, which is the failure the ceiling exists to prevent. So the deadline path terminates the process group on the normal-exit branch too: a leader that exited with a real status still gets its stragglers killed, and the guard cannot stick either way. Whether the leader had already exited only decides the verdict — its real status outranks a timeout. The install shell is a session leader and its descendants inherit the output pipes, so signalling only the leader left them running and the drains blocked on a pipe nobody would close. Escalation keys off the *group's* liveness rather than the leader's, since a descendant that ignores SIGTERM outlives the leader and would otherwise never receive the group SIGKILL. Reaping the killed child and finishing the drains share one bounded grace, so a termination that failed outright cannot extend the ceiling that just fired. ## Output capture Each stream drains into a bounded capture that is *shared* with the reader rather than returned by it, so whatever arrived before a stall is readable at the ceiling — exactly when the output matters most. Output of any size costs a fixed amount of memory. One capture holds two independently bounded views of the same bytes: | View | Head / tail | Cut marker | |------|-------------|------------| | UI (`InstallStepResult`) | 512 B / 1024 B | `... (N bytes omitted) ...` | | Log file | 128 KiB / 128 KiB | `... [N bytes omitted at cap] ...` | The UI budget is screen space; the log's is disk. Both markers are inline, so neither ever implies completeness it does not have. Both ends are cut at arbitrary byte offsets, so a partial character is trimmed and the partial token each cut left behind is dropped — the marker's byte count includes both trims. ## Install log `steps` carries only the last attempt of each step, truncated for display. Everything else — earlier retries, the prerequisite step that actually broke, the managed-Node bootstrap — used to be discarded. `InstallReporter` now appends one self-contained record per attempt of per step to `install-<runtime-id>.log` beside the agent logs, and `InstallRuntimeResult.log_path` carries the file to the UI, where a failure message ends with `Full log: <path>`. Each record is bounded independently by the log-scale capture that produced it, so a first attempt that printed megabytes cannot push out the later record explaining the failure; the run's total is bounded by steps × attempts × per-record cap. Every early return builds its result through one `InstallReporter::failed` helper, so no failure path can omit the log pointer, and synthesized steps go through `record_step` — a step that reaches the UI without passing it would be invisible in the file. Install output can echo a registry token or proxy credential from the environment it ran in, and the file is written unattended. Redaction keys off the *names* of the environment variables the install inherited, snapshotted once per run, rather than a list of known secret value prefixes: a credential with no recognisable shape is exactly the one a prefix match misses. Three name rules apply, because the variables need different treatment: | Rule | Variables | Redacted | |------|-----------|----------| | URL userinfo | `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NPM_CONFIG_PROXY`, `NPM_CONFIG_HTTPS_PROXY`, `NPM_CONFIG_REGISTRY` | `user:password` only | | Exact name | `NPM_CONFIG_KEY`, `NPM_CONFIG__AUTH`, `NPM_CONFIG_OTP` | whole value | | Marker substring | `*TOKEN*`, `*SECRET*`, `*PASSWORD*`, `*_PAT`, … | whole value, 8-byte floor | A proxy or registry keeps its host and port, because an install that fails behind one is diagnosable only if the record still says which one it went through, and a bare `user@` with no password is not treated as a credential. npm's own settings are listed by exact name rather than matched on `KEY` or `AUTH` substrings — both occur throughout an ordinary environment on values that are paths and people's names — and they bypass the 8-byte floor, since a six-digit one-time password is a credential at that length. Matching is case-insensitive, which is what npm's lowercase `npm_config_*` spelling needs. `0o600` is set by the create rather than a later `chmod`, which would leave a window where the umask decides. A runtime id that cannot safely be a filename yields no log rather than a sanitized one — a rewritten id could collide with another runtime's log. The file holds exactly one run. A run opens its own session after the runtime id has been canonically resolved — the previous file rotates to `.1` and any older `.1` is removed before the rename, since a rename that will not replace its destination would otherwise wedge rotation permanently on Windows. The session writes a header naming the runtime, the app version (`app.package_info().version` on the Rust side — cannot be mocked or fail), the OS (`std::env::consts::OS`), and the start time: a Windows failure and a macOS one on the same runtime are different bugs, and a stale app version explains a failure that no longer reproduces. Each record carries its attempt's elapsed time. ## Live output line A 15-minute ceiling with nothing behind it but a spinner is indistinguishable from a hang. The same drain seam feeds an `acp-install-output` event carrying the newest complete line, and the three install entry points — Doctor harness rows, the harness catalog dialog, and onboarding runtime cards — render it under the spinner with `aria-live="polite"`. Ordering is keyed on a `seq` monotonic across the whole install, not on the attempt number, which restarts at 1 for every step: keyed on attempt, one step succeeding on attempt 2 would make the next step's attempt-1 output look stale and freeze the display for the rest of the install. Each executed attempt begins with an unthrottled `line: null` clear signal, so a stale failure line cannot sit under the spinner while the retry runs. Events are otherwise throttled to four per second, and the throttle *retains* the newest pending line and flushes it when the window reopens rather than dropping it — at an attempt boundary a drop would silently eat the new attempt's first line. The subscription is mounted for the runtime's whole lifetime rather than started when the install begins. The install command is invoked from the click handler, so the clear and a fast command's first lines can be emitted before React has committed the pending state, and nothing replays them — a subscription that waited for that state would lose the entire output of a short install. The run boundary resets the ordering key when the install settles, since `seq` restarts for the next run, and the line renders only while installing, so a straggler from a finishing drain cannot appear under a fresh Install button. The 15-minute ceiling deliberately stops waiting on stuck drain threads — a hung installer must not freeze the app. That means a drain thread can outlive its `InstallReporter`. Without a generation guard, a drain that calls `offer` after the run settles would publish an event with the run's high `seq`, poison the permanent listener's React state, and cause the next install's restarted `seq=0` events to be rejected. `Live` now carries a `lifecycle: Arc<RwLock<bool>>`; drain threads hold a **shared read guard** from the admission check through the `(self.emit)(...)` call, making the check-then-emit pair atomic with respect to shutdown. `InstallReporter::drop` takes the **exclusive write guard** and stores `false` — this blocks until every in-flight drain publication releases its read guard, then prevents any new admission. Deactivation is bounded: the write lock holds only for the flag store, so it can block at most for the duration of one emit call (microseconds to low milliseconds). Rust drops locals in reverse-declaration order, so `reporter` drops before `_guard`, ensuring the exclusive write completes before the per-runtime concurrency guard releases and a new install can start. ## Also Install result types move to `desktop/src/shared/api/installTypes.ts`, following the existing `searchTypes.ts` / `workflowTypes.ts` convention, and are re-exported from `tauri.ts` and `types.ts` — both already over the file-size cap, so neither can grow to carry them. Two comments described `AdapterOutdated` as applying only to the deprecated package; it also covers a version below the supported floor. Report: block#2401 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary - target the visible thread branch collapse guide in the messaging smoke test - avoid clicking the underlying collapse rail when the guide overlaps it - retain the existing post-click assertions that verify the two-reply branch collapses ## Context `main` CI failed because Playwright repeatedly attempted to click the lower `thread-collapse-rail` while the matching `thread-collapse-guide` intercepted pointer events. Both controls dispatch collapse for the same branch; the guide is the actual topmost user target and is already used by `thread-unread.spec.ts`. Failing run: https://github.com/block/buzz/actions/runs/30575425126 ## Validation - focused Playwright smoke test: 1 passed - pre-push hooks: desktop check passed; 3,835 desktop tests passed - `git diff --check` ## Review Princess Donut reviewed the test-only approach and locator determinism with no blockers. Mongo review is pending. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
…block#3358) Team catalog projections (`kind:30178`) embed every member's system prompt, so they need the same read gate personas already have: only the author sees an unshared event. The gate was hardcoded to `kind:30175` at six read surfaces plus the SQL pushdown, so rather than adding a second special case it becomes kind-generic over `SHARED_GATED_KINDS = {30175, 30178}`. ## Kind 30178 New parameterized-replaceable kind, addressed by `(pubkey_o, 30178, team_id)`. It embeds sanitized member projections instead of referencing `kind:30175` heads — a foreign reader of a shared team could not otherwise hydrate members whose own persona events are unshared or, for built-ins, absent entirely. `kind:30176`'s wire body is untouched, so device sync keeps its contract. ## Kind-generic shared gate `buzz_core::kind` replaces `is_persona_shared_kind` / `is_unshared_persona_event` / `persona_event_is_shared` with `SHARED_GATED_KINDS` and the kind-agnostic `is_shared_gated_kind` / `is_unshared_gated_event` / `event_is_shared`. Every read surface consults the set: | Surface | File | |---|---| | REQ historical delivery + `ids` lookup | `crates/buzz-relay/src/handlers/req.rs` | | Live fan-out | `crates/buzz-relay/src/handlers/event.rs` | | COUNT fallback | `crates/buzz-relay/src/handlers/count.rs` | | NIP-98 HTTP `/query`, `/count`, `/search` | `crates/buzz-relay/src/api/bridge.rs` | | Pre-`LIMIT` SQL pushdown | `crates/buzz-db/src/event.rs` | The SQL clause generalizes from `kind != 30175` to `kind NOT IN (...)` bound from `SHARED_GATED_KINDS`, still applied before `ORDER BY … LIMIT` so a page of newer private events cannot starve an older shared one off the candidate set. `EventQuery::persona_reader` is renamed `shared_gated_reader` and `needs_persona_filtering` to `needs_shared_gate_filtering` to match. Because the `buzz-core` rename has consumers outside the relay, the four desktop call sites of `persona_event_is_shared` travel with it: `desktop/src-tauri/src/commands/personas/pending.rs`, `desktop/src-tauri/src/event_sync.rs`, and two in `desktop/src-tauri/src/managed_agents/persona_events.rs`. Each call is unchanged apart from the name — the persona `shared` projection behaves exactly as before. ## Ingest validation `validate_persona_envelope` splits into two reusable pieces — `validate_shared_tag` (exactly-two-element `["shared","true"]`, at most one occurrence) and `single_bounded_d_tag` (exactly one `d` tag, non-empty, `<=64` chars, no ASCII control characters or whitespace). `validate_team_catalog_envelope` composes both; personas additionally keep the slug grammar `^[a-z0-9][a-z0-9_-]{0,63}$`. `kind:30178` deliberately does **not** get the slug grammar. Team ids are UUIDs or built-in identifiers such as `builtin-team:welcome`, and the colon is not slug-legal; rewriting ids to fit would break NIP-33 addressing against the team's own `kind:30176` head. The non-empty and exactly-one checks are load-bearing regardless — without them generic NIP-33 storage maps a missing `d` onto `(pubkey_o, 30178, "")` and every team overwrites its predecessor. The exact two-element `shared` shape is enforced because the SQL visibility clause is JSONB containment (`tags @> '[["shared","true"]]'`), which would match a three-element superset such as `["shared","true","extra"]`. `kind:30178` is also added to the `Scope::UsersWrite` allowlist and to `is_global_only_kind`, so a stray `h` tag cannot channel-scope an owner-authored definition. ## Deferred `kind:30176` is deliberately not a gate member. Its writers never emit `shared`, so catalog opt-in semantics do not describe it — it needs owner-private reads driven by an authenticated principal set, tracked as a separate follow-up. ## Tests - 19 new `ingest.rs` unit tests covering the 30178 envelope (UUID and colon `d` tags, 64-char boundary, non-ASCII bound, empty/valueless/duplicate/missing `d`, embedded newline, `shared` false/three-element/duplicate, scope and global-only membership). - Persona regressions for the valueless `["d"]` shapes, since the `d`-tag helper is shared by both validators. - Existing `kind.rs` gate tests generalized and extended to assert the gate applies to 30178 as it does to 30175. - New `crates/buzz-test-client/tests/e2e_team_catalog.rs`: 9 WS-level tests over a live relay covering author reads of unshared heads, foreign omission from REQ, `ids`-lookup denial, COUNT existence-leak, share and unshare transitions, and the mixed-kind filter case. - `.github/workflows/ci.yml` adds `--test e2e_team_catalog` to the Relay E2E job so the new suite runs. ## Docs `docs/nips/NIP-AP.md` gains a "Team catalog projection: kind:30178" section and an "Ingest validation: kind:30178" subsection, records the gate as kind-generic, documents 30178 deletion vs. unshare semantics, and adds a security note that sharing a team exposes every member's instructions even when that member's own `kind:30175` head is unshared. Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…lock#3657) ## What problem this solves Tailwind v4 compiles every `hover:` variant inside `@media (hover: hover)`. Some Windows hosts answer that query `false` **even with a mouse attached**, and then every hover-revealed control in the app is permanently `visibility: hidden`. Measured in the app's own WebView2 devtools console, on a mouse-driven Windows 11 desktop: ```js matchMedia('(hover: hover)').matches // false matchMedia('(any-hover: hover)').matches // false matchMedia('(pointer: fine)').matches // false matchMedia('(any-pointer: fine)').matches // false navigator.maxTouchPoints // 10 ``` Windows itself, on the same machine at the same moment, reports a mouse present and an integrated digitizer: ``` GetSystemMetrics(SM_DIGITIZER) = 197 // INTEGRATED_TOUCH | INTEGRATED_PEN // | MULTI_INPUT | READY GetSystemMetrics(SM_MAXIMUMTOUCHES) = 10 SystemInformation.MousePresent = True ``` So this is not "the user has no mouse". Windows knows a mouse is attached, and Chromium still reports `any-pointer: fine: false` and `any-hover: false` — the `any-*` queries exist precisely to describe *any* available input device, and they are wrong here. The presence of an integrated touch digitizer collapses the reported capability to touch-only. The compiled rule that never applies: ```css .group-hover\/member\:visible { &:is(:where(.group\/member):hover *) { @media (hover: hover) { visibility: visible; } } } ``` The row genuinely matches `:hover` (verified: `row.matches(':hover') === true`), the button is in the DOM, the utility class is generated — and the declaration still never lands. ## Why this is more than one control Not a single menu. Confirmed newly-ungated in the production bundle after the change: | utility | media-gated before | after | |---|---|---| | `group-hover/member:visible` | yes | no | | `group-hover/inbox-item:opacity-100` | yes | no | | `group-hover/channel-row:opacity-100` | yes | no | | `group-hover/attachment:opacity-100` | yes | no | | `hover:bg-muted` | yes | no | On an affected host the channel-member action menu (remove member, change role, start/stop agent) has **no reachable affordance at all**: `visibility: hidden` also removes the button from tab order, so there is no keyboard path either. ## The fix One line, at the root, next to the existing variant override: ```css @custom-variant hover (&:hover); ``` This trusts the actual hover event rather than the capability query. Chromium only fires `:hover` when a real pointer is present, so behaviour on hosts that report the capability correctly is unchanged. Verified against a production `vite build`, not just the dev server — the override cascades to the *named* group variants (`group-hover/member`, etc.), which is the part that matters here. ## Prior art in this repo block#2849 overrides Tailwind v4's `dark:` variant default at the *exact same insertion point* in this file, for the same class of reason (a v4 default that does not match how this app actually works). This change follows that precedent. **Note for whoever merges second: block#2849 and this PR will conflict textually** — both append a `@custom-variant` immediately after `@config`. The resolution is to keep both lines; they are independent. ## Scope Desktop only. `web/src/shared/styles/globals.css` has the same Tailwind v4 default, but `web/src` contains **zero** `group-hover` usages, so there are no hover-revealed affordances to strand there. Adding the override to web would be speculative. One `hover` capability query is deliberately left in place — `.buzz-wave-hover-trigger` in `animations.css` gates a decorative wave-hand animation on `(hover: hover) and (pointer: fine)`. That is a cosmetic flourish rather than an affordance, so it stays inert on affected hosts instead of widening this diff. ## Reproducing The trigger is **an integrated touch digitizer anywhere on the machine**, not the display you are actually working on. This was found on a touch-capable laptop docked to an ordinary non-touch external monitor, driven entirely by a mouse — so "I'm on a desktop monitor" does not rule you out. Check with: ```js matchMedia('(hover: hover)').matches // false ⇒ affected ``` Not reproducible on macOS, or on a Windows machine with no digitizer at all — `hover: hover` is true there and every affordance works normally. If you are on such a host, emulate it in devtools by forcing `hover: none` / `pointer: coarse`, then open a channel's member list and hover a row: no action menu appears. ## Tradeoff worth naming On a genuine touch-only device, a bare `&:hover` can latch after a tap and stay applied until the next interaction, where the media-query default would have suppressed it. That is the real cost of this change. The judgement here is that a stuck hover style is a cosmetic annoyance, while an unreachable "remove member" button is a functional dead end — and that the affected hosts are overwhelmingly mouse-driven machines that merely *happen* to ship a digitizer, as the `MousePresent = True` reading above shows. If you would rather scope this to `@media not (hover: hover)` as an additive fallback instead of overriding the variant, I am happy to rework it. Signed-off-by: sumit-m <33051892+sumit-m@users.noreply.github.com>
## Summary - report the relay as connected immediately after socket open and successful AUTH - keep rate-limited subscription replay, the connect promise, and reconnect listeners unchanged - cover authenticated reconnect while replay is held behind the shared rate-limit gate ## Why After WARP recovery, the socket could reopen and authenticate successfully while subscription replay waited behind the existing rate-limit gate. `connect()` kept `ConnectionState` at `reconnecting` during that intentional delay, so the desktop displayed “Can’t reach the relay” despite authenticated traffic already flowing. This is separate from block#3774: that fix keeps routine operations from bypassing scheduled reconnect backoff. This patch preserves those protections and only corrects the authenticated transport-state boundary. ## Failure semantics If replay fails after the early `connected` transition, the existing `replayLiveSubscriptions()` catch calls `resetConnection()`, closes the socket, returns state to `reconnecting`, and schedules recovery. Operation waiters and reconnect notifications still do not complete until replay succeeds. ## Validation At commit `c8a4308e1079f4f9e6a72f0f0bfba280fe822ec0` with a clean working tree: - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 3,847 passed - `pnpm --dir desktop check` — passed; two pre-existing informational template-literal notices - `pnpm --dir desktop exec playwright test tests/e2e/relay-reconnect.spec.ts` — 8 passed - regression test proven red before the production ordering change (`reconnecting` after 3 seconds) and green after it Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
…ock#3811) Local `desktop-tauri-clippy` fails on macOS with dead-code errors for `PROD_ORIGIN`, `DEV_ORIGIN`, and `is_trusted_media_origin`, which are only used inside `#[cfg(target_os = "linux")] enable_media_capture`. The items are intentionally platform-independent so unit tests run everywhere. Added `cfg_attr` allow attribute to suppress the warnings on non-Linux targets. Since [block#3607](block#3607), this affects all Rust developers on macOS. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Buzz's NIP-11 document advertised `limitation.max_limit: 10_000`, but the effective websocket REQ page ceiling was `1_000` — a 10x lie. The websocket REQ path never sets `EventQuery::max_limit`, so `query_events` applied its own `unwrap_or(1000)` clamp to every historical query. Only the COUNT fallback (`apply_count_fallback_limit`) ever raises that clamp. A client that trusts the advertised value asks for 10,000 events, silently receives 1,000, and — with no error and no continuation signal — reads that short page as exhaustion. Up to 9,000 events are dropped without anyone noticing. `MAX_HISTORICAL_LIMIT = 2_000` in `handlers/req.rs` was dead weight for the same reason: nothing clamped to 2,000 could survive the DB's 1,000 clamp one layer down. ## Change `buzz_db::DEFAULT_MAX_PAGE_LIMIT` (`1_000`) is now the single source of truth. It is the `query_events` clamp default, the value both REQ clamp sites use, and the value advertised as NIP-11 `max_limit`. `MAX_HISTORICAL_LIMIT` is removed rather than re-pointed — an alias for a constant used four lines away adds a name without adding meaning. The NIP-50 search path carries a second, independent bound. It clamps its emission target to the shared ceiling like any other REQ, but how many FTS candidates it will scan was bounded separately, by a bare 10-page loop over 100-hit pages. That product only coincidentally equalled the ceiling, so raising the ceiling — or shrinking a page — would shrink the scan relative to what clients may now request, degrading search quality while nothing in the code registered the change. The page count is now ceiling-divided from `DEFAULT_MAX_PAGE_LIMIT` over a named `SEARCH_PAGE_SIZE`, so the scan budget tracks the advertised ceiling by construction. That budget is a resource policy, not a delivery promise. It bounds candidates *scanned*, not events *emitted*: post-filtering (NIP-01 match, channel access, reader visibility, dedup) discards an unpredictable share of every page, so a search result smaller than the requested limit remains possible. This is not a NIP-11 violation — `max_limit` is defined as a clamp the relay applies to a requested `limit`, not a guaranteed count in the response. Two guards hold the pair together: - `req_filter_limit_clamps_to_advertised_nip11_max_limit` reads `max_limit` back out of a built `RelayInfo` and asserts the REQ path clamps to exactly that number. - `search_scan_capacity_covers_advertised_nip11_max_limit` asserts the scan budget covers exactly one advertised ceiling's worth of candidates — no less, and with no spare page of slack, so the derivation can't be quietly replaced by a hand-tuned constant that happens to pass today. ## Behavior Websocket behavior is unchanged: 1,000 was already the real ceiling on every path, including NIP-50. The advertisement now tells the truth about it. Raising the effective limit is a capacity decision and is deliberately not made here. The generic HTTP bridge's page-2+ offsets do change, as a consequence of the corrected clamp. `extract_page_offset` sizes a page from `query.limit` *before* the DB clamp applies, so an absent limit previously produced an offset of 2,000 and a requested 1,500 produced 1,500 — while the page actually returned held at most 1,000 rows. Both now produce 1,000. This corrects paging that had been skipping rows the previous page never returned; `extract_page_offset_sizes_pages_from_clamped_limit` locks it down. ## Scope note The bridge's per-endpoint ceilings — `BRIDGE_WINDOW_MAX_LIMIT` (200) for channel windows and `BRIDGE_THREAD_MAX_LIMIT` (500) for thread reads — are endpoint contracts on a non-NIP-01 transport, not values NIP-11 speaks for, and are unchanged. Fixes block#3757 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Why The Profile settings action still says “Sign Out,” while its confirmation action says “Delete My Data.” Both buttons trigger the same destructive local-data wipe and should name it consistently. ## What - Label both destructive actions “Delete my data” - Assert the matching section and confirmation labels in the existing Playwright coverage ## Risk Assessment Low — copy and test assertions only; sign-out behavior is unchanged. ## References - Follow-up to block#2208 - block#2216 also touches this copy and should preserve “Delete my data” when rebased - `just desktop-check` - `just desktop-test` (3,275 tests) - Desktop E2E build and sign-out Playwright spec (2 tests) Generated with Codex Signed-off-by: Bradley Axen <baxen@squareup.com>
First slice of block#2216, scoped to the system/status lines in the chat timeline. ## Why Two problems on the same surface. **Clearing a channel topic renders as empty quotes.** The relay reports a clear as a `topic_changed` event carrying an empty string — there's no separate "cleared" event type. So the timeline printed: > Alice > changed the topic to “” which reads as if the topic were *set to* two quote marks. Same for purpose. **The membership caption reads like a headline, not a metadata line.** `title` and `action` render on separate lines — the member's name sits in the header row with the avatar and timestamp, and the caption sits beneath it. So the caption was "was added by Alice Chen" standing alone under a name, while its siblings on that same line are "joined the channel" and "left the channel". ## What - Blank, missing, or whitespace-only topic/purpose now reads **"cleared the channel topic"** / **"cleared the channel purpose"**. - Membership captions drop "was": **"added by Alice Chen"**, matching "joined the channel" and "left the channel". - The wording moves to `lib/systemEventCopy.ts` as a pure function, so it's assertable in a unit test instead of only reachable through the DOM. That also removes two JSX fragments from `SystemMessageRow.tsx`, taking it 911 → 900 lines. ## Two E2E assertions this exposed Both were measuring something other than what they claimed, and the copy change tipped them over. Neither is a product bug, but both would have failed the next person too. 1. **`mentions.spec.ts:1245`** asserted a button was un-underlined while the mouse was still parked from a previous `hover()`. Any reflow — new rows, scroll-to-bottom, a different text wrap — can slide that button under the stationary pointer, so the assertion measured *where the mouse happened to be* rather than the resting style. Dropping four characters changed the text wrap, changed the row height, changed the scroll offset, and the pointer landed on it. Now parks the pointer off-target first. 2. **`mentions.spec.ts:1253`** used a bare `role=tooltip` lookup. Once the first tooltip animates out while the second opens, two elements match and strict mode trips. Now scopes to the open tooltip via `:not([data-state="closed"])`. ## Deliberately out of scope - **Timestamps.** The day divider, per-message clock times, the Inbox thread pane, and the inbox list have three divergent date implementations and none fully match the writing standard's Today/Yesterday/weekday/date progression. That's its own slice of block#2216. - **Whose avatar shows.** An addition puts the *added* member in the header; a removal puts the *remover* there. Possibly intentional, but it's a design question, not copy. - **`the channel` vs `this channel`.** joined/left/removed say "the channel"; created/archived/unarchived say "this channel". Worth normalizing, but it touches lines this PR otherwise leaves alone. ## Validation - `pnpm check`, `pnpm typecheck` — clean - Unit: **3781/3781**, including 6 new tests in `systemEventCopy.test.mjs` covering set/blank/undefined/null/whitespace for both fields, plus a guard that no variant can emit empty quotes - Smoke E2E `mentions` + `messaging`: **85/85** - The previously fragile test run with `--repeat-each=5`: **5/5** Signed-off-by: Clay Delk <clay.delk@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary Update Amp's runtime catalog description to use its current tagline: > The coding agent and development environment that runs anywhere and everywhere. ### Related issue N/A. This follows the Amp description update in block#3758. ### Testing * `pnpm -C desktop check` * `pnpm -C desktop typecheck` * `pnpm -C desktop test` (3,835 passed) No screenshot is included because this changes only the catalog description text. It does not change layout or interaction behavior. Signed-off-by: AJKemps <AJKemps@users.noreply.github.com> Co-authored-by: AJKemps <AJKemps@users.noreply.github.com> Co-authored-by: Alex Kemper <alex@ampcode.com>
## Summary
Adds a locally stored **NIP-49 encrypted key backup** (`ncryptsec`) to
the desktop app, per the plan reviewed in buzz-development (Rev 3,
approved 9/10 by Wren; implementation also reviewed and approved 9/10).
**Two-artifact design — canonical bytes originate entirely in Rust:**
- `create_ncryptsec_backup` runs under the `identity_mutation` lock:
encrypt → decrypt-verify against the live pubkey → atomic `0o600` write
to `{app_data_dir}/identity.ncryptsec` → reread/byte-compare → return
the exact persisted bytes. The frontend never re-derives or re-encrypts.
- `save_ncryptsec_copy` writes a portable copy via the save dialog
(parse-gated, secret-file semantics) and never mutates canonical state.
- `generate_backup_passphrase`: 6 words from the EFF short wordlist via
`OsRng` (custom passphrases min 12 chars).
- Import accepts `ncryptsec1` with optional password; the raw-`nsec`
path is untouched. Different-pubkey import and sign-out wipe the
app-managed backup (post-commit, best-effort — a failed import can never
destroy the still-live identity's backup; regression-tested).
**Never-relay guarantee (egress guard + tripwires):**
- `egress_guard.rs` fail-closed at all 8 `/events` submission boundaries
(relay submit funnel, 3× `relay.rs`, huddle STT, both engram submitters,
native WS choke point), rejecting `ncryptsec1`/`NCRYPTSEC1` in text and
binary frames. Scope is deliberately ncryptsec-only: pairing
intentionally carries raw nsec inside its encrypted session.
- Site-granular `/events` inventory tripwire: per-file (`/events` count,
guard-call count) pairs; unlisted files expect zero. Mutation-style
tests prove a ninth site in an existing file, a removed guard, and a new
unlisted file all fail the scan.
- ncryptsec source-allowlist scans in **both** trees (Rust + TS).
**Frontend:** onboarding `BackupStep` is encrypted-by-default — the
default path never invokes `get_nsec` (e2e asserts the command log).
Raw-nsec export stays behind an explicit click with prior semantics.
Shared `EncryptedBackupCreator` powers onboarding + a new settings row;
the import form auto-switches to encrypted mode on `ncryptsec1` paste
(case-insensitive HRP).
**Open product call for @tlongwell-block:** onboarding default is
*encrypted* in this PR; flipping to raw-default is a small change either
way (documented in the plan).
Review history: plan Rev 3 and the implementation were both iterated
with Wren to 9/10 (two blockers from round 1 — import ordering,
inventory granularity — plus an uppercase-bech32 hardening gap, all
fixed in `dde37183e`). Thread: buzz-development.
### Related issue
Follow-up to the direction explored in block#385 (NIP-PB, closed) — this
ships local NIP-49 (the standard) instead of a new NIP. No open
duplicate found.
### Testing
All at exactly `dde37183e` (same shell, HEAD verified):
- `cargo test` — 1680 passed / 0 failed / 14 ignored (includes a
deliberate ~70s log_n-18 NIP-49 round trip, spec vector, wrong-password,
NFKC, uppercase-vector decrypt, injection test per egress boundary,
inventory mutation tests, import-ordering regression tests)
- `cargo clippy --all-targets -- -D warnings` — clean; `cargo fmt
--check` — clean
- `pnpm typecheck` — clean; JS unit suite 3529/3529; biome (repo-pinned
2.4.16) clean
- Playwright `onboarding-backup` / `onboarding` /
`onboarding-agent-defaults` / `profile-nsec-reveal` — 86 passed, 1 known
avatar-reservation flake (passed on rerun; untouched by this diff).
`passThroughBackupStep` now exercises the encrypted default, so every
downstream onboarding spec covers the new path.
- Note: browser e2e fakes the crypto via the mock bridge (fixed
spec-vector blob); decryption correctness is proven in the Rust tests.
## Latest onboarding integration
The current head adds an additive `IdentityInfo.storage` field
(`ephemeral`, `system-keyring`, `local-file`, or `environment`) so
onboarding can accurately explain where the active identity is
protected. It surfaces storage metadata only—never key material—and
leaves the existing lost/keyring-locked recovery behavior intact.
---------
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
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>
…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>
## 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>
## 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>
…lock#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 block#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>
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 block#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>
# Kubernetes backend plugin (crates/buzz-backend-kubernetes) + desktop deploy path Implements docs/remote-agents.md (merged @ 28ae6cd) as ONE PR: the provider binary, the desktop changes that make it work, the harness inactivity reaper, the Sprig image, and the conformance/live-test suites. Channel: buzz-remote-agents (29414326-dba7-402d-b384-b1b34d63a2e6), thread c42b70ef. ## What's here (by lane) - **crates/buzz-backend-kubernetes** (Dawn): stdin/stdout JSON provider, info + deploy; pure classify.rs (one match arm per spec state-machine row); reconcile/GC with ownership-marker gate + same-clock orphan check; per-attempt immutable Secrets; three-tier env with clear-then-write authoritative tier. - **Desktop** (Mari): KD3 launch block from resolved descriptor, KD5 pre-secret negotiation gate (resolve-once → stage-and-digest → info → protocol gate → deploy), KD1 Windows extension strip, bundling (externalBin + Justfile + release/canary workflows + stub loops), tauri.windows.conf.json platform override (Decision B: no Windows artifact). - **buzz-acp** (Max): KD4 BUZZ_ACP_EXIT_AFTER_INACTIVITY reaper (pool-independent; reset only at accepted dispatch; in-flight turn/heartbeat defers, never resets); BUZZ_ACP_EXIT_AFTER_INACTIVITY + BUZZ_ACP_NO_PRESENCE reserved. KD8 fix. - **Image + tests** (Perci): Dockerfile.sprig (digest-pinned bases, exec buzz-acp PID 1, relay-scoped credential config), image contract script, provider conformance suites (golden wire fixtures shared with desktop tests), live-local runbook (namespace-scoped, shared-cluster safe). - **Docs** (Sami, first commit): citation re-pin c1bca1b → 28ae6cd (44/49 were already byte-exact; 3 offsets fixed) + I3 presence-bound correction (below). ## Named spec deviations (deliberate, each with rationale) 1. **No baked default image yet.** ghcr.io/block/buzz-sprig is unpublished (verified: anonymous pull 403 vs control 200). Omitted `image` returns an in-band field-required error instead of a default. 2. **Image override STRICTER than spec §Image:** digest-only (`name@sha256:<64hex>`); ALL tags rejected; `name:tag@digest` normalized. With no baked default the override is the only path, so tag-acceptance would make mutability the v1 norm. Strictness is reversible; a moved tag under an nsec is not. Baked digest default + tag re-acceptance = follow-up with image publish. 3. **imagePullSecrets not in schema (v1).** Explicit user images may rely on namespace-preprovisioned pull credentials — the substrate boundary. Field added only if the publish decision proves it necessary. 9-field budget intact. 4. **Decision A closed: writable empty workspace.** Nest projection = named follow-up; no image-side scaffolding. 5. **Decision D overridden by Tyler (event b55398d8):** provider ships bundled with the desktop like buzz-acp/buzz-agent; spec §Distribution's separate release workflow deleted for v1. 6. **I3/vision presence bound corrected 90s → 180s.** PRESENCE_TTL_SECS moved in block#3783 during this spec's base→merge window; the number was inherited, not chosen. Spec :206/:216/:928 + inline quote + VISION_REMOTE_AGENTS.md:59 corrected. ← Tyler: the vision is your document; this edit is flagged for your explicit eyes. 7. **Spec citations are pinned to 28ae6cd** (main at spec merge) and resolve there, not at this PR's head — this PR's own lanes move crates/buzz-acp/src/lib.rs by ~100 lines (19 citations across KD4/KD6/KD7/ §Stop/§Launch data). Known Defects rows fixed BY this PR retire on merge; the section documents main as of the pin. 8. **KD7 grace tension declared:** pod terminationGracePeriodSeconds=60 vs KD7's measured ~87s shutdown tail at parallelism 10 (~197s at cap 32). KD7 is ruled out of scope, so L1-3's "enough grace for full graceful shutdown" is NOT met at default config — deliberate, resolved by the KD7 follow-up, not silently. ## Question for Tyler Will ghcr.io/block/buzz-sprig publish PUBLIC? If private-by-policy, §Image needs an imagePullSecrets story before the baked-default follow-up can land. ## Out of scope (named follow-ups) KD6 exit-code contract + KD7 shutdown budget (gate OnFailure), OnFailure restart policy, Windows provider binary, PVCs/nest projection, mesh deployability, sprig image publish workflow + baked multi-arch digest default. ## Reproduce locally (four traps that cost us real time) **1. Git hooks inherit the invoking shell's PATH — pin the shell, not just your verification commands.** `rust-toolchain.toml` pins `1.95.0`, but the rustup shim that honors that pin lives in `~/.cargo/bin`. If Homebrew's cargo is earlier on PATH, `cargo` in this repo is 1.89.0, which cannot build the workspace at all: ``` $ /opt/homebrew/bin/cargo check -p buzz-db error: rustc 1.89.0 is not supported by the following packages: sqlx@0.9.0 requires rustc 1.94.0 ... # exit 101 ``` Verifying with `PATH="$HOME/.cargo/bin:$PATH" cargo test` does *not* protect the push: lefthook's `pre-push` → `just test-unit` re-resolves `cargo` from the shell's own PATH, so a green local run is followed by a hook failure on a crate you never touched. Export the PATH for the whole shell, not per-command. This bit twice. **2. Line-scope your mutations, or the mutation edits its own detector.** When mutation-testing the respond-to guard, a whole-file `sed` on the mode literal touches 5 sites — the guard *and* the fixtures/assertions that test it. The mutation and its detector move together and the suite stays green, which reads as "this code is dead" when it actually means "you deleted the experiment": ``` # WRONG — 5 sites, guard and tests mutate together $ sed -i '' 's/"allowlist"/"allowlist-DISABLED"/g' src/env.rs test result: ok. 145 passed; 0 failed # false survivor # RIGHT — 1 site, anchored to the guard's own definition line $ sed -i '' '/^const RESPOND_TO_ALLOWLIST/s/"allowlist"/"allowlist-DISABLED"/' src/env.rs failures: env::tests::allowlist_mode_with_an_empty_list_is_refused env::tests::an_allowlist_entry_that_is_not_64_hex_is_refused test result: FAILED. 143 passed; 2 failed # real kill ``` Restore by copying a pristine file back and confirming `git diff --stat` is empty, not by re-running an inverse `sed`. **3. A completeness guard is not a correctness guard.** The shared wire fixture `tests/fixtures/provider-wire/deploy-full-launch.request.json` passed every test we had while containing four classes of invented data (wrong `respond_to` encoding, an env key no emitter writes, allowlist entries that fail the harness's own 64-hex rule, a `launch.env` key from no descriptor layer). The provider's tests could not have caught this: its types are deliberately indifferent to these values (`Option<String>`, `Vec<String>`, arbitrary map), so "the provider parses it" was never evidence that the desktop emits it. The fix was not a stronger provider assertion but a rule about provenance — "recorded" means executed-and-transcribed, and the desktop's whole-object equality test is the only enforcement that can exist. See the fixture README. **4. Every drift this arc was a value that agreed with itself.** Five invented values were found, and not one was caught by an assertion failing — each was caught by someone asking where a value came from. A named constant referenced symbolically on both the fixture and assertion side. A `sed` that mutated its own detector. Six probe rows that all died at the same unrelated error. A descriptor struct literal compared against a fixture built from that literal (`launch.args: ["run","--session"]`, which the resolver actually returns as `["acp"]`). The general defense is not more assertions but provenance: a stub is a control that varies nothing, and the more faithful it looks the better it hides. Ask what executed, not what passed. *Fixture-test determinism caveat (post-verification, Quinn + Dawn).* The desktop's whole-object fixture test calls the real resolver, which consults a process-global harness registry whose own docs require `registry_test_lock` for any test touching it. The fixture test holds no lock and is nonetheless deterministic — but by containment, not by ordering. Measured, not derived: planting a definition with `id: "goose"` directly into the registry (bypassing the loader) changes the resolved descriptor from `args: ["acp"]` to `args: ["--poisoned"]`, so `resolve_effective_harness_descriptor` **does** reach the registry for this id — it does not short-circuit on the builtin table first. Two controls discriminate: an empty registry and a registry poisoned under a *different* id both return `["acp"]`. What actually protects the test is that the registry has exactly one writer (`update_loaded_harness_registry`, reached only via `warm_harness_registry_from_dir`) — but that writer concatenates **two** sources of unequal strength (`custom_harnesses.rs:319-326`). Custom files pass through `load_custom_harnesses`, whose `check_id_collision` rejects the reserved builtin id `goose` case-insensitively at the loader — and that leg is tested (`load_applies_id_collision_check` writes a real `goose.json` and asserts the loader drops it). Preset definitions (`preset_harness_definitions`, `presets.rs:177-193`) are a bare `.map` over `PRESET_HARNESSES` with **no collision check** — exhaustive call-site enumeration at `60007fda4` finds four production `check_id_collision` sites, none on the preset path. That leg holds only because `goose` is not in the preset table today (intersection of TIER1 and preset ids is empty) — executed, not just read: adding a preset with `id: "goose"`, `args: ["--poisoned"]` and warming via the normal preset-only path (`warm_harness_registry_from_dir(None)`, no custom dir, no direct writer) flips the fixture's emitted `launch.args` from `["acp"]` to `["--poisoned"]` at `60007fda4`, command/env/policy_env unchanged. So: no test in the suite can put a `goose` entry in the registry via the custom path, and no preset currently carries one, so no interleaving can perturb this fixture — containment with one checked leg and one coincidental one. A future fixture built on a **non-builtin** runtime id has no containment at all — it would be order-dependent against whatever registry-writing test ran last and must take the lock. *Late instance, found while reviewing the mode guard.* The guard exact-matches `respond_to` untrimmed and case-sensitively, which is only correct if clap's `ValueEnum` derive is case-sensitive. `config.rs` gives two answers: the derive at `:448-453` carries no `ignore_case`, while the crate's own tests call `RespondTo::from_str(s, true)` — `ignore_case = true`. Reading the source supports either. Measured on the built binary instead: `owner-only` starts, `OWNER-ONLY` / `Owner-Only` / `ALLOWLIST` / `NOBODY` all exit rc=2 `invalid value`. Case-sensitive at the CLI, so the guard is right — and right for a reason the source does not state. The `from_str(_, true)` tests exercise a different surface and are not evidence about the CLI. *Corollary, and the sharper half.* When a test helper **reimplements** production instead of calling it, the helper is a fork — and a fork can be right while production is wrong, or wrong in the same way, and the suite reports green either way. Both `BUZZ_ACP_ALLOWED_*` gates are forked like this: production compares **strings** while the helpers compare **post-parse enums** (`config.rs:2623`) or re-derive the split (`buzz-cli/.../channels.rs:1296`). Production and the helper each carry their *own* copy of the empty-entry filter (`:1025` and `:1300`), so fixing one says nothing about the other. Measured on `buzz-cli`, restoring byte-exact between runs: | tree | result | |---|---| | baseline | 274 passed | | drop the empty-filter in **production** only (the real fix) | **274 passed** — no signal | | drop it in the **test helper** only | **273 passed, 1 failed** (`channels.rs:1338`) | Two independent defects, stacked, and worse together than either alone: production can be fixed with no test ever noticing, *and* the helper cannot be corrected without a false alarm demanding the bug back. The root cause is one bit of type information — `check_allowed_channel_add_policy(allowed_raw: &str, ..)` cannot represent "unset", while production reads `env::var(..) -> Result`, where unset and `""` are different states. A helper whose parameter type can't represent all of production's input states isn't testing production's states — it's testing a subset it silently chose. Same family as the struct-literal descriptor and the fixture drift: the test and the thing it tests agreeing with each other, rather than the test measuring the thing. Neither defect is in this PR's diff (`git diff --name-only 28ae6cd <head> -- crates/buzz-cli` is empty); both are now filed as NIP-34 issues on this repo: the fail-open + fork-helper defect at issue event `0524a4113f2d97fd…` and the respond-to self-lock at `e32837498969b5e7…` (filed 2026-08-02 after Quinn measured that no prior filing existed — zero hits on GitHub `block/buzz` open *or* closed and zero on the relay's kind:1621 issues, against working positive controls). The prescription was itself mutation-tested before being written down: repairing the fork's signature (`Option<&str>` + assertion → `None`) still let the reintroduced production bug ship 274-green — an expressive fork is still a fork; it never executes production. So the `buzz-cli` fix has **three parts and one explicit keep**: drop the production filter; **delete** the helper and point its tests at the real `cmd_set_add_policy` (which self-discriminates by error variant — `Usage` = refused, `Network(BadScheme)` = passed the gate — no relay needed); serialize the env-var tests behind one **`tokio::sync::Mutex::const_new`** lock taken with `.lock().await`, including the pre-existing `:1362` integration test (the fork was silently buying test isolation — without the lock, parallel runs flake nondeterministically; a `std::sync::Mutex` held across `.await` trips `clippy::await_holding_lock` under `-D warnings`); and **keep** the then-dead `!allowed.is_empty()` clause with a comment saying why. It is unreachable-false (`split(',')` never yields an empty vec), but it is the only thing that keeps the reintroduced production bug detectable — mutation-tested: on a tree that deletes the clause, reintroducing the empty-filter bug survives 275/0, because `""`/`","`/`" "` refuse either way and the filter goes semantically inert. Dead code can be load-bearing for tests: "provably unreachable" is an argument about behavior, never about coverage. When a helper forks production, the fix has to delete the fork: any change that leaves two implementations standing can only ever be verified against the one the tests call. *Final shape:* the keep and the broad lock are both artifacts of the fork surviving in some form. The extraction variant (Dawn, mutation-tested at `60007fda4`) removes the tension: extract one `check_channel_add_policy_allowed(Option<&str>, &str)` that **production calls**, with the `Option` placed at the env boundary where the `Result<String, VarError>` bit actually lives. 5/6 mutants killed; the empty-filter survivor is proven **equivalent** (exhaustive 6174-pair check, 0 divergences, with a diverging negative control; independently re-derived by a second generator — different tokens and shape — 0 divergences on admitted policies, 500 on a non-admitted control), not a coverage hole — on a one-implementation tree there is no fork left to witness, so no dead clause needs keeping. One scope line on that equivalence: it is **caller-conditional**, a property of the only current caller, not of the gate function — `cmd_set_add_policy`'s own match at `:1027-1034` admits only three policies before the gate runs; a second caller reaching the gate with arbitrary strings resurrects m1 as a real hole. The lock does not disappear, it narrows (Dawn's own correction, caught by Mari): lock exactly the tests that mutate the process env — three-plus-one on a fork tree, two on the extraction tree — behind one `tokio::sync::Mutex`, and the lock is part of the assertion, not hygiene: with it deleted, the gate test fails 8/8 runs deterministically by receiving `Network(BadScheme)` where it expects `Usage` — the unset test's `remove_var` clobbers the other's `set_var`, and **the gate test passes straight through the gate**, a false negative on the exact authz assertion the test exists to make. State it as an outcome: these two tests must not observe each other's env writes. 276/0 stable across 5 parallel runs, clippy `-D warnings` clean; independently verified (patch applied to a second worktree: result blob `d67e584be` matches the patch index, full mutant matrix reproduces row for row). One new row no earlier prescription covered: collapsing unset into `Some("")` fails **closed** — an unconfigured deployment refuses every policy — killed by the unset test. Patch: `OUTBOX/BUZZ_CLI_ADD_POLICY_GATE_EXTRACT_FIX.patch`. The filed issue (`0524a411…`) carries the fork-shape prescription; whoever picks it up should prefer the extraction shape, drop the dead-clause keep with it, and keep part 3 outcome-shaped: serialize whichever tests mutate the env. ## Verification (final HEAD `60007fda4`) - Full touched-package suites at each integration merge (log in plan file). At candidate parent `00e5b5fe9`: buzz-backend-kubernetes 154, buzz-acp 673, desktop tauri 2100+3, pnpm 3908, workspace clippy/fmt/tsc all clean. The only delta to `60007fda4` is one character in `scripts/test-k8s-sprig-image-live.sh` (heredoc escape so the readlink probe evaluates pod-side, not host-side at render); `crates/` tree hash is byte-identical at both SHAs, so the Rust receipts attach by tree identity. buzz-backend-kubernetes suite re-run in-shell at `HEAD == 60007fd`: 154 passed. - Adversarial one-HEAD gate (Sami): guard matrix 12/12, predicate mutants 7/7, doomed-invocation finding closed end-to-end; tree-hash carry to `60007fda4` confirmed (crates/buzz-backend-kubernetes blob unchanged). - Live-local pass per TESTING.md + skill-buzz-testing (Perci, at `60007fda4`): explicit `docker-desktop` context, digest-qualified image imported into node containerd `k8s.io` namespace, pull policy `Never`; pod printed `DIGEST_ABI_OK`, `resolved_spec` and `image_id` both the exact requested digest, script exit 0. Dedicated per-run namespace, ownership labels on every object, scoped cleanup verified empty after. - Implementation review (Wren) at `60007fda4`: 9.6 minimalness / 9.4 elegance / 9.3 correctness, no blocker. - `origin/eva/k8s-backend` == `60007fda4` (ls-remote verified; SHA identity is byte identity). --------- Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
…and swipe gestures (block#3778) ## Problem Two related gaps in global back/forward navigation. Fixes block#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 block#3775 (filed alongside this fix). block#3078 / block#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>
…t sprig image to published digest (block#4392) ## What Two changes, both fallout/follow-up from block#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 block#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 block#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>
…ent-acp (block#4395) `claude-agent-acp` (since v0.6.0 / PR block#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>
### 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>
…g keystrokes (block#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 block#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 8eb7680 - `pnpm check` + `tsc` clean, `pnpm test` 3926/3926; - new spec 3/3 green (and 3/3 red on the unfixed component); - pre-push lefthook: desktop-test, desktop-check, desktop-tauri-checks, rust-tests, mobile-test all green. --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…block#4524) ## Summary Official Linux desktop packages (`.deb` / AppImage) are built without `--features mesh-llm`, so they ship the `mesh_llm_stubs` backend and Settings → Compute always fails with `mesh-llm feature not enabled`. This PR adds the feature flag to the two Linux build commands: - `release.yml` → `release-linux` job - `linux-canary.yml` → canary build That's the whole diff — 2 lines. Fixes block#3788 (Linux); see also block#3841 (dup with UI-gating PR block#3914) and the Windows twin block#2836/block#3223. ## Why no native prebuild step (unlike the macOS job) The macOS job carries Metal llama prebuild/cache steps from block#798. Linux doesn't need an equivalent: - `mesh-llm-host-runtime` is compiled with `dynamic-native-runtime` and installs the recommended runtime on first use (verified by sha256 checksum over HTTPS; upstream's signature verification path is not yet implemented — default policy is `RequireChecksum`, per `mesh-llm-runtime-install/src/lib.rs`) (`desktop/src-tauri/src/mesh_llm/mod.rs` — `initialize_mesh_native_runtime`), so release builds work on clean machines without bundling llama.cpp. - Upstream publishes Linux x86_64/aarch64 runtime bundles for the pinned `v0.74.0` line, and `scripts/ensure-mesh-native-runtime.sh` already maps `meshllm-native-runtime-linux-x86_64-cpu` / `linux-aarch64-cpu` for local/e2e use. - The unmerged branch `micn/mesh-node-download` (`96f29417a`) treats even the macOS prebuild steps as removable dead weight for the same reason. ## Background The omission is historical drift, not a decision: Linux packaging predates the mesh feature flag (block#693), mesh became opt-in for build-cost/reliability reasons (block#823, block#1183), and block#1221 re-enabled it for releases by editing only the macOS build line. `release-linux` and the later `linux-canary` copy were never revisited. The mesh shutdown hard-exit/relaunch path is gated `all(mesh-llm, target_os = "macos")` because ggml/Metal destructors abort on macOS; ordinary mesh shutdown (`shutdown_mesh_runtime`) is cross-platform, so Linux falls through to the generic path. ## Validation - [x] `./bin/cargo check --manifest-path desktop/src-tauri/Cargo.toml --features mesh-llm` green at base `2c0ac2467` (feature graph compiles at the pinned v0.74.0 line) - [ ] Linux canary run with this change: AppImage/.deb build succeeds and binary contains real `mesh_llm` symbols (not `mesh_llm_stubs`) - [ ] Installed package: cold-start → Settings → Compute → runtime download → serve → clean shutdown The last two need a Linux run/host. **Note (from review):** `linux-canary.yml` is `workflow_dispatch`-only and its `Require main` step rejects non-main refs, so the canary cannot run on this branch pre-merge — and `.github/workflows/**` matches no ci.yml paths-filter, so this PR's own CI does not exercise the changed lines. Validation sequencing is therefore merge → dispatch linux-canary on main → live-package pass, with a trivial 2-line revert as the escape hatch. Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## Summary
- Refine the mobile composer with compact and expanded states, shared
footer fades, haptics, reliable keyboard dismissal, and full-width
camera and photo surfaces.
- Standardize popovers, filters, and section menus with consistent type,
strokes, radii, spacing, icons, and destructive styling.
- Align message presentation with desktop through consistent system
rows, typing and loading feedback, emoji placement, and predictable
photo viewing.
## Validation
- `just mobile-check`
- `just mobile-test` — 1,037 passed, 1 skipped
- Tested on Pixel 10 and a connected iPhone
## Snapshots
<table>
<tr>
<td align="center">Compact composer</td>
<td align="center">Attachment menu</td>
<td align="center">Recent photos</td>
</tr>
<tr>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--01-compact-composer.png"
width="260" /></td>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--02-attachment-menu.png"
width="260" /></td>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--03-photo-surface.png"
width="260" /></td>
</tr>
</table>
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
…ue model override (block#3580) All seven normalized config fields resolve through sanitized `InheritedConfigTiers` passed wholesale to `read_config_surface`. The reader's precedence tiers now match spawn's Layer 2b exactly — including harness-definition env — and the equal-value model-override regression is fixed. ## Changes **`config_bridge/types.rs`** — add `InheritedConfigTiers`: persona env, global env, harness definition env, structured model/provider/prompt for both tiers. Add `HarnessDefault` `ConfigOrigin` variant for harness-definition env values. **`commands/agent_config.rs`** — `build_inherited_tiers` now resolves the harness definition env using the same lookup path as spawn (`record.runtime` → `persona.runtime` → empty string) and applies `sanitize_inherited_env` to it. `resolve_config_surface` is unchanged in shape — tiers passed to the reader now include `definition_env`. **`config_bridge/reader.rs`** — `env_candidates` extended to 4-element return (record, persona, global, definition). All five field builders that use env candidates now include the definition-env slot below global env and above the structured block, matching spawn Layer 2b. Magic `configured[..6]` slice replaced with `configured[..configured.len()-1]` (named split: all non-file candidates). Equal-value model-override arm falls through to the normal resolve path instead of early-returning `RuntimeOverride`, so the panel shows the baseline origin (e.g. `BuzzExplicit`) rather than a spurious "Live override" label for a no-op switch. **`config_bridge/reader_tests_ext.rs`** — three new Layer 2b tests: definition env beats structured persona model, global env beats definition env, reserved-key-absent fallthrough. **`commands/agent_config_tests.rs`** — `genuine_explicit_live_switch_to_same_model_yields_clean_field` updated to assert `origin == BuzzExplicit` (not `RuntimeOverride`); wrapped in `with_no_goose_config` for hermeticity. New `reserved_key_in_definition_env_shaped_map_is_stripped_by_sanitize` test pins the shared sanitization contract. **`AgentConfigPanel.tsx` / `types.ts`** — `HarnessDefault` origin variant wired end-to-end: TS union type and provenance sentence ("Inherited from harness definition"). --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…int dialog (block#4140) Fixes a write-once dead-end in the card mint dialog where a user with an expired OpenAI key had no way to replace it. **Source-aware key status (Rust + TypeScript).** `card_mint_key_status` returns a layer discriminant (`"none" | "global" | "persona" | "agent" | "process"`) instead of a boolean. A pure `resolve_key_layer()` helper in `card.rs` owns the classification logic; `card_mint_key_status` delegates to it, so the production path is under direct test with no duplicate logic. **Mint form always reachable.** The key panel replaces the mint form only for `none` (first-time setup) or when the user explicitly opens the edit panel (`editingKey`). Keys from agent/persona/process layers show an inline provenance row on the mint form with a "Why?" affordance; clicking it shows the read-only redirect in a panel with a Cancel button that returns to the mint form — never a terminal state. **Precise auth-error matching.** The 401 handling in `cardMintStore.ts` matches `startsWith("Card mint failed (HTTP 401 ")` plus the specific `Incorrect API key` text, so avatar-fetch 401 errors pass through unchanged. **Tri-state key status row.** "Using your saved OpenAI key · Update" renders only when `keyLayer === "global"` (confirmed writable key). Query pending or errored hides the row without asserting key existence. **Real tests.** Panel visibility derivations live in `cardMintKeyUtils.ts`, which `AgentCardMintDialog.tsx` imports directly. Tests cover all layers including the mint-reachability invariant (Mint reachable for every resolved layer; only `none` gates setup). - `card.rs` — new `resolve_key_layer()` pure helper; `card_mint_key_status` delegates to it; 999 lines (under the 1000-line ratchet) - `card/tests.rs` — precedence test calls `resolve_key_layer()` directly (no test-local closure); adds process-layer and blank-value cases - `tauriPersonas.ts` — `CardMintKeyLayer` type; updated `cardMintKeyStatus` signature - `cardMintKeyUtils.ts` — `showKeyPanel`, `showReadOnlyRow`, `showCancelButton`, `keyPanelTitle`, and helpers; component imports all of them - `AgentCardMintDialog.tsx` — inline provenance rows for all key sources; key panel only for setup/edit; no unused variables - `cardMintStore.ts` — precise 401 prefix matching - `e2eBridge.ts` — `card_mint_key_status` stub returns `"global"` (not boolean) - Tests: 3959 JS passing, 2089 Rust passing, `tsc --noEmit` clean Related: [block#4406](block#4406) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg <9a232143a0336e34814a6a0f4947e11bc50bc641f21572c886fd0ae1f7a4e867@buzz.block.builderlab.xyz>
…key (block#4406) Two different credentials were presented under the same name throughout the app. The top-level credential field for non-Anthropic providers (OpenAI, OpenAI-compatible, OpenRouter) was labeled "OpenAI API Key" via a hardcoded binary ternary repeated in three dialogs. The card-minting key (`OPENAI_API_KEY`) and the runtime credential (`OPENAI_COMPAT_API_KEY`) have independent endpoint namespaces and consumers (`OPENAI_COMPAT_BASE_URL`/`OPENAI_COMPAT_API_KEY` for runtime, `OPENAI_BASE_URL`/`OPENAI_API_KEY` for minting) and must remain separate — either may require a different credential. This PR makes them impossible to confuse in the UI. ## Changes **Provider-accurate labels from the credential table.** `PROVIDER_CREDENTIAL_CONFIG` entries now carry an `apiKeyLabel` paired with `secretEnvVar` as a discriminated union (both present or neither — a future provider cannot ship a secret field with no label). `getProviderApiKeyLabel(providerId)` is the single source of truth. The three hardcoded ternaries in `AgentConfigFields`, `AgentInstanceEditDialog`, and `AgentDefinitionDialog` are replaced by this helper. Labels: `openai` → "OpenAI Runtime API Key", `openai-compat` → "OpenAI-compatible Runtime API Key", `openrouter` → "OpenRouter API Key" (was incorrectly "OpenAI API Key"), `anthropic` → "Anthropic API Key" (unchanged). **Field names its backing env var.** `PersonaProviderApiKeyField` renders the env var name as a monospace hint beneath the label with `aria-describedby` wiring. All three call sites pass their `secretEnvVar`. A user who sees `OPENAI_API_KEY` in the mint dialog can now confirm at a glance that the credential field shows `OPENAI_COMPAT_API_KEY` — a different key. **Signpost visible at the decision point.** `CARD_MINT_KEY_ANNOTATIONS` is exported from `agentConfigOptions.tsx` (single source) and passed as `keyAnnotations` to all three generic env editors: both `EnvVarsEditor` branches in Agent Defaults, `EditAgentAdvancedFields`, and `PersonaAdvancedFields`. `CardMintKeyCue` — a new small component — renders an always-visible muted cue beneath the Advanced toggle when `OPENAI_API_KEY` is present in global env (Advanced is collapsed by default, so the per-row annotation is invisible until the cue guides the user to open it). **Model discovery error copy.** The `OPENAI_COMPAT_API_KEY required` message now reads "Enter an OpenAI runtime API key (OPENAI_COMPAT_API_KEY) to load OpenAI models." — naming the env var explicitly so it cannot be confused with the mint key. ## Tests - `getProviderApiKeyLabel` helper: pinned correct label per provider including the new distinct labels for `openai` and `openai-compat` - `PersonaProviderApiKeyField` render: semantic label present; env-var hint rendered when `envVarName` provided; `aria-describedby` wired to hint id; hint and describedby absent when prop omitted - `EnvVarsEditor` render: annotation appears exactly once on the matching row; absent for non-matching rows - `personaModelDiscoveryStatus`: pinned new copy naming `OPENAI_COMPAT_API_KEY` explicitly - Playwright: stale `"OpenAI API Key"` selectors updated; new `card-mint-key-cue-visible-and-annotation-in-advanced` test covers Will's exact path (databricks_v2 global provider + saved `OPENAI_API_KEY` → cue visible before opening Advanced → annotation present after opening) ## File sizes (post-format) | File | Lines | |------|-------| | `AgentConfigFields.tsx` | 994 (≤ 996) | | `AgentInstanceEditDialog.tsx` | 1228 (≤ 1228) | | `AgentDefinitionDialog.tsx` | 1045 (≤ 1047) | Related: [block#4140](block#4140) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg <9a232143a0336e34814a6a0f4947e11bc50bc641f21572c886fd0ae1f7a4e867@buzz.block.builderlab.xyz>
…k#4539) ## What When editing an agent, show where it runs. The edit dialog previously showed nothing about the backend; the "Where to run" section only existed in the create flow. This adds a read-only **Run on** section to `AgentInstanceEditDialog`: - **Local agents:** "This computer". - **Provider agents (e.g. Kubernetes):** the provider id plus its saved config rows — context, namespace, image, resources, etc. — with labels humanized from the stored keys and rows in provider-schema order (locators first, request/limit pairs adjacent, alphabetical spillover for unknown providers). - Copy states these are the settings **saved at creation** and that the run location can't be changed afterwards (a new agent is required). ## Design decisions (from thread review with @wren + @sami) - **No provider probe on edit.** `info` is executable work, and its schema reflects the plugin *today* (including a freshly generated random namespace default) — not what this agent was deployed with. The stored record is the only honest source. - **Saved settings, not effective settings.** Optional fields a record omits (e.g. `service_account`) are defaulted by the provider at deploy time; we render only what was persisted and never synthesize today's defaults. - **Safe rendering of opaque provider config.** Values render as safe scalars only; arrays/objects degrade to a summary row (React throws on object children — a hand-edited record must not crash the dialog). Falsy-but-present values (`0`, `false`) render honestly. Secret-shaped keys are redacted using the same word-split heuristic as the create-time `validate_provider_config` gate — one definition of "looks like a secret". The gate already blocks such keys on every app write path; display-side redaction is screenshot hygiene and covers hand-edited records. - **`backendAgentId` intentionally excluded:** deploy-time runtime state written on start, not saved creation intent. - **Read-only, no form state.** The backend is immutable post-create (`UpdateManagedAgentRequest` has no backend field), so the section renders straight from `agent.backend` with no reset effect. - `ADVANCED_FIELDS_MOTION_TRANSITION` was duplicated in both agent dialogs; hoisted to `agentConfigOptions` (also keeps the edit dialog inside the file-size ratchet). ## Testing - Unit contract for `summarizeRunOn` (9 tests): scalar honesty incl. `0`/`false`, structured-value fallback, secret redaction fail-safe, preferred ordering with spillover, key humanization. - Playwright spec (4 tests, registered in the smoke project): kubernetes agent with the exact eight-key record a real create flow persisted, local agent, blox agent (`workstation_name`), and redacted secret-shaped keys from a hypothetical future provider. - `pnpm typecheck`, `pnpm check`, full `pnpm test` (3937 pass) green at this head. - Live screenshots posted in the originating Buzz thread. --------- Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## Summary - show relevant unread threads and active agents when hovering a channel - keep channel-level unread emphasis separate from thread activity dots - make activity rows navigate to the thread and remove demo-only data ## Test plan - `just ci` (all stages passed except the final duplicate native check, which ran out of disk after its earlier clippy pass) - `cd desktop && pnpm exec playwright test tests/e2e/channel-activity-popover.spec.ts --project=smoke` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
**Category:** fix **User Impact:** Users can save password-protected identity backups directly to protected macOS folders such as Downloads. **Problem:** Signed macOS builds could not save a portable `.ncryptsec` backup to Downloads because the atomic writer created an unauthorized sibling temporary file. This surfaced as an “Operation not permitted” error after the user completed backup creation. **Solution:** Portable exports now write only to the exact path authorized by the native Save panel, sync and verify the saved bytes, and refuse to truncate an existing backup. Buzz’s app-managed backup retains its atomic writer and durability guarantees. <details> <summary>File changes</summary> **desktop/src-tauri/src/commands/export_util.rs** Clarifies that secret exports use a dedicated writer compatible with native Save-panel authorization. **desktop/src-tauri/src/commands/identity.rs** Routes portable NIP-49 exports through the Save-panel-compatible writer while preserving canonical app state. **desktop/src-tauri/src/key_backup.rs** Adds an exclusive-create portable writer with owner-only permissions, disk sync, byte verification, and cleanup on failure. Keeps the existing atomic writer for app-managed backups. **desktop/src-tauri/src/key_backup_tests.rs** Covers portable export permissions, absence of sibling files, and preservation of existing backups. </details> ## Reproduction steps 1. Install a signed macOS build containing this change. 2. Open **Settings → Profile → Private key → Create backup** and complete backup creation. 3. Save a fresh `identity.ncryptsec` file into `~/Downloads` and confirm Buzz reports success. 4. Open and verify the saved backup with its password. 5. Repeat the save using an existing filename and confirm Buzz preserves the existing file and asks for a new filename. ## Verification - Full desktop Tauri suite: 2,049 passed, 14 ignored - Diagnostic suite: 3 passed - Focused backup coverage: 30 passed - Tauri clippy (`--all-targets -D warnings`), Rust formatting, and `git diff --check`: passed - Push hooks: org safety, branch skew, and desktop Tauri checks passed Signed-production Downloads smoke remains required after merge because the signing workflow is restricted to `main`. Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary - move **Channel templates** from Communities to Personal settings - always expose the template picker in New Channel, using **None** as the no-template value - create a channel template directly from the picker and select it on return - preview the selected template's current visibility, canvas, agents, and teams - order the channel-creation controls as **Type / Visibility / Template** and mark Template **Optional** - cover populated and empty libraries, inline creation, selection, visibility overrides, mixed agent/team inventory, field order, optional labeling, and settings navigation in Playwright ## Validation Validated at desktop-only tip `76442270c88aa1d533ddca5de9f87cd615183919` with a clean worktree: - focused channel-template Playwright: 2/2 passed - Type / Visibility / Template ordering and muted Optional treatment visually inspected in the replacement screenshot - `git diff --check origin/main...HEAD` passed - PR diff contains exactly nine Desktop files and no Mobile files The pre-push hook was bypassed only for the corrected history push because the inherited Mobile test `keeps follow mode off while a tall newest message stays visible` passes in Linux CI but fails on macOS because its offscreen-child mounting assertion is platform-sensitive. No Mobile code or tests are changed by this PR. ## Screenshot  Originating Buzz channel: `efba7343-e147-48b7-a2aa-15a5f04abc57` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
…aned Node (block#4382) This PR fixes two Windows-specific install failures: Windows Defender blocking the bare `irm|iex` PowerShell install command, and managed Node shims pointing at a version-bumped (now-absent) Node directory. The Defender block (Trojan:Win32/Commando.A!ml) fires before PowerShell runs and is not clearable via Allow. The Node orphaning means shims in the managed npm prefix resolve but fail at runtime with 'node not recognized' because they reference the deleted old Node path. - Replace all three Windows CLI install commands (Goose, Claude, Codex) with a two-step shape — `Invoke-RestMethod` to a named temp file, then execute — to eliminate the dropper signature; a new `windows_install_command!` macro in `discovery/windows_install.rs` generates all three strings at compile time so the shape cannot drift between runtimes - `$ErrorActionPreference='Stop'` aborts on download failure instead of falling through to a missing-file exit-0; `exit $LASTEXITCODE` propagates the vendor script's own exit code - Add `probe_node(executable, expected_version, timeout)` as a bounded seam: stdout goes to a temp file (not a pipe) so no exit path can block on an inherited handle; the child runs in its own process group on Unix so an unconditional group SIGKILL on every exit path terminates all descendants; on Windows `taskkill /T /F` provides the same tree-wide cleanup; `managed_node_runtime_ready()` is a thin wrapper that resolves the managed Node path and calls the seam - Add `resolve_adapter_path()` in `managed_node.rs`: resolves the candidate first, then calls `should_invalidate_adapter()` — a pure predicate that returns `true` only when the resolved path is under `buzz_managed_npm_bin_dir()` AND the managed Node runtime is orphaned; external adapters outside the managed prefix are always preserved Note: CI cannot reproduce the Defender block (no live Defender ML classifier). Proof of fix is structural — the command shape no longer matches the dropper signature. Canary validation on a real Windows machine with Defender enabled is the definitive check. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…4545) ## The bug buzz-agent emitted its `usage_update` notification in exactly one place: after `ctx.run()` returned. Until that moment a turn's token counters lived only in the prompt task's stack frame. **A turn killed mid-flight reported nothing at all** — the provider had already billed every round it completed, and no consumer ever saw any of it. That is not a corner case for anything that ends a turn on a clock. It is the normal case for a long-horizon benchmark run that relaunches its agent between phases. ## How big Measured against a provider's own billing ledger over one run's window: | | provider ledger | what we recorded | |---|---|---| | the relaunched lead seat | $485 / 348M tok | $98.99 / 90.3M tok | | the two seats that were not relaunched | $29.90 / 856M | $25.81 / 765M — reconciles | 97% of that run's usage rows came back all zeros, against 1–4% for comparable runs that never relaunch. In one 450-phase trial exactly 7 phases recorded any usage — and each of those carries 177k–437k input tokens, a whole session's worth landing in the one phase that happened to end gracefully. Worth being precise about what was *not* wrong, since both were plausible and both were checked: - **Not pricing.** The rates were verified against the provider's endpoints API and match what we charge. - **Not a truncation bug.** The usage files were intact and internally consistent. The tokens were never captured in the first place. ## The fix The run loop now emits a session-cumulative `usage_update` after every usage-bearing provider response, so an interrupted turn has reported everything but its single in-flight request. - **Emitting more than once per turn is already part of the contract.** buzz-acp's `UsageTracker` advances its committed baseline only at publish time, and goose behaves the same way — which is why the tracker was written to tolerate it. - **The turn-start session baseline is snapshotted into `RunCtx`** so the mid-turn figure stays *session*-cumulative. A turn-local number would be discarded by a high-water-mark consumer and lose the turn entirely; there is a test for exactly that. - **Snapshot by value, not a session handle.** The loop reports once per round, and taking the sessions lock on each would serialise concurrent sessions behind one another's provider round-trips. Nothing else advances those counters while the turn holds `busy`, so it cannot go stale. - **One shared `wire::usage_update_payload`** for both call sites, so the mid-turn and end-of-turn shapes cannot drift. A drift there would present as tokens silently vanishing, which is the failure this reporting exists to prevent. ## Why not a SIGTERM handler That was the obvious shape and it does not work. At signal time the counters are not sitting anywhere a handler could reach — they are in the turn's stack frame, and the value the handler would need has not been folded into the session yet. Making usage durable *during* the turn is what actually fixes it; once it is, a handler adds nothing beyond the in-flight request, whose cost is unknown until its response lands. ## Tests - `usage_is_reported_after_each_round_not_only_at_turn_end` — two rounds; asserts the **first** notification carries round 1's counts alone, proving it went out before round 2 returned. - `mid_turn_usage_includes_earlier_turns` — a mid-turn report must be session-cumulative, not turn-local. buzz-agent 18/18 on the `fake_llm` suite, 382 unit. `cargo fmt` / `clippy` / `cargo check --workspace --all-targets` clean. ## Scope Agent-side only, against `main`. The matching harness change — settling usage on the timeout path, which was skipped on the reasoning that an incomplete turn has nothing to flush — is **block#4553**, against the benchmark branch, since that harness does not exist on `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Atish Patel <atish@squareup.com> Co-authored-by: Claude Code <noreply@anthropic.com>
## Summary - document exact-head trusted approval as the only desktop tagging authorization - explicitly require `desktop_ref=desktop-v<version>` for the internal desktop handoff - replace the stale `squareup/sprout-releases` repository name with `squareup/buzz-releases` ## Audit coverage Compared `block/buzz` release documentation and automation with `squareup/buzz-releases` `main` (`5b09e5c5d71c80a0849a33458f4e45695df515d7`), including its README, agent guide, Buildkite field hint, desktop validator, release validation tests, and protected updater promotion instructions. ## Validation - `bash scripts/test-release-ref-contract.sh` - `git diff --check origin/main...HEAD` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - replace a platform-dependent mounted-`RichText` assertion with the production follow-mode boundary predicate - retain the jump-to-latest assertion as the visible consequence of follow mode remaining off - leave production behavior and desktop PR block#4549 unchanged ## Why `ScrollablePositionedList` may keep an offscreen item mounted within cache extent on macOS while Linux does not. Mounting therefore does not establish whether reversed-list item 0 is at the latest boundary. The replacement reads the list's public `itemPositionsNotifier` and applies the same `index == 0 && abs(itemLeadingEdge) < 0.01` contract used by `message_list.dart`. ## Validation At commit `bc88617e61d8e9edf8fea832baa8d918163ee212` on macOS with repo Flutter 3.41.7: - `cd mobile && ../bin/flutter test` — 1088 passed, 1 skipped - `cd mobile && ../bin/flutter analyze` — no issues - pre-push `mobile-test` and `branch-skew` hooks — passed Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.4 - **Frozen main:** `6de85fe31d781122756aecf954bae7d357a56b9a` - **Reviewed candidate:** `5836cb8f0af478ed3ee3bc6464a20fa4cc91303f` - **Previous desktop release:** `desktop-v0.5.3` - **Proposed immutable tag:** `desktop-v0.5.4` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; 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>
Signed-off-by: Dekan Brown <dekanbro@gmail.com> # Conflicts: # desktop/src-tauri/src/lib.rs # desktop/src-tauri/src/linux_media.rs
|
Important Review skippedToo many files! This PR contains 549 files, which is 449 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (18)
📒 Files selected for processing (549)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Summary
desktop-v0.5.4into the RaidGuild forkConflict resolution
Upstream independently added
linux_media.rs. The merge keeps the upstream implementation and its cross-platform lint guards while retaining the fork’s safe query/fragment origin handling and tests. It also keeps the new upstream identity storage and key backup modules registered in the Tauri app.Validation
cargo fmt --manifest-path desktop/src-tauri/Cargo.toml -- --checkpnpm --dir desktop typecheckpnpm --dir desktop test— 4,022 passedgit diff --checkThe targeted native Rust test could not link on the local host because
pkg-configand GTK development libraries are unavailable; fork CI will validate the platform-native build.