Commit Graph

836 Commits

Author SHA1 Message Date
Nur Fatima
139f8903d5 fix(desktop): set window title to "Buzz" for Windows taskbar (#6317)
## What
    
The Tauri window title field in tauri.conf.json was an empty string. On
Windows, the taskbar draws its label from the window caption, so the
Buzz entry rendered as an icon with no name.
    
## How
    
Set app.windows[0].title to "Buzz" in desktop/src-tauri/tauri.conf.json
(line 19).
    
This is the only change — one line in one file. The hiddenTitle: true
field on line 26 only affects the macOS title bar and does not interfere
with the Windows taskbar label.

## Before / After
    
    |                 | Before             | After         |
    |-----------------|--------------------|---------------|
    | Windows taskbar | Icon only, no text | Icon + "Buzz" |
    
## Testing
    
    - Verified the JSON is valid after the change
    - hiddenTitle: true is unchanged — macOS behavior is unaffected
    
Fixes [#6295](https://github.com/block/buzz/issues/6295)

Signed-off-by: Fatima Nur <fatimanur424@example.com>
Co-authored-by: Fatima Nur <fatimanur424@example.com>
2026-10-19 15:36:57 -07:00
Will Pfleger
6f67dcdac4 fix(desktop): bump zip to 9 for RUSTSEC-2026-0367/0368/0369 (#8412)
🤖 Bumps the desktop app's direct `zip` dependency from 8 to 9 to clear
RUSTSEC-2026-0367, RUSTSEC-2026-0368 and RUSTSEC-2026-0369 on the
managed Node.js archive extraction path.

zip 9 makes `ZipArchive::name_for_index` and `ZipFile::name` fallible.
Managed Node.js zip validation now rejects an entry whose name can't be
decoded, and the "unsafe path" error prints the raw entry name lossily.
The absolute-path, drive-prefix, traversal and `enclosed_name()` checks
are unchanged.

The lockfile update is scoped to `zip`; it pulls in newer `zstd`,
`lzma-rust2` and `ppmd-rust` that zip 9 requires.

**`zip` 8 is still in release builds.** With `--features mesh-llm`,
`mesh-llm` v0.76.2 (`mesh-llm-system`, `mesh-llm-plugin-manager`,
`mesh-llm-host-runtime`) still depends on `zip` 8.6.0. That goes away
when the `mesh-llm` pin moves to a release that includes
Mesh-LLM/mesh-llm#2471.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-10-18 10:41:21 -07:00
Wes
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>
2026-10-14 20:32:00 -06:00
Logan Johnson
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>
2026-10-14 15:16:47 +00:00
klopez4212
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>
2026-10-13 16:24:27 -04:00
Bradley Axen
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>
2026-10-11 04:39:21 +00:00
Bradley Axen
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 27b5a294: 386/387 pass. The one
failure is
`boot_lifecycle::hanging_redis_peer_times_out_before_the_health_listener_binds`:
it went over its timing slack under full-suite load (8.03 s) and passed
when rerun alone.
- buzz-relay unit tests pass (1476). One mesh demo test timed out once
under load and passed when rerun alone.
- `cargo clippy -p buzz-relay -p buzz-cli -p buzz-acp --all-targets -D
warnings` is clean.
- Live test against a local relay built from this branch (27b5a294), run
by ART at baxen's request as the sign-off test. alice subscribed over
WebSocket to kind 20002 in a private channel; bob (a bot member with no
WebSocket) and carol (outsider) used `POST /events` with NIP-98. 16/16
checks passed:
- bob's top-level pulse → 200 `accepted: true`, and alice received it
live
- bob's thread pulse (`h` + root `e` + reply `e`, the buzz-acp shape) →
delivered with tags intact
- 2 KiB content, a 2 KiB tag value, or `h` + 10k empty tags → 400
`typing indicator too large`; alice received nothing
- carol → 400 `restricted: not a channel member`; alice received nothing
  - carol posting an event signed by bob → 403
  - no `h` tag → 400; kind 20001 → 400 `only accepted via WebSocket`
  - 0 kind-20002 rows in Postgres afterwards

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz>
Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz>
Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz>
2026-10-09 22:44:31 +00:00
Will Pfleger
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>
2026-10-09 16:18:58 -04:00
Luke Tornquist
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>
2026-10-09 15:23:27 -04:00
Will Pfleger
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>
2026-10-08 13:58:07 -04:00
chiller
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, &current_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)
2026-10-06 14:24:15 -07:00
Will Pfleger
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>
2026-10-06 13:31:05 -04:00
tbedor-square2
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>
2026-10-06 12:07:26 -05:00
Will Pfleger
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>
2026-10-06 08:58:50 -06:00
benthecarman
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>
2026-10-05 18:50:18 +00:00
klopez4212
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>
2026-10-05 13:42:39 -04:00
Will Pfleger
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>
2026-10-02 19:28:59 +00:00
Will Pfleger
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>
2026-10-01 17:04:33 -04:00
Will Pfleger
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>
2026-10-01 18:43:21 +00:00
klopez4212
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>
2026-10-01 17:00:57 +01:00
Will Pfleger
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>
2026-10-01 11:32:16 -04:00
Will Pfleger
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>
2026-09-30 18:53:13 -04:00
Will Pfleger
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>
2026-09-30 18:28:54 +00:00
Alex Rosenzweig
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>
2026-09-30 10:52:29 -05:00
Shawn Hank
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>
2026-09-29 19:36:01 -07:00
Will Pfleger
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>
2026-09-29 18:31:44 -04:00
Jobi Armstrong
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>
2026-09-29 13:58:25 -07:00
Will Pfleger
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>
2026-09-28 16:36:40 -04:00
Jordan Mecom
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>
2026-09-28 12:46:43 -07:00
Will Pfleger
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>
2026-09-28 14:46:56 -04:00
Will Pfleger
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**
![Relay admin panel with Advanced: admin origin disclosure open before
probe](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/02-origin-setup.png)

**Authorized console — all three tabs**
![Authorized relay console showing Connected as operator status and
Reports/Feedback/Staffing
tabs](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/03-origin-authorized.png)

### Reports

**Reports queue — open, processing, resolved across two relay groups**
![Reports tab showing community-grouped list with spam/open,
harassment/processing, illegal/resolved
rows](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/04-reports-queue.png)

**Open report detail — reporter, target, message snapshot, resolve
form**
![Open spam report detail with reported message content and
Delete/Kick/Ban/Timeout/Dismiss/Escalate action
buttons](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/05-report-detail-open.png)

**Processing report — PROCESSING/HARASSMENT, ban enforcing state**
![Processing harassment report detail showing enforcement state with ban
action in enforcing
status](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/06-report-enforcement.png)

**Resolve form — Delete selected, audience disclosure copy**
![Resolve form with Delete action selected showing "Sent verbatim to the
affected user and posted publicly in the room" audience
copy](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/07-resolve-form.png)

**Resolved report — enforcement history block**
![Resolved/illegal report detail showing Resolved by, Resolved at, and
Act ID fields from completed
enforcement](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/08-resolved-history.png)

### Feedback

**Feedback list — community grouping with status badges**
![Feedback tab showing relay.example.com group with new/reviewed items
and source-community-removed
group](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/09-feedback-list.png)

**Feedback detail — mutable status control (operator mode)**
![Feedback detail with new/reviewed/archived status tab control visible
for
mutation](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/10-feedback-mutable.png)

**Feedback detail — passive status badge (read-only/disabled mode)**
![Feedback detail in disabled mode showing passive "Status NEW" badge
with no mutation
control](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/11-feedback-readonly.png)

### Staffing

**Staffing roster — config/owner\_fallback/db source labels**
![Staffing tab showing three operators with config, owner_fallback, and
db source
labels](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/12-staffing-roster.png)

**Add form — new pubkey filled, ready to submit**
![Staffing add form with a new 64-hex pubkey entered and moderator role
selected](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/13-staffing-add.png)

**Duplicate rejection — inline error for existing principal**
![Staffing add form showing "Already an operator: operator. Use Remove
to revoke before re-adding with a different role." error for an existing
config-backed
operator](https://raw.githubusercontent.com/block/buzz/f92559f9fab694b51afd60fdf76cb276de2fa830/docs/pr4768/14-staffing-dup.png)

---------

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>
2026-09-24 17:34:25 -04:00
Wes
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>
2026-09-24 12:53:21 -06:00
Logan Johnson
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.

![Create-agent form with the ACP command picker beside Agent
harness](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/01-persona-create-acp-selector.png)

### 2. Installed wrapper commands are offered by name
Here Buzz has discovered `buzz-janet-acp` alongside the stock transport.

![ACP command menu offering Buzz ACP and
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/02-discovered-wrapper-dropdown.png)

### 3. The wrapper is selected before deployment

![Create-agent form with buzz-janet-acp
selected](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/03-wrapper-selected-before-deploy.png)

### 4. The deployed profile reports the effective command

![Deployed agent Runtime profile showing
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/04-deployed-profile-effective-acp.png)

### 5. Editing the deployed agent reopens its definition with the choice
preserved

![Edit-agent form retaining
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/05-edit-shows-persisted-wrapper.png)

## 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>
2026-09-24 11:33:24 -04:00
Mic Neale
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>
2026-09-24 07:32:07 -06:00
Salman Mohammed
5621006bcf refactor: move agent Git bootstrap into ACP harness (#7819)
discovered in
https://github.com/block/buzz/issues/7742#issuecomment-5783512985

## Summary

Goose uses its native shell, so it bypassed the Git identity and signing
setup inside `buzz-dev-mcp`. Agent commits could use the host Git
identity, while MCP shells had a separate config. This moves Git setup
into `buzz-acp`, where both paths receive the same agent identity, Nostr
signer, and relay-scoped credential helper.

Before:

```text
Buzz Desktop
  └─ buzz-acp                 ← Desktop supplied relay Git authentication
      ├─ Goose → native shell ← no agent Git author or signing setup
      └─ Buzz Agent
          └─ buzz-dev-mcp
              └─ shell         ← MCP created the keyfile and Git signing config
```

After:

```text
Buzz Desktop or Sprig
  └─ buzz-acp                 ← owns the keyfile, identity, signing, and scoped credentials
      ├─ Goose → native shell ← inherits the complete Git environment
      └─ Buzz Agent
          └─ buzz-dev-mcp
              └─ shell         ← receives the same Git environment via mcpServers[].env
```

The harness keeps the signing key in a private temporary file and
removes it on normal exit, startup failure, and graceful termination. It
composes inherited `GIT_CONFIG_*` entries, forwards the complete block
to MCP servers, and bundles the helpers through the ACP multicall
entrypoint. For standard `buzz-acp` launches, Desktop and Sprig no
longer write Git config. A custom Desktop ACP command bypasses the
harness, so Desktop preserves its prior relay-scoped credential helper
for that supported override.

This changes Git settings for every ACP runtime. The startup signal path
also changes so a termination during adapter initialization removes the
keyfile. The temporary file can remain after SIGKILL or a machine crash.

### Related issue

Related: #6177 covers stricter commit identity enforcement and touches
the same Git paths. That PR and this one need to be reconciled before
either lands. No issue was found for this runtime bootstrap gap.

### Testing

- Ran the ignored `git_runtime_tests` with real Goose 1.50.0 and Buzz
Agent ACP sessions. `real_goose_native_git_shell` passed again on this
PR branch with `BUZZ_TEST_BIN_DIR` pointing to built binaries. Goose
made a signed commit and tag through its native shell; Buzz Agent did
the same through its MCP shell. Both verified signatures, identity,
credential scoping, and keyfile cleanup.
- Built the Linux Sprig image and ran `scripts/test-sprig-image.sh
buzz-sprig:acp-git-bootstrap`. The container made and verified a signed
commit and tag, then confirmed keyfile removal after SIGTERM.
- `just ci` passed on the original branch. After rebasing onto `main`
and addressing review feedback, the ACP Git bootstrap integration tests,
focused desktop smoke test, custom-command regression test, formatting,
and ACP/Desktop Clippy passed. The pre-push hook hit two
timing-sensitive ACP deadline tests under parallel load; both passed
alone and the complete ACP package passed serially (964 library, 2 Git
bootstrap, 9 pool lifecycle tests). The push used `--no-verify` after
those checks; GitHub CI is rerunning on the new head.
- Under injected `BUZZ_ACP_ALLOWED_RESPOND_TO`, `BUZZ_ACP_LAZY_POOL`,
and `BUZZ_ACP_IDLE_POOL_SLEEP`, the clean-env serial package command in
`crates/buzz-acp/TESTING.md` passed: 964 active library tests, 2 Git
bootstrap tests, and 9 pool lifecycle tests. Both named Databricks OAuth
tests passed locally; another reviewer reproduced their
environment-specific failures on `main`.
- No authenticated relay clone or push was run. The local probes used
disposable repositories and did not contact a relay.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-23 14:41:00 -04:00
Will Pfleger
b6a26556ec feat(canvas): add version history with atomic restore (#6780)
🤖 Adds append-only channel canvas history, restore, and
relay-authoritative optimistic concurrency so stale edits cannot
silently replace a newer canvas.

## What this adds

- **Desktop history and restore** — browse revisions with author,
timestamp, preview, and diff; restore an older revision as a new signed
head after explicit confirmation. Persisted empty canvases remain
reachable, mutation outcomes are announced accessibly, and focus moves
to a stable destination after save or restore.
- **CLI history and restore** — `buzz canvas history --channel …`, `buzz
canvas get --channel … --revision <id>`, and `buzz canvas restore
--channel … --revision <id>` use keyset pagination over the retained
revision stream.
- **SDK canvas builders** — optional `expected-revision` tags, bounded
ancestry classification, and shared timestamp discipline keep
first-party writes ahead of the head they read without propagating
poisoned future timestamps.

## Authoritative concurrency contract

Tagged canvas writes use compare-and-swap semantics in the relay and
database:

- `expected-revision=none` succeeds only when no live canvas exists.
- An event ID succeeds only when it names the canonical live head.
- A byte-identical replay succeeds only when that event is already the
canonical live head.
- A successful candidate must sort strictly ahead of the current head
under `created_at DESC, id ASC`.
- Malformed preconditions and stale or non-advancing writes are rejected
before event rows, mentions, or fan-out are committed.

The database serializes tagged writes, unconditional kind `40100`
writes, and canvas soft deletion on the same `(community, kind,
channel)` advisory key. Untagged `buzz canvas set` remains an
unconditional write, but participates in that serialization boundary so
it cannot race a tagged save or deletion. Deletion classification is
derived inside the datastore transaction; unrelated event kinds do not
take the canvas lock.

## Read and timestamp consistency

Precondition and post-write ancestry reads request strong consistency
from `/query`, pinning them to the writer when replica routing is
enabled. Display-only history remains replica-eligible.

First-party canvas writers stamp `created_at = max(now, head + 1)` with
a conservative client skew limit. The relay applies a canvas-specific
future-timestamp ceiling before persistence, preventing an accepted
boundary head from making subsequent first-party writes invalid while
preserving the broader ingest limit for other event kinds.

## Durable outcomes and recovery

After an accepted publish, a failed verification read is reported as
durable success with `verified: false` rather than a failed save.
Desktop and CLI preserve the new event ID and direct users to History
when verification is unavailable. A successful read walks the bounded,
cycle-guarded `expected-revision` ancestry chain so transitive
descendants are not misclassified as supersession. Canvas caches
invalidate for every settled mutation outcome, including a retained but
superseded revision.

The 409 reconciliation path in `cmd_restore_canvas` now separately
establishes persistence evidence from ancestry classification.
`canvas_write_survived` checks reachability only; a secondary
writer-pinned IDs lookup distinguishes "A absent" from "A present but
superseded by a legacy write." The four-way outcome (survived /
persisted-but-superseded / genuinely-absent / read-fail) is now correct
for all lost-response shapes including unconditional legacy writes.

---------

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 <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-09-23 11:03:54 -04:00
Wes
ec7ea38f62 chore(release): release Buzz Desktop version 0.5.24 (#7817)
## Buzz Desktop release v0.5.24

- **Frozen main:** `fe56d311a7156802408afb04023c3f75d4015007`
- **Reviewed candidate:** `3befaf16002d802a97aa79007be29b23623ceb3f`
- **Previous desktop release:** `desktop-v0.5.23`
- **Proposed immutable tag:** `desktop-v0.5.24`

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>
2026-09-22 19:54:44 -06:00
Wes
c213d90d5c fix(desktop): bound startup request bursts and recover quota refusals (#7790)
Prepared by Brain on Wes’s behalf.

## Summary

Reduce startup request amplification and let quota-refused background
work recover without changing relay quotas or bypassing the existing
Send gate.

- Coalesce nest regeneration into one worker plus the latest pending
generation. Keep the stale-write fence; bound the archive read so an old
workspace cannot strand newer work.
- Claim pending channel subscriptions before awaiting setup; cancel
removed/unmounted entries and preserve still-owned subscriptions across
reconnect handoff.
- Recheck extended cooldown deadlines before retry dispatch; distinguish
an explicit zero-second hint from a missing hint.
- Recover native finite/unread queries within their original caller
deadline and a three-retry budget, preserving verified unique events,
complete filters and terminal errors.
- Pace renderer initial/live-CLOSED REQs at one per 250ms: visible
channel and explicitly interactive TTS/read-state/observer owners first,
then live-only auxiliaries, then background history. Publication and
finite queries bypass this queue. Reconnect replay now promotes explicit
interactive owners alongside the visible channel with stable
registration-order ties; its existing capped batches and cooldown remain
intact.

**Scope at `867495426`:** 39 source/test files, including 15 production
files (+665/-235) and 24 test/fixture files. The diagnostic recorder,
HTTP instrumentation, trace launcher and raw captures are deliberately
excluded. No profile reset, grouping, quota increase or new global
scheduler.

## Before / after evidence

These are observations from instrumented, human-operated development
Tauri/Vite runs against a production relay, plus explicitly identified
controlled regressions—not a packaged-release startup benchmark.

| Measurement | Before | After | Evidence boundary |
| --- | --- | --- | --- |
| Archive snapshot requests (kind 13535) | 290 | 3 | Recorder-only
baseline → nest-only intermediate candidate; unequal 67.436s / 39.273s
captures and hydration differences |
| Archive responses: HTTP 200 / 429 | 277 / 13 | 3 / 0 | Same
baseline/nest-only captures; not a claim that all quota pressure
disappeared |
| Successful archive responses’ advertised content-length sum |
21,208,505 bytes | 229,695 bytes | Header values, not actual transferred
bytes |
| 154-channel StrictMode replay | 308 REQs | 154 REQs | Controlled real
React hook → real session → mocked Tauri transport regression |
| Add one channel while 154 setups are pending | 309 REQs | 155 REQs |
Same production-boundary regression; ordinary mount remains 154 → 154 |
| Broad renderer request burst / spacing | 110 broad REQs within
10.766ms in pre-drain capture | 250.061ms minimum / 252.131ms median
between broad REQ writes in final longer capture | Different attended
runs; pacing rather than a universal request-rate cap |
| Native unread query outcomes | Baseline: 8 EOSE / 146 quota CLOSED out
of 154 queries | Final longer run: all 156 observed queries reach EOSE;
eight refused queries each recover after two refusals | Different
runs/workloads; query completion, not badge rendering |

### Timing metrics

| Observation | Measured value |
| --- | ---: |
| Baseline’s two accepted chat EVENT socket writes → positive OK |
173.340ms / 85.556ms |
| Nest-only intermediate candidate’s two accepted writes → positive OK |
277.854ms / 78.775ms |
| Final candidate, short run, accepted chat writes → positive OK |
80.636ms / 77.506ms |
| Final candidate, longer run, accepted chat writes → positive OK |
120.712ms / 86.895ms |
| Last initial EOSE among 163 observed broad channel registrations,
longer run | 64.550s after trace start |
| Last EOSE among 156 observed native unread queries, longer run |
73.972s after trace start |
| Native retry waits after 3s then 0s hints, longer run |
3000.7–3002.8ms, then 2001.5–2002.6ms |
| Renderer finite-query retry after a 2s hint, longer run | 2061.365ms |

Both Sends in each final run were accepted, with no recorded negative
OK. Every quota-refused query in the longer run subsequently completed
the same query. Its two Sends happened early (17.878s and 21.655s), not
near the end. These small samples do not support a percentage latency
improvement.

**Tradeoffs and limits**
- 296 background subscriptions imply about 74s of paced registration
before extra cooldowns/high-priority work. The longer capture observed
163 registrations; it did not independently enumerate the expected
membership set or record a queue-empty marker.
- This is not a global 4-REQ/s cap: finite queries, native requests and
reconnect replay are outside the drain and still share relay quota.
Quota refusals recover rather than disappear.
- Socket-write → OK starts after composer submission and any publisher
gate wait. No click-to-visible, launch-to-usable or badge-paint latency
is claimed.
- Native EOSE is not proof of command return, classification, React
state application or visible unread badges. Existing per-channel kinds,
since floors, limits, h-less routing, unread lease/scope guard and
eight-query concurrency remain intact.
- The attended measurements belong to the instrumented candidate, not a
fresh release run of this fixes-only commit. At the initial packaging
checkpoint (`1bf3d060`), eighteen of twenty files were byte-identical;
the other two removed diagnostic gate calls and isolated the nest test’s
temporary paths. The subsequent interactive priority/reconnect and
readiness fixes have controlled hook/session/browser evidence, not
another attended voice/performance run.

### Evidence provenance (captures retained locally, not uploaded)

| Capture | SHA-256 |
| --- | --- |
| Baseline `STARTUP_20260921T194814Z_98811.jsonl` |
`f4f0a89ad9aea91084bd6a7432510488166de4cbc906600d88c26d557f380994` |
| Nest-only `STARTUP_20260921T202610Z_28642.jsonl` |
`ce517b02a3f717c36d96b36bb8616e8fa80b03b12b356bacfd40475925ea3f3e` |
| Final short `STARTUP_20260922T013628Z_13506.jsonl` |
`bfea6f3cf80cd7b7e4e676632a3df2f16271fade5bfa0edec72b7bcbbe7839ff` |
| Final longer `STARTUP_20260922T014637Z_15846.jsonl` |
`f4abece613d108c4c2fe3cb808e633a0486bdc9993ca92323822dc00c3fae6ec` |

Final instrumented build fingerprint:
`b0b4fc27d793c3e643f4e10289b5afdb4620ee2e13a8b33fa0595dd3417fd5ca`. All
observed dropped-record counters in these final captures were zero.
Source manifests and offline correlation analyses are retained locally;
no raw captures, profile data or agent sessions are part of this PR.

## Validation

Current head: `86749542657a60abdfe82eed5cc576d3e162fe5d`, based on
`77729abfb692b25a0f4ec4a69add86af2e32c0dd`.

Review follow-ups:
- `8842dbd8`: explicit interactive TTS admission using the existing
main-window owner, independent of companion/timeline selection.
Real-hook/session/IPC coverage with 296 cold owners proves initial and
CLOSED priority, unchanged filters/cooldown/pacing, background progress
and deduplicated speech.
- `8cb088106`: reconnect preserves interactive priority even with no
visible channel. Stable ordering among foreground ties; real
hook/session reconnect regressions cover cooldown, capped batching, all
297 owners, deduplication and unmount cancellation. Independent reviewer
mutation restored the prior replay implementation, failed both reconnect
rows, then passed the restored candidate.
- `68e76d7c`: read-state and observer reuse interactive priority. Stop
and ModelPicker wait on the existing observer owner inside send
callbacks, after installing result listeners/deadlines. No new owner,
timer or scheduler; inherited CLOSED/fallback/error semantics remain,
rather than a new strict EOSE guarantee. The real Stop panel browser
probe holds observer IPC across the click and checks
observer-before-control order and correlated outcome. ModelPicker
component ordering is source-reviewed only.
- Mock live admission now replays matching stored history with
per-filter time/limit constraints, deduplication, untagged auxiliary
channel fallback and authoritative explicit h-tags. Kind-specific
fixture admission waits and retry-resolving hover actions fix
startup-dependent test setup without weakening event/badge assertions.
- `48e9c0dad` and `867495426`: steady-state scroll-prepend setup
observes live admission, subsequent head dispatch, successful/idle query
completion and initial bottom geometry. Phase-2 genuine-growth and ≤32px
anchor assertions are unchanged. This isolates steady-state prepending:
it **does not fix or certify production early-scroll/head-reset
behavior**.

Current validation:
- Normal push hooks at `867495426`: **6,582/6,582 renderer tests**, zero
failed/skipped/cancelled; lint/static checks, typecheck and differential
file-size gate passed. Native source is unchanged from the earlier
checkpoint; no new native run is claimed for these renderer/test
follow-ups.
- E2E build passed at `48e9c0dad` plus the exact subsequently committed
test-only delta (spec blob `469be2eed868c677ba80b859cc871208000fee60`).
The completed affected-smoke run passed **153/153** across
agent-control, badge, entity links, messaging, profile status and scroll
history; the full scroll file additionally passed twice (**36/36**). No
full CI-smoke pass is claimed from this six-file local run.
- Pinky independently reviewed production and test follow-ups, including
the final scroll spec blob `469be2eed868c677ba80b859cc871208000fee60`,
with no blocking finding. Review is source evidence, not an independent
test run.
- **Not merge-ready:** new-head required CI must complete, exact-range
security review requires authorization, and GitHub currently reports
REVIEW_REQUIRED; fresh exact-head reviewer approval is still needed. No
approval, dismissal or merge performed. No new packaged/native audio,
launch-to-usable or badge-paint observation is claimed.

Initial fixes-only checkpoint validation
(`1bf3d06024d284a63326fdd353baced0842aeb81`):

- Pre-commit formatting hooks passed; worktree clean.
- Renderer package hooks: 6,570 tests passed, zero
failed/skipped/cancelled; lint/static checks, TypeScript and
differential file-size gate passed.
- Native package hook passed: default and mesh-enabled all-target
Clippy, then the complete native workspace test suite. App library:
3,222 passed / 0 failed / 19 ignored; terminal/integration suites
passed. The real nest trigger/stalled-body test passed on macOS.
- Independent whole-change source review: no blocking findings at this
commit; all 20 file hashes matched the committed blobs and
review/validation manifests.
- Regression coverage includes real hook/session cancellation,
StrictMode/membership re-entry, reconnect ownership, extended/zero
cooldowns, burst pacing, auxiliary readiness (including EOSE before IPC
settles), native socket refusal/retry/deadline/cancellation, unread
fetch/classification and real nest trigger/HTTP/file persistence.
- Earlier isolated renderer mutations that started readiness before
enqueue or removed auxiliary priority each failed the real presence
regression; restored controls passed. No native mutation claim.
- Nest fixture uses explicit app-data/nest paths instead of assuming
Windows known-folder APIs obey HOME/APPDATA. Windows runtime execution
is not claimed; CI/build results remain separate.

Originating conversation:
buzz://message?channel=46ced308-71df-495d-a144-4ebf08334703&id=bc43c3422a3b0b2141268d0fa149b898421c824992b662aed8b73c4eee50404f

No approval or merge recommendation is implied by opening this PR.

---------

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
2026-09-22 18:31:44 +00:00
Wes
a3117a4762 fix(desktop): register macOS badges for new and existing installs (#7783)
## Summary
- Request macOS badge authorization alongside alerts and sounds.
- Repair existing bundled installations at startup only when native
status is `Authorized` and `badgeSetting` is `NotSupported` (the
omitted/unregistered interaction). Leave user-disabled badges, denied
notifications, new users, and provisional/ephemeral authorization
untouched.
- Reuse the native query/request path in one background startup
operation. No frontend changes, read-state resets, persisted migration
flags, timers, or in-process retries.

## Validation
- Full `just ci` passed at implementation head `e6c4bc640`, before the
test-seam extraction and additional regression coverage.
- Native patch head `8ab8cf6d6`: normal pre-commit/pre-push hooks
passed, including macOS Tauri clippy (default and mesh-llm), complete
Tauri test suite (3,208 passed, 19 ignored), and repository file-size
gate.
- Regression coverage: all five native authorization statuses × all
three badge states; request exactly once only for Authorized +
NotSupported; settings/request failure propagation; startup wiring;
complete authorization options.
- Mutation verification: removing the startup task and removing the
Badge option each fail the corresponding regression.
- Isolated bundled native app on macOS 26.6.2: requesting Alert | Sound
produced Authorized + badge NotSupported; requesting Alert | Sound |
Badge then produced badge Enabled with no error. A direct AppKit Dock
label of 7 was visibly rendered in the isolated app.
- Independent review found no remaining code blocker at final head.

## CI repair (`f9a6ff2b0`)
- The original browser smoke failure was a 500ms startup assertion
racing loaded-runner rendering. Hosted failure snapshots contained all
14 expected rows; the unchanged test also reproduced the failure locally
with 6× CPU throttling.
- Reuse the existing one-shot mock channel response gate in the affected
test. Wait for startup settlement, defer a second real revalidation,
mutate display state while pending, release, then assert after query
settlement. No production changes in this follow-up.
- All 10 sidebar snapshot E2Es pass at clean head `f9a6ff2b0`; the
repaired case also passed 3/3 CPU-throttled runs before commit.
Temporarily removing display preservation or persisting the display
mutation each caused the corresponding assertion to fail; both mutants
were restored.
- Normal pre-commit/pre-push gates passed, including desktop
checks/typechecking, all 6,508 desktop tests, and the file-size gate.
Independent artifact/source review found no blocker in the repair.
- New hosted CI is running:
https://github.com/block/buzz/actions/runs/35633308957. This is not a
claim that hosted CI is green. Exact-range security review still
requires organization-member authorization.

## Runtime limitation
This fixes a verified missing authorization registration and provides an
automatic existing-install path. It is **not yet proof that the reported
blank Dock badge in the released Buzz app is fully resolved**: patched
bundled Buzz startup and unread-to-Dock recovery have not been
exercised. The isolated app is native API evidence, not a replacement
for that release smoke test. Installed Buzz and its read state were not
modified.

Before release, verify an existing Alert/Sound-only install gains the
badge setting on launch and renders an unread Dock badge; verify a
user-disabled badge remains disabled.

---------

Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-21 18:12:38 +00:00
Jordan Mecom
f18ea45698 Replace personal and internal data in desktop test fixtures (#7748)
Replace personal contributor identities, copied internal image content,
and production Databricks hostnames in desktop tests and mocks with
synthetic fixtures. The related path and mock-profile fixtures use the
same example identity, and the image regression still verifies reserved
layout space while its mocked request remains pending. Contributor
matching and configuration decoding behavior are preserved.

Checked on Blox: 36 targeted JavaScript tests, 30 focused Rust tests,
the Playwright image-height regression, TypeScript/Biome, formatting,
security/file-size gates, 6,508 desktop tests, the full Tauri test
workspace (including 3,204 main-crate tests), and desktop/web builds.

The full `just ci` run could not finish mobile checks because Blox
connections to `pub.dev` were reset and a locked dependency was missing
from its offline cache. The broader Rust unit run passed 16 lanes but
hit an unrelated catalog timeout in `buzz-agent`; that exact test passed
in isolation.

Signed-off-by: jm <jm@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-18 22:26:19 +00:00
Wes
8953cbfff5 fix(desktop): keep managed agent avatars usable across communities (#7732)
## Summary

Fix Desktop-managed agents losing avatars after joining another
community. The saved persona/instance stores one desired source URL, but
authenticated media belongs to a community. Republishing community A’s
URL in B does not make that image accessible in B.

- Before comparing or publishing kind-0, reuse or copy
configured-community avatar media into the pinned destination. Keep
exact image bytes, including animation, and leave the saved source
unchanged.
- Keep the previous profile on transfer/publication failure so existing
reconciliation can retry. Reuse destination bytes during source outages;
after source removal, reuse only an already-published signed destination
picture with the same original content hash.
- Refresh source permissions through narrow, serialized IPC without
reconnecting or resetting the active community. Startup waits until the
latest permission update completes before restoring agents.

### Related issue

Refs #2366 (avatar portion only, not runtime availability). Related
#2659 deliberately excludes avatar/media mirroring; this change
addresses that media boundary without introducing runtime fan-out.
Searched existing issues/PRs for `avatar community` before opening.

## Contract and safety

Desktop-managed profiles still follow the saved persona source; this
does not introduce community-specific persona editing. Agent-managed
profiles still opt out of automatic reconciliation.

Authenticated transfer requests use the fixed agent key/NIP-OA tag,
origin-scoped Blossom auth, and the pinned destination, never mutable
active-workspace owner credentials. They refuse redirects, enforce
30-second request timeouts and bounded bodies, verify SHA-256 and
detected image MIME, and validate the returned descriptor’s
origin/hash/MIME/size. Source permission is rechecked after an awaited
destination HEAD miss. The removed-source fallback verifies the
destination profile’s signature, kind, signer, origin, and
original-media hash without contacting the removed source.

Ordinary external images retain passthrough behavior and receive no
community credentials. No public-media policy change, image-reader URL
rewriting, new persistence, background service, or global profile merge.

## Testing

Normal commit and push hooks passed on exact head
`925781eece66d5f6a4b084b3b3abd6309cda357e`; working tree clean before
and after:

- Desktop formatting/lint, typecheck, and **6,508 tests**.
- Full native workspace suite, including **3,204 desktop-library tests**
(19 ignored), integration tests, terminal crates, and doc tests.
- Native Clippy on default and `mesh-llm` workspace/all-target
configurations with warnings denied; repository file-size and
branch-skew gates.

Regression coverage exercises production reconciliation/shared
publication against two local HTTP fixtures: add/edit/restart, upload
and rejected-publication retry, removed-source local-image preservation,
fixed signer/destination, redirect/hash/descriptor/size failures,
revocation during pending HEAD, and opt-out no-I/O. Mounted
initialization tests cover inactive-list changes without workspace
reset, two deferred permission updates, and rejection of either update.

Earlier working-tree validation at base
`4ab4f786085a23fe6126529861840eff6048ceee` also passed the E2E frontend
build and four Chromium mock-bridge community flows (boot, switch,
active removal/fallback, leave-final/setup). Subsequent production
changes only serialize permission writes and wait for a stable latest
update; the complete suites above cover the final commit. Mutations
independently removing serialization or stable waiting failed the new
regression.

## Remaining validation and limitations

- No live production avatar was changed. Native GUI rendering against
two hosted communities remains to be verified with a build containing
this fix; local HTTP fixtures and the browser mock bridge do not
establish that outcome.
- A removed/unreachable source with no existing destination copy cannot
be recovered automatically. Choose a reachable image/source in that
case.
- This is a publication/reconciliation fix, not a visual redesign. No
before/after native GUI screenshots are claimed. Broad repository CI
remains required; it was not duplicated locally.

## Review

Independent security/scope and caller/ordering reviews are clear after
the bounded fixes. Final hook-ordering reread covered the staged tree
that became this commit. Commit authorship/sign-off use the implementing
Carl identity and configured Carl signing key.

Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-18 06:14:28 -06:00
Brad Seiler
6d1f488d22 refactor(desktop): share relay request construction (#7667)
## Summary
This is a behavior-preserving refactor that gives the desktop relay HTTP
bridge one helper for authenticated request construction. It moves the
repeated `Authorization`, JSON content type, optional `x-auth-tag`,
body, and timeout setup into `build_authenticated_relay_request`.

The helper uses the final URL, auth header, and body bytes passed by the
caller. It does not sign, reserialize, normalize, send, parse, classify
errors, choose a client, or handle rate limits.

Excluded paths: media, WebSocket, Git, agents, mobile, and NIP-FI. The
query send helper only routes through the new request builder so the
timeout and `x-auth-tag` behavior are covered by the same seam.

### Related issue
None found. This prepares for the enterprise identity follow-up to be
smaller than #7663.

### Testing
- `cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check`
passed at `356676a2f70f6bdf80cfdcb9084f1f7548359ecb`.
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
relay::tests::authenticated -- --nocapture` passed at
`356676a2f70f6bdf80cfdcb9084f1f7548359ecb`.
- `just desktop-tauri-check` passed at
`356676a2f70f6bdf80cfdcb9084f1f7548359ecb`.
- `just desktop-tauri-test` passed at
`356676a2f70f6bdf80cfdcb9084f1f7548359ecb`.
- `just desktop-tauri-clippy` passed at
`356676a2f70f6bdf80cfdcb9084f1f7548359ecb`.

Generated with Codex

Signed-off-by: coder 0 <d97ebdbb198c7237c94f84ea8bb8a73583ea067407eebd0062abbb3962527fb1@buzz.block.builderlab.xyz>
Co-authored-by: coder 0 <d97ebdbb198c7237c94f84ea8bb8a73583ea067407eebd0062abbb3962527fb1@buzz.block.builderlab.xyz>
2026-09-16 09:48:06 -04:00
klopez4212
81fba7bda4 Refresh desktop onboarding flow (#7528)
## Summary
- apply the refreshed card layout across desktop onboarding
- update identity backup, recovery, and key presentation flows
- clarify AI connection choices and default model settings
- simplify the backup ceremony from creation through optional
verification
- bring the harness and provider setup flow forward with validation,
preserved state, and consistent navigation
- fix action alignment and stray card scrolling in the refreshed layout

## Validation
- pre-push desktop lint, typecheck, frontend tests, and native tests
passed
- focused onboarding Playwright coverage passed
- manually reviewed the refreshed onboarding and backup flow in the
native app
- just ci passed all sections except one unchanged process-cleanup
timing test; its exact rerun passed

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Clay Delk <clay.delk@gmail.com>
2026-09-15 05:58:16 -04:00
Tom Brow
7c789dee09 Fix Apple Silicon iOS simulator builds (#7646)
Apple Silicon simulator builds fail because MLImage and MLKit contain
device-labelled ARM64 objects.

Enable the Flutter ML Kit package's upstream compatibility helper, which
restores device labels for iPhone builds. This preserves the MLKit-based
functionality (avatar background removal) and complements the
notification-extension linker isolation in #7187.

Adds a complete iOS simulator build to CI to catch this build
regression.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-09-14 22:14:06 +00:00
Salman Mohammed
6c35e82bd5 fix(acp): improve hints in pi agent setup guide (#7594)
## Summary

Handle missing Pi adapter setup and update its install guidance.

### Related issue

None found.

### Testing

No manual testing.

Generated with Amp

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-09-12 17:40:15 +00:00
Salman Mohammed
78618804ec Configure ACP session scope per agent (#7578)
## Summary

PR #6732 taught the ACP harness to honor `BUZZ_ACP_SESSION_POLICY`, but
the desktop exposed that policy as one global experiment. Turning it on
changed every managed agent at once, even though conversation scope is
part of how an individual agent should behave. Some agents need
continuity across a channel, while agents such as validators may need
clean context for each thread.

This moves the choice onto the agent definition under Advanced as
**Conversation context**, with **Entire channel** and **Each thread**
options. **Entire channel** remains the default, so existing definitions
keep their current behavior.

The policy now travels through create and update IPC, returned agent
summaries, persona events, snapshots, teams, and community catalog
copies. Launches pass the effective definition value to the harness.
Editing a running agent uses the same lifecycle as a model edit: it
shows the restart-required state, leaves the deployed process untouched,
and applies the new policy on restart.

The global Thread Scoped ACP Sessions experiment and its desktop
command/state have been removed because the setting no longer has a
useful global meaning. There is intentionally no migration for the old
preview override. Users who enabled it can choose **Each thread** for
the agents that need it.

Unknown or null policy values degrade to **Entire channel** across
stored agents, persona events, and catalog projections. This keeps
forward-version data from dropping complete agent records.

### Related issue

No matching issue found. Builds on #6732.

### Testing

- Focused Playwright create flow selected **Each thread** and verified
`create_persona` received `sessionPolicy: "thread"`.
- Focused Playwright catalog flow added a thread-scoped agent and
verified the local copy stored `session_policy: "thread"`.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-11 20:38:41 +00:00
Taylor Ho
ec11f8e2f3 fix(avatars): scale agent squircles from normalized paths (#7307)
## Overview

**Category:** fix

**User Impact:** Agent avatars and empty agent-team placeholders now
keep the same intentional squircle silhouette at every size on desktop,
while human avatars remain circular.

**Problem:** Desktop agent avatars used percentage-rounded rectangles,
which made the silhouette vary by surface and required each avatar
implementation to recreate the shape. Empty team placeholders also drew
a regular CSS border inside the clip, leaving straight inset edges that
looked like a cropped square. **Solution:** Reuse one normalized SVG
clip path across desktop artwork. Empty team placeholders use a
separately clipped squircle outline behind the clipped surface, so both
the fill and outline follow the canonical silhouette.

> **Scope correction:** Mobile is deferred from this PR because a real
Flutter capture could not be obtained reliably in the current build
environment. Previously attached mobile images were hand-composed
synthetic layouts, not captures from the running app; they have been
removed and must not be treated as validation. The mobile implementation
is being reverted in one follow-up commit so it can be reapplied and
verified separately.

### Related issue

Follow-up to #7106.

## Changes

<details>
<summary>File changes</summary>

**desktop/src/features/agents/ui/AgentCreationPreview.tsx**
Applies the shared squircle clip to agent creation preview images and
fallbacks.


**desktop/src/features/agents/ui/AgentSessionToolItem/CompactMessageSummary.tsx**
Uses the shared agent-avatar silhouette in compact session summaries.

**desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx**
Clips agent initials with the normalized squircle instead of a
percentage radius.

**desktop/src/features/agents/ui/TeamIdentityCard.tsx**
Uses the shared squircle for agent identities and gives empty teams a
separately clipped squircle outline instead of a regular inset border.


**desktop/src/features/agents/ui/activityRenderClasses/UserMessageBubble.tsx**
Keeps agent avatars in activity message bubbles on the canonical
silhouette.

**desktop/src/features/home/ui/InboxListPane.tsx**
Applies the shared clip to agent avatars in the inbox list.

**desktop/src/features/home/ui/InboxMessageRow.tsx**
Applies the shared clip to agent avatars in inbox message rows.

**desktop/src/features/huddle/components/ParticipantList.tsx**
Uses the normalized squircle for agent participants while preserving
circular human avatars.

**desktop/src/features/messages/ui/MessageRow.tsx**
Clips agent message avatars through the shared definition.

**desktop/src/features/messages/ui/SystemMessageAvatars.tsx**
Uses the canonical agent shape in system-message avatar groups.

**desktop/src/features/messages/ui/TypingIndicatorRow.tsx**
Uses the canonical agent shape in typing indicators.

**desktop/src/features/profile/ui/ProfileAvatar.tsx**
Scales the same squircle to the larger agent profile avatar.

**desktop/src/features/profile/ui/SelectedRecipientChip.tsx**
Uses the shared shape for selected agent recipients.

**desktop/src/features/projects/ui/IssueAssigneesRow.tsx**
Applies the agent squircle to issue assignees without changing human
avatar geometry.

**desktop/src/features/projects/ui/ProjectCards.tsx**
Uses the canonical shape for agent avatars on project cards.

**desktop/src/features/projects/ui/ProjectEntityListRow.tsx**
Uses the canonical shape for agent identities in project entity lists.

**desktop/src/features/projects/ui/ProjectsActivityFeed.tsx**
Uses the shared clip for agent avatars in project activity.

**desktop/src/features/projects/ui/ProjectsOverviewRail.tsx**
Uses the shared clip for agent avatars in the project overview rail.

**desktop/src/main.tsx**
Mounts the single shared SVG clip-path definition once for the desktop
application.

**desktop/src/shared/lib/cn.ts**
Registers `rounded-squircle` in the border-radius class group so
shadcn-style `cn(...)` composition resolves it against `rounded-full` by
normal last-class precedence.

**desktop/src/shared/styles/globals/utilities.css**
Registers the reusable Tailwind `rounded-squircle` utility and
references the normalized SVG clip path without agent-specific CSS
selectors.

**desktop/src/shared/ui/AvatarClipPaths.tsx**
Owns the reusable normalized object-bounding-box `rounded-squircle` path
so all desktop sizes reuse one definition.

**desktop/src/shared/ui/UserAvatar.tsx**
Routes the existing squircle shape option through the shared SVG clip.

**desktop/tests/e2e/agents.spec.ts**
Verifies team facepile and empty-team placeholder surfaces/outlines use
the canonical clip without a rectangular border.

**desktop/tests/e2e/messaging.spec.ts**
Verifies agent message and profile avatars reuse one clip definition and
scale across rendered sizes.

</details>

## Reproduction steps

1. Run the desktop app and open a channel containing both agent and
human messages.
2. Compare agent avatars in the message list, inbox, huddle
participants, projects, and profile panel. Agent images and fallbacks
should share one smooth squircle silhouette at each size; human avatars
should remain circular.
3. Open **Agents** and inspect an empty agent-team card. Its placeholder
fill and outline should both be smooth squircles, with no straight inset
cropped-square edges.
4. Open an agent profile from a message and confirm the larger profile
avatar keeps the same proportions as the smaller message avatar.

## Testing

- Desktop Biome check passed for the changed empty-team component and
Agents E2E spec.
- Desktop typecheck and differential file-size check passed.
- Full desktop unit suite passed: 6,453 tests.
- Full Agents integration spec passed: 37/37, including the empty-team
outline regression.
- Post-rebase pre-push gates passed at `304b159a2`: desktop checks,
TypeScript typecheck, differential file-size gate, and all 6,453 desktop
unit tests.
- The shadcn-aligned `rounded-squircle` registration passed changed-file
Biome, desktop TypeScript/build, and pre-push desktop checks at
`27e0c12d3`; the full desktop unit suite passed 6,459/6,459.

## Screenshots

Screenshots are temporarily omitted. The previous captures used the
desktop E2E mock bridge and were not evidence from a live running app,
so they were removed.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
2026-09-11 06:08:44 -10:00
Wes
f3408fc620 fix(desktop): isolate quota backoff and reuse channel discovery rosters (#6998)
Pinky, an AI agent, is updating this PR on Wes's behalf.

## Targeted repair, not a startup rewrite

This replaces the previous readiness/replay candidate. Current head:
**`83ac81ff6ae439b0e68f4a3dafd4b2af6dbadcdb`**, two independent commits
on main base `3c7f288c60d67df78577b237e27c3dfc8831aaa1`:

1. **`0b6681de2` — keep quota backoff on its transport.** HTTP 429s
retain native HTTP backoff without arming the renderer WebSocket gate.
WebSocket quota/concurrency CLOSED retains its retry deadline without
damping HTTP's separate quota. The explicit `rate-limited: shared
admission unavailable` signal still damps HTTP too. Reuse existing
all-channel live streams for Home mention invalidation instead of a
second per-channel mention subscription; retain kind, recipient, self,
membership, replay-deduplication, notification and unread boundaries.
2. **`83ac81ff6` — reuse complete membership rosters within each channel
fetch.** Discovery already returns full paginated rosters. Retain those
results and request only uncovered directory/pending-owner channel IDs.
Skip empty fallback filters and preserve covered counts/pubkeys if that
tolerant fallback fails. The next fetch reads current membership again;
there is no persistent cache or new invalidation state.

No send-route, AUTH-readiness, replay-ordering, scheduler, pacing, relay
policy, dependency, or authorization changes. Refresh triggers and
activity batching stay unchanged. Independent tracing did not establish
a safe-to-delete duplicate normal-startup refresh; recovery
invalidations at different replay stages are not interchangeable.

**Size:** 18 files, +1,066/-350 overall; nine production paths +76/-205
(**129 fewer production lines**). The roster follow-up itself touches
only `fetch.rs` (+31/-14) and its new 378-line regression file. Test
bulk is not new startup machinery.

## Comparative roster measurement

Actual native HTTP fetch code against a synthetic Axum loopback fixture:
265 channels × 100 members, deterministic signed events, debug build.
Seven iterations per condition; medians below exclude the first warm-up.
Baseline is `0b6681de2` production fetch, candidate is `83ac81ff6`
production fetch, with the same temporary benchmark harness.

| Measure | Baseline | Candidate |
| --- | ---: | ---: |
| HTTP reads per fetch | 7 | 6 |
| JSON response-body bytes per fetch | 4,803,136 | 2,453,380 |
| Median, no artificial request delay | 262.9 ms | 149.3 ms |
| Median, 25 ms artificial delay per request | 376.7 ms | 282.7 ms |

Channel/member assertions and output hash (`fa7cc551bc855e4d`) match
throughout. This removes **one batched roster request**, not 265
requests. The measured ~43%/~25% reductions describe this large-roster
synthetic fixture, **not production startup latency**. Activity requests
run concurrently with roster enrichment, so real savings vary. Counts
now use the discovery snapshot rather than a later reread within that
same non-atomic fetch.

## Verification

At pushed head **`83ac81ff6`**:

- All enabled pre-push gates passed without bypass: `push-head-scope`,
`branch-skew`, `desktop-check`, `file-size-check`, `desktop-typecheck`,
`desktop-test`, `desktop-tauri-checks` (native fmt/Clippy/tests).
- Full Desktop frontend suite: **6,457 passed**, zero failures; separate
tooling suite: **10 passed**.
- Full native Desktop workspace: **3,276 passed**, zero failures, 20
ignored, across unit/integration/doc bins. The main native lib accounts
for 3,175 passed / 19 ignored; this is not 3,276 lib tests.
- `pnpm build` passed the protected-feature artifact matrix; `pnpm
build:e2e` also passed. Existing chunk-size/dynamic-import warnings
remain. No full repository `just ci` run locally.
- **130 selected headless mock-bridge smoke cases passed across two
runs**: messaging, reconnect, reconnect affordances, inbox live updates,
sidebar snapshots, send-channel binding, boot splash. The first
invocation hit the shell's 300-second limit after 117 passes and no
failures; all remaining 13 passed in a separate invocation. This is not
a claim the interrupted invocation completed.
- Six new tests invoke production `fetch_channels` through the real
native HTTP client and cover 265/501-channel pagination (including
same-timestamp continuation), exact requests, complete/deduplicated
rosters, directory/pending fallback, hidden DMs, repeated
membership/identity/relay changes, empty lists and errors.
- Falsification in an isolated source copy: restoring the redundant
baseline fetch fails four roster regressions; removing missing-roster
fallback fails two. Restored candidate passes all six. Earlier transport
mutations fail HTTP→WS coupling, WS→HTTP coupling, omitted shared-outage
damping and duplicate-mention regressions. No mutation was applied to
the committed worktree.
- Brain's independent static reviews found no blockers in the transport
repair or immutable roster patch. Source tracing confirms relay
discovery serializes complete rosters, not projected matching p-tags.
This was source review, not independent test execution or approval.

### Hosted CI

[CI run
34185670504](https://github.com/block/buzz/actions/runs/34185670504)
**completed successfully** for this head. GitHub checked out merge ref
`25bcf09627e129887481d4441a1af3c6cb0af1ff`; its Git tree is identical to
`83ac81ff6` (`49a01f3c2ebf4e547bbe3cd31406f984ac6cefe8`), verified
through the GitHub Git API. The final PR rollup has 33 successful checks
and 28 skipped, with none failing or pending.

- Desktop Core passed frontend/native suites and compiled-feature
verification. Windows Rust tests, Windows Desktop build, macOS
`.app`/`.dmg` build, all four smoke shards and both relay-backed E2E job
shards passed, as did Rust lint, DCO, Semgrep and zizmor.
- Browser job totals: **1,620 passed, 13 flaky cases passing automatic
retry #1, five skipped, zero terminal failures**. Smoke accounts for
1,389 passed / 12 flaky / three skipped; integration jobs report 231
passed / one flaky / two skipped. No manual CI reruns or source changes
to obtain green.
- The retries cover channel-browser, empty-edit, huddle menu,
runtime-install, overscroll, onboarding avatar, video attachment and
workflow UI assertions. They are disclosed, not collectively declared
unrelated. The video case reaches the changed real TS all-channel hook
despite mocking native I/O: independent tracing found no regression
mechanism or blocker, but the missing second-video player remains
**unexplained**, and an indirect timing effect is not excluded. The
self-authored, unmentioned event cannot use either the new mention
branch or the removed old mention handler as a delivery path. A passing
retry does not diagnose its cause.
- The separate “Desktop Release Candidate” check skipped its validation
step; it is not additional release validation. Codex security review was
skipped, not passed.

## Remaining acceptance limits

Wes's earlier manual trial of the transport-only candidate reported
improvement; relay readback found both a top-level message and a thread
reply. That was not a fingerprinted or timed trial of the new roster
commit. The remaining few-second top-level send wait is
**unattributed**, and this PR does not claim to resolve every startup
delay.

No visible app launch, relaunch, laptop input automation, or production
test messages during this follow-up. Local native tests use fixtures;
browser suites do not run the Tauri backend. CI building an app does not
validate bundled macOS alerts or inactive-channel notification delivery.
Those bundled real-workflow checks remain outstanding. No visual UI
changes or screenshots; no approval or merge performed.

Originating conversation:
buzz://message?channel=5b130804-d759-40ad-a564-d64cc907fa8e&id=cb432417e290e06a1842639cfe0268f9d43d186defbcb59260930d129a26af83

---------

Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
2026-09-11 09:11:39 -06:00
Wes
9847b0967d test(desktop): isolate login-shell probe measurements (#7570)
Pinky, acting on Wes’s behalf.

## Summary

Fix a pre-existing false-failure mechanism in the login-shell discovery
tests, independently of the thread-window work.

- Run the two process-global-counter assertions in separate, bounded
libtest processes. The global counter still sees all of each test’s
auth/worker threads; no production discovery or cache behavior changes.
- Require exact test selection, successful child exit, and a completion
receipt after the assertion body. Reject counter resets outside
isolation.
- Keep the cold/repeated cheap-discovery zero assertions and
forced-discovery positive control unchanged. Add parent-probe exclusion
and own-worker controls.

Three test-only files, +103/-4. Base
`00209076c7a10d9e4a475466c313e8ebecf041f5`; head
`07f8e86c58a0aa5378537a3daa4610361fdbad55`.

## Reproduction and causal checks

On the main-based source, cheap discovery alone counted 0. Running the
existing missing-binary version test on another thread inside the
measurement interval made the repeated-cheap assertion fail with 1. This
reproduces interference, not the exact scheduling/caller behind the
original full-suite failure. The counter measures `run_in_login_shell`
invocations, not exact OS process counts.

Mutation checks:
- Removing isolation fails the measurement guard.
- Removing the real production `record()` call fails the original forced
positive control (0).
- A reviewer-identified early-return mutation initially survived; the
final helper owns the assertion closure instead. Deleting that
invocation now fails the external worker assertion (0 vs 1).

Independent read-only review by Brain found no remaining blocking
finding at the final head.

## Validation at the final head

- **Full `just ci`: passed.** Desktop Tauri: 3,175 passed, 19 ignored;
desktop JS: 6,488 passed; mobile: 2,098 passed. Remaining Rust unit,
formatting/lint/static, and desktop/web build lanes passed. An initial
five-minute tool timeout interrupted compilation; the subsequent
complete run used a longer deadline, not altered gates.
- **Normal pre-commit and pre-push hooks: passed.** Push includes
default + mesh Tauri Clippy, complete Tauri workspace tests, size and
branch-scope checks. No bypass.
- **Real local relay, following `TESTING.md`: passed.** Built release
`buzz-relay`, `buzz`, and `buzz-admin` from this head. Fresh isolated
Postgres/Redis, additive migrations, dedicated loopback ports,
disposable NIP-98 identity; health, create channel, signed root/reply
send, exact-ID get, and thread retrieval passed. Relay and both owned
containers stopped afterward. Dev auth-token mode follows the guide; not
a production-auth or thread-window/native UI acceptance claim.

## Scope and limits

This is process/cache isolation, not a filesystem sandbox. The existing
bounded-command helper bounds child runtime/output; its Unix
process-group behavior does not guarantee ownership of nested probe
groups. Existing individual probe deadlines remain unchanged.

No UI change, screenshots, thread-window commits, feature enablement,
merge, or deployment included.

Originating conversation:
buzz://message?channel=0b73400d-6922-4fe3-9b97-454da04d011b&id=984528c883faa8829971d19394870f0f2d543d5c1950565fd55ffddc1b8d8022

Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
2026-09-11 09:11:05 -06:00
Salman Mohammed
813bbd1412 fix(acp): integrate the Buzz Pi adapter fork (#7552)
## Summary

PR #7335 worked around missing Pi adapter support by generating a
private Pi launcher and injecting Buzz's standing prompt and skills at
process launch. The Buzz Pi fork now carries the required adapter
extensions, so this removes that launcher and returns prompt
construction to the normal ACP session path while retaining the
base-prompt composition introduced by #7335.

The Pi preset now installs `salman1993/pi-acp` and launches its renamed
`buzz-pi-acp` binary. Buzz adds `-- --skill
<harness-cwd>/.agents/skills` only when launching that binary, sends the
complete composed prompt as the `_meta.systemPrompt` replacement string
on `session/new` only when `initialize.agentInfo.name` is `buzz-pi-acp`,
and sends the scoped title alongside it as `_meta.sessionTitle`. The
fork identity is treated as system-prompt capable regardless of its
reported ACP protocol version, which prevents duplicate legacy
user-message framing. Upstream `pi-acp` does not receive either
fork-specific behavior. Observer transcript projection accepts the
string, `{ replace }`, and `{ append }` metadata forms.

The fork now stores restore metadata in one atomic file per session
under `~/.pi/buzz-pi-acp/sessions/`. This prevents concurrent Buzz
workers from overwriting another session's prompt or title. The fix
landed in
[salman1993/pi-acp#9](https://github.com/salman1993/pi-acp/pull/9).

This supersedes the closed #7508. No agent-configuration rules changed;
this changes the Buzz Pi adapter contract and launch arguments.

### Related issue

#7329

### Testing

Installed fork commit `09cf07e436b8f18e52401558f988f31a15702313` through
the documented Git URL. The installed bundle matched the committed
`dist/index.js` byte for byte and contained the `~/.pi/buzz-pi-acp`
metadata path. The fork's 106 non-skipped tests, typecheck, and lint
pass.

Ran the ignored real-Pi integration test through Buzz's production
session composer. The test exercised the renamed package,
`agentInfo.name`, and the new per-session metadata store. Base, persona,
team, core-memory, huddle, canvas, and skill markers each appeared once
after switching sessions and again after restarting the adapter, while
the other session and Pi's native default prompt were absent.

Added regression coverage proving `buzz-pi-acp` receives fork-specific
system-prompt metadata and managed skills while upstream `pi-acp` does
not. `just ci` passes.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-10 21:38:42 -04:00