mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
cdfc38ecbd4eb1a1cf5d45de9a5e982292574d5d
1817 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7b11c6147d |
test: fix recurring CI flakes in desktop and Postgres lanes (#8343)
Fixes the CI flakes that failed most often across `main` and unrelated branches from Oct 5 to Oct 14. The counts come from 150 failed `CI` runs. Every change except the nextest config is test-only. | Flake | Runs hit | Root cause | Fix | |---|---|---|---| | `probe_codex_acp_version_…descendant_holds_pipe_open` | 5 | The test writes its script in-process and then execs it. If a sibling test spawns a process in that window, the child inherits the open write fd and Linux rejects the exec with `ETXTBSY`. The probe swallows that error as `None`. | Write probe scripts from a short-lived `/bin/sh` (`write_executable_script`) | | `empty-edit-delete.spec.ts` (`:51`, `:74`, `:95`) | 11 | The seeded `mock-general-welcome` id isn't hex, so `formatTimelineMessages` can't overlay its edit. The post-subscribe window refetch then reverts the row. | Emit a per-test message with a real id after the exact channel subscription is live | | `huddle-transcription.spec.ts:798` | 9 | The composer's delayed initial autofocus (TipTap `focus()` runs in rAF) lands after the voice popover opens and dismisses it. | Wait for `message-input` focus before opening the popover | | `voice-note.spec.ts:906` | 6 | The fixture `ping.mp3` is about 1s long, so playback can end before `paused === false` is observed. | Loop the fixture audio | | `workflows.spec.ts:291` | 5 | The `Message text` fill runs before the inspector swaps to the new step, so it fills the trigger's condition instead. | Scope the fill to the step inspector after its `Action` menu appears | | `cluster_global_probe_commits_tokens_and_sessions_prove_coverage` | 3 | The `max-threads = 1` group only serializes these tests with each other. Other tests' sessions still mask cluster-wide `pg_stat_activity` (`MaskedActivity`). | `threads-required = "num-test-threads"` | | `test_usage_metrics_lock_has_single_owner_and_releases_on_drop` | 1 (this PR) | The test retries the lock right after dropping the leader. Postgres releases a session advisory lock only after the backend notices the disconnect, so the retry can run first. | Poll for the successor, bounded at 5s | ### Validation - **ETXTBSY:** a Linux experiment with Rust 1.95 using the same write→chmod→spawn pattern under concurrent spawns hit `ETXTBSY` 4–7% of the time. Using the `sh` writer, it hit 0 of 6,000. I did not catch it inside the full suite locally, so this fix is based on the mechanism, not an in-suite repro. All 11 probe tests pass on Linux and macOS. - **empty-edit-delete:** the baseline failed 16 of 45 runs (`--repeat-each`, 6 workers, no retries). The spec fix alone brings that to 4/45. The remaining failures come from a separate product race, described below. - **huddle:** the baseline failed 5 of 10 runs. With the fix, 60 of 60 passed without retries. - **nextest:** checked with a throwaway crate on 0.9.136. A grouped test with `num-test-threads` ran alone, even with `max-threads = 1`. - I have not stress-run the voice-note and workflows fixes; they rely on CI. ### Not in this PR - **Product bug in `useChannelLinks`:** the debounced `#channel` autocomplete close lets Enter accept a stale suggestion for about 120ms. That accounts for the remaining `empty-edit-delete` failures, and it affects users too. With the spec fix plus a synchronous close, the spec passed 120 of 120 runs. This needs a product decision, so it's held for a separate PR. - **Composer autofocus race:** the delayed autofocus can close an overlay the user just opened. The real fix is to revalidate when the delayed focus runs. Overlaps #6530. - **`thread-head-stale-edit.spec.ts:55`:** a forced click on a pointer-events-none action bar is suspected, but I couldn't reproduce it. - **Remaining ETXTBSY exposure:** about 40 other Rust tests write and then exec scripts, and 17 specs still edit the non-hex seed message. Root-cause lanes by Princess Donut, Mongo and Mordecai. Carl integrated and verified them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <5d97ac8c272fa949af56c71e586146b1706e5c4c3daa125db5b4625e12e0686c@buzz.block.builderlab.xyz> Co-authored-by: Mongo <81cabd2ca1792494372ed410283c00770c0cf35afca052e940fa522883212b45@buzz.block.builderlab.xyz> Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
67c4fe5e17 |
fix(deps): bump rustls to 0.23.46 for RUSTSEC-2026-0346 and -0347 (#8319)
🤖 ## Summary The required Security check now fails on every branch, including main. The dependency audit (`cargo-deny`) reports two new advisories for rustls 0.23.45, the TLS library behind the relay, CLI and desktop connections: - **RUSTSEC-2026-0346:** printing a TLS client session value with `Debug` (for example, in a log line) shows its TLS secret. - **RUSTSEC-2026-0347:** when a server rejects Encrypted Client Hello (ECH, which hides the server name a client asks for), data the client buffered can go to the outer server. rustls 0.23.46 fixes both. This PR moves the root and desktop lockfiles to it, so the Security check passes again. ### Related issue None found. #8277 fixed an earlier advisory in the same way. ### Testing - `cargo-deny check` passes: "advisories ok, bans ok, licenses ok, sources ok". - `cargo tree --locked` resolves both lockfiles without more changes. Only the rustls version and checksum change. A plain `cargo update -p rustls` also moved unrelated `syn` and `getrandom` edges, so this PR leaves those out. Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
5dc693aeaf |
Extract shared Huddle audio client for desktop consumers (#8129)
Huddle audio was embedded in the v0 desktop client, forcing other clients to duplicate its protocol and codec behavior. This extracts `buzz-audio-client` so v1 can depend on a pinned library revision without building the v0 application. The crate owns the bounded v2 audio handshake, roster parsing, wire format, Opus sender, and per-peer NetEQ jitter buffer. The v0 human and agent senders use it, and the desktop retains room admission, identity/signing, TLS policy, device routing, mute, and teardown. Receive decoding preserves v1's support for 120 ms Opus packets. No relay protocol changes, speech models, or model downloads are included. Docker builder stages include CMake because cargo-chef compiles the workspace dependency graph. Dependency security: Opus 0.4 replaces the unmaintained `audiopus_sys` backend with `opusic-sys`. NetEQ is pinned exactly to 0.8.3 because its published Rust sources never reference its declared ringbuf dependency. RUSTSEC-2026-0293 is documented as unreachable for this audited source, and a wrapper restriction rejects every other consumer of vulnerable ringbuf versions. Independent review confirmed the scope; re-audit/remove the exception when changing NetEQ. NetEQ 0.9.1 still declares ringbuf 0.3, so a version bump alone does not remove it. Local `cargo deny check` passes with this constrained exception. Validation: - All 21 shared-crate tests pass, including real loopback WebSocket authentication/cancellation, real Opus-to-NetEQ playback, malformed input, and 120 ms decoding. These tests are included in `just test-unit`. - Shared-crate, root workspace, and desktop Tauri Clippy pass; `just desktop-tauri-check` passes. - The desktop Tauri test workspace passes; native two-person listening/device checks remain separate. - All mandatory push checks pass at `d06bac5dc`, including the complete Rust unit gate and desktop workspace checks. Existing agent timing and relay log-capture tests failed under concurrent load, passed separately, and passed in the full serial test run; no tests or hooks were skipped. - One test-only fixture change allows cold Perl startup before the escaped-process test's inner timeout (2 seconds instead of 300 ms). Its independent 5-second watchdog, 300-second surviving pipe writer, and all assertions remain intact. All eight process-helper tests pass; independent review confirmed this still tests bounded drain completion. - Dependency inspection confirms the library brings in no desktop application, Tauri, identity store, relay server, or speech-model crates. - Independent source review found no correctness blockers. Draft: a native two-person call, mute/leave/cancellation, and microphone/speaker switching still need a runtime tryout and human review. Full `just ci` was stopped during the unrelated mobile toolchain download because of disk pressure; it is not a passing gate. Consumer integration is tracked in block/buzz-app#489. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
30873ba46b |
docs(nest): replace commit attribution policy with the runtime facts (#8236)
## What The nest `AGENTS.md` template (`desktop/src-tauri/src/managed_agents/nest_agents.md`) ships to every Buzz user's agents. Its "Git Commit Attribution" section is cut from a general attribution policy (separate claims, author "per applicable policy", hypothetical repos that require a human author, "stop and ask") down to what the runtime actually does: - `buzz-acp` sets a default git identity and signing. Agents use it, but change the author when a repository or their human needs a different one. The operator can opt out with `BUZZ_GIT_IDENTITY=user`. - Credit the other party with `Co-authored-by`: the human you work for when you commit as yourself, or yourself when you commit as them. - Follow each repository's own `AGENTS.md` for sign-off (DCO) and other attribution rules. The buzz-acp base prompt gets the same co-author default. `NEST_AGENTS_VERSION` goes from 6 to 7 so existing nests refresh, and the template test now asserts the new content. ## Why Agents read the old section as a puzzle to solve on every commit. They looked for an applicable policy, wondered whether the human should be the author, and stalled. DCO and sign-off rules are per-repository and already live in each repo's own `AGENTS.md` (this repo says `git commit -s`), so they don't belong in a template that every OSS user's agents read. Companion buzz-app change: block/buzz-app#833. ## Validation `cargo test --lib managed_agents::nest` in `desktop/src-tauri`: 55 passed, including the legacy-attribution upgrade test. I pushed with `--no-verify`: the change is Markdown plus a version constant and a test, and the nest tests above are the relevant suite. CI runs the full set. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Bradley Axen <baxen@squareup.com> Co-authored-by: bundled00 <f172256d14c11991d58561c98a594e7397d64087502e7bc90f2c0e8d0d45363f@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6c388bde62 |
feat(relay): accept typing indicators over HTTP POST /events (#8207)
## Summary Agents hosted inside the Buzz app sign their own events but don't hold a WebSocket, so they couldn't show "typing…" in a channel. `POST /events` rejected kind 20002 (typing indicator) with `restricted: unknown event kind`. Now, when `POST /events` receives a kind 20002 event, the bridge passes it to the same channel broadcast the WebSocket path uses. Live subscribers in the channel get the pulse. Nothing is written to Postgres. Before broadcasting, the relay checks, in order: - the event has an `h` tag naming the channel - the serialized event is at most 2 KiB (real typing events are ~600 bytes). Measuring the whole JSON, not just content and tag values, means piling on empty tags can't get around the cap - the signature and id are valid - bans and timeouts - the community write fence (over HTTP, a quiescing community's host already fails to bind with a 404, so this catches quiescing that starts after the tenant is bound) - the sender is a channel member, or the channel is open (same rule as WebSocket) - the signer is the pubkey that authenticated the request (checked first; a mismatch returns 403) The existing HTTP admission rate limit and relay-membership gate still run before any of this. Other ephemeral kinds are still rejected over HTTP. `publish_channel_ephemeral` is the old WebSocket channel-broadcast code moved into a shared helper, so both paths use the same membership check and fan-out. Unlike the WebSocket path, HTTP requires the `h` tag. Over WebSocket, a 20002 with no channel goes to every subscriber. Points left for review: - HTTP typing pulses count against the same per-pubkey HTTP quota as message posts (default 300/min). One pulse every 3 s is 20/min per active channel. - A failed Redis publish is logged and the POST still returns `accepted: true`, same as the WebSocket path. A pulse is resent every 3 s and expires after 8 s, so a retry would only resend a stale pulse. The `sadscan:disable` comment on an existing test URL in `ingest.rs` is there because the secret scanner reads every file a commit touches. It uses the same form as the other local test fixtures in the repo. ### Related issue Needed for Agents2 typing indicators in buzz-app. The client PR depends on this one and will follow as a draft. ### Testing - 9 new Postgres-backed tests in `api/typing_postgres_tests.rs` cover these cases: a member's pulse is published and not stored; a non-member gets a 400; a timed-out member gets a 403; a signer mismatch gets a 403; a missing `h` tag gets a 400; a tampered event gets a 400; kind 20001 is still rejected; oversized events get a 400 (2 KiB of content, one 2 KiB tag value, or `h` + 10k empty tags); a community that starts quiescing after the tenant is bound gets a 400 `community writes are fenced`, and nothing is published. Two mutation checks: with the fence removed, the fence test fails; with the old content-plus-tag-values cap, the empty-tags case is accepted and the test fails. - The full buzz-relay Postgres lane on |
||
|
|
c1370cc654 |
chore(release): release Buzz Desktop version 0.5.28 (#8222)
## Buzz Desktop release v0.5.28 - **Frozen main:** `5142cf6b2a0f44686eeda1ed07905ee3560fd78e` - **Reviewed candidate:** `f87c10895ea35a9a2cea629fef1d7532a3fecbc5` - **Previous desktop release:** `desktop-v0.5.27` - **Proposed immutable tag:** `desktop-v0.5.28` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
5142cf6b2a |
test(desktop): move CSP guards from Rust to the desktop JS suite (#8172)
## Summary Moves the packaged-app CSP guards from `desktop/src-tauri/tests/csp.rs` to `desktop/src/app/contentSecurityPolicy.test.mjs`. No production code changes beyond one added `export`. `csp.rs` never touched Rust app code. All seven tests parse `tauri.conf.json` and compare CSP strings. The MediaPipe check also `include_str!`'d `desktop/src/features/profile/lib/animatedAvatarCapture.ts` and pulled the `MEDIAPIPE_WASM_BASE` literal out by splitting the text. That made a Tauri test depend on a frontend file. With path-filtered CI (#8156), a change to only that `.ts` file would skip the check. The JS version imports `MEDIAPIPE_WASM_BASE` directly (it's now exported) and reads `src-tauri/tauri.conf.json` from disk. The `desktop/**` CI filter covers both files, so the check runs in Desktop JS whenever either one changes, and the filter needs no cross-boundary special case. The seven tests and their assertions carry over one-for-one, along with their rationale comments. **What got simpler:** we delete a 204-line Rust integration test, the `include_str!` of a frontend file, and the hand-rolled string-literal scraper, and the CI path filter doesn't need to know about this file. ### Related issue Addresses the CSP coupling raised in review of #8156. Related: #4614 (introduced the guards). ### Testing - New suite on Linux: 7/7 pass. - I falsified each test by mutating the real policy or code, one mutation at a time. Each mutation fails exactly the matching test, and the suite passes again once restored: - moved `MEDIAPIPE_WASM_BASE` outside `@mediapipe/` - dropped `'wasm-unsafe-eval'` - dropped the MediaPipe scope from `script-src` - added a bare `https://cdn.jsdelivr.net/` origin - added `'unsafe-inline'` - removed `img-src` - dropped `ws:` from `connect-src` - made the pinned-source predicate always pass - Blox `just ci` at tree `5664c6ce` (same tree as head `f798f94b`): - Passed: fmt, clippy, desktop/Tauri/web checks, security-review/file-size checks, `desktop-test` (6,819 + 139 jsdom), `desktop-build`, `desktop-tauri-check`, `desktop-tauri-test`, `web-build`. - `test-unit`: 94/95 suites pass. The two failing `buzz-acp` `git_bootstrap` tests fail because the workstation's Git 2.43 predates the credential `authtype` protocol. They pass 4/4 with Git 2.51, and this PR touches no `crates/`. - Not run: `mobile-check`/`mobile-test`, because the workstation can't reach pub.dev. No `mobile/` changes. - Hosted CI green: https://github.com/block/buzz/actions/runs/37801901659. This PR selects the Tauri lanes because it deletes `src-tauri/tests/csp.rs`. Later edits that only touch the constant select Desktop JS only. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
99924a5a69 |
fix(desktop): repair the Windows notification shim's false denial on read (#5600)
Addresses #2445. Closely related to #2483 -- see "Relationship to #2483" below; I am happy to close this in favour of that one. ## The problem On Windows the notification plugin's injected shim decides its permission at startup like this ([`guest-js/init.ts:12-18`](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/notification/guest-js/init.ts)): ```ts if (window.Notification.permission !== 'default' || __TEMPLATE_windows__) { return await Promise.resolve(window.Notification.permission === 'granted') } return await invoke('plugin:notification|is_permission_granted') ``` `__TEMPLATE_windows__` is replaced with the literal `true` on Windows, so the `||` short-circuits and the shim compares its own freshly-initialised `'default'` against `'granted'` -- `false` -- and stamps **`denied`**. The line that would ask the backend is unreachable, and the backend would have said yes: `permission_state()` returns `PermissionState::Granted` unconditionally on desktop. So on every launch of the Windows app, `window.Notification.permission` is `denied` and nothing in the OS is denying anything. ## Why the toggle is only half of it `getDesktopNotificationPermissionState()` trusts that value, so `refreshPermission()` reports `denied` at mount, and this effect in `hooks.ts` runs before the user touches anything: ```ts React.useEffect(() => { if (settings.desktopEnabled && (permission === "denied" || permission === "unsupported")) { setSettings((current) => ({ ...current, desktopEnabled: false })); } }, [permission, settings.desktopEnabled]); ``` It writes `desktopEnabled: false` and persists it. Measured on a real install: with alerts successfully enabled and `desktopEnabled` persisted as `true`, a clean relaunch comes back with it persisted as `false`. That means fixing only the enable path leaves a Windows user re-enabling alerts after every restart. ## What this changes One place: `getDesktopNotificationPermissionState()` repairs the bogus value instead of reporting it. ```ts if ( window.Notification.permission === "denied" && isTauri() && isWindowsPlatform() ) { try { return await requestDesktopNotificationAccess(); } catch { return window.Notification.permission; } } ``` `requestPermission()` is the only call that reaches the backend, and on Windows it raises no prompt, returns `Granted`, and rewrites the shim's cached value -- so this is self-limiting: the next read short-circuits as `granted` and never requests again. There is a test for exactly that. Repairing at the read rather than at the toggle fixes every consumer at once, including `sendDesktopNotification()`, which gates delivery on `=== "granted"` and would otherwise drop notifications at boot. - `denied` stays terminal on macOS, on Linux, and on ordinary Windows web pages. - A denial that survives the request is still reported as `denied`. - A rejected request falls back to the current value instead of propagating; callers do not handle rejections today. - Adds `isWindowsPlatform()` to `shared/lib/platform.ts`, alongside the existing `isMacPlatform()` / `isLinuxPlatform()`. - No new dependencies. No changes to `hooks.ts` or to notification delivery. ## Verification on a real Windows 11 install Buzz Desktop 0.5.9 release build, Windows 11 Home 10.0.26200, WebView2 151.0.4129.72, attached over CDP. Clean boot, nothing injected: ``` window.Notification.permission -> "denied" localStorage[...].desktopEnabled -> false (written by the effect above) await window.Notification.requestPermission() -> "granted" window.Notification.permission -> "granted" new window.Notification("Buzz", { body: ... }) ToastNotificationManager::History .GetHistory("xyz.block.buzz.app") -> 3 -> 4 (real toast delivered) CreateToastNotifier(...).Setting -> Enabled ``` The OS was permissive throughout: no `ToastEnabled` override, no notification policy keys under HKCU or HKLM, and the app present under `HKCU\...\Notifications\Settings\xyz.block.buzz.app`. Full detail in [this comment](https://github.com/block/buzz/pull/2483#issuecomment-5255972424). To be precise about scope: this confirms the mechanism the patch relies on, measured on a stock release build. The patched build itself is covered by the unit tests below, not by a packaged Windows run. ## Tests `pnpm test` -- full desktop suite. New cases in `lib/desktop.test.mjs`: - Windows Tauri repairs the shim's false denied state on read - the repaired state is cached, so later reads do not request again - a denial that survives the request is reported as denied - denied stays terminal outside the Windows Tauri app (Windows web, Linux Tauri, Linux web) - granted is returned untouched and never triggers a request Plus `shared/lib/platform.test.mjs` for `isWindowsPlatform()` across `Win32` / `Win64` / `Windows` and `MacIntel` / `Darwin` / `Linux x86_64`. ## Relationship to #2483 #2483 fixes the same root cause at the toggle: it retries the request inside `setDesktopEnabled`. That makes the toggle work, which is what #2445 reports, and its `ensureDesktopNotificationPermission` helper is a nicer shape than an inline condition. The difference is where the repair happens. Because #2483 leaves the mount-time read as `denied`, the effect quoted above still turns the setting off on every launch, so the fix does not survive a restart. Repairing at the read covers the toggle path too, since `setDesktopEnabled` calls `refreshPermission()` first and finds `granted`. The two overlap: both add `isWindowsPlatform()` to `platform.ts`, so whichever lands second needs a trivial rebase. If maintainers prefer #2483's shape, the equivalent of this PR is to call its helper from `refreshPermission` as well as from `setDesktopEnabled` -- I am glad to close this and send that instead. I did not want to push changes into someone else's open PR uninvited. ## Once this lands #2445 can close. I am running a per-machine launcher workaround for a small Windows team until then, which this makes unnecessary. Signed-off-by: VSCteam <144167621+Joxyko@users.noreply.github.com> |
||
|
|
8f66f7051c |
feat: raise hosted community limit to fifty (#8186)
## Summary - Raise the stock relay active ownership quota from 5 to 50 for creation and ownership transfer. Raise the absolute lifetime quota from 20 to 50 reserved hosts; completed owner deletions still permanently consume lifetime capacity to bound create/delete host squatting. - Honor the owner-list `can_create` projection across Desktop settings, create flow, and onboarding instead of imposing a client-side count. Display server `quota_used` / `quota_limit` when available; when the projection is missing, let the server's `limit_reached` decide. - Cover 49→50 and 50→51 admissions, deleted-owner lifetime exhaustion, and rendered create gates under 5/50, 0/50 exhausted, and missing projections. ## Verification - Desktop full `pnpm test` and `pnpm typecheck` pass after the 50-cap update; `cargo fmt --all -- --check` passes. Four targeted Playwright cases and `pnpm build:e2e` passed for the original dynamic-quota change at the previous commit, before changing test fixtures from 20 to 50. - `cargo test -p buzz-db --lib` cannot compile here: macOS dyld rejects the `sqlx_macros` dylib with `mis-aligned LINKEDIT string pool` on this host before the crate's tests run. CI must validate the DB package. ## Rollout / manual validation - Confirm `BUZZ_MAX_COMMUNITIES_PER_OWNER` is unset or 50 on **every** production relay replica; this per-process cached override takes precedence over the stock default. - Confirm Builderlab's list/create/transfer proxy passes the relay's `quota_used`, `quota_limit`, `can_create`, and `limit_reached` without its own 5-community gate. Its backing implementation is not in this repo and was not verified here. - Human test: with an owner at 49 live communities, create the fiftieth, verify the fifty-first returns `limit_reached`; transfer into an owner at 49 then at 50 and confirm the same boundary. Check a deleted-owner account at 0 active/50 lifetime displays the generic limit and disables creation. This manual check remains outstanding. --------- Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
836e7d0d06 |
feat(desktop): browse and moderate every community in the Admin Console (#8027)
The Admin Console only ever saw the community the desktop happened to be connected to. With #8011's community reads, the console can now browse every community on the relay and, for signed staff, moderate any of them by name without switching connections. 🤖 ## What changes - **Communities tab.** Lists every live community on the relay, searchable by host prefix with "Load more" paging. The connected community is pinned at the top by exact host match. Report rows get a community badge that opens that community. - **Community page.** Reports, Restrictions, Members and Actions, each scoped to the community on the page. The Actions form has no host field: it always acts on the page's community, and the TypeScript host normalization is gone from the Actions tab. - **Native reads** (`admin_list_communities`, `admin_search_members`, `admin_get_member`, `admin_get_event`). Every request validates the explicit community host and NIP-98-signs the real method and URL. Failures come back as a structured error (`status`, `bodyComplete`, `bodyEmpty`, `code`), so the UI can tell an older relay's empty 404, a coded `event_not_found` and a transport failure apart, and never says "not found" unless the relay did. - **Explicit-host lifts.** `admin_lift_restriction` sends the page's community host, not the active relay's. It freezes the relay and signer the intent was reviewed under, and refuses to send if either has changed. - **Pending actions survive navigation.** A panel-level `DirectActionsProvider` holds the frozen intent, so switching communities or tabs mid-confirm doesn't lose it. Only Discard drops it. A signer change remounts the controller and drops it. - **Fenced lookups.** Member and event lookups are keyed by origin, signer, community, target and a generation counter, so a late answer for one community can't unlock Review in another. - **Shared member picker** (`MemberSearchResult`, also used by Add Member), showing the truncated npub so members with the same name can be told apart. Delete accepts a note link and previews the message before Review. Relay staff are blocked before Review. - **Disabled admin auth is read-only.** With `BUZZ_ADMIN_AUTH=disabled` the Communities tab and community pages still render, and report badges still open them, but every ban, timeout, delete, add-member and lift control is disabled. The settings badge says the console is read-only and that acting needs `nip98`. Staffing stays hidden. - #7904's protections in the Actions tab are kept: frozen intent, `requestId`, Retry, `notSent` and the key-backup guard. - **Readable load errors.** Admin list and detail loads show the relay's error message (for example "request is not authorized"), never the raw JSON envelope. - **Failed kicks reload the report.** A resolve that fails with `enforcement_failed` reloads the report, so its failed action and "Cancel & reopen" appear immediately. - **Key backups never leave in a search.** Every admin API request URL passes the NIP-49 key-backup guard before it is signed or sent: community and legacy reads, mutations, the connection probe (before its first, unsigned request) and feedback-attachment fetches. The community and member search boxes, including the directory's Load more, refuse a pasted `nsec` or `ncryptsec` anywhere in the input without calling the relay. - **`just admin-seed` for multi-community testing.** Besides the local community, it seeds `beta.localhost:3000`, `gamma.localhost:3000` and 50 filler communities, with named members, a ban and a timeout in each named community, a report with Kick available, and a message for the delete preview. Rerunning it restores all of that to the starting state. It refuses, writing nothing, while a moderation action is unfinished on a seeded report. - The bodyless `GET /events/{id}` preview is added to the events-URL egress inventory. It sends no body, so it carries no key material. ## Tests Native (Rust): - `empty_404_and_405_read_as_complete_and_empty`, `coded_404_carries_the_relay_code`, `truncated_and_over_cap_bodies_are_incomplete`, `transport_failure_has_no_status`: the structured read error tells an older relay, a coded refusal, a cut-off body and a transport failure apart. - `a_401_retry_is_signed_by_the_same_keys`, `lift_signs_the_send_and_401_retry_with_the_checked_keys`: the 401 retry never switches keys. - `read_urls_validate_the_explicit_host_and_encode_the_query`, `lift_url_sends_the_page_host_not_the_active_relay`, `list_url_carries_host_and_cursor_and_refuses_a_changed_relay`, `lift_refuses_before_sending_on_signer_change_or_bad_host`: requests target the explicit host and refuse to send after a relay or signer change. jsdom: - `actions-pending-nav`: a pending intent survives a community change, and only Discard drops it. - `actions-fenced-lookup`, `fenced-load`: a late lookup for an old key never shows under, or unlocks, the new one. - `actions-same-ids`: the same pubkey and event id are read separately in each community. - `actions-community-change-drops-member`, `actions-page-host`, `actions-success`, `actions-identity`: the form acts only in the page's community, and confirm sends the frozen intent. - `actions-preview-errors`, `actions-delete-link`, `actions-staff-target`, `actions-disabled-auth`: preview errors, delete by note link, the staff block, and community pages rendering read-only with every action control disabled under disabled admin auth. - `communities-directory`, `communities-unsupported`, `community-badge`, `community-badge-disabled-auth`: the directory, older relays, and report badges, which open community pages under disabled admin auth too. - `restrictions-lift-signer-change`. The existing restriction tests now run on the community page and use `admin_lift_restriction`. Mutation checks: removing the pending-navigation guard turns `actions-pending-nav` red, and dropping the host from the fence key turns `actions-fenced-lookup`, `actions-same-ids` and `fenced-load` red. Ungating restriction lifts on `canMutate` turns `actions-disabled-auth` red. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
d742fc5f30 | Use Silo for bundled object storage (#8023) | ||
|
|
5ff83d82f8 |
fix(desktop): address workflow deletions at the workflow's actual owner (#2669)
Fixes #2671. ## Problem Deleting an agent-created workflow from the desktop app appears to succeed, but the workflow keeps running and stays in the list. Since workflows are commonly created by agents, this makes most workflows undeletable from the UI. ## Root Cause `delete_workflow` builds the NIP-09 `a`-tag coordinate with the **caller's** pubkey: ```rust let builder = events::build_workflow_delete(&workflow_id, ¤t_pubkey_hex(&state)?)?; ``` kind:30620 is addressable by `(author, d-tag)`. For an agent-created workflow, `30620:<caller>:<workflow-id>` names a record that does not exist. The relay accepts the kind:5 (a self-addressed delete always passes validation) and deletes nothing; the UI reports success. Observed live: a delete of an agent-owned workflow produced `["a","30620:<human-pubkey>:<workflow-uuid>"]` while the kind:30620 event is authored by the agent — the coordinate can never match. ## Fix Fetch the workflow's kind:30620 event first (the same `#d` query `update_workflow` already uses) and build the coordinate from the event's author. Relay-side validation already authorizes the owning human for agent-authored coordinates via `is_agent_owner`. Deleting an unknown workflow id now surfaces `workflow not found` instead of silently publishing an unmatchable delete. This is the client half of the fix. The relay half is #2242: today the relay executes a-tag deletions scoped by the delete's signer, so a human-signed delete of an agent workflow still no-ops server-side. Without this change, #2242 alone won't help desktop users, because the client addresses a record that doesn't exist. Together they make delete work end to end. Related but deliberately not included: deleted workflows leaving their kind:30620 event queryable (ghost list entries) is #2390, with a relay-side fix open in #1340. ## Validation - `cargo test` in `desktop/src-tauri`: 1617 passed, 0 failed, 14 ignored. - `cargo clippy --all-targets` and `cargo fmt --check`: clean. - Live repro against the hosted relay: the stock client's kind:5 for an agent-owned workflow named the signer in the coordinate; the relay returned `accepted: true` and the workflow remained live and kept firing. - With the fix: the delete event carries `30620:<agent-pubkey>:<workflow-id>`; an owner-signed deletion of that exact coordinate was verified to remove the workflow record. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
001290a34d |
chore(release): release Buzz Desktop version 0.5.27 (#8125)
## Buzz Desktop release v0.5.27 - **Frozen main:** `af5bb0af488784c3707e99c87e0675a729711a6e` - **Reviewed candidate:** `7a2fb3cc2d401e91e67d9819741d9025278cbec0` - **Previous desktop release:** `desktop-v0.5.26` - **Proposed immutable tag:** `desktop-v0.5.27` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
403c19f673 |
fix(desktop): show OpenAI-compatible endpoint settings in the baked-env UI (#8117)
## Summary `is_safe_to_reveal` (`desktop/src-tauri/src/managed_agents/env_vars.rs`) is the single default-deny allowlist for showing baked env values. It lists `DATABRICKS_HOST`/`DATABRICKS_MODEL`/`DATABRICKS_MODEL_FILTER` but not their OpenAI-compatible counterparts, so a build that bakes `OPENAI_COMPAT_BASE_URL`, `OPENAI_COMPAT_MODEL`, `OPENAI_COMPAT_API` shows them as `••••••`. Adds those three non-secret settings to the allowlist: an endpoint URL, a model id, and an `auto|chat|responses` enum. **No secret is revealed: `OPENAI_COMPAT_API_KEY` is deliberately not added and remains masked.** ### Related issue None found. ### Testing - New test `baked_env_openai_compat_endpoint_is_unmasked_but_key_stays_masked` covers the three revealed settings and asserts the API key stays masked; `baked_env_allowlist_is_case_insensitive` extended. - `cargo test --lib baked_env` in `desktop/src-tauri`: 11 passed. `cargo fmt` clean. - Not exercised in the running desktop UI. Signed-off-by: Tom Bedor <tbedor@squareup.com> Co-authored-by: Tom Bedor <tbedor@squareup.com> |
||
|
|
9c6fa14aa0 |
feat(desktop): offer Claude effort levels in agent edit and create (#8059)
Claude Code agents now get a thinking-effort dropdown in the agent Edit and Create dialogs, matching the one buzz-agent and Codex agents already have. ## Behavior - **Where levels come from:** Claude effort levels come from the Claude model data for the model that will actually run after Save, resolved in order: explicit model, then inherited default (linked definition or global), then adapter default. Stored Claude session levels are not used, because the stored session data does not record which model produced them. - **Edit:** for an agent linked to a definition, effort follows the definition's model, which stays authoritative. Save never sends a model for linked agents. - **Create:** effort is offered for local agents only and is stored on the instance. Allowed levels are recomputed on every render, so collapsing Advanced and then switching to a model without effort (e.g. Haiku) drops effort from the request. Non-local effort is rejected before any create side effects. - **Persistence:** `create_managed_agent` sets the picked effort when it builds the record and drops stale effort env aliases. Its body runs over any Tauri runtime, so a mock-app test drives the real create path and reads the record back from disk. That test runs in a child process with temp app-data and nest folders, and a test-only switch turns keychain use off so agent keys stay inline in the temp agent file and the OS keychain is never touched. - **Observer store:** live session-config captures are written to the store before the dialog refreshes; archived captures are read-only. - **Codex:** its existing session-native effort picker is unchanged. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
fcf5f044b3 |
fix(desktop): use object-scale-down for inline markdown images (#4686)
Fixes #4384. A dim-less markdown image with no cached decode gets `DEFAULT_IMAGE_RESERVE = { height: 256, width: 384 }` and `useFixedReserveBox: true` in `resolveImageReserveBox`. The frame is that big on first view, and the inline `<img>` uses `object-contain` which scales UP to fill the frame — so a 46×20 shields badge renders as a giant blurry block. The same message renders at the natural size on the second view because `rememberDecodedImageDimensions` populates the cache, but the inconsistency between first and subsequent view is the bug. Switches `object-contain` → `object-scale-down` on `ProgressiveImage.tsx`'s shared `IMAGE_CLASS`. Both `object-contain` and `object-scale-down` scale down to fit, but only `object-scale-down` also refuses to scale up — a small image now sits centered in the reserve box on first view, never upscaled. The layout-shift guarantee for large images is preserved (they still scale down to fit the reserve), matching the issue's stated constraint. Chose option 2 ("Alternatively/minimally") from the issue over option 1 because: - It's a one-class-name change with zero new state, zero new effects, and zero risk to the existing layout-shift guarantee that the surrounding `useFrozenImageReserve` is explicitly designed to preserve. - Option 1 (collapse the frame on decode) is the issue author's preferred direction but requires deciding whether the layout shift is acceptable *shrink*-only — a direction question that should be confirmed with the maintainer first. The issue explicitly says "Happy to open a PR for option 1 if the direction is acceptable" — that's the sign-off step, not this PR. If maintainers want the full first-view-equals-second-view behavior, option 1 is a follow-up that builds on this base. Not added: a regression test. The current desktop test infra (`desktop/src/shared/ui/markdown/markdown.test.mjs` and siblings) doesn't cover `ProgressiveImage` directly; a DOM-style snapshot would need a new test harness. The change is one CSS class name; the visual regression is plain to eyeball in the screenshots CI already captures. Verified: - `pnpm check` (biome + file-sizes + px-text + pubkey-truncation) — clean - `npx tsc --noEmit` — clean Signed-off-by: kushaim <carlossilvajimenez@gmail.com> Co-authored-by: kushaim <carlossilvajimenez@gmail.com> |
||
|
|
964b556a42 |
fix(desktop): detect Kimi Code in its default install path (#3700)
## Summary By default on linux kimi is installed at `~/.kimi-code/bin/kimi` and this was not detected automatically. This now fixes it by adding this path to the detection ### Related issue N/A ### Testing Before it did not appear / did not detect. Now it does :) Signed-off-by: benthecarman <benthecarman@live.com> |
||
|
|
5173fad660 |
feat(pairing): enter desktop codes on mobile (#8085)
## Summary Enter a desktop-only, random six-digit code on mobile to pair without a desktop confirmation click. Verification uses the encrypted session, limits guesses to five, and never treats the QR-derived transcript as approval. Older clients retain desktop confirmation. Adds digit-only error shakes and native haptics, a separate biometrics choice, and a direct community-icon arrival animation. Fixes dropped early offers and resets completed pairing so reopening or signing out cannot get stuck. ### Related issue Related: #8060; no duplicate code-entry PR found. <img width="780" height="1688" alt="01-enter-code" src="https://github.com/user-attachments/assets/a99ced3e-4305-4ac6-a6cc-d598ced423e7" /> <img width="780" height="1688" alt="02-incorrect-code" src="https://github.com/user-attachments/assets/19cdb109-1f10-40a9-a661-1d7ba89c81c1" /> <img width="780" height="1688" alt="03-biometrics" src="https://github.com/user-attachments/assets/af1b2924-9ab6-4d1a-b4b1-fccfdf446404" /> <img width="780" height="1688" alt="04-community-loading" src="https://github.com/user-attachments/assets/84145d82-843a-4c25-aef7-6492fc241d4b" /> <img width="897" height="388" alt="05-desktop-code" src="https://github.com/user-attachments/assets/d8a2e469-7e42-466a-988f-bab6b1df85fc" /> ### Release order Deploy the updated pairing relay first, then desktop, then mobile. The relay now allows eight events per connection (seven for a successful fifth guess plus room for cancellation), with a corresponding bounded delivery budget. Existing clients remain supported; older client combinations keep desktop confirmation. ### Testing - 103 mobile pairing tests, 270 core tests, 51 relay integration tests, and the new real-relay fifth-guess flow passed. - The real-relay regression verifies proof, decrypted identity import, and completion after four wrong guesses. Restoring the old six-event cap makes it fail. - A paused-clock desktop regression confirms slow readiness cannot extend the visible QR beyond protocol expiry. Mobile regression covers typing before negotiation completes. - Core/pair-relay Clippy and full native desktop Clippy/workspace tests passed. Mobile analysis passed; broad mobile runs each hit an unrelated failure (a gateway-script timeout and a native-menu missing-plugin error), and both affected test files passed separately. - Repository-wide Rust testing exhausted local disk space while compiling unrelated packages. The final push omitted that resource-blocked hook and avoided repeating the desktop/mobile runs above; hosted CI remains required. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
5a4b2d3dcc |
fix(desktop): show readable Claude Code model names and descriptions (#8053)
Claude Code agents' model dropdown showed raw IDs (`opus[1m]`,
`claude-fable-5-1[1m]`, `haiku`) and its effort choices showed
`low`/`xhigh`. Claude Code's ACP adapter labels config options and
choices with `name`, but Buzz read `displayName`, which no adapter
sends.
**Adapter labels and descriptions**
- `normalize_agent_models` reads `name`, keeps each model's
`description`, and uses the model option's `currentValue` as the agent's
default model (ahead of the unstable `currentModelId`).
- `parse_config_options` / `parse_option_values` read `name` for option
and choice labels, so effort shows "Low"/"Xhigh".
- The create (`PersonaModelCombobox`) and edit (`PersonaDropdownField`)
model pickers show the description as a muted second line, through both
option builders (`modelDropdownOptions` and
`buildTemplateModelDropdownOptions`).
**Generated names for full Claude model IDs**
- Claude Code's own names drop versions ("Sonnet" for
`claude-sonnet-5`). The desktop's Databricks label grammar already
parses a bare Claude ID once its wrapper is removed, so that body parse
is exposed as `generateBareModelLabel`; Databricks entry points and
outputs are unchanged.
- For the `claude` runtime, `claudeCodeModelLabel` formats full IDs and
keeps a `[1m]` suffix as "(1M context)". Short names (`opus`, `haiku`,
`opus[1m]`), unknown bracket suffixes, and IDs containing any non-ASCII
character keep the adapter's name.
- This applies to the model dropdowns in the create and edit dialogs and
the Agent defaults editor. The "Default model (…)" row appears in the
edit dialog and the Agent defaults editor; the create dialog still
requires an explicit model and shows no Default row. Model labels on
agent cards are unchanged.
- In the edit dialog, the Default row keeps the discovered label (e.g.
"Default model (Opus)") unless a global default model is set, in which
case it shows that model. When the adapter's default is its own
`default` entry (e.g. "Default (recommended)"), the row reads plain
"Default model" and keeps that entry's description.
**`models` debug command**
- The harness's readable `models` output reads option and choice labels
from `name` and the option id from `id` (falling back to `configId`), so
it no longer prints raw IDs for Claude Code. JSON output is unchanged.
| Model ID | Before | After |
|---|---|---|
| `opus[1m]` | `opus[1m]` | Opus · Opus with 1M context · … |
| `claude-fable-5-1[1m]` | `claude-fable-5-1[1m]` | Claude Fable 5.1 (1M
context) · Fable 5.1 · … |
| `claude-sonnet-5` | `claude-sonnet-5` | Claude Sonnet 5 · Sonnet 5 · …
|
| `claude-sonnet-4-6` | `claude-sonnet-4-6` | Claude Sonnet 4.6 ·
Previous Sonnet version |
| `claude-sonnet-4-6[1m]` | `claude-sonnet-4-6[1m]` | Claude Sonnet 4.6
(1M context) · Sonnet 4.6 for long sessions · … |
| `haiku` | `haiku` | Haiku · Haiku 4.5 · … |
🤖
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
cc528dbe05 |
feat(agents): request summarized Claude thinking and label empty rows (#8051)
Claude Code agents on Opus showed Thinking rows that opened to nothing.
Opus returns no readable thinking unless Claude Code is asked for
summaries, and Buzz never asked. Sonnet returns readable thinking either
way.
**Request summaries from Claude Code.** `session_new_full` now adds
`_meta.claudeCode.options.extraArgs: {"thinking-display": "summarized"}`
for the Claude adapter. claude-agent-acp spreads `extraArgs` into the
CLI's arguments; 0.36.1, 0.60.0 and 0.66.0 all read the same shape. The
value is merged into the existing `_meta`, so `systemPrompt` and
`sessionTitle` are kept. `session_new_full` is the only place Buzz sends
`session/new`, so agent sessions and model discovery both get it.
**Only for a CLI that accepts the flag.** `--thinking-display` first
shipped in Claude Code 2.1.94, and older CLIs exit on unknown flags,
which would fail every session start. When the adapter is spawned, Buzz
runs `$CLAUDE_CODE_EXECUTABLE --version` once with a 5-second limit,
reading at most 256 bytes of output and killing its whole process group
on timeout, and sends the flag only on 2.1.94 or newer. The flag is left
off, and sessions start exactly as before, when:
- the CLI is older than 2.1.94;
- `CLAUDE_CODE_EXECUTABLE` is unset (the adapter then runs its own
bundled CLI, which Buzz can't inspect) or empty;
- the binary hangs, fails, or prints anything other than a clean
`major.minor.patch` version;
- `BUZZ_ACP_LAUNCH_PREFIX` is set, since the wrapper can change which
CLI the adapter actually runs.
The result is logged once at debug level.
**Label rows that still have no text.** A Thinking row whose text is
empty or only whitespace keeps its header and shows a muted "No readable
reasoning tokens", with nothing to expand. This covers models or CLIs
that still return no readable thinking. If text arrives later in the
same row, it renders as an expandable section as before.
`ThoughtActivity` is the only component that draws Thinking rows.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
|
||
|
|
47654215eb |
fix(acp): wake agents for mentions added by edits (#6131)
🤖 ## Summary Today an agent does not wake up when someone edits an existing message to @mention it. With this PR, adding the mention in an edit is enough to reach the agent. The agent then works on the original message, as #4741 set up. - Agents now receive message edits by default. Before, they subscribed only to new messages (and a few other event types). - The desktop app puts a mention tag on an edit only for people whom that edit newly mentions. So an agent wakes once, for the new mention. It does not wake again when the same message is edited later. This PR also fixes one case in the desktop app: when the original also mentioned someone whose profile was not loaded, every mention counted as new, so a typo fix woke the agent again. Now the recipients that the app knows from the original always count as already notified. - This default applies in normal mode, to channels the agent joins while it is running, and in setup mode. - If you give an explicit `--kinds` list, it is used without change. - Sessions do not change in this PR. An edit uses the session that #4741 chooses: with per-thread sessions, the original message's thread; with channel sessions, the channel. ## Details - The change adds the edit event kind (40003) to one shared default list. The startup rules, the relay subscription filters, and the setup rules all use that list. - An agent accepts an event only if both the relay subscription and its local rules allow it. The normal listener's startup rules now come from one function, `startup_subscription_rules`. The harness and the tests both call it, so the tests check the rules that actually run. ### Stack **2 of 3**, based on #4741. Next: #6132. ### Related issue Fixes #2540. ### Testing - `cargo test -p buzz-acp --all-targets` passes. The one test that fails when the git setting `nostr.keyfile` is set is described in #4741. - Admission tests run on the rules the harness installs, for the normal listener and for setup mode. An edit that mentions the agent is accepted. An edit that does not mention it is rejected. - I removed kind 40003 from each of the two defaults, one at a time. Each time, these tests failed. - Config tests check that edits are in the default kinds for startup channels and for channels joined later. - Desktop regression: `@Agent @Missing User please chek` edited to `check`, with only the agent's profile loaded, sends no new notification. It fails on the old code. --------- Signed-off-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz> Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
d1b7da45a2 |
feat(acp): add BUZZ_GIT_IDENTITY switch for agent commit identity (#8024)
🤖 This adds an operator switch for whose identity an agent's commits carry, and removes the reason agents were clearing their git identity env vars. Replaces #6177. Since #7819, every agent authors and signs commits as its nostr identity through `GIT_CONFIG_*` env vars, which beat repo and global config in every shell the agent runs. Operators had no way to opt out of that. **`BUZZ_GIT_IDENTITY=agent|user`.** It's read once in `GitEnvironment::install`. Unset or `agent` keeps today's behavior. `user` keeps only relay git auth (the credential helper and keyfile) and installs no identity, no `git-sign-nostr` and no signing config, so git uses the operator's own config. It also drops inherited `GIT_CONFIG_*` entries for the agent identity and signing keys, `author.*`/`committer.*`, and `include.path`/`includeIf.*.path`, so a nested agent in `user` mode can't pick up its parent agent's identity. Any other value fails startup with an error naming the variable and both allowed values. The resolved mode is forwarded in the managed env to MCP servers, which `buzz-agent` starts with a cleared env, so a nested `buzz-acp` launched from an MCP shell keeps the operator's choice instead of falling back to `agent`. **Known limitation:** in `user` mode, an operator whose own git config signs with `git-sign-nostr` has commits refused with a key-mismatch error, because the agent shell's `BUZZ_PRIVATE_KEY` (needed by the `buzz` CLI) loads ahead of `nostr.keyfile` and NIP-GS requires the signer to exit when the loaded key doesn't match `-u`. Nothing is signed with the wrong key. SSH and GPG signing are unaffected. **Desktop reserves `GIT_CONFIG*`.** User env overrides are layered onto the spawn command after the relay credential helper, so a single `GIT_CONFIG_COUNT=0` used to silently drop it. `GIT_CONFIG` and every `GIT_CONFIG_*` name are now reserved keys. **`session_new_forwards_complete_git_block_without_duplicate_names` no longer reads the runner's env.** Run inside an agent session, it picked up the session's own inherited `GIT_CONFIG_*` and failed. Agents worked around that by unsetting `GIT_CONFIG_COUNT`, and any commit made in that shell fell back to the human's git identity and signing key. The test now hides the runner's `GIT_CONFIG*` and `BUZZ_GIT_IDENTITY` under `ENV_LOCK` and restores them afterwards. **Agent instructions.** `base_prompt.md` and the nest `AGENTS.md` now say the runtime sets the agent's commit identity and signing, and that agents shouldn't override them with `user.*` config, `-c user.*`, `--author` or another signing key. Trailers are added only when the repository or the person the agent works for requires them. If a repository requires a different author, the agent tells the operator, who can switch to `BUZZ_GIT_IDENTITY=user`, instead of overriding the identity. The nest `AGENTS.md` scopes this to agents Buzz runs through its stock `buzz-acp` harness, and notes that custom harnesses get only relay git auth and control their own attribution. `NEST_AGENTS_VERSION` goes from 5 to 6. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
04b040ef6c |
test(desktop): pin mention undo and status expiry smoke behavior (#7592)
🤖 ## Summary Make two desktop smoke tests reliably check existing behavior: manually mentioning an agent after turning off automatic mentions, and editing a status while the saved status expires. This changes only tests, not production UI behavior, helpers, or configuration. - **Manual mention recovery:** the old test expected a manual mention to restore the agent's automatic-mention badge. The test now checks that the badge disappears after undo and stays absent when the user manually mentions the agent, while the sent message goes to exactly that agent. Choosing **Automatically mention** explicitly restores the badge and keeps the agent's mention in the composer after sending. - **Status expiry while editing:** the old test gave the saved status two seconds to expire, racing the dialog setup. The test now controls the page clock, seeds a five-minute status, and establishes an open dialog with unsaved edits before advancing past expiry. The existing checks still require the saved status to disappear from the sidebar without losing the draft, show a future-duration warning, disable Save, and allow recovery by choosing **This week**. The two-file repair is unchanged from its previous version. This branch includes current main, including the MinIO startup fix in https://github.com/block/buzz/pull/7599; that infrastructure change is not part of this PR's test-only diff. ### Related issue none found ### Testing Both specs exercise the UI through the existing test fixtures. Fresh normal CI for `ae6ff228f86f42d5203050c99bcb99cdce3a4626` is pending; historical test results and review of the earlier version do not validate this combined tree. Local dependency installation was blocked by network policy in the authoring environment, so no new local pass is claimed. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
83aab8cb5c |
feat(desktop): remove communities locally and stop their agent pairs (#7997)
🤖 Leave contacts the relay before it removes anything locally: it checks the relay's `/info` for NIP-43 membership support, then waits for the relay to accept a signed leave request. When a relay has been torn down, that check fails, Leave throws, and the community stays in the rail. Removing a community also left its relay's managed agent pairs running, and they kept reconnecting to it until the app was quit. **Remove from this device** - A new community action, **Remove from this device**, sits under **Leave community** in the profile menu's community actions. It never contacts the relay. A confirm dialog explains that relay membership isn't revoked. - `useCommunityNavigationTransitions` splits the old `removeCommunity` into `removeCommunityFromDevice` (local-only cleanup and navigation to a fallback community or the discovery screen) and `leaveAndRemoveCommunity` (calls `leaveCommunity`, then runs the same cleanup). Leave's flow hasn't changed. - When Leave fails, the error now adds: "If the community no longer exists, use Remove from this device." The error shown when community discovery state can't be saved no longer says "Membership was removed", so its wording fits both actions. **Agent pairs on a removed relay** - Once the local removal completes, the shared cleanup stops every live agent pair on that relay, including manually started ones. It matches relays with `canonicalRelayUrl`. Stopping is best-effort: failures are logged and the removal still completes. Pairs on other relays aren't touched, and nothing is stopped if another community still uses the same relay. - Both reconcile call sites (`useManagedAgentRuntimeReconciliation` and `bootstrapManagedAgentRuntimePairs`) now go through `reconcileConfiguredManagedAgentRuntimes`. A reconcile that is already running in Rust can't be cancelled, so after it resolves, this helper stops any pair it started whose relay is no longer in the saved community list. - Launch restore runs in the background after `apply_workspace` and can finish after its community is removed. `apply_workspace` takes a restore ticket (the current `launch_restore_generation` in `AppState`) before handing its lock to the restore task. Removing the active community calls the new `invalidate_launch_restore` command, which bumps that generation while holding the runtime transition lock. `restore_managed_agents_on_launch` re-checks its ticket under the same lock and spawns nothing if it changed. If restore takes the lock first, its pairs are registered before invalidation returns, and the removal stop then finds them. Removal never changes `relay_url_override`: the outgoing relay stays applied until the next `apply_workspace`, exactly as on `main`, so in-flight work never falls back to the default relay. Invalidation never takes `workspace_apply_lock`, which restore holds while waiting on the transition lock, so lock order is unchanged. - With no relay applied, backend readers fall back to the default relay, the same state as launch before the first `apply_workspace`. A normal quit and relaunch doesn't bring a pair back on a removed relay: restore targets the applied workspace, and the legacy per-record relay pin is ignored. **Tests** - A `community-rail` e2e case makes the mocked relay's membership check fail, the way an unreachable relay does. It checks that Leave shows the error and keeps the community, and that Remove from this device then removes it, switches to the fallback, stops the agent pair on the removed relay, and leaves the pair on the other relay running. - Unit tests in `managedAgentRelayCleanup.test.mjs` cover relay matching, a stop or listing failure not throwing, and the reconcile fence stopping a pair that started on a relay removed mid-flight. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
036215636f |
Allow following channel messages before their first reply (#7692)
## Summary Channel messages only offered Follow thread after receiving a reply. Add Follow/Unfollow to delivered messages immediately, so users can opt into future replies. Keep the same follow state when broadcast replies gain replies of their own. Threads you authored, participated in, or explicitly followed offer Unfollow. A mention alone leaves Follow available. These controls apply to channel timelines; DM notification behavior remains unchanged. ### Related issue None found in existing issues or PRs. ### Testing - Browser regressions cover keyboard follow/unfollow, persistence after reload, first-reply unread and notification delivery, suppression after unfollow, pending-message exclusion, broadcast reply transitions after revealing buffered arrivals, and immediate unfollow for your own messages. - Additional regressions cover following after a mention and the absence of Follow/Unfollow controls in DM timelines. - Desktop unit tests, typecheck, lint, and production build pass. - Browser notification routing is covered; native OS banner appearance was not tested. ### Screenshots | Before | After | |--------|--------| | <img width="979" height="301" alt="before" src="https://github.com/user-attachments/assets/1a739187-5bc3-4e23-a334-723ca69672f5" /> | <img width="979" height="337" alt="after" src="https://github.com/user-attachments/assets/57df4ff9-bac4-4b9d-a39f-5664e0f9ece0" /> | --------- Signed-off-by: Matthew Boston <mboston@squareup.com> |
||
|
|
0ee2ad7fbf |
Fix rounded macOS icon on older Macs (#8018)
Older macOS versions display Buzz’s opaque icon as an oversized square. Bake rounded corners and transparent padding into the macOS `.icns`, preserving the original bee artwork and leaving other platforms unchanged. Generation uses Hermit-pinned uv, Python 3.14.3, and Pillow 12.2.0. `scripts/macos-icon.sh --check` regenerates temporarily and compares all decoded representations; macOS CI runs it before packaging. Pixel reproducibility is verified independently of Apple’s ICNS container encoding. Validated native AppKit preview, all 10 representations, fresh-cache generation, one-pixel/missing-representation drift rejection, workflow lint, and pre-push checks. Full local `just ci` is running. Packaged Dock checks on older/current macOS remain outstanding. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
bd0896f5c0 |
fix(desktop): read channel metadata and member lists from writer after writes (#7999)
🤖 This fixes two problems with the same cause. Since #3268, the relay can answer an unpinned `/query` from the Aurora read replica, as long as the replica has proved it's within `BUZZ_REPLICA_READ_MAX_AGE_MS` (1000 ms) of the writer. That's fine for display reads, but it doesn't guarantee a client sees its own write. **Problem 1: channel creation (Wes's report).** Right after creating a channel, the desktop read back the channel's kind:39000 metadata by `#d`. If that read landed on a replica that hadn't replayed the event yet, it failed with `channel created but metadata not yet available`. The channel existed, but the chosen template (canvas, agents) was never applied. **Problem 2: member lists.** After any membership change, the member-list refetch could also hit a lagging replica. The relay returned the list from before the change, and the desktop cached it as fresh for five minutes (`CHANNEL_MEMBERS_STALE_TIME_MS`). Someone just removed could still show as a member, or someone just added could be missing. ## Fix 1: metadata read-backs go to the writer `channel_metadata_filter` is a shared builder that adds `"consistency": "strong"`, so the relay serves the read from the writer. It follows the same pattern as the canvas head reads. All four metadata read-backs that follow a write use it: `create_channel`, `update_channel`, `fetch_starter_channel_metadata` (its retry and fallback are unchanged) and the open-DM refetch in `dms.rs`. A unit test fails if the flag is removed from the builder. The template's agent-add step also reads channel members (kind:39002) right after create. `get_channel_members` takes an opt-in `read_your_writes` flag that sends `"consistency": "strong"`. Only `createChannelManagedAgents` sets it on that path, so the agent-add step sees the creator's membership. ## Fix 2: a 31-second writer-read window after membership changes Every membership write the desktop makes calls `noteChannelMembershipChange` for each channel it changed: - the native write wrappers `createChannel`, `openDm`, `joinChannel`, `leaveChannel`, `addChannelMembers`, `removeChannelMember` and `changeChannelMemberRole` - the Bestie conversation open - huddle agent add and remove, for both the huddle channel and its parent - `ensure_starter_channels` and `sync_agents_to_active_huddle`. Both return every channel they created, joined or changed in `changed_channel_ids`, including ones that landed before a later step failed. The original error text is rethrown unchanged. - live join, leave, removed and `admin_kick` system messages, and member-added and member-removed notifications - a report kick resolved from the Admin Console, once the relay confirms it succeeded and only when the report's channel is in the active community. The kick goes through the admin HTTP API, not a channel event. Ban, timeout and their lifts only change `community_bans`, not channel membership, so they need no record. For 31 seconds after a change, measured on `performance.now()`, `getChannelMembers` for that channel sends `readYourWrites`. Why 31 seconds: the relay caps its replica staleness budget at `FENCE_STALENESS` (30 s), so once the window closes, no supported configuration can serve the pre-change list. The constant's comment ties it to `FENCE_STALENESS`. One app-level listener cancels any roster fetch already in flight and refetches. When it registers, it also refreshes every channel whose window is still open. All other member-list reads still use the replica. **Community switch.** The window map is cleared when the community changes. Each awaited write captures a generation through `beginChannelMembershipWrite()` before its first native call, and the reset advances it. So a write that settles after a community switch records nothing in the new community. Live events don't need this fence, because the reset disconnects the relay client first and frames from the old connection are dropped. ## Coverage - A Rust test drives `ensure_starter_channels` against a stub relay that accepts the creates but never serves metadata, and checks that both created starter channels are still reported. - Desktop tests cover the writer-read window, the Admin Console kick, and writes that settle after a community switch. - The desktop E2E test bridge (`desktop/src/testing/e2eBridge.ts`) mirrors the strong reads and `changed_channel_ids`, so E2E runs exercise the fixed behavior. This doesn't address the smaller race in the relay's per-pod accessible-channels cache, and it doesn't change mobile. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
f001ebbb6e |
feat(desktop): add Admin Console Actions tab for direct staff actions (#7904)
Builds on #7883 (merged). Adds an **Actions** tab to the Admin Console so relay staff can ban, time out, or delete directly through the #7883 endpoints (`POST /members/{pubkey}/ban`, `POST /members/{pubkey}/timeout`, `POST /events/{id}/delete`), with no report required. ## Design - **Frozen intent.** Review freezes the whole request (origin, relay, signer, community, action, target, reason, duration, `requestId`). Confirm and Retry resend it unchanged, so a manual retry reuses the same `requestId` and stays idempotent on the relay. The intent is cleared only on a definitive non-409 4xx (`preserveRequestIdOnError`). A 409 or a 202 pending keeps it, except `409 request_id_conflict`, which is final for that id and drops the intent so no futile Retry is offered. A 202 pending renders as a neutral notice, not an error. - **Bound to signer and relay.** The intent carries `expectedPubkey` and `expectedRelay`. Native `admin_direct_action` refuses before sending any request if either is blank or no longer matches (`assert_expected_signer` / `assert_expected_relay_scope`). The tab is keyed on pubkey + origin, so an identity or relay switch remounts it and drops any frozen intent. - **Survives tab switches.** `ActionsTab` stays mounted while the panel is open and is only hidden on other tabs, with the same pubkey + origin key, so a pending, ambiguous or in-flight action (and its `requestId`) is still there on return. - **Nothing-sent refusals.** Every native refusal before the first request leaves (malformed host, changed relay or signer, bad target or duration, the key-backup guard on Reason, a missing HTTP client, the first request build, the signing-key lookup) returns `AdminMutationError` with `notSent: true`; the tab drops the intent and offers no Retry. The request build for the retry after a 401 stays ambiguous, because the first request already went out. The changed-relay refusal says "active relay changed since the action was confirmed; nothing was sent". - **Disabled auth.** With `canMutate=false`, Review, Confirm and every field are disabled. - **Community host.** The host is prefilled from the active relay URL by `communityHostFromRelayUrl` (URL host, lowercased, default port and trailing dot stripped, matching the relay's `normalize_host`) and shown read-only as "Community: <host>" with a Change link. It follows the active community until the operator edits it; with no relay URL the input shows directly. Review derives the host from the same `getRelayWsUrl()` value it freezes into `expectedRelay`, so the two can't disagree. The host is validated natively (`validate_community_host`) and sent only as the `communityHost` query, never in the body. The body is `{requestId, reason?, expirationSecs?}`, built by the pure `direct_action_request`; only a timeout takes a duration. - **Member picker.** Ban and timeout pick the member the way `AddMemberDialog` does: `useUserSearchQuery` name search, `parsePubkeyInput` for a pasted npub or hex in any case, and a selected chip. The target is always sent as lowercase hex. Name search is a profile search on the active relay, not a member list, so it runs only when the host is the connected community; for another host the field takes npub or hex only, and the hint stays visible even with a key selected. Editing the host to a different community clears the pick and the search query, so returning to the original host doesn't bring it back; pressing Change alone keeps it. Each named result shows a short npub so same-name profiles can be told apart, the selected chip opens the full key, and the confirm step shows "Name (npub1…abcd)" plus the full npub (`PubKey variant="full"`). A search result's name is frozen for display only; a pasted key uses `useUserProfileQuery` on the active community, otherwise the short npub. Result buttons follow the disabled state. Changing the member or host drops a frozen intent. Delete keeps its hex event id (native `Hex64` route type, shared with pubkeys). A query that starts with `nsec1` or `ncryptsec1` in any case is never searched; the picker shows "That's a secret key. Never paste it here." instead. - **Errors.** `target_is_staff` and `request_id_conflict` get dedicated copy; other codes fall back to the relay message (`adminErrorCode`). An empty 404/405 from a relay without these routes reads "This relay doesn't support direct actions yet."; an uncoded 404/405 with a body keeps its own text. Success shows a toast. - **Key-backup guard.** Every admin JSON mutation body (the three direct routes and the existing report-resolve routes) passes `egress_guard::assert_no_key_backup_bytes` in `helpers.rs` before signing. The egress inventory points at that guarded boundary. - **One signer snapshot.** `admin_direct_action` reads `signing_keys()` once, checks `expectedPubkey` against it, and signs the request and any 401 retry with that same `Keys`. - **Serialized Review.** Review and its inputs lock while the relay URL resolves; a late Review cannot replace a frozen or submitting intent. - **Reason audience.** Before confirmation the tab states that the reason is delivered to the affected user, and for channel deletes posted publicly in the room tombstone (same copy as the Reports tab). Confirmation shows the frozen reason. ## Fix → test → mutation | Fix | Test | Mutation that turns it red | |---|---|---| | Manual retry reuses `requestId` | `actions-retry` | mint a new id per attempt | | Intent preserved across retryable outcomes | `actions-retry`, `actions-errors` | always clear the frozen intent | | Confirmation tied to signer + relay | Rust `direct_action_refuses_a_changed_signer_or_relay` | skip the signer check | | Identity/origin change drops intent | `actions-identity` | drop the tab key | | Disabled auth keeps Submit off | `actions-disabled-auth` | remove the `canMutate` gate | | Key-backup bytes never leave (`ncryptsec1…` / `NCRYPTSEC1…`, zero requests; ordinary reason sends) | Rust `direct_action_refuses_key_backup_reason_before_any_request` | remove the guard call → panics (request sent) | | Checked signer is the signing key | Rust `direct_action_signs_only_with_the_validated_snapshot` (identity swapped after the signer snapshot is taken, before validation) | re-read `signing_keys()` for signing → signed with the swapped key | | Late Review cannot replace the intent | `actions-review-race` (Review, Review, Confirm ambiguous, Retry → one `requestId`) | drop the Review lock | | Pending is a neutral notice | `actions-pending-neutral` | store pending in the error state → `pending notice must render` | | `request_id_conflict` offers no Retry | `actions-request-id-conflict` | keep the intent on every 409 → `request_id_conflict`, `no Retry for a spent request id` | | Reason audience copy + frozen reason | `actions-audience` | drop the audience copy; drop the frozen reason | | Host prefilled read-only, Change reveals the input | `actions-host-prefill` | drop the prefill → `host must be prefilled from the active relay` | | Name result sent as hex and named on confirm | `actions-member-search` | — | | npub and uppercase hex sent as lowercase hex | `actions-member-keys` | drop the direct key result → `no direct result for npub1…` | | Another community turns name search off with a hint | `actions-foreign-host` | — | | A host change clears the pick, even after returning to the first host; hint stays with a key selected | `actions-host-change-drops-member` | hide the pick by host instead of clearing it → `returning to the first host must not restore the old pick` | | Same-name results differ by key; confirm shows the chosen full npub and sends that key | `actions-same-name` | drop the full npub from confirm → `confirm must show the full npub` | | Results can't be selected once auth is disabled | `actions-results-disabled` | — | | Secret key never reaches search, warning shown | `actions-secret-key` | remove the check → `a secret key reached search_users` | | Pending action survives a tab round-trip, Retry reuses `requestId` | `actions-tab-roundtrip-pending` | render the tab conditionally → `pending notice survives` | | Ambiguous failure survives a tab round-trip | `actions-tab-roundtrip-ambiguous` | render conditionally → fails | | In-flight failure on another tab shows on return | `actions-tab-roundtrip-inflight` | render conditionally → fails | | Pre-send refusal drops the intent, no Retry | `actions-not-sent`, Rust `direct_action_marks_a_pre_send_refusal_not_sent` | ignore `notSent` → `intent dropped; no Retry` | | Key-backup refusal is nothing-sent; Review editable again | Rust `direct_action_refuses_key_backup_reason_before_any_request`, `actions-backup-reason-not-sent` | drop `not_sent` on the guard → panics at the `notSent` assert; stub `notSent: false` → `no Retry after a pre-send refusal` | | Only an empty 404/405 reads as an old relay | `actions-old-relay` | drop the empty-body condition → `404 admin API error: Not Found` | --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
965e1997b5 |
fix(agents): stop built-in prompts from teaching sleep polling (#7992)
An @mention wakes an agent only once its running tool call returns, so agents that loop `sleep` + `buzz messages get` (or poll CI) delay every reply that arrives in the meantime. Buzz's own prompts taught that loop. - `crates/buzz-acp/src/base_prompt.md`: replace "No push notifications — poll with …" with a rule to never use a tool call (`sleep`, `--watch`) to wait on CI, teammates, or other external events; hand off and end the turn. `buzz messages get --since` stays as a one-shot catch-up. "keep working until you publish the verified result" now reads "the result, handoff, …" so agents stop reading it as wait-for-green-CI. - `desktop/src-tauri/src/managed_agents/nest_skill.md`: the "Polling Pattern" sleep-and-repeat loop becomes a "Catching Up" one-shot `--since` read. 🤖 Opened by Duncan. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
95b018c871 |
feat(acp): wrap workers at the subprocess launch boundary (#7985)
## Summary Replacing `BUZZ_ACP_AGENT_COMMAND` with a sandbox launcher hides the real adapter from Buzz: for example, Goose loses its default `acp` argument. Add optional `BUZZ_ACP_LAUNCH_PREFIX`, a JSON argument array applied at the shared subprocess boundary, after normal worker configuration. On Unix, every spawn runs `prefix... worker args...` without shell interpolation. Worker identity, environment setup, stdio and existing process cleanup are preserved. Invalid or unavailable prefixes fail launch without falling back to the worker; unset keeps direct launch on every platform. Configured prefixes fail closed on non-Unix because per-worker process-tree cleanup is unavailable. Desktop reserves the key against saved user-environment overrides, and spawn failures name the executable. ### Related issue Prerequisite for [Buzz-App #415](https://github.com/block/buzz-app/pull/415). One commit directly on `main`, independent of #7942–#7944. No duplicate launch-prefix PR found. Policy enforcement, verified launcher staging, protected paths and the supporting runtime pin remain Buzz-App/plugin work. ### Testing - Original head `9ae9d82f8`: complete ACP suite (990 passed, three existing ignored), shipped local-task test, real-Goose probe, and full repository-wide `just ci` passed. - Review fixes: five launch tests and 44 Desktop environment-filter tests pass locally, covering the real production platform gate, save rejection, both merge layers, case variants, and executable error context. - Subprocess tests cover direct/wrapped argument defaults, Pi skills, Hermes/Codex environment, adapter identity, repeated spawns and invalid/missing prefix refusal. - Shipped `buzz-acp run` entrypoint completes a task through the wrapper and deterministic ACP peer. - Real Goose 1.52.0: direct and wrapped `auth-methods` return identical results; the wrapper receives `goose acp` with initially empty worker args. - Independent agent review: no blocking findings. - Required push checks passed on the updated head. The existing native Windows CI job now explicitly runs the platform-contract tests. Its result is pending; the non-Unix refusal branch was not exercised on this macOS host. Repeated-spawn coverage is not a full lazy-pool wake/crash test. Protected Goose in the Buzz-App UI and human acceptance remain pending. To try the hook after building `buzz-acp`, use an installed Goose path: ```sh BUZZ_ACP_LAUNCH_PREFIX='["/usr/bin/env"]' target/debug/buzz-acp auth-methods \ --agent-command /absolute/path/to/goose --agent-args '' --json ``` Expect the same authentication methods as without the prefix. Changing the prefix to `["/missing-launcher"]` must fail. Draft pending human testing. Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> |
||
|
|
2664d14316 |
Add Kimi Code's default install path to managed-agent binary discovery (#5997)
## Summary Buzz Desktop fails to detect a freshly-installed Kimi Code CLI, even though `kimi --version` works fine in the terminal. Root cause is in binary discovery, not in Kimi's install. ## Problem After running Kimi Code's installer, Buzz Desktop's harness list doesn't pick up the `kimi` binary — it stays undetected until the user manually sources their shell config. ## Repro 1. Install Kimi Code: `curl ... code.kimi.com/kimi-code/install.sh | bash` — installs to `~/.kimi-code/bin`, and the installer appends a PATH export only to `~/.zshrc`. 2. Open a fresh terminal or restart Buzz Desktop without `source ~/.zshrc`. 3. Buzz Desktop does not detect `kimi`, even though it's on disk and executable. ## Root cause `find_via_login_shell()` in `desktop/src-tauri/src/managed_agents/discovery.rs` resolves binaries by spawning `zsh -l -c 'command -v <bin>'`. A login shell sources `~/.zprofile`/`~/.zshenv`, but zsh only sources `~/.zshrc` for *interactive* shells — so a PATH export that lives only in `~/.zshrc` (which is where Kimi's installer puts it) is invisible to this probe. `~/.kimi-code/bin` is also absent from the static fallback list in `common_binary_paths()`. This is the same root-cause class already handled for `nvm` in this file (see the `find_nvm_default_bin` comment) — Kimi just doesn't have an equivalent fallback yet. ## Fix Add `~/.kimi-code/bin` to the Unix branch of `common_binary_paths()`, alongside the existing `.volta/bin` / `.bun/bin` / `.asdf/shims` entries. ## Scope note This PR only fixes detection (finding the binary). It does not address that Kimi shows a "Ready" badge regardless of login status — that's a separate, larger gap affecting all five preset harnesses (Kimi, OpenCode, Amp, Hermes, OpenClaw), tracked separately. ## Test plan - [x] `just ci` passes - [x] Manual: fresh Kimi install per repro steps above, confirm Buzz Desktop shows it without sourcing `~/.zshrc` Signed-off-by: Shawn Hank <shawn@shawnhank.com> |
||
|
|
12dbb114aa |
test(desktop): fix flaky forum empty-draft replacement e2e (#7981)
## Summary
The `replacement=` cases (`add` and `publish`) of `forum source visit …:
A→B→A retains source draft` in `forum-agent-invitation.spec.ts` are
flaky. They fail at line 449 because the source draft still reads
`@RemoteScout hello`.
Cause: Playwright's `fill("")` on a contenteditable sets a DOM selection
and then presses `Delete`. ProseMirror handles `Delete` with its own
selection, which sometimes hasn't synced from the DOM yet. When that
happens the key press does nothing, `onUpdate` never fires, and the
clear is never recorded. I confirmed this with temporary logging:
failing runs have no authored-content update after the fill. Non-empty
`fill` uses `insertText`, which goes through ProseMirror's DOM input
path, so it isn't affected.
Fix (test only):
- Clear the draft with `ControlOrMeta+a` followed by `Delete`, so
ProseMirror owns the selection.
- Assert the replacement text before navigating away.
### Related issue
None found. The spec hasn't changed since #7125. This test failed in 10
of the last 40 CI runs of `Desktop Smoke E2E (2)`, for example #3700.
### Testing
- `--repeat-each=100 --retries=0 --workers=4` on the two affected cases:
- Before the fix: 14 of 200 runs failed at `:449`.
- After the fix: 200 of 200 passed.
- Full `forum-agent-invitation.spec.ts`: 19 passed.
- Pre-push hooks passed (desktop typecheck and desktop tests).
Signed-off-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz>
|
||
|
|
fccc07ebe3 |
chore(release): release Buzz Desktop version 0.5.26 (#7980)
## Buzz Desktop release v0.5.26 - **Frozen main:** `7ebf1beb44139264e9c5c7fee6970429ff66f934` - **Reviewed candidate:** `2b4b138dc5cf2d9cc1a0ceb21d9063ff56fe8bf4` - **Previous desktop release:** `desktop-v0.5.25` - **Proposed immutable tag:** `desktop-v0.5.26` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
7ebf1beb44 |
fix(desktop): correct Grok Build setup guide URL (#4846)
## Problem The **Grok Build** harness preset points its "Setup guide" link at `https://build.x.ai/docs`, which is unreachable — the hostname has no DNS record at all, so the button leads to a dead page rather than a 404. ```console $ getent hosts build.x.ai (no output — no record) $ getent hosts docs.x.ai 2606:4700::6812:1350 docs.x.ai $ curl -sS https://build.x.ai/docs curl: (6) Could not resolve host: build.x.ai ``` This is user-facing: the link sits behind the **Setup guide** button in the harness picker, so anyone trying to connect Grok Build hits it while looking for install instructions. ## Fix Point the preset at xAI's live Grok Build documentation. ```diff - install_instructions_url: "https://build.x.ai/docs", + install_instructions_url: "https://docs.x.ai/build/overview", ``` The preset's `command` and `args` are **unchanged and remain correct** — Grok Build ships native ACP over stdio, which `grok agent stdio` exposes: ```console $ grok agent --help Commands: stdio Run the agent over stdio headless Run the agent headlessly over the Grok WebSocket relay ... ``` ## Testing - `just ci` passes locally (fmt, clippy, desktop lint, Tauri clippy, unit tests, mobile tests). - Verified `https://docs.x.ai/build/overview` resolves and returns `200`. - Verified the harness itself still connects: installed Grok Build 0.2.118 via `irm https://x.ai/cli/install.ps1 | iex` and confirmed `grok agent stdio` matches the preset's invocation. No UI changes — this is a string constant, so no screenshots apply. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: armstrongj2001 <110222228+armstrongj2001@users.noreply.github.com> Co-authored-by: Diem Nguyen <ngthuydiem@users.noreply.github.com> |
||
|
|
c96553eb13 |
fix(desktop): resolve unlisted project channel requests (#7619)
## Summary - resolve project channel-creation approvals through the authoritative home-channel selector - permit owner-visible unlisted project homes when their channel and repository binding match - add regression coverage for the unlisted authoritative-home path ## Validation - `just ci` - focused project-home regression tests (10/10) - full Desktop suite (6,451/6,451) - `pnpm check` - `pnpm build` ## Manual verification After merging into a Desktop build, approve the pending LBX Copywriting workroom request from the Litbox OS project. Signed-off-by: crgallego <chris.cokerconstruction@gmail.com> Co-authored-by: crgallego <chris.cokerconstruction@gmail.com> |
||
|
|
6918dd3a83 |
Schedule the deletion drain safely (#7827)
## Why The deletion engine already exposes a one-shot `buzz-admin deletions drain` command, but operators have no safe way to schedule it. A generic workload wrapper could expose arbitrary commands or inherit relay credentials, which would add a second control plane and widen the blast radius. ## What This PR adds an opt-in Kubernetes CronJob for the typed deletion drain command. It is stacked on #7818, which adds owner-request admission. ## How The chart keeps the job disabled by default. The CronJob uses `Forbid`, zero Kubernetes retries, bounded runtime and history, and only the database, Redis, and object-store settings required by the executor. PostgreSQL leases, checkpoints, and retry timing remain the sole durable retry authority. The closed `_operator-jobs.tpl` helper centralizes safe CronJob mechanics without creating an arbitrary command or environment registry. This leaves the touched system simpler than a general job framework would. ## Risk The deployment blast radius is low because the job is disabled by default. When enabled, it can run destructive deletion work that the database has already approved, so the chart limits concurrency, retries, credentials, and execution time. ## Testing Rendered every `deploy/charts/buzz/ci/*.yaml` fixture with `helm template`; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`. Ran the full independent Desktop, Tauri, and web lanes; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`. Flutter package resolution made no progress within three minutes, so the mobile lane is not claimed green. ## Bigger picture A later stack layer must inventory and approve admitted owner requests. Until then, this scheduler cannot make a new owner request destructive. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Signed-off-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
f1e50be410 |
test(desktop): wait for channel head refresh before paging thread summary test (#7955)
🤖 ## Summary Main's Desktop Smoke E2E (4) job fails on `scroll-history.spec.ts` ("thread summary badge survives a retained older-history prepend"). This change fixes the test's timing. It does not change app code. When a channel opens, the app starts a live subscription. When that subscription is ready, the app fetches the newest page again, and that fetch replaces the whole loaded history. This test scrolled up before that refresh happened. On slower CI runners, the refresh came after the older page loaded but before it showed on screen. The older rows went away, the reader stayed at the top, and no more pages loaded. All three attempts failed that way. The test now calls `waitForMockChannelHeadReady` before it scrolls, like the other pagination tests in this file already do. That helper waits for the live subscription and its refresh to finish. ### Related issue Main CI failure at `15d44dcc` (https://github.com/block/buzz/actions/runs/36470599751/job/109092076235). The same failure was reported on #7827. ### Testing - Added temporary logs to see the order of events. Locally, the refresh ran after the older page had loaded: the loaded history went from 2 pages back to 1. The test passed only because it checked before the next render. With the wait, the refresh runs first and nothing replaces the loaded pages later. - `playwright test --project=smoke tests/e2e/scroll-history.spec.ts`: 18 passed. This test with `--repeat-each=8`: 8 passed. ### Follow-up (not in this PR) The app has the same race for real users. If someone scrolls up before the live subscription is ready, or during a reconnect, the refresh can drop the older history they loaded. If they are at the top of the list, paging stops until they scroll down and back up. Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
9b1c4f98e2 |
🤖 perf: bound long-thread aux reads and make query deadlines terminal (#7854)
Long threads (over ~100 replies) time out in desktop. The aux hop that
loads reactions and other e-tag references builds its filter as one
`tags @> …` term per id, joined with `OR`. Past about 100 ids the
planner switches to walking each partition's primary key backward,
expecting to stop early. When few rows actually match, it scans the
whole community. With a dirty GIN pending list, a 188-reply thread never
finishes.
### What changes
- **Set-based e-tag reads (`buzz-db`).** The aux e-tag query now puts
the id set behind a `MATERIALIZED` CTE and matches with `ANY`. Each
e-tag read also runs under a transaction-local `statement_timeout` of 20
s.
- **Terminal timeout contract (`buzz-relay`).** When a read hits the
deadline, the client gets a terminal timeout (`CLOSED` for REQ, the
timeout error for COUNT and search), not a retryable failure.
- **Subscription lifecycle (`buzz-relay`).** The per-connection
subscription map lock is now the lifecycle lock. Claims, replacement,
client `CLOSE`, owner-checked timeout and search cleanup, revoke, and
connection teardown all change the map, the registry, topic refcounts
and terminal frames while holding it. As a result, a timed-out
subscription's `CLOSED` goes out before any replacement with the same id
can claim it, and claims made after teardown are refused.
- **Dropped terminal frame disconnects (`buzz-relay`).** If the `CLOSED`
for a retired subscription (owner-checked timeout or channel eviction)
can't be queued, the relay cancels the connection instead of leaving the
client holding a subscription that will never receive fan-out. A
client-initiated `CLOSE` ack is exempt.
- **COUNT under the same deadline (`buzz-db`).** The fast COUNT path
runs e-tag counts under the same transaction-local 20 s cap as SELECT,
so WS COUNT and HTTP `/count` produce the terminal timeout too.
- **Replica reads surface the deadline (`buzz-db`).** When a
replica-routed read is cancelled by `statement_timeout` (SQLSTATE
`57014`), the error is returned instead of re-running the query on the
writer, which would run the same slow statement. Other replica errors
still fall back to the writer.
`PLANS/REPLICA_FULL_READ_ROUTING_DESIGN.md` records the exception.
- **Deadline handling (desktop).** Only the relay's `query timed out`
counts as a query deadline. A client-side `request timed out` stays
retryable. For one-shot reads a deadline is terminal: the global query
retry (budget 1), thread-replies retry and reaction hydration don't
re-send the same slow query. Reaction ids held back by a deadline are
released when the channel is opened again, so reopening retries them
while same-channel re-renders don't. Live subscriptions, in both the
renderer and the native archive-sync socket, keep treating a deadline
`CLOSED` as transient: they back off and resubscribe with the same
filter instead of being dropped.
- **Deadline handling (mobile).** Mobile recognizes the deadline on both
transports: HTTP `503 {"error":"query timed out"}` and the WS `CLOSED`
reason.
- The root `ProviderScope` retry policy returns no retry for deadline
errors and keeps Riverpod's default backoff for everything else.
- Background thread-summary recounts don't retry a deadline.
- Forum posts polling pauses while the last load settled on a deadline;
reopening the forum retries.
- A channel's newest-window load surfaces a deadline instead of falling
back to legacy WS history, and the channel list's batched queries return
"unavailable" on a deadline instead of fanning out to per-filter
fallbacks. An unavailable batch is not treated as empty. Known
last-message timestamps and unread state are kept, and unread catch-up
moves `lastMessageAt` forward (never backward) the same way live events
do, so a cold start with no known timestamp still gets one.
- A live subscription that gets a deadline `CLOSED` backs off and
resubscribes with the same filter. History requests still fail on it.
- Settled load failures show a Retry control, matching desktop's
thread-replies error card: a shared `LoadErrorView` on the channel
timeline, forum thread and forum posts list, and an icon button beside
the thread replies summary. While the thread query has no value, the
thread page shows the route snapshot plus live, cached and optimistic
replies, with the loading or error status still visible.
- Other re-sends (reconnect, foreground resume, live events, the unread
backstop, scroll) are not suppressed; each is bounded by the relay's 20
s statement deadline.
- **Long-thread seeder.** `just seed-long-thread [replies]` posts one
thread (default 187 replies) with some reactions to the local dev relay,
for reproducing long threads in the apps. It uses a throwaway key,
refuses non-loopback relays and ignores ambient `BUZZ_*` credentials.
Writes are paced to fit the stock relay rate limit.
**Lifecycle concurrency.** The lock serializes one connection's
subscription changes. Other connections can still be delayed by pressure
on the shared pubsub command queue, and by serial eviction loops waiting
behind a blocked connection. This PR does not claim strict isolation
between connections.
**Recovery vs. load.** Clients stop automatic retries of the same
one-shot read, but they don't remember deadlines across other triggers:
reconnect, resume, live events and the unread backstop can each send the
read again later, bounded by the 20 s statement deadline. A live
subscription that hits the deadline resubscribes with its original
filter under the existing 1–30 s backoff, and against a filter that
keeps timing out it retries at the 30 s cap indefinitely, so the worst
case is one bounded query per subscription every 30 s. `since` is
deliberately not advanced to now on resubscribe, because that would skip
events missed while the subscription was closed. Unchanged from `main`:
when mobile's channel history sync fails with cached messages available,
the cached messages are shown without the error or Retry control.
### Benchmark
Local reproduction on an isolated bench stack (not committed):
- PG16, migrations applied, one 188-reply thread plus 5M background
events in the same community.
- Each request replays the desktop thread-replies filter through the
relay, with `include_aux` on. Times are total ms for 3 runs, with a 60 s
client cap.
GIN pending-list state is controlled with `VACUUM` (clean) or by
refilling the pending list (dirty), and recorded per partition with
`pgstatginindex`. Dirty runs held 402 pending pages on every partition's
tags index at the start and end of each matrix. Clean runs held 0–1
pages (relay startup writes).
| build | GIN pending state | limit 10 | 50 | 100 | 200 (188 ids) |
|---|---|---|---|---|---|
| `main` | clean | 26 / 16 / 13 | 56 / 51 / 52 | 92 / 93 / 95 | 662 /
639 / 641 |
| `main` | dirty | 315 / 298 / 327 | 22141 / 15888 / 16769 | 28992 /
27819 / 28080 | timeout ×3 (>60 s) |
| this PR | clean | 35 / 21 / 14 | 57 / 49 / 48 | 82 / 80 / 75 | 153 /
150 / 135 |
| this PR | dirty | 290 / 254 / 251 | 1257 / 1204 / 1206 | 2432 / 2378 /
2382 | 4333 / 4706 / 5104 |
Baseline `origin/main` `b37e47721`; this PR at `5f2650f5f`, whose e-tag
read builders are identical to the current head. At head `1ad81d868`
against `origin/main` `15d44dcce`, limit 200 measured 145–179 ms clean
(`main` 683–715) and 4.41–4.51 s dirty (`main` timeout ×3).
Every completed cell returns the same signed event set: 209 events, 187
replies at limit 200. With `include_aux` off, every cell is ≤66 ms. The
thread-window loader (`select_aux`) is unchanged from `main`, and its
timings and captured plans match `main`'s.
Actual aux-hop statements captured from each relay binary with
`auto_explain` (analyze + buffers), execution ms:
| build | clean, 100 ids | clean, 188 ids | dirty, 100 ids | dirty, 188
ids |
|---|---|---|---|---|
| `main` (N-way `OR`) | 40 | 298 | 28785 | 62892 |
| this PR (`MATERIALIZED` fence + `ANY`) | 23 | 45 | 1992 | 3539 |
**What the fence changes**
- On `main`, the planner combines `ORDER BY created_at … LIMIT` with an
overestimated match count and walks each partition's primary key
backward, expecting to stop early. With few real matches it scans the
whole community: the dirty 188-id statement touches ~4.9M shared buffers
on `main` vs ~0.6M with the fence.
- The fence prevents that early-stop plan. Across the measured cases
(1–188 ids, both pending states), the planner used the GIN tags index.
The fence doesn't force GIN in general; a broad enough filter could
still get a different plan.
**Tradeoff:** the fence materializes every match before sorting. In this
bench small limits stayed within run-to-run noise of `main`. A single
hot id (5,000 reactions, limit 100) measured 30–34 ms unfenced vs 35 ms
fenced.
**Deadline:** each e-tag read (SELECT and COUNT) runs under a
transaction-local `statement_timeout` of 20 s. It only tightens: a
shorter operator timeout (`BUZZ_DB_STATEMENT_TIMEOUT_MS`) is kept. It's
a per-statement limit, not a per-request one, so a request that runs
several pages or hops can take longer than 20 s in total. The benchmark
does not force the deadline.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
|
||
|
|
cc36d25bec |
Add native HPKE encryption for nsec backups (#7849)
## Summary Add a Rust API that encrypts an nsec into a backup envelope using RFC 9180 Base mode: DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, and AES-256-GCM. --------- Signed-off-by: jm <jm@squareup.com> Signed-off-by: Jordan Mecom <jm@squareup.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
548718e042 |
test(desktop): scope video menu e2e probes to emitted messages (#7953)
## Summary
Fixes the `Desktop Smoke E2E (4)` failure on `main`. The test at
`desktop/tests/e2e/video-attachment.spec.ts` ("right-click menus expose
distinct selectors for links, relay video, and off-relay video") fails
3/3 attempts on main and on every PR branch since ~Sept 25. Because
"Desktop" is a required check, that blocks merges.
**Cause:** the relay-video and off-relay-video probes picked their
player with `getByTestId("video-player").last()`. When mock messages
share a second, timeline ordering can put a different video last. The
locator then resolves to the wrong player, and the "Download video" menu
detaches before the click, so the test times out.
**Fix:** scope each probe to the `data-message-id` of the message
`emitVideoMessage` just returned. Test-only change, 16 lines.
## Attribution
This is a cherry-pick of `dfd5db4e0` by @tornquist, originally part of
#7827 (the deletion-drain cron, which is stacked on another feature
branch). It's pulled out on its own so main can go green without waiting
on that stack. Authorship is preserved.
## Testing
- `pnpm build:e2e && playwright test tests/e2e/video-attachment.spec.ts
--project=smoke`: 13/13 passed locally, including the target test.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signed-off-by: tornquist <tornquist@squareup.com>
Signed-off-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Co-authored-by: tornquist <tornquist@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
|
||
|
|
52c4c08c9e |
fix(sidebar): converge stale-at-open state across devices (sections/sort/stars/mutes) (#7805)
Adds bounded-backoff recovery reads to all four sidebar sync hooks (sections, sort, stars and mutes). A stale relay read at app open now converges without a user edit or a reconnect. ## What this does **Shared recovery hook (`useStaleReaderRecovery`).** All four hooks use one retry effect. It reads immediately on mount, then at 5, 10, 30 and 60 s, staying at 60 s after that. A change of `visibilityState` to `visible` triggers an immediate read. Reads are single-flight. The effect restarts whenever `pubkey` or `relayUrl` changes, so a hook that stays mounted while switching relays drops the old relay's in-flight reads. **Guards that stop a recovery read from rolling back a local edit:** 1. **Skip the read while an edit is pending.** If an edit is pending when a tick starts, the tick skips its read and just schedules the next one. Without this, the read could return the pre-edit blob after the edit published and pending cleared. At that point neither apply-time guard could reject it. 2. **Pending check inside the state updater.** `hasPending()` and the effect lifetime are re-read inside `setStore(prev => ...)`. That way the check runs when React executes the updater, not when the response arrives. 3. **Local-mutation revision.** Each hook counts its local edits. The count is snapshotted when a read starts and checked again at apply time. If an edit landed while the read was in flight, the response is discarded. 4. **Head advanced only after a successful cache write.** A failed `write*Store()` leaves the same head retryable on the next tick. **Ownership-aware pending clear.** Each sync manager's `doPublish(store)` now clears `pendingStore` only while it still holds that same `store`, on both the identical-payload early return and the success path. Before, an older publish that finished after a newer edit had replaced `pendingStore` wiped the newer edit's pending marker, so a recovery read before the newer debounce could apply the older relay head over it. `destroy()` still clears unconditionally. ## Scope Reader-side recovery plus the ownership-aware pending clear. No outboxes, arbitration or replay changes. It applies to `useChannelSections`, `useChannelSortPreference`, `useChannelStars` and `useChannelMutes`. Related: [#7806](https://github.com/block/buzz/pull/7806) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
f7ff4294fa |
feat(nip-fi): harden Blossom kind-24242 verifier to NIP-FI spec (#7288)
Brings `buzz-media`, `buzz-relay`, all first-party kind-24242 minters, and desktop token minting into compliance with [NIP-FI §kind-24242](https://github.com/block/buzz/blob/main/docs/nips/NIP-FI.md). ## What changed **`buzz-media/src/auth.rs`** - Add `BlossomStrictness` enum (`Strict` | `Permissive`). `Strict` applies full NIP-FI rules (active modes); `Permissive` preserves pre-NIP-FI Off-mode behavior byte-identical [FI-INV-15]. - Rewrite `verify_blossom_auth_event_for_verb` with count-based cardinality: exactly one `t`/`expiration`/`server` in `Strict`; duplicate `x` also rejected. `Permissive` keeps main's boolean semantics exactly: any valued `t` other than the verb (including `""`) rejects, valueless `t` is ignored. - `Strict`: mandatory `server` tag on all proofs (upload + read); absent or mismatched → `evidence_rejected` (`ServerMismatch`). - `Strict`: 60-second proof window (`now - created_at <= 60s`, `expiration <= created_at + 60s`). `Permissive`: 3600s / optional server. - **[F1]** Permissive server validation uses any-match semantics: a proof with multiple `server` tags is accepted if any tag matches the bound host. This restores origin/main base behavior. Strict duplicate-rejection unchanged. - **[F1 compat]** Permissive server gate uses `server_values.is_empty()` (valued tags only) rather than `server_count > 0` (includes valueless tags). A lone valueless `["server"]` tag is treated as absent in Permissive/Off mode, matching pre-NIP-FI base behavior [FI-INV-15]. - **[F4]** Valueless `x` tags in Strict get reads are tracked before `filter_map` so they cannot become absent scope (host-wide authorization). Any `x` tag present in Strict must contain the exact requested sha256. - **[F3 unit]** Cases A–D in `auth.rs` are helper-level unit tests: they call `verify_blossom_upload_auth` and `verify_upload_hash_only` directly. They do not enter the upload pipeline (`process_upload`/`process_video_upload`). Retained as helper evidence. **`buzz-media/src/error.rs`** - Add `DuplicateTag(&'static str)` variant. - Add `blossom_denial_kind()` method: classifies each auth error as `MissingEvidence`, `EvidenceRejected`, or `AuthorizationDenied`. Non-auth errors return `None`. - `IntoResponse` retains the legacy JSON 401 shape [FI-INV-15]. **`buzz-relay/src/api/media.rs`** - Add `MediaDenial(MediaError, BlossomStrictness)` wrapper mapping to byte-exact NIP-FI fixed responses in `Strict`; falls through to legacy JSON in `Permissive`. - `blossom_strictness_from_state()` selects `Strict` when `state.config.nip_fi.is_enforce()`, otherwise `Permissive`. Strictness is threaded through the `admit_nip_fi_http_on_state` NIP-98 closures on upload and read, and through the post-admission X-SHA-256, hash-binding, and membership denials. - `extract_blossom_auth` keeps first-value `Authorization` extraction; Enforce rejects repeated fields at the `admit_nip_fi_http` cardinality gate, and Off keeps legacy behavior [FI-INV-15]. - Both membership-denial call sites use `media_denial(RelayMembershipRequired, strictness)` so Strict mode produces the correct fixed-text response. - `upload_blob` serving-write fence conversion (`:406`) uses `map_err(|e| media_denial(e, strictness))` — a community fence committed between auth admission and lease acquisition no longer resets Strict to Permissive. - Mode-driven regressions in `mod postgres_tests` (selected by the nextest `postgres-ci` profile via `test(/postgres_tests::/)`, `#[ignore = "requires Postgres"]`), run through the full relay router with a real Enforce verifier and assertion: Enforce rejects Permissive-only proofs (300s lifetime, missing `server` tag) on both upload routes and reads with `evidence rejected`; Off accepts the same proofs with pre-NIP-FI responses; DenyProtected answers 503 before any proof check; membership denials are `authorization denied` text/plain in Enforce and exact legacy JSON 403 in Off, with a member positive control. GET and HEAD are each discriminated (HEAD with a suppressed body). Both upload routes pin the handler's `X-SHA-256` missing/malformed/unsigned and post-body hash-mismatch denials in both modes. Off rejects an empty-valued `t` beside a valid one with legacy 401 JSON in either order, and ignores a valueless one. Exact status, Content-Type, challenge, and body bytes are pinned. **`buzz-media/src/upload.rs`** (`mod minio_tests`) - **[F3 pipeline]** Four tests exercising the actual post-body call sites (`process_upload` `:85`, `process_video_upload` `:413`) with real MinIO I/O: buffered/streaming × expired-proof-accept + mismatched-hash-reject. The positive cases use `fresh_strict_upload_auth` (`created_at = now-58`, `expiration = now+2`, lifetime = 60s) — Strict-admissible at sign time — followed by a real 3s sleep so the proof is expired when the upload completes. `nostr::Timestamp::now()` reads the OS wall clock directly; paused Tokio time does not advance it. The negative cases use a pre-expired proof with a mismatched `x` tag. All four are `#[ignore = "requires MinIO"]`; no CI lane runs them automatically. **`buzz-media/src/validation.rs`** - `minimal_valid_mp4()` promoted to `#[cfg(test)] pub(crate)` — a 618-byte pre-computed H.264/avc1 fast-start MP4 available to all `buzz-media` test modules; smoke-tested by `test_minimal_valid_mp4_passes_validate_video_file`. **`desktop/src-tauri/src/commands/media.rs`** - `sign_blossom_upload_auth`: `server` tag mandatory; upload expiry 60s (was 3600s/300s). - `MEDIA_GET_AUTH_EXPIRY_SECS`: 60s (was 600s). **`desktop/src-tauri/src/media_proxy.rs`** - Single re-mint+retry on 401/403 for range requests; non-auth failures do not retry. **`crates/buzz-cli/src/client.rs`** - Read expiry: 60s (was 600s). Upload expiry: 60s. `server` tag mandatory on upload. **`crates/buzz-dev-mcp/src/view_image.rs`** - `MEDIA_GET_AUTH_EXPIRY_SECS`: 60s (was 600s). Test updated. **`mobile/lib/shared/relay/media_auth.dart`** - `_mediaGetAuthLifetimeSeconds`: 60 (was 600). Refresh margin updated. **`mobile/lib/shared/relay/media_upload.dart`** - `_uploadAuthLifetimeSeconds`: 60 (was 300). `server` tag mandatory. **`mobile/lib/features/channels/media_viewer_page/video_viewer.dart`** - **[F2]** Remove Android-specific `VideoPlayerController.networkUrl` streaming path. `video_player_android` 2.9.5 freezes headers into static `DefaultHttpDataSource` request properties — a 60s proof minted at controller creation time becomes stale on seeks. All platforms use the authenticated local-file download path. - **[F2 transport]** `unawaited(request.sink.close())` before `client.send()`. `AbortableStreamedRequest` carries no request body; without closing the sink, `IOClient.send()` awaits `stream.pipe(ioRequest)` which blocks until the sink ends — every video download hung in loading indefinitely on every platform. - **[F2 pending-ctrl]** `pendingController` ref set before the first `await (initialize())`, cleared in all exit paths. Effect cleanup disposes `pendingController.value` so a close-during-init releases the native player even if the initialized event never arrives. - **[F2r(a)]** On native init failure (post-create, pre-initialized-event), the inner catch starts disposal detached via `unawaited(localController.dispose().catchError(...))` — the `.catchError` handler absorbs any disposal exception so it does not reach the test error zone — then rethrows the original load error. `video_player` 2.11.1 completes the init future with an error without disposing the native player. - **[F2r(b)]** Non-2xx error body cancelled via `_cancelVideoResponse()` (subscribe+cancel) instead of `drain()`. `drain()` waits for the upstream to close; a stalled server body blocked `initializeVideo` indefinitely. - **[F2r(d)]** `unawaited(localController.dispose().catchError(...))` in the catch — not `await`. Two failure mechanisms exist: (1) when `createWithOptions()` throws, `_creatingCompleter` is never completed and `await dispose()` deadlocks at `:682-683`, blocking the outer catch from setting error state. (2) when `createWithOptions()` succeeds but initialization fails, `dispose()` runs but its thrown exception becomes an unhandled async error reaching the test zone. Both are fixed: `unawaited` lets the outer catch run immediately and set `error.value`; `.catchError(...)` on the detached disposal future absorbs any disposal exception. **`mobile/test/features/channels/media_viewer_page/video_viewer_test.dart`** - `_FinalizingFakeClient`: calls `request.finalize().drain()` — a test without the sink fix times out because the drain never completes, validating the transport contract. - F2r(a): `forceInitError=true` fake emits `PlatformException` on subscribe; `disposeCallCount >= 1` verifies disposal even on init failure. - F2r(b): `stalledBody` `StreamController` with `onCancel` Completer (bounded completion signal, not a sleep). Asserts: (1) `bodyStreamCancelled=true` while body stream is still open (impossible with `drain()`); (2) error UI visible while body remains open. Restoring `drain()` breaks both. - F2r(c): `neverInitialize=true` fake never sends initialized event; unmount asserts `disposeCallCount >= 1` via `pendingController`. Distinct from the event-error case. - F2r(d): `forceCreateError=true` fake throws `PlatformException` from `createWithOptions()` itself (before any player ID is returned). Asserts error UI visible within a bounded deadline. With the old `await dispose()` the outer catch is blocked — `error.value` is never set and the error UI assertion fails at the deadline; with `unawaited(dispose())` the error text appears immediately. **`mobile/test/features/channels/media_viewer_page/video_viewer_transport_test.dart`** (separate file) - Real-IO loopback probes using `flutter_test` (not `package:test`) to avoid `depend_on_referenced_packages` lint. No `TestWidgetsFlutterBinding` — no HttpClient override interfering with `dart:io`. - Probe 1: `HttpServer.bind(loopbackIPv4, 0)` reads the full request body before replying. Without `sink.close()`, `req.drain()` hangs and the test times out. - Probe 2: server signals request arrival via a Completer (not a sleep); abort fires after confirmed arrival; `client.send()` must throw `RequestAbortedException` (typed assertion) within a bounded 5s deadline. **`scripts/test-video-upload.sh`** - Proof minted with `+60` expiry and mandatory `server` tag. **`buzz-test-client/tests/e2e_media*.rs`** + **`buzz-relay/src/api/media.rs` test fixtures** - All fixtures updated to 55s expiry + `server` tag. **`RELEASING.md`** - New "NIP-FI upload proof freshness (relay floor)" section documenting that 60s proofs require the first relay release carrying this PR's post-body hash-only check (the first tag > `v0.2.1` — update once cut) for reliable large-file uploads. Operators on older self-hosted relays (≤ `v0.2.1`) must upgrade before receiving these clients. ## Finding 3: 60s upload proofs vs old-relay compatibility Desktop/CLI/mobile upload proofs are now 60s. Older relays re-run full expiry verification after the body is transferred. A valid upload whose body transfer takes >60s will fail on an old relay. **Resolution (relay floor prerequisite):** `RELEASING.md` requires that verified supported deployments (relay releases confirmed to carry the post-body hash-only check, commit `75e9bef748`) be in place before distributing 60s-proof client builds. Unknown or unverified self-hosted relay deployments are excluded from distribution until they upgrade. This makes the verified-deployment status a prerequisite for release distribution, not a post-distribution advisory. The concrete tag (`> v0.2.1`) is to be recorded once cut. ## Sequencing note Builds on #7264 (merged): Blossom strictness is derived from its `config.nip_fi` mode in this PR, so enabling `BUZZ_NIP_FI_MODE=enforce` activates the strict verifier with no further wiring. ## GIF compatibility exception An unauthenticated Off-mode request on a configured tenant with no GIF provider changes from 404 to 401 (base `gifs.rs:269-275` checked an unconfigured provider first; the current code authenticates first). All other Off-mode Blossom responses are byte-identical to pre-NIP-FI behavior. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
02c6309f53 |
fix(ci): select runtime suites from PR changes only (#7843)
A documentation-only update to #7809 selected desktop builds and E2E tests because path detection compared an old PR base SHA with GitHub's newer synthetic merge commit. The [failing run's path-detection log](https://github.com/block/buzz/actions/runs/35789957773/job/106955810630) includes four unrelated desktop files from `main`; `VISION_MOBILE.md` did not match a runtime filter. Use GitHub's PR file list for pull requests so unrelated changes in the synthetic merge commit cannot select runtime suites. The existing directory filters are unchanged: root and docs/ Markdown do not select runtime suites, while Markdown under runtime directories still selects its affected suites. Mixed code/documentation changes retain normal coverage. Always-on security, policy, and source-contract checks and full push-to-main coverage are unchanged. Added regression coverage runs the pinned paths-filter action against real fixture repositories, including a synthetic merge containing unrelated upstream desktop code. All 35 selection scenarios and 26 required-check scenarios pass; the new regressions failed before the fix. Existing required-context isolation, file-size policy, and security-review contract checks pass, as do script lint and workflow syntax validation. Full workflow lint reports the same two pre-existing shell-quoting findings in the untouched dead-token guard. A desktop E2E build was run to diagnose the unrelated required smoke failure. Related: #7809 (incident, unchanged) and #5756 (shared-input path coverage, separate scope). No matching issue found. PRs with at least 3,000 changed files now select every runtime suite, avoiding GitHub's PR-file-list ceiling. Boundary tests cover 2,999, 3,000, and 3,001 files, including a runtime file omitted beyond the API cap; disabling the safeguard makes the latter two regressions fail. Required checks now run and fail when path selection fails, is cancelled, or is skipped. Regression coverage exercises the workflow conditions and shell checks for all 13 required wrappers, plus an API-denial case against the pinned action. Mutating either the scheduling guard or the result check makes all 13 failure regressions fail. The required mention-settings smoke test expected a pin after explicitly disabling automatic mentions. Waiting for its old avatar to exit reproduced the CI failure 3/3; the test now checks that subsequent mentions remain manual and retains outgoing recipient-tag assertions. Corrected browser regression: 20/20 repeated runs; related picker unit tests: 18/18. No desktop production behavior changed. The always() requirement is now pinned in required-wrapper regression tests; deleting it fails all 26 gate cases. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
a1fb6645d9 |
test(desktop): synchronize upload edit smoke test (#7903)
## Summary Fix the upload/edit smoke-test race observed on main at `6410e685a80d42db0645fadc9bbe710559ef911e`. - Hold the mock upload at the Tauri IPC boundary and release it explicitly after checking edit rejection, rather than assuming menu interaction finishes within one second. - Wait for the Radix menu to unmount: edit dispatch happens in `onCloseAutoFocus`, after the click. Without that wait, the negative assertion can pass before the edit callback actually runs. - Keep the existing attachment-preservation and subsequent successful-edit assertions. No production code or CI coverage is changed. ## Evidence and scope - [Main failure](https://github.com/block/buzz/actions/runs/36189213149/job/108250341388): `opening edit during an immediate photo upload preserves the draft`, including an upload-progress timeout on retry. - Investigated from #7791, which changes only `docs/nips/NIP-AR.md`. Its PostgreSQL failures and video-menu timeout are separate; PostgreSQL passed on this main run, and the video spec passed locally. This PR does not claim to fix those failures or the separate pointer-interception failure seen in the main job. - #7843 already addresses unrelated runtime suites being selected for docs-only PRs. ## Validation - Full desktop `pnpm test`: 6,676 Node tests + 92 jsdom tests passed. - Upload/edit regression repeated 5 times: passed. Holding the upload without waiting for menu teardown failed all 5 runs, demonstrating the second race. - Complete file-attachment and video-attachment Playwright specs: 30 passed. - Pre-push desktop lint, typecheck, full desktop tests, and file-size gate passed at `46e3d4392e5b56dcb74ea864aa19aa10287f27d3`. - `just ci` attempted: initial formatting issue corrected; second run exceeded the 5-minute local limit during Tauri clippy. Not claiming full repository CI green. ## Review / human verification Self-reviewed the diff against the test contract; no production behavior changes. Draft pending human verification and CI. To verify: activate Hermit, then `cd desktop && pnpm test:e2e:smoke file-attachment.spec.ts --grep "opening edit during" --repeat-each=5`. Expect all five runs to pass, retaining the upload in the draft and allowing edit only after upload release. Originating conversation: buzz://message?channel=aa7f2b48-d367-4ca1-be09-116a2eb45b99&id=1b68585ba4483a87f5a453fd7bbdd11d434e332dcb4aff8acfe1f5d2601b6561 Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> |
||
|
|
8f8c4dfade |
feat(desktop): relay admin console for the /api/admin/v1 operator surface (#4768)
Adds a relay admin console under Settings — the single surface for relay-level operator work — replacing the previously unreachable moderation section. Operators authenticate with their own Nostr key over NIP-98 — no browser extension, no bearer token — to view deployment-wide moderation reports and product feedback, resolve/dismiss/escalate/reopen reports, update feedback status, and manage the operator/moderator staffing roster. ## Nav reachability The old `moderation` section id was defined in `settingsSections` but never wired into any group in `settingsNavGroups`, so the sidebar never rendered it — unreachable since #1617. This PR replaces it with a `relay-admin` section wired into the Communities nav group, pointing at the relay admin console, and drops the separate `admin-console` section id so a single surface owns relay-level trust & safety. The Relay admin nav entry is always visible. Auth gates the panel itself; the nav entry is a door, not a credential — hiding it behind discovery state would strand an operator whose relay transiently fails NIP-11 discovery with no path back to the connection settings. ## Discovery and connection On mount the console reads the connected relay's NIP-11 document for the optional `admin_api` field. When a valid origin is discovered **and its host matches the connected relay's host** (case-insensitive, host identity only), it is automatically saved and probed — the panel renders immediately with no Save step required. A cross-host advertisement is pre-fill only: it populates the manual origin field under Advanced and is never saved or probed without an explicit Save, so a relay cannot direct an unconsented NIP-98 signature at a third-party origin; residual exposure on the same-host path is bounded by NIP-98's URL/method/payload binding. Saving is only required for manual origin changes. The advertised origin is untrusted input: revalidated through `AdminOrigin::parse`, falling back to manual entry when absent or invalid. A saved manual origin always takes precedence over discovery. ## Auth and transport (Rust, `src-tauri/src/commands/admin/`) - `AdminOrigin` value object: validates scheme + host + optional port, rejects credentials/path/query/fragment; `http://` only for loopback. The relay side normalizes its configured admin host at config load and compares inbound `Host`/`Origin` hosts case-insensitively. - `AdminRoute` closed enum: no IPC surface accepts arbitrary URLs, so the NIP-98-signed URL is byte-identical to the fetched URL. - Dedicated no-redirect `reqwest` client as an SSRF guard: a relay `3xx` is an error and the NIP-98 header never crosses origins. - `admin_probe` signs `GET /probe` and returns a typed state (`nip98Authorized` / `nip98Denied` / `disabled` / `notAdminApi` / `networkOrIntercepted`). The response body is validated against the full relay contract — `nip98Authorized` only when the complete NIP-98 invariant holds, `disabled` only on a coherent disabled-mode body; any incoherent or unrelated JSON classifies as `notAdminApi`, so the console never fails open at the network boundary. Unknown fields are tolerated for forward compatibility. - NIP-98 signing via `AppState::signing_keys()`, one retry on `401` with a fresh signed event; response size bounded by a `Content-Length` preflight and a streaming byte counter; per-pubkey origin storage with atomic `0o600` writes. ## Relay admin console The console is gated behind the relay `OPERATOR`/`MODERATOR` roles from #3777; every `/api/admin/v1/*` route returns `403` to anyone else. Community self-administration (`community-members` / Invites) and in-channel enforcement (the 9040–9043 signed commands) are a separate axis and are untouched. The previously-duplicate community report queue (`ModerationQueueCard`, `moderationQueue.ts`, and three dead hooks) was unreachable dead code and is deleted in its own commit; `useBanMemberMutation` and the rest of `features/moderation/hooks.ts` remain in use by the members sidebar and message menu. ## Console UI (TypeScript, `src/features/admin-console/`) - `AdminConsoleSettingsCard`: discovery + probe flow, honest copy for every probe state, manual origin under an `Advanced` disclosure at the bottom. The role (operator/moderator) is rendered in the connection status line as plain text ("Connected as operator") — no badge-shaped elements above the tab row. The origin provenance (relay config / database) appears inside the Advanced card as small muted text. Section title changed to `Admin`. - `AdminConsolePanel` with Reports / Feedback / Staffing tabs (Staffing gated on `role === "operator"`); reports and feedback grouped by community for cross-community triage. - **Reports**: always requests `scope=all` so the existing resolve/cancel/reopen controls can reach every status (`open`, `processing`, `resolved`, `dismissed`, `escalated`). The relay's omitted-scope default is `escalated`-only (the platform-safety backstop); `scope=all` is explicit and scoped to this console. Full action matrix per `target_kind` (event → delete/kick/ban/timeout/dismiss/escalate; pubkey → ban/timeout/dismiss/escalate; blob → dismiss/escalate); kick is suppressed when the report carries no `channelId`. A `processing` row stays navigable — enforcement state lives in the detail view. A `failed` action (always pre-mutation) offers a single **Cancel & reopen** via `POST /reports/{id}/cancel` with `{actionId}` fencing; there is no client-side retry. `pending`/`enforcing` actions belong to the relay recovery worker and offer no button; a `409` reloads detail. Lists refetch on back-navigation after a mutation. - **Report failure UX**: `reportErrorMessage()` strips 4xx/5xx status prefixes and surfaces the relay's own error reason in the toast (e.g. relay returns `"400: you cannot report this content"` → toast shows `"you cannot report this content"`). Self-report blocking UI removed — the relay has no self-report gate, and the failure was purely client-side error surfacing. - **Reason audience disclosure**: the Reason (optional) field shows exact copy under the input based on selected action — Delete discloses that reason is sent verbatim to the affected user and posted publicly in the room; Kick/Ban/Timeout disclose affected-user only; Dismiss/Escalate disclose reporter only. These mirror the actual relay notice paths. - **Enforcement history**: the report detail carries the governing action even after the report leaves `processing` — a report enforced then reopened is `open` yet still shows its `succeeded` action as executed history alongside the resolve form; a reopen does not un-happen a ban. The resolve form gates on `open` status alone. - **Reopen**: terminal reports (`resolved`/`dismissed`/`escalated`) can be returned to `open` for re-triage via `POST /reports/{id}/reopen`. Reopen never reverses applied enforcement, and the copy says so. - **Feedback**: list, detail, and status control (`new`/`reviewed`/`archived`) with a generation-fenced attachment viewer. A purged source community severs provenance to `null` rather than deleting the row; severed rows bucket under a "source community removed" heading. - **Staffing**: Add is create-only with display names resolved via `useUsersBatchQuery` and npub cross-fade on hover (`HoverStaffingIdentity`). An entered key that already appears in the authoritative roster (config, owner-fallback, or DB) is rejected locally with an inline duplicate-key message and no request is issued; a `409 Conflict` from add, role change, or remove surfaces the relay's parsed error body via `adminErrorMessage(e)` — distinguishing config-backed key conflicts from last-operator conflicts without HTTP-status inference. Roster with `config`/`owner_fallback`/`db` source labels; config-backed entries disable remove client-side. In-place role change via `<select>` calling `putAdminOperator`; the relay responds with the effective `OperatorEntry` (`pubkey`, `effectiveRole`, `sources`) in the same shape as the roster list. Read-only badge for config-managed entries. - **Timeout expiry**: `useTimeoutState` clears the store when derived state is `INACTIVE` but the store is still active; the 1s tick is scoped to entries with a known `expiresAtMs` and owned by `ComposerTimeoutBanner` (mounted only while a timeout is active), while `ChannelPane` subscribes to the boolean `useTimeoutActive()` — the pane re-renders when a timeout starts or ends, not every second. `useMembersSidebarModeration` ticks `nowMs` only while the sidebar is open, the viewer can moderate, and some member has a future `mutedUntil`; the tick after the last expiry stops the interval. - Staffing refreshes the principal after a self-demotion or self-removal: `AdminConsolePanel` passes `onSelfMutation` to `StaffingTab`, which re-probes the saved origin so the role badge and tab visibility update without a manual re-probe. - Staffing mutation failures, including both 409 variants (config-backed key vs. last operator), surface the relay's parsed error via `adminErrorMessage`, so the last-operator recovery guidance is shown. - Mutations confirm via `sonner` toasts; failures surface the relay's parsed error `message`, not raw JSON. The `disabled` auth mode renders read-only. ## Mutation idempotency Resolve and reopen carry a per-attempt `requestId`. On first submit the whole command (`{requestId, action, reason, expirationSecs}`) is frozen; after an ambiguous failure (409, 5xx, transport error, truncated response) the snapshot is retained, action/reason/duration controls are locked, and the retry sends the exact same payload byte-for-byte. A definitive pre-commit rejection (non-409 4xx with `bodyComplete: true`) discards the snapshot, unlocks controls, and the corrected submit uses a fresh payload. On success the toast derives from the authoritative `AdminReportResolution` (`activeAction.action` when present; otherwise `status` for decision-only outcomes) — never from the mutable form selection. The native mutation commands — including operator delete — reject with a typed `AdminMutationError` carrying the relay's HTTP status (`relayStatus`, `null` when no verdict exists) and a `bodyComplete` flag that is `true` only when the full response body was read. ## Restriction routes contract change `GET /members/restrictions` and `DELETE /members/{pubkey}/ban|timeout` now take `communityHost` instead of `communityId`; the relay resolves the tenant itself and returns `400 unknown_community_host` for an unmapped host. This is a breaking contract change with no compatibility shim: the relay must run this build before a desktop that uses it, scripts sending `communityId` must switch, and rolling the relay back breaks the desktop Restrictions UI. `docs/admin/README.md` documents the three routes in the route inventory and carries the migration note. Restriction calls also carry the relay the list was loaded from, and the native side rejects a mismatch before building any URL. ## Hardening - Saved admin origins are stored per pubkey and per relay host. The host slug parses the relay URL, keeps the port-drop for DNS names, and encodes IPv6 hosts canonically, so two IPv6 relays never share a file and no IPv6 slug can match a DNS slug. - NIP-11 admin discovery has its own 10s request deadline and reads `/info` success and error bodies through the bounded reader with a 64 KiB cap (`Content-Length` is only a pre-check). - `PUT /operators` builds its `OperatorEntry` response from the validated body (`effectiveRole = role`, `sources = ["db"]`) instead of re-reading the roster, so a concurrent change can't turn a committed write into a `403`. Related: #3777 (relay `OPERATOR`/`MODERATOR` role model + NIP-98 auth + `admin_api` NIP-11 advertisement — provides the runtime and the discovery field this console consumes) Related: [builderbot-platform-core-infrastructure#140](https://github.com/squareup/builderbot-platform-core-infrastructure/pull/140) (prod admin ingress allowlist and NIP-98 rollout) ## Screenshots Captured headless at `197f0ba0c` against the mock bridge (1280×720). ### Setup **Origin setup — Advanced disclosure open, idle state**  **Authorized console — all three tabs**  ### Reports **Reports queue — open, processing, resolved across two relay groups**  **Open report detail — reporter, target, message snapshot, resolve form**  **Processing report — PROCESSING/HARASSMENT, ban enforcing state**  **Resolve form — Delete selected, audience disclosure copy**  **Resolved report — enforcement history block**  ### Feedback **Feedback list — community grouping with status badges**  **Feedback detail — mutable status control (operator mode)**  **Feedback detail — passive status badge (read-only/disabled mode)**  ### Staffing **Staffing roster — config/owner\_fallback/db source labels**  **Add form — new pubkey filled, ready to submit**  **Duplicate rejection — inline error for existing principal**  --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> |
||
|
|
02753722a7 |
chore(release): release Buzz Desktop version 0.5.25 (#7867)
## Buzz Desktop release v0.5.25 - **Frozen main:** `797012ff01a6d499959b45ed2e56f7927c6a4d6b` - **Reviewed candidate:** `c8f73213089cbd5a0f1e675d3193558280d46e10` - **Previous desktop release:** `desktop-v0.5.24` - **Proposed immutable tag:** `desktop-v0.5.25` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
dd1c2afc2a |
Discover alternate Buzz ACP commands (#6948)
🤖 I’m Larry. ## Summary Managed-agent definitions can now choose which installed ACP transport will launch the agent. The choice sits beside **Agent harness** while creating or editing an agent, so it is made before first deployment and remains visible afterward. Buzz discovers executable commands named `buzz-*-acp`, keeps **Buzz ACP (default)** as the safe default, and preserves existing custom command values. Discovery and launch share the same resolver, so a command offered in the picker is the command Buzz will execute. ## UX walkthrough ### 1. The ACP command picker appears while creating the agent The default is explicit, and the helper text explains that this selection controls deployment.  ### 2. Installed wrapper commands are offered by name Here Buzz has discovered `buzz-janet-acp` alongside the stock transport.  ### 3. The wrapper is selected before deployment  ### 4. The deployed profile reports the effective command  ### 5. Editing the deployed agent reopens its definition with the choice preserved  ## Details - Definitions carry the selected ACP command through relay publication, snapshots, imports, team adoption, deployment, and later edits. - Existing definitions without this field continue to use `buzz-acp`. - Unknown persisted commands remain visible but unavailable rather than being silently replaced. - Discovery is read-only and never executes candidates. It filters non-files and non-executables, supports Windows `.exe`, `.cmd`, and `.bat` shims, deduplicates aliases, and returns a stable sorted list. - Command names remain portable; Buzz does not persist machine-specific absolute paths. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@nostr> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
4544ba1713 |
chore(mesh): upgrade MeshLLM to 0.76.2 (#7559)
## Summary Upgrade Buzz from MeshLLM `v0.76.0-rc9` to the latest stable [v0.76.2 release](https://github.com/Mesh-LLM/mesh-llm/releases/tag/v0.76.2) (via v0.76.1). - Update all eight Mesh git dependencies across desktop and relay smoke examples; both lockfiles resolve all 47 Mesh packages to `a0c1e66b0ac037dd56544b9e2d94969ea43d694f` (tag `v0.76.2`). All non-Mesh lock records are unchanged by the 0.76.1 → 0.76.2 bump. - Match Mesh's `ed25519-dalek = 3.0.0` and `rusqlite = 0.40` dependencies. - Explicitly enable rusqlite's `fallible_uint` feature, preserving Buzz's existing `u64` SQL conversions when **mesh is disabled**. Mesh-enabled builds previously supplied this transitively, so testing both configurations matters. - Temporarily pin **only** the optional `sqlx-sqlite` package to the merged upstream compatibility fix [transact-rs/sqlx#4402](https://github.com/transact-rs/sqlx/pull/4402), revision `94aafe3a68884d923b0798a767c8d7f6cfda89d2`. SQLx 0.9.0 caps `libsqlite3-sys` below 0.38, while Mesh 0.76 requires 0.38. Cargo resolves the conflict even with SQLite disabled. The fix raises that upper bound; remove the patch once released. Buzz's active PostgreSQL driver stays on crates.io SQLx 0.9.0. - Preserve the explicit Linux keyring 3 `crypto-rust` feature so Secret Service sessions retain DH encryption independently of Mesh keyring 4. - Preserve the dedicated examples-only `buzz-mesh-smoke` crate and lifecycle workflow wiring, keeping Mesh/native-runtime dev dependencies out of relay CI builds. - v0.76.1 → v0.76.2 upstream delta: self-updater fix for composed product-v2 bundles (Mesh-LLM/mesh-llm#1844) plus release plumbing — no API changes affecting Buzz. - Merged `origin/main` (`7c789dee0`) without rewriting shared PR history. ### Validation at `ae4adba15` (local macOS ARM64, Hermit toolchain) - **Passed:** `cargo test --locked -p buzz-mesh-smoke --all-targets` (all six example harnesses compile; no live harness execution). - **Passed:** desktop Tauri workspace tests, both default and `--features mesh-llm` graphs (3,296 tests with mesh enabled, 0 failures). - **Passed:** `just desktop-tauri-clippy` (both feature graphs, `-D warnings`), root workspace clippy `-D warnings`, `cargo fmt --all --check`. - **Passed:** `--locked` metadata resolution for both workspaces; lockfile diff audited to touch only Mesh package records. - **Passed:** full pre-push hook suite on the final push (branch-skew, rust-tests, desktop-tauri-checks, file-size-check) — no hooks bypassed. - **Not run:** live Mesh inference/admission smoke or installed-app UI exercise. CI lifecycle smoke coverage applies. Originating Buzz conversation: buzz://message?channel=dffbc117-845a-4c02-bc3f-eaeeefc4deaa&id=670f9c262e1ec0a4df5600b3173a0c5de2abe17e08ba9ccfdd8eff109fadf1a8 --------- Signed-off-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz> Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Galadriel <b81c841ed20a8e17caf4e6fac2f6f3bc61d304c84b8394720f6d00f37538c47e@meshllm.communities.buzz.xyz> Co-authored-by: Gu <gu@buzz> |
||
|
|
d01e5f8205 |
feat(agents): humanize uncurated Databricks model ids with a label grammar (#7844)
New Databricks endpoints (Claude Opus 5.5, GPT-6 Astra/Sol, etc.) showed as raw FQNs in the model picker until someone added a curated corpus record for each. This adds a strict token-class grammar as a third label tier inside the Databricks label helpers (Rust `model_capabilities.rs` + TS `modelCapabilities.ts`): exact record → unique alias → **generated label** → raw id. In TS it fires only for an explicit `databricks_v2` provider; providerless ids stay raw. ## Grammar Takes the last component of a UC FQN (or strips one wrapper: `databricks-`, `goose-`, `kgoose-`, `builderbot-`; an id that is neither stays raw), splits on `-`, and classifies each token as version, letter+digit version, size, trailing date (dropped), or word. Labels are built only from the id's own text, and the grammar never reinterprets a version. When in doubt it returns raw: - **Known families only.** The family (leading letters of the first token) must be in the manifest's existing `label_family_tokens`. Custom endpoints such as `prod.ai.rag-agent-3` or `builderbot-pr-reviews-v2` stay raw. Supporting a new vendor means adding one entry to that list, not a record per model. - **Attached digits stay whole** (`gpt10` → `GPT10`). If the family already carries digits and the next token is a version (`goose-qwen3-5-122b-a10b`, `system.ai.llama3-1-8b`, `system.ai.claude3-5-sonnet`), the id stays raw; this is checked before any reorder. - A third consecutive number (`gemini-3-7-1-flash`) returns raw rather than splitting off a patch version. - Any non-ASCII input returns raw, checked before trimming or case folding. - Any unclassifiable token, or no version-bearing token (`builderbot-pr-reviews`), returns raw. `(latest)`/`Preview` never appear unless an exact record supplies them. Special cases, each justified by an unseen-id shape rather than the corpus score: - `meta-llama-` → `Llama`: a namespace convention. - Claude numeric-first reorder: Goose names Claude endpoints `claude-4-7-opus`. Only 1–2 version tokens followed by `opus`/`sonnet`/`haiku` reorder (`claude-5-6-opus` → `Claude Opus 5.6`); anything else keeps its order (`claude-5-6-thinking` → `Claude 5.6 Thinking`). Both current numeric-first ids also have exact records. - Qwen compact decimal: Qwen's own naming `qwen35` → `Qwen3.5` when the second digit is nonzero. ## Colliding labels in the picker The same model is often served from several sources, so humanized labels collide (four rows are all `GPT-5.5`). `disambiguateModelLabels` in `formatAgentModelLabel.ts` suffixes only colliding Databricks labels with their source: - UC FQN: its `catalog.schema`, e.g. `GPT-6 Astra (system.ai)` / `GPT-6 Astra (sandbox.public)`. - Workspace endpoint: its wrapper, e.g. `GPT-5.5 (databricks)` / `GPT-5.5 (goose)` / `GPT-5.5 (kgoose)`. - Still colliding after that (e.g. a date variant in the same schema): the full id, e.g. `Gemini 3.7 Flash (system.ai.gemini-3-7-flash-0925)`. Unique labels, raw-id rows, the default row, and other providers are unchanged. It runs in `getDiscoveredPersonaModelOptions` (covering `AgentDropdownSelect`, `PersonaModelCombobox`, `PersonaDropdownField`, whose closed triggers read the option label) and in `ModelPicker`. Model search in `AgentDropdownSelect` and `PersonaModelCombobox` also matches the raw id, so searching `system.ai` finds those rows. Rust `ModelEntry.name` is unchanged. ## What labels drive Picker values, persistence, routing, catalog dedup, `resolve()` and capabilities all stay keyed on the raw id; two ids sharing a label both remain selectable. Labels do drive picker search (alongside the raw id), the workspace catalog's equal-time sort, and `TeamIdentityCard`'s "Mixed models" summary, which compares lowercased base labels, so `goose-` and `system.ai` GPT-6 Astra read as one model there (correct, since they are the same model). ## Tests Shared fixtures in `scripts/databricks-label-fixtures.json` are replayed by both Rust and TS. Each row asserts its tier: `exact` rows match a record, `alias` rows resolve with generation off, `generated` rows return nothing with generation off (so a future alias or record cannot mask them), and `raw` rows return nothing either way. A curator guard requires that across all 57 exact records, the ids the grammar does not reproduce equal exactly the 10 curator-only ids. `disambiguateModelLabels` has unit tests for namespace, wrapper, and full-id-fallback collisions, untouched rows, other providers, and case-insensitive grouping. The defaults-picker e2e checks the suffixed rows and trigger, raw-id search, and that choosing the second uncurated FQN by its visible name persists its raw id and restores its label. An edit-agent e2e covers the existing-agent model dropdown (`#edit-agent-model`): both suffixed rows render, the `system.ai` row is chosen by name, and after save and reopen the agent stores the `system.ai` id and shows `GPT-6 Astra (system.ai)`. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
48884848f5 |
fix: route databricks claude fqns to anthropic messages (#7829)
Fix Databricks V2 Unity Catalog Claude service routing without erasing verified model capabilities. - preserve exact provider-qualified record precedence for `data_workflow_tools.goose.goose-claude-opus-5-5`, including adaptive thinking, explicit `output_config.effort` (default medium), and signed native tool continuation - route other boundary-matched `claude-*` service components to Anthropic Messages while preserving the full FQN and advertising no unverified effort controls - keep GPT-5+ service components on OpenAI Responses and other uncurated FQNs on MLflow Chat Completions; catalog/schema text does not affect routing - keep Rust and TypeScript resolver precedence aligned and regenerate the 168-vector normative corpus Validation at `1204f1eacff17ebe05eb2bba06b5eee210e21c25`: - `cargo test -p buzz-agent` - `pnpm -C desktop test` — 6,628 passed - `pnpm -C desktop typecheck` - `cargo fmt --all -- --check` - `cargo clippy -p buzz-agent --all-targets -- -D warnings` - `just test-unit` — passed on isolated rerun - pre-push file-size, desktop check, desktop typecheck, desktop tests, and Tauri checks passed; the repository-wide Rust lane exited without identifying a failing test in two concurrent hook runs, while its isolated `just test-unit` rerun passed --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |