mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
cec5c8fd9280d30f56effac701e1e19d5cfe6fea
2635 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cec5c8fd92 |
fix(desktop): let inbox title and message author names truncate under narrow panes (#7550)
## Summary Fixes two instances of the same dead-truncate pattern in the desktop app, where a flex item's implicit `min-width: auto` prevented `truncate` from engaging, so long text painted over adjacent controls instead of ellipsizing: - **Inbox detail title** (`InboxDetailPane.tsx`): the clickable context-title button sized to its text instead of shrinking with the pane, overlapping the header controls (open-in-channel, members, huddle, more menu). Fixed by adding `max-w-full`. - **Message author names** (`MessageHeader.tsx` / `UserProfilePopover.tsx`): the `UserProfilePopover` inline-flex trigger wrapper refused to shrink below the name's nowrap width, running long author names under the hover action bar and off the pane edge. Fixed by adding a `triggerClassName` prop to `UserProfilePopover` and passing `min-w-0 max-w-full` at the author call site. Two other suspected instances (project file breadcrumb, drafts pane title) were stress-tested and already truncate correctly — no change. ### Related issue N/A — none found. ### Testing - New Playwright regression tests for both fixes (`inbox-title-overlap.spec.ts`, `message-author-overlap.spec.ts`, registered in the smoke project), each proven to discriminate: they fail with the fix reverted (real measured overlap) and assert the ellipsis actually engages with non-zero title width, so they can't pass vacuously. - Typecheck, lint, and full desktop unit suite green (pre-push hooks); full desktop e2e smoke suite run earlier: 1402 passed, 3 pre-existing unrelated failures (each fails identically with the fix reverted). **Inbox title — before** (long title paints under the header controls):  **Inbox title — after** (truncates with ellipsis, controls stay clear):  **Author name — before** (long name runs past the header row edge):  **Author name — after** (clean cutoff):  🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: cynfria <yescynthia@gmail.com> Signed-off-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz> |
||
|
|
051c3a270b |
fix(buzz-acp): report missing models without retrying (#7538)
## Summary When an agent reports model-not-found, Buzz retries the unavailable model and delays the failure reply until retries are exhausted. Stop retrying this error and immediately post a threaded recovery notice. The notice tells users to select a different model in agent settings, save, restart the agent to apply the configuration, and re-send their request. This adds one error-handling branch and regression coverage in `buzz-acp`. It matches `-32002` errors containing `model not found`. Other resource-not-found errors, such as stale sessions, retain the existing retry behavior. Detailed error events remain available for diagnosis. The existing restart policy is unchanged. ### Related issue None found in existing issue/PR searches for model-not-found recovery. ### Testing Playwright captured and visually checked the thread UI with seeded conversation data and the exact recovery text. The check opens the request's thread, confirms no reply before the failure, injects the notice, and verifies the full text is visible. [Before/after screenshots](https://github.com/block/buzz/pull/7538#issuecomment-5608196506) show the corrected save-and-restart instructions. These are local test captures, not a deployed provider recovery flow. Generated with Codex --------- Signed-off-by: Diem Nguyen <diem@squareup.com> |
||
|
|
12023a3cba |
fix(desktop): require a Codex adapter with Astra support (#7427)
## Summary Buzz considers codex-acp 1.6.2 current because the supported adapter floor is still 1.1.7. That adapter bundles Codex 0.148.0, so updating a separate Codex CLI to 0.153.4 leaves managed agents on the older runtime and unable to use GPT-6 Astra. Raise the supported adapter floor to the published 1.10.0 release, which depends on `@openai/codex ^0.153.3`. Existing discovery and installation code then classifies older adapters as outdated and offers the managed reinstall path. Update the availability and install-plan regressions to cover the observed 1.6.2 installation and the new minimum. This follows the existing version-floor policy. It does not automatically update a running installation: the user must complete Buzz’s offered adapter upgrade. Future upstream compatibility changes may require another floor update. ### Related issue No exact duplicate found in searches for Astra, CODEX_PATH, bundled Codex, outdated runtime, and codex-acp 1.10. Related: #3097 raised the older floor to 1.1.7 (already present on main); #2422 covers lost error details for runtime mismatches. Neither resolves this version gap. Originating conversation: buzz://message?channel=3286cd76-f83e-4c7d-8317-10a16580744d&id=8b79a73078217222b870fff144c27e7d27bcd5a67c966869c18fe726db716898 ### Testing - Isolated npm install of codex-acp 1.10.0 resolved bundled Codex 0.153.4, with no CODEX_PATH override. - Live macOS ACP probe: initialize protocol v1 → session/new → select gpt-6-astra[medium] → prompt. Received `OK` and `stopReason: end_turn`; usage metadata confirms gpt-6-astra. - Existing adapter 1.6.2 initialized but advertised no Astra model in the same probe. - Desktop Rust formatting and `git diff --check` pass. - `just desktop-tauri-test`: 3,266 passed, 20 ignored, zero failures across the Desktop workspace and integration tests. - Workspace and Desktop Clippy, frontend static checks, and `just file-size-check` pass. - Repository `just ci`: still running the remaining mobile/build/workspace-test stages. The installed Buzz app and managed adapter were not replaced or restarted. The live check validates the new adapter/runtime path; a complete packaged Desktop upgrade workflow remains untested. Signed-off-by: Stephen DeLorme <stephen@d.elor.me> |
||
|
|
ad9591c43b |
fix(desktop): order unnamed roster members by full canonical npub (#7503)
🤖 ## Summary - Channel members appear in the Members sidebar. A member who has never set a display name is listed under an abbreviated form of their public key (npub), and the sidebar previously sorted those unnamed members by that short label. Short labels are not unique — different keys can share one — so the order of unnamed members could look arbitrary or unstable. Unnamed members now sort by their full public key, so the order is deterministic. - When two members display the same name, the previous tiebreak was membership order (who joined first), which is not visible to a reader and can shift as roster data loads in. The tiebreak is now the full public key, so identical display names always land in the same order. - Nothing gets noisier on screen: the full key is used only for sorting, and the sidebar still shows the compact abbreviated form. Priorities are unchanged — authored (custom) names still outrank fallback labels, and role/current-user grouping still applies. - Scope is the desktop app's Members sidebar and member management: the two existing sort comparators. Mobile and other lists in the app are untouched. ### Related issue Based on #7495 (introduced the abbreviated npub labels this follows up on). The original five presentation PRs remain independently reviewable. No closer duplicate found. ### Testing - 6469 desktop unit tests, typecheck, and check pass. - The 3 existing consumer-seam E2E tests still pass; a new E2E test asserts the sidebar lists unnamed members in full-key order, with fixture members deliberately inserted in the opposite order so incoming membership order cannot mask the sort. - Negative check: reverting only this change makes the new ordering assertion fail, so it genuinely binds the new sort. - CI has not run on this PR yet. Buzz provenance: channel 1f0e4a3d-7e01-4efe-bb16-843b357f85c9 / task 340c3de9b27dbedb8453c0c7652220f9080d30fcc70a7c4f6e27fdd4fa378056 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
82656ffea0 |
fix(mobile): standardize public-key identity display on npub (#7493)
🤖 ## Summary In the mobile app, anyone who hasn't set a display name shows up as a raw 64-character hex key (e.g. `3a5d4f9c…`) — unreadable, and unrecognizable as the same identity across screens. Profile and Settings also let you copy that raw hex. Nostr public keys have a standard readable form — `npub1…`, the same encoding other Nostr apps and our desktop app already display. This PR makes every mobile identity surface render npub instead: - **Unnamed people everywhere** — message and thread authors, reactions, typing indicators, member lists, channel details, DM headers and tiles, inbox, search, forum cards, Pulse notes and reply context, mention suggestions, and invite rows — now show a compact npub label: first 8 + last 4 characters of the full npub joined by an ellipsis (`npub1abcd…wxyz`), the same truncation desktop uses. Previously these showed truncated raw hex. - **DM fallback avatars and blank names** — 1:1 DM tiles and headers key their fallback avatar to the same non-self counterpart the label names, including self-first participant order; a self-DM keeps its hex-key-derived initial. Blank or whitespace-only display names fall back to the compact npub instead of rendering empty, while nonblank authored names render verbatim (padding included). - **Profile sheet → "Copy public key"** now copies the full canonical npub — never raw hex. When the identity string isn't a valid public key, the copy tile is disabled, so a malformed key never reaches the clipboard. - **Settings → Identity (pubkey)** displays and copies the full npub; an invalid identity reads "Identity unavailable" with copy disabled. - **Invalid identities never leak truncated raw hex** into the UI anywhere — they render a neutral "Unknown identity" label. - **Unchanged on purpose:** display names and verified handles (NIP-05 — the `name@domain` badge) still render as before. Unnamed avatars keep distinct per-key initials, derived from the underlying hex key rather than the npub — otherwise every unnamed key would render the same "N" initial. Event IDs are not public keys, so they keep their hex truncation (in Pulse's "Replying to", the parent author shows npub while an event-id fallback still shows hex). The nevent share link, private keys, and internal hex storage are untouched. Inputs that accept a key (invite/member entry) accept both hex and npub and keep working in hex internally. ### Related issue N/A. Searched open issues/PRs for npub identity display on mobile — closest related: none found. Desktop's parallel npub standardization lives in the stacked desktop PRs (#7488 foundation, #7489 controls, #7495 display surfaces); this is the independent mobile slice (based directly on `main`, not on those branches). ### Testing At exact head `5a620e420a1fd57d9d8011ac26434eed32fcf765` (base: `main` `44316ff72`; 40 files, +1,345/−154): - Full mobile suite: 2,098 tests passing (`cd mobile && flutter test`); `flutter analyze` clean; `dart format --set-exit-if-changed .` clean — the same checks CI runs. - Widget/unit coverage at production seams: compact labels and hex-keyed avatar initials for DM headers/tiles, member rows, mention suggestions, and Pulse reply context; DM fallback avatars keyed to the labeled counterpart (self-first order and self-DMs); blank/whitespace display-name npub fallback with nonblank authored labels verbatim, including the Activity inbox sender and profile-sheet heading (each with its own empty/whitespace production-seam regression); full-npub copy and disabled-copy semantics in profile and settings; invalid-key suppression; and hex↔npub input round-trips. Verified via unit and widget tests — no device/simulator validation is claimed. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
2226b6f95b |
fix(desktop): npub identity controls across profile, agents, and workflows (#7489)
🤖 ## Summary Building on #7488's npub foundation, this PR finishes the identity display change for the controls where you actually manage people and keys: profile, settings, agent access, and workflows. Everywhere in these surfaces, an identity key shows — and copies — as its canonical npub (npub is the human-readable encoding of a Nostr public key: the compact `npub1j57...fjmv` form where space is tight, the full npub where the whole key matters), and accepts npub as input. After this change: - Profile panel: the public-key row and the managed-by / declared-owner copies show the full npub. If a key can't be encoded, you see "Unavailable" with no copy button — never a raw or partial key. - Settings: the identity card shows and copies the npub. The hosted-communities account identity derives from the bound key (`pubkey_hex`) — the same authority as the mismatch gate and hosted operations — so the display can never disagree with what the app acts on; an unusable hex falls back to a neutral label instead of rendering the unverified server npub. The connected claim and a community's Connect action require that same usable bound key to match the local one — with no usable binding the card cannot claim connected or start Connect, while the community list, linking, and delete/rebind recovery stay available. - Hosted create/onboarding: the account and device identity rows in the create flow and owner onboarding derive from the same authoritative fields (bound key / local key), with the same neutral fallback; readiness requires a usable bound key that matches the local one. - Respond-to allowlist (controls who may respond to an agent): entries can be typed or pasted as hex or npub; both spellings of the same key are recognized as one entry and dedupe. Search results, chips, and remove buttons use the compact npub. - Workflow key fields: to/from keys display as npubs in the form and save back as canonical hex. Templates like `{{trigger.author}}`, roles, and free text pass through untouched; placeholders accept both spellings. - Recipient and agent dialogs: the verify popover is npub-only (the raw-hex line is gone); denied-membership screens never show a raw key. - The Rust-side truncated display name (used for native surfaces) shows the same compact npub, so those surfaces match the web UI. Internal representation is unchanged: keys are still stored, sent, and matched as canonical 64-character hex — npub is a display and input spelling, normalized to hex at the boundary, so existing data and integrations keep working. Bound-key usability and comparison use one normalized form (trimmed, lowercased, 64 hex characters; npub rejected), so padded or mixed-case spellings of the same key match. Display names, private keys, and event IDs are untouched. ## Details - `respondToAllowlist` / `RespondToField`: npub entries normalize to canonical hex; cross-form dedupe; compact npub in rows and chips; direct-add accepts npub and stores canonical hex. - `workflowFormTypes` / `WorkflowStepCard`: hex → npub for display, npub → canonical hex on save; templates, roles, and free text pass through in both directions (roundtrip-tested). - `UserProfilePanelFields`, `ProfileSettingsCard`, `HostedCommunitiesSettingsCard`, `MembershipDenied`, `SelectedRecipientChip`, `AddAgentToChannelDialog`: npub display and copy; invalid keys → "Unavailable" with no copy; hosted identity rows derive from the bound `pubkey_hex` (create/onboarding rows from the bound and local keys), never the unverified server npub; connected/readiness/Connect gates use the same usable-bound-key predicate, and the settings Connect invocation callback re-checks it before starting. - `src-tauri/src/commands/identity.rs`: `truncated_display_name` compacts to the first 8 + last 4 characters of the npub (above a 12-char threshold), mirroring `truncateNpub`. - e2e: profile key rows and clipboard polls assert npub forms and raw-hex suppression; the display-screenshots spec pins the npub-only popover; hosted specs drive the real settings card, create flow, and onboarding rows through their real providers, and the unlinked/npub-only identity cases assert no connected claim and no Connect action. ### Related issue - Fixes: N/A. No separate issue; the related work is the stack below. - Stack: builds on #7488 (shared npub foundation), now merged; this PR is rebased onto main and stands on its own. ### Testing At head `303c90ffa` (base: main `bfc38485`; 24 files, +1125/−146): - Focused unit suites (respondToAllowlist, workflowFormTypes, hostedCommunityApi bound-key helpers) green; mutation-checked — dropping allowlist canonicalization fails the dedupe case, and dropping bound-key normalization fails the npub-in-hex and padded same-key cases. - Full desktop unit suite 6,477/6,477, `desktop-typecheck`, `desktop-check` (formatting fixed narrowly with `biome check --write` on the touched files only), and a fresh E2E build at the current head; the add-community + hosted-communities-settings specs 18/18 and onboarding integration 69/69 on a fresh dedicated port, with focused new-case runs 4+4 covering padded same-key (ready, Connect kept — no false rebind) and npub-in-hex (neutral label, recovery, no Connect) across the settings card, create flow, and first-community onboarding, plus the unlinked-account settings regression asserting Connect cannot occur. - `cargo fmt`/clippy (both feature sets) and `cargo test identity` (71 pass) passed at the earlier full-change head; since then, the only production changes in this PR's delta are the hosted identity display authority and its fail-closed bound-key gating/normalization above (base-side fixes carry #7488's receipts) — every other change is test-only. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
ad2a84131e |
fix(desktop): npub identity displays for mention, member, and workflow surfaces (#7495)
🤖 ## Summary Every Buzz account is identified by a long public key. Before this change, when someone had no display name, surfaces fell back to inconsistent labels — mostly raw hex fragments like `abcd1234…wxyz`, sometimes a generic role label with no key — so the same person looked different from surface to surface, and nothing looked like an npub address. This PR applies the npub identity foundation from #7488 to the everyday surfaces: a person without a display name now falls back to the same compact npub everywhere — `npub1xxxx…yyyy`, the human-readable spelling of their public key (first 8 + last 4 characters of the full npub) — across messages and mentions, reactions, huddles, member and participant lists, the sidebar and channel activity, search, projects, tray, notifications, and workflow surfaces. - **Mentions and messages**: key-only mention chips render the compact npub. Pasting a copied mention back still re-binds it byte-exactly to the identity it declares, for both the new npub chips and legacy hex-truncated chips copied by older clients — wrong, missing, or tampered key qualification is rejected instead of silently degrading to plain text. - **Reactions and huddles**: huddle reaction events and the huddle roster/participants render the compact npub for unnamed participants; workflow reaction triggers describe authors with the same form. - **Members and sidebar**: channel and community member lists, add-member results and invites, the members sidebar, the channel-activity popover, search, projects (assignees/reviewers/PR panels), the tray menu, and desktop notifications all fall back to the compact npub; titles and aria labels keep the machine-readable full labels. - **Profile labels**: panel/popover display names and owner handles fall back to the compact npub (never raw hex) when there is no name; linked-event (nevent) message metadata shows the npub-shaped author fallback while the event lookup and event IDs are unchanged. - **Workflows**: author-picker secondary labels, step destination keys, and trigger-author references render compact npubs; event and blob IDs keep their existing hex compacts (they are not identities). - **Avatars stay distinct**: fallback avatars for key-only identities derive initials from the key's tail, so prefixed role labels like "Participant npub1…" no longer collapse every unnamed participant onto the same initials; people with names keep their name initials. Preserved exactly: display names and distinct avatars, internal hex keys (storage/API forms unchanged), clipboard identity roundtrips, event/blob ID compaction, private keys (no nsec path is touched), and nevent link handling. Scope: this PR changes what identity labels **display**, not identity controls — profile/settings copy controls, the respond-to allowlist, workflow key fields, and agent dialogs are the sibling slice #7489, and the shared primitives (`canonicalNpub`, `truncateNpub`, the `<PubKey>` gate, strict input parsing) come from the foundation #7488. ### Related issue - Fixes: N/A. Searched existing issues/PRs for duplicates — none found; the related work is the npub identity stack this slice belongs to. - Base/dependency: stacks on #7488 (foundation) — this PR does not stand alone on main. - #7489 is a sibling slice on the same #7488 base (profile/agent/workflow controls), not a dependency: this PR does not require #7489, and #7489 does not require this PR — both only require #7488. ### Testing At exact head `4763cbeae1dd521309755e6d61f657324cb98667` (base: `fix/desktop-npub-identity-d1a` @ `5f3a4a8111998c8aa41ad77cf66992bd1c85343c`; 71 files, +656/−189 — production +277/−136, test support +379/−53): - At this head: targeted `mentions.spec.ts` (1/1), the e2e build, typecheck, and biome — green. - 9 changed/related unit files: 100/100 green; typecheck, e2e build, biome, and px text/truncation checks clean; huddle-roster focused run green; channel-activity e2e 11/11; mutation checks confirm the fallback wiring (removing it collapses shared initials and drops fallback rows). - Known pre-existing local e2e failures, unchanged by this PR and reproduced identically at the upstream merge-base: huddle-transcription voice-menu attribution (25 pass / 1 fail) and the `workflow-local-controls` 438px caret drift. Not claimed green locally. - Update at head `236af9e6137386737e84d3a474d6bc808a704c50` (test-only follow-ups `1143af345` + `236af9e6`): the `workflow-local-controls` races were fixed in the test drivers, and the 438px diff was shown to be a stale Darwin snapshot baseline (name-row enable switch already absent and `message_posted` already MessageSquare at recording commit `9390e11c9`) and refreshed — the focused screenshot test, including keyboard/caret assertions, now passes locally (twice). The full spec was not rerun after the snapshot refresh; the huddle-transcription item above is unchanged. Label/copy text changes are asserted by the e2e specs (`mentions`, `mention-recipients`, `pubkey-display-screenshots`, `huddle-transcription`, `channel-activity-popover`, `workflow-local-controls`) rather than new screenshots; the screenshot spec pins the compact npub text forms. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
bfc3848558 |
fix(desktop): shared npub identity foundation (canonicalNpub, PubKey gate, strict parser) (#7488)
🤖 ## Summary Identity keys in the desktop app are displayed as raw 64-character hex. A person's key shows up as something like `953d3363…` — unreadable, impossible to recognize as the same identity on another screen, and a hazard when copied by hand. Nostr (the protocol Buzz runs on) has a human-readable spelling for identity keys — the `npub1…` form — but the desktop app did not use it consistently. This is the foundation of the desktop npub changes: it adds the shared pieces every identity surface builds on, and two follow-up slices stack directly on this branch — #7489 converts the identity controls (profile, settings, allowlist, workflow key fields) and #7495 converts the everyday display surfaces (mentions, member lists, sidebar, and other name fallbacks). After this change: - The shared identity widget shows the compact npub form — `npub1j57...fjmv` — instead of a hex prefix, everywhere it renders (for example the owned-agent public-key row on a profile). Copying it puts the full npub on the clipboard. - Copy is a real interaction, verified end-to-end: both popover variants put the exact canonical npub on the actual clipboard — never the raw hex the popover also lists, never a truncation — and a portaled popover's clicks no longer steal focus from the new-DM To-field mid-copy. Pointer copy, a natural Space-then-Enter path, and inner/outer Escape are covered. - Anything that isn't a valid identity key fails neutrally: short or corrupt values — including degenerate values that technically encode to a checksum-valid npub but aren't real identity keys — show "Unavailable" with no copy button, instead of a misleading value. - Both valid npub spellings display: all-lowercase `npub1…` and all-uppercase `NPUB1…` (Bech32, npub's encoding, permits either casing) both render the same canonical lowercase npub. Mixed case is rejected by the display path as written — `canonicalNpub` and the widget don't case-normalize input — while input parsing (`parsePubkeyInput`) keeps its trim-and-lowercase normalization and accepts mixed-case npubs; both paths require the decoded payload to be exactly a 64-character identity key. - Identity-key input is strict on payload: an npub whose decoded payload isn't exactly a 64-character identity key is rejected, matching the validation the app's Rust side already applies to agent allowlists. Intentional scope boundary: only surfaces that render through the shared widget change here. Outer profile copy, settings identity cards, the respond-to allowlist, and workflow key fields still show hex — they move to npub in the controls follow-up (#7489). Nothing else changes identity representation: display names, private keys, event IDs, and the hex the app stores, sends, and matches internally are untouched; only the user-facing spelling of an identity key changes. ## Details - `desktop/src/shared/lib/pubkey.ts` — `canonicalNpub()`: strict canonical full-npub helper (64-char hex in any case, or a checksum-validated npub, returns the canonical npub; anything else returns `null`); `truncateNpub()`: the compact display form; existing exports unchanged. - `desktop/src/shared/ui/PubKey.tsx` — the shared widget's identity gate validates through `canonicalNpub`; the popover copies the npub only. - `desktop/src/shared/lib/nostrUtils.ts` — `parsePubkeyInput` rejects npubs whose payload is not exactly a 64-character identity key. - `desktop/src/features/messages/ui/NewMessageScreen.tsx` — the To-field focuses its search input only for clicks that land inside the field itself, so portaled recipient popovers keep their focus while open (a popover click previously dismissed it mid-copy). - Unit suites cover the helper, widget, and parser (including the degenerate-encode and uppercase regressions); the e2e specs that render these rows assert the npub display. ### Related issue - Fixes: N/A. Searched existing issues/PRs for npub identity display — no existing match. - Stack: #7489 is based on this branch and builds on these primitives; it does not stand alone on main. ### Testing At head `b3310c248` (base: main `44316ff72`; 12 files, +440/−39): - Focused unit suites (pubkey, PubKey, parsePubkeyInput): 20/20 green; mutation-checked — removing the decoded-length predicate fails the short/empty checksum-valid-npub assertions in `canonicalNpub` and the widget, and a wrong-identity clipboard value fails the new copy assertions. - `pnpm typecheck` and `pnpm check`: pass; full desktop unit suite 6459/6459 at this exact head. - Targeted e2e at this exact head: 8/8 across the two specs that own the clipboard flows — `agent-access-warning.spec.ts` (compact variant, agent-access owner hint) and `pubkey-display-screenshots.spec.ts` (full variant, new-DM recipient verification: pointer copy, popover surviving the copy, inner/outer Escape, Space-then-Enter). - No Rust-side or build files change in this PR, so those results are unaffected. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
93761e411c |
fix(mobile): render push notification sender identity as npub (#7494)
🤖 ## Summary When an iOS push notification comes from someone the app has no cached name for, the notification title showed the first characters of the sender's raw public key — for example `aa4fc866…`. That fragment is unreadable and doesn't match how the same person appears anywhere else in Buzz. This PR changes that title to the compact form of the sender's npub (npub is the human-readable encoding of a Nostr public key): first 8 and last 4 characters — for example `npub14f8…9nsy`, the same identity shape used across the desktop and mobile apps. - Unnamed senders: raw hex fragment → compact npub. - Named senders: unchanged — a sender the app has a display name for still titles the notification with that name. - Unverifiable sender identities (malformed keys, or lookalike strings that are not literal 64-hex-digit keys) now render a neutral "Someone" instead of partial raw key material. - Everything else about the notification is unchanged: body text, subtitle, thread matching and grouping, deep-link navigation, thread identifiers, and the internal hex public key the resolver matches on. The native iOS notification-service package (`BuzzPushKit`) gains a minimal in-house bech32 codec (bech32 is the checksummed string encoding npubs use) — checksum-validated, 32-byte keys only, and no new external dependency. The hex input branch accepts exactly a 64 ASCII hex digit key before any parsing, so strings that merely parse like hex (for example a run of `+a` pairs) cannot become a displayed identity; this is input validation for presentation. Event signature verification is untouched. ### Related issue Fixes: N/A. Searched existing issues/PRs for push-notification npub identity — closest related: none found. ### Testing At head `3e3f2813b8864b76257ccb50dea3a4b31fa4de0d` (base `44316ff72f5f7de014c66b01cbf534298a70c249`; 4 files, +321/−4): - CI `Mobile Swift` lane, at this exact head — all passed: `swift test` (73 tests, 0 failures), the SwiftPM debug and release builds of `mobile/ios/BuzzPushKit`, and the unsigned iOS release build. - Test coverage: npub encoding cross-checked against independent nostr-rs/NIP-19 vectors; rejection of bad checksums, mixed case, wrong lengths, invalid alphabet, padding, and non-32-byte payloads; resolver boundary matrix — hex/npub/invalid sender keys render compact npub or "Someone" while body, subtitle, sender key, and thread identifier pass through; named senders keep cached display names. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
c045321a7f |
fix(acp): pace targeted overflow recovery on consumer capacity (#7325)
🤖 ## Summary When a Buzz agent falls behind on incoming messages, its connection can make the backlog worse while trying to recover. The connection buffers messages from the relay server until the agent is ready to process them; if that buffer overflows, recovery previously requested history for **every subscribed channel** and paused socket reads while sending those requests. That adds traffic to an already overloaded connection. This change requests history only for affected subscriptions, once the code consuming those messages has room, with at least five seconds between attempts. The recovery path now: - Combines repeated losses into one pending recovery per affected subscription, keeping the oldest dropped timestamp so replay starts early enough. - Waits until at least half the consumer queue is free and the relay's existing rate-limit delay has expired. The queue wakes recovery when space becomes available; recovery does not periodically sample capacity or hold queue space away from live messages. - Attempts one subscription at a time, choosing the least recently attempted so a busy channel cannot crowd out other channels or membership notifications. The five-second delay starts when an attempt finishes, including a failed write; failed writes leave recovery pending. Recovery is paced by available capacity, not by how often messages are lost. This is not a larger buffer or a cutoff that abandons recovery. Subscription identifiers, message filters, replay timestamp overlap and duplicate filtering are unchanged; no downstream agent changes are required. This targets a reproducible overload **amplifier**, not every cause of overload or every catch-up limitation. The initial live overload's cause has not been established. Recovery remains best effort: a successful request write is not proof of delivery, and existing history/retention limits, bounded duplicate tracking and replay limitations still apply. There is no exactly-once or complete catch-up guarantee. A stalled write can still pause socket reads for the existing ten-second timeout; the pacing bound does not cover initial subscriptions, reconnects or other retry paths. ### Related issue Closest related: #5014 (channel re-subscription); also #6661 (membership reconciliation) and #6090 (relay backpressure gap signaling). This addresses local overflow recovery scheduling, not those separate mechanisms. ### Testing Recorded offline comparisons against the previous behavior, with the final implementation at `8000636f3073167c5a5107bb179c7d91160f1729`: | Same fixture: 18 subscriptions, three overload rounds | Before | After | | --- | --- | --- | | Recovery history requests | 108 | 3 | | Ping-response delay | About 4.6 seconds | Below the measurement's 1 ms resolution | A separate bounded-history fixture delivered all 320 events plus subsequent live traffic in **both** versions. Regression coverage exercises the real socket-handling task, including intermittent consumer capacity, fairness, failed writes and cancellation of capacity waits before live delivery. These are synthetic results, not production throughput measurements or evidence of a deployed cure. The full local `RUST_TEST_THREADS=4 just ci` run passed on September 4, 2026. Earlier unsuccessful local runs remain part of the validation history. The [recorded validation evidence and separate desktop follow-up](https://github.com/block/buzz/pull/7325#issuecomment-5540592398) preserve the original desktop mock-history scroll failure, its passing rerun and the remaining investigation. That desktop path does not run the agent connection code; neither this repair nor the passing rerun fixes the observed scroll problem. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> |
||
|
|
218633b8fd |
fix(link-preview): keep composer fetches user-paced (#7211)
**Category:** fix **User Impact:** Link previews can keep loading while a message is being composed, while sending still has a finite escape hatch and stalled network transports cannot occupy preview slots forever. **Problem:** Native metadata and image deadlines could collapse slow previews into fallback cards while the user was still composing, and a shared image-host cooldown made pasted batches fail inconsistently after one rate limit. **Solution:** Keep preview resolution user-paced with no aggregate request deadline, bound transport inactivity (15s DNS/connect, 30s idle read), serialize image requests by host, and allow at most one server-directed cooldown wait of up to 30s across an image fetch and its redirects. The existing bounded post-Send preparation and immediate Skip paths remain unchanged. <details> <summary>File changes</summary> **desktop/src-tauri/src/commands/link_preview.rs** Removes aggregate native deadlines so composer metadata work can complete at the user's pace, while retaining DNS/connect/idle-read liveness bounds. Adds bounded host-paced image request coordination that releases its gate during cooldown, waits inline at most once for at most 30 seconds, and cannot renew that wait through redirects or the outer transient retry. Same-host image and favicon requests remain deliberately serialized to align with host rate limits. **desktop/src-tauri/src/commands/link_preview_rate_limit.rs** Adds a fixed-size striped host gate so concurrent image requests are serialized without retaining an unbounded attacker-controlled hostname map. **desktop/src-tauri/src/commands/link_preview_tests.rs** Moves native link-preview tests into a dedicated module and covers the user-paced metadata contract, bounded one-shot cooldown behavior, and gate release while a rate-limited request sleeps—including a different host sharing the same bounded gate stripe. **desktop/src-tauri/src/commands/link_preview_youtube.rs** Removes the thumbnail fetch deadline so YouTube previews follow the same composer lifecycle contract while using the shared bounded transport. **desktop/src/shared/lib/useResolvedLinkPreviews.ts** Adds development-only metadata outcome diagnostics with elapsed time and image/fallback state, without logging encoded image payloads. </details> ### Reproduction steps 1. Open the desktop composer and paste several GitHub pull request links whose OpenGraph images share a host. 2. Observe that image requests are paced by host instead of racing, and slow-but-progressing preview work remains pending rather than immediately becoming a completed favicon fallback. 3. Send while preview work is still pending and confirm **Preparing link preview** remains bounded by the existing post-Send budget. 4. Use **Skip** during preparation and confirm the message proceeds immediately. 5. In a development build, inspect the console for `[link-preview] metadata fetch completed` diagnostics containing elapsed time and image state without base64 payloads. ### Related issue N/A — scoped from the linked Buzz implementation room. ### Testing At current head `dfb394aafbee537e9ffb04ad3732d08f65f30b8e`: - Production-bound paused-time metadata regression passed through `fetch_link_preview_metadata`; restoring the former 10-second aggregate wrapper makes it fail at the pending assertion. - Native link-preview module: 19/19 passed. - `cargo check --manifest-path desktop/src-tauri/Cargo.toml` passed. - Rust formatting and `git diff --check` passed. - Pre-push `push-head-scope`, org safety, differential file-size, branch-skew, and `desktop-tauri-checks` hooks passed. At prior head `59e2dcf167b15c7a3e637ad2608008b7f9cef5f3`: - Full Tauri Rust suite: 3,056 passed, 19 ignored; integration crates 7 + 3 passed. - Focused native link-preview suite: 26/26 passed. - The pasted multi-preview workflow was exercised in the desktop app and confirmed improved before draft publication. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@users.noreply.github.com> |
||
|
|
44316ff72f |
feat(mesh): upgrade to mesh-llm 0.76.0-rc8 and recommend Qwen3.8 27B (#6189)
Upgrades Buzz's mesh-llm dependencies through the released `v0.76.0-rc8` tag (`2040765d`), including the Qwen3.8 curated recommendation and rc8's scheduler/runtime improvements. **Scope note:** the earlier open-relay/unenforced-admission mode has been removed from this PR at Mic's direction — it is not a product mode we want. Mesh admission remains roster/allowlist driven, exactly as on `main`: on a relay with no NIP-43 membership snapshot the mesh runs self-only. No NIP-11 mode probing, no mode-transition restarts. A future perimeter/admission strategy for open relays will be designed separately. This PR also: - seeds `BUZZ_AGENT_LLM_TIMEOUT_SECS=660` for mesh agents, above MeshLLM's 600-second backend timeout; - makes `desktop-tauri-clippy` lint both default and `mesh-llm` cfg graphs; - runs the feature-enabled desktop test suite in CI; - recommends Qwen3.8 27B Q4_K_M for 64 GB-and-larger machines, then ladders down through Gemma 4 E4B and Qwen 9B for smaller machines; - keeps stored shared-compute `auto` translated to MeshLLM's supported wire model `mesh`. RC8 verification: - `just ci` passed locally at `92ecc7ec933bdd4df804cc9f28a2b51efa5313c5`. - Pre-push differential gates passed, including both desktop Tauri cfg graphs and package tests. - A prior isolated runtime smoke used the RC8 binary's OpenAI endpoint for a Buzz-shaped system/user/tool/tool-result/final-response loop; all assertions passed and the isolated process was shut down. Perf previously measured on M5 Metal, Qwen3.8-27B-Q4_K_M: TTFT 0.22–0.32 s, ~25 tok/s streaming; agent-shaped turns ~1 s to first token after the first (prefix cache). --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Alessandro Joabar <sandro@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Co-authored-by: Mic Neale <305999590+micspiral@users.noreply.github.com> Co-authored-by: Alessandro Joabar <sandro@squareup.com> |
||
|
|
cd54e26823 |
fix(agent): route GPT-5+ model-service FQNs to Responses (#7358)
## Summary Route Databricks Unity Catalog model services to OpenAI Responses when their service name matches GPT-5 or newer. These models can reject tools plus reasoning on Chat Completions. Match only the service component, using the existing family-token boundaries and a numeric major version. Catalog and schema names cannot select the protocol. Keep neutral effort capabilities and the full model ID unchanged; other services still use MLflow Chat Completions. Keep the Rust and desktop resolvers in sync, add shared boundary cases and a captured-HTTP regression for completion and summarization, and update the documented FQN rule. ### Related issue No duplicate found in searches for “FQN responses” PRs or “astra” issues. Related: #6918 introduced Unity Catalog discovery. Originating conversation: buzz://message?channel=0b881928-a3a6-4c01-b981-8e64268f01ce&id=770949343bc96a9ed88acd90a1b37d358a0efc52c79237d0fdb491ce02b8d4ed ### Testing No live Databricks inference test. The gateway must accept the full model-service ID on its OpenAI Responses route; this remains the integration risk. The local `just ci` attempt exceeded its five-minute deadline during `mobile-check`, so the full repository gate was not completed. All push-hook checks passed. Generated with Codex Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> |
||
|
|
86c189e857 |
fix(buzz-acp): wake held ACP threads and fence forked sessions (#7340)
## Summary Adds an independent deadline wakeup so held thread work dispatches after its 10-second bound even when the relay loop is otherwise quiet. Fences session ownership by generation so a worker returning after a fork cannot make an older provider session claimable again. This follows up on the two post-merge findings from [#7337](https://github.com/block/buzz/pull/7337#pullrequestreview-5116329341). ### Related issue Follow-up to #7337. ### Testing - `cargo test -p buzz-acp` - `cargo clippy -p buzz-acp --all-targets -- -D warnings` - Pre-push file-size, differential Rust test, and desktop Tauri gates No UI changes. --- **Update Sep 4, 15:35:** Addressed both Codex review findings. - Queue-cap eviction now prunes orphaned hold deadlines. - An expired hold stays expired until a worker is successfully claimed. - Hold timers remain disabled while every worker is busy; worker return wakes dispatch directly. - Added regressions for queue eviction and pool exhaustion. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
fa1b27bcc8 |
fix(mobile): style inline code with the app mono face (#6631)
## Summary Inline code on mobile renders as **bold body text on a faint background wash** — no monospace face, no chip, and it cannot wrap. #5257 diagnosed this as a missing `highlightBuilder`. That is no longer the right fix. `gpt_markdown` 1.2.0 deprecates `highlightBuilder` (removal in 2.0.0), renders inline code as a real chip, and adds `InlineCodeStyle` for restyling it. The package author confirmed this on the issue. So this PR is an upgrade — 1.1.6 → 1.2.1 — plus one theme declaration, rather than the builder the issue originally asked for. **Where the style is declared.** `GptMarkdownThemeData` goes in `AppTheme._buildTheme`, which both `light()` and `dark()` call. That reaches all four `GptMarkdown` call sites — `message_content`, `transcript_item_widget`, `token_pill`, `custom_emoji_render` — so the style is stated once instead of per widget. A widget-level `inlineCodeStyle` would have covered channel messages only, leaving the other three on the package's defaults. **What is declared.** Face, size, ink, chip fill and outline — not the face alone. A face name on its own leaves the rest on the package's defaults, which put inline code at 14.1sp beside a fenced block's 13, on a neutral `onSurface` tint rather than the app's code surface. In dark that tint is *lighter* than the surface, while every other code surface in the app is recessed, so the chip read as a different kind of object. All of it now comes from one `CodeStyle` declaration that the fenced block reads from too, so the two cannot be edited apart. **Three adaptations the upgrade requires.** Each was found by running the gate, not by reading the changelog: 1. **`imageBuilder` widened** to `(context, url, width, height)`. This is a hard compile error, and it is **not listed in the package's migration guide**, which states "nothing here stops code compiling". Worth reporting upstream. 2. **`autolink` now defaults to `true`.** `normalizeBareLinks()` already rewrites bare URLs into Markdown links before rendering, so both would run. `message_content` opts out with `autolink: false` to keep current behaviour exactly. The migration guide argues for dropping the pre-processor instead — a better fix, but a behavioural change that belongs in its own PR. 3. **`gpt_markdown.dart` now re-exports `markdown_config.dart`**, making two direct imports redundant. `flutter analyze` reports `No issues found!` on 1.1.6 and flags both on 1.2.1, so these warnings are new, not pre-existing. **Deliberately out of scope.** The three non-message call sites now autolink bare URLs, since only `message_content` has a pre-processor to collide with. Custom inline components (`_MentionMd`, `CustomEmojiMd`, `_ChannelLinkMd`) could additionally declare `allScopesExceptLinkLabel` — 1.2.0 offers it as the fix for a `WidgetSpan` chip going blank inside a link label on iOS — but current behaviour is unchanged without it, so that stays a separate change. ### Related issue Fixes #5257 Duplicate scan: searched `gpt_markdown`, `inline code mobile`, `highlightBuilder` and `InlineCodeStyle` across both PRs and issues. No open PR touches inline code styling. #6135 (link labels) and #6166 (text selection) also touch mobile Markdown but address different defects. ### Testing Full gate, `just ci` — exit 0: | Stage | Result | |---|---| | Rust (33 suites) | 4768 passed, 0 failed | | Desktop | 5799 passed, 0 failed | | Mobile | **2011 passed**, 0 failed | | `flutter analyze` | `No issues found!` | | Desktop + web build | ok | Run on the branch with `main` merged in, so these numbers match what CI builds. **New regression test** — `renders inline code in the app code style`. It resolves the `CodeTextSpan` the package tags inline code with, which carries both the resolved `TextStyle` and the colours the chip behind it is painted with, so face, size, ink, fill and outline are all asserted rather than a widget's presence. It is negative-controlled: reverting only the theme declaration fails it with ```text Expected: a numeric value within <0.001> of <13.0> Actual: <14.1> ``` and dropping the declaration entirely falls back to `packages/gpt_markdown/JetBrainsMono` — so the test measures the real thing, and it would catch a future regression that silently drops the theme extension. The test passes `baseStyle: messageBodyTextStyle`, the style the message surfaces actually use; the widget's own fallback is the smaller `bodyMedium`, which would move the expected size. The test finds paragraphs with `find.byWidgetPredicate((widget) => widget is RichText)`, not `find.byType(RichText)`: inline code renders through `BidiRichText`, a `RichText` subclass, and `byType` matches exact runtime types. That is a hazard for any test that reads text back out of a paragraph, and one landed after this branch was cut: `message_content_custom_emoji_test.dart` arrived with #6996 and its `code keeps literal emoji while adjacent known tokens render` case reads a code span through `find.byType(RichText)`. It passes on `main` and fails on the merge result, which is what CI builds, so it went red only once CI was authorized. It now uses the same predicate. The two other `byType(RichText)` call sites — the rest of that file and `message_author_meta_test.dart` — were re-run and pass: their content carries no code span, so the exact type still matches. They were left alone. ### Screenshots Rendered through the real `MessageContent` widget with the app's own fonts loaded, at 390pt wide, 3x DPR. Sample text: ``Set `BUZZ_RELAY_URL` before launch, then run `just mobile-test` to verify.`` | | Before (1.1.6) | After (1.2.1) | |---|---|---| | Light |  |  | | Dark |  |  | Before: bold Inter on a flat wash, no chip edge, and `just mobile-test` breaks across the line with the wash simply ending. After: Geist Mono in a bordered, rounded chip, and the wrapped fragment gets its own chip on each line. --------- Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com> Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com> |
||
|
|
3c7f288c60 |
chore(release): release Buzz Desktop version 0.5.23 (#7381)
## Buzz Desktop release v0.5.23 - **Frozen main:** `dad5a33865fc81a2e55b3b60746632f615ec1e3a` - **Reviewed candidate:** `b9392d9d78744df365f9276e1ffe8c1baa5ea903` - **Previous desktop release:** `desktop-v0.5.22` - **Proposed immutable tag:** `desktop-v0.5.23` 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> |
||
|
|
dad5a33865 |
fix(desktop): keep packaged frontendDist relative so Windows embeds assets (#7177)
## Root cause
`tauri-command.mjs` points `frontendDist` at a `mkdtemp` directory so
concurrent OSS/internal packages cannot overwrite each other's assets.
On Windows that is an absolute path with a drive letter.
`FrontendDist` is an untagged serde enum whose **first** variant is
`Url(Url)`, and `C:\Users\...` is a valid WHATWG URL with scheme `c:`,
so serde selects `Url`. `tauri-codegen` then does:
FrontendDist::Url(_url) => Default::default(), // embed nothing
A missing *directory* panics with a clear message; a URL is silent. The
build exits 0 and produces an installable app with no frontend assets,
which boots to `ERR_FILE_NOT_FOUND` in the WebView.
Linux and macOS are unaffected — `/tmp/...` has no scheme, so it falls
through to `Directory`.
This affects every Windows build that goes through `pnpm tauri build`,
including `release.yml`'s NSIS job and `windows-canary.yml`.
## Fix
Pass the path relative to the config's own directory. `tauri-codegen`
resolves `frontendDist` with `config_parent.join(path)`, so a relative
path reaches the same directory and cannot parse as a URL. When the temp
directory is on another drive there is no relative form, so the scratch
root is created beside the config instead.
`BUZZ_PROTECTED_BUILD_OUTPUT` still receives the absolute path, and
cleanup is unchanged.
## Testing
`tauriCommand.test.mjs` asserted against the value it had just been
handed, so it could not observe this. Its fake CLI also resolved
`frontendDist` against the process cwd, which is not what Tauri does.
- Fake CLI now resolves against the config directory, matching
`config_parent.join(path)`.
- New case asserts the packaged `frontendDist` is not absolute and does
not parse as a URL. The absolute check is what fails on Linux/macOS, so
the regression stays covered on every platform.
- Verified the new case fails on the unpatched wrapper and passes with
the fix; the two existing cases pass either way.
- Desktop suite: 5844 passed. `useDocumentVisible` has a pre-existing
load-dependent flake that also reproduces on an unmodified checkout.
- Biome check clean on both files.
Verified end to end by rebuilding the Windows NSIS installer: embedded
asset keys in `buzz-desktop.exe` went from 0 to 490, and the app
launches.
---------
Signed-off-by: Jeff Hedlund <jhedlund@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f038cbbb0d |
fix(sidebar): simplify unread indicators and emphasize priority activity (#7134)
**Category:** fix **User Impact:** The sidebar now presents unread activity consistently: unread rooms are bold, offscreen activity is counted by destination, and DMs or directed activity receive stronger emphasis. **Problem:** Sidebar unread state was split across competing signals: agent work could trigger overflow, message totals inflated its count, non-DM rows showed redundant numerals, and routine room activity looked as urgent as DMs or directed messages. This made the sidebar noisy and made the overflow value harder to interpret. **Solution:** Use one stable offscreen unread control that counts rooms and DMs—not messages—and remove agent work from that signal. Keep ordinary room activity quiet; promote DMs, mentions, broadcasts, and relevant thread replies; preserve DM avatars and targeting; bold every unread room; and retain thread dots while removing non-DM numerals. <details> <summary>File changes</summary> **desktop/src-tauri/src/unread_catch_up.rs** Classifies relevant thread replies as priority activity during native unread catch-up so startup state matches live rendering. **desktop/src/app/AppShell.tsx** Passes the priority unread destination set into the sidebar. **desktop/src/features/channels/useUnreadChannels.ts** Projects unread destinations, priority state, and thread replies consistently while keeping Dock badge behavior separate from sidebar emphasis. **desktop/src/features/sidebar/lib/useOffscreenActivityChannelIds.test.mjs** Removes tests for the superseded agent/activity overflow projection. **desktop/src/features/sidebar/lib/useOffscreenActivityChannelIds.ts** Removes the old agent-plus-message overflow projection so agent work alone no longer creates the unread indicator. **desktop/src/features/sidebar/lib/useSidebarActivityOverflow.ts** Replaces activity-volume overflow state with unread-destination overflow state. **desktop/src/features/sidebar/lib/useSidebarUnreadOverflow.test.mjs** Covers priority detection and destination-count labels for the new overflow projection. **desktop/src/features/sidebar/lib/useSidebarUnreadOverflow.ts** Counts distinct offscreen unread destinations and determines quiet versus primary treatment per direction. **desktop/src/features/sidebar/ui/AppSidebar.tsx** Renders one stable overflow control, preserves protected-DM visibility filtering, and prioritizes visible unread DMs for previews and navigation. **desktop/src/features/sidebar/ui/AppSidebar.types.ts** Adds the priority unread destination set to the sidebar contract. **desktop/src/features/sidebar/ui/CustomChannelSection.tsx** Stops forwarding non-DM unread counts into custom channel rows. **desktop/src/features/sidebar/ui/MoreUnreadButton.test.mjs** Updates control coverage for explicit emphasis, destination labels, and DM targeting. **desktop/src/features/sidebar/ui/MoreUnreadButton.tsx** Applies quiet or primary treatment without changing geometry, accessibility text, DM avatars, or click behavior. **desktop/src/features/sidebar/ui/SidebarSection.tsx** Removes non-DM row numerals while retaining unread weight and thread preview affordances. **desktop/src/shared/ui/UnreadPill.tsx** Shares one composition between quiet and primary states so only color treatment changes. **desktop/tests/e2e/badge.spec.ts** Covers destination counting, promotion without count inflation, DM/thread priority, row bolding, and removed numerals; also captures the reviewed UI states. **desktop/tests/e2e/channel-activity-popover.spec.ts** Updates channel activity assertions for the numeral-free row treatment. **desktop/tests/e2e/channels.spec.ts** Updates channel unread expectations to use bold text rather than a row count. **desktop/tests/e2e/thread-unread.spec.ts** Keeps thread unread-dot and popover coverage while asserting the room itself is bold. </details> ## Reproduction steps 1. Open a workspace with enough sidebar destinations to scroll rooms above or below the viewport. 2. Receive ordinary unread activity in an offscreen room; verify one quiet `N unread` indicator appears and counts the room once regardless of message volume. 3. Receive a mention, broadcast, or relevant thread reply in an offscreen room; verify the same count becomes primary without changing its geometry or adding a badge. 4. Receive an unread DM, including thread-only activity; verify the indicator is primary, shows the DM avatar when available, and navigates to that DM. 5. Scroll the destination onscreen or mark it read; verify the count and treatment update from the remaining offscreen destinations. 6. Inspect unread non-DM rows; verify their names are bold, numeric badges are absent, and thread dots still open the unread-thread preview. ## Screenshots ### Ordinary unread room Routine offscreen room activity uses the quiet treatment.  ### Directed unread activity The same destination count becomes primary when an offscreen room has directed activity; geometry and label remain unchanged.  ### Thread-only unread DM A DM stays primary and retains its avatar even when only its thread has unread activity.  --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
4d447b9c20 |
Add generic information-flow control core (#7293)
Adds a dependency-free `ifc-core` crate implementing reader-set confidentiality labels and monotonic per-computation flow state. - Defines flow ordering, join, and meet over caller-supplied principal universes. - Fails closed for unknown or cross-universe input and checks reader widening at egress. - Includes property tests for lattice laws and monotonic taint, plus the design paper that motivates the broker integration. The crate deliberately contains no Buzz, Nostr, channel, membership, or grant policy; those remain in the Buzz adapter. --------- Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
4beffef697 |
feat(buzz-acp): update base prompt; add buzz context and skills to Pi agents (#7335)
## Summary - launch Pi through a private Buzz wrapper so managed Pi agents receive Buzz's base prompt plus the configured thread/channel session model via `--system-prompt` - load the Buzz workspace skill directory with Pi's repeatable `--skill` flag, making `~/.buzz/.agents/skills/buzz-cli` available alongside Pi's normal global skill discovery - report Pi and `pi-acp` installation states separately so setup guidance points to the missing component `pi-acp` does not currently consume the ACP `session/new` system prompt, but its normal new-session and restore paths do honor `PI_ACP_PI_COMMAND` as the executable used to launch Pi. Buzz reserves that variable and sets it to a private generated launcher. The launcher invokes `pi` from Buzz's effective `PATH`, adds `--system-prompt <file>` and `--skill <workspace>/.agents/skills`, and forwards `pi-acp`'s RPC and session arguments unchanged. This keeps the integration entirely in `block/buzz`, without changes to Pi or `pi-acp`. ### How PI_ACP_PI_COMMAND works in `pi-acp` `PI_ACP_PI_COMMAND` selects one executable; it does not accept arguments. Therefore: ```bash # Does not work PI_ACP_PI_COMMAND="pi --skill ~/.buzz/.agents/skills" ``` Buzz does not expose this variable as user configuration. It creates a private launcher and sets `PI_ACP_PI_COMMAND` only on the `pi-acp` child. Conceptually, that launcher executes: ```sh #!/bin/sh exec pi \ --system-prompt "$PRIVATE_SYSTEM_PROMPT" \ --skill "$HOME/.buzz/.agents/skills" \ "$@" ``` ### Related issue Follow-up to #7208. No duplicate issue or open PR found. ### Testing - `just ci` - `cargo test -p buzz-acp pi_launcher` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml managed_agents::env_vars::tests` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml managed_agents::discovery::presets::tests` - pre-push branch checks No screenshots: the UI change is state-dependent setup guidance only. --- **Update Sep 4, 13:35:** Made `PI_ACP_PI_COMMAND` entirely Buzz-owned. - Managed agent configuration now rejects the variable as a user override. - Buzz always launches `pi` from its effective `PATH` and rejects inherited values before creating its private launcher. - Removed the custom Pi executable discovery path and its internal environment alias. --- **Update Sep 4, 14:03:** Reject inherited `PI_ACP_PI_COMMAND` values instead of replacing them. - Pi startup now fails with an actionable message telling the user to unset the variable. - Removed the Pi-specific exception from the generic ACP environment injection path. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
4afef8649a |
fix(desktop): restore mention chip identity icons (#7338)
## Summary Restore the missing **@ glyph for people and robot icon for agents** after #7133, and incorporate the requested compact public-key display. - Wrapping mention chips render their existing bounded icon-bearing leading fragment. - Readonly chips show bound keys using the same `8 leading…4 trailing` formatter as the channel member list: `Scout (150b20bd…15dc)`. - Full literal labels and exact keys remain authoritative in metadata, profile targets, title/accessible-name attributes, editor text, saved bodies and recipient tags. Display abbreviations are never used for recipient lookup. - Copy/paste restores the full literal label for a complete compact chip; partial selections remain plain text. Two keys sharing the same abbreviation still round-trip to their separate exact recipients. - No recipient-resolution, authorization, wire-format, composer, or CSS changes. Existing labels, icons, cloud markers and ordinary mentions remain intact. ## Verification Published candidate `9365ab9bc9d9c5d10802580cd576f8e378ff492f`, based on main `e09f715c9d0ee2cb7bf8a39061e601f3a502f588`: - **6,444 desktop unit tests passed** on this candidate's final source tree. - **44 mock-Chromium tests passed, zero retries in the final run** across mention recipients, clipboard and cloud provenance: exact recipient selection, ambiguity rejection, send/edit/reopen, forwarding, full/partial copy, mismatched-key rejection, matching-abbreviation collisions and 100%/150% narrow-window geometry. - TypeScript, desktop Biome/check guards, protected-feature production build and E2E build passed. - The compact-renderer regression fails with the formatting call removed. Original missing-icon and hidden-text accessibility regressions have red/green evidence. - Fresh self-review traced rendering, full-key metadata, copy classifier, paste normalization and identity trust. The same display formatter owns the accepted compact form on both clipboard sides. Iteration exposed an existing team-insertion separator flake (passed final full run) and two new fixture assumptions: non-member sends require invitation, and Chromium rich paste may retain an NBSP separator. Tests now exercise invitation and normalize only that separator when comparing the captured full body and exact tags; no product change was needed for either. Earlier local repository-wide `just ci` completed in two invocations because its initial call hit the ten-minute tool limit during Tauri compilation. Unchanged native/mobile/backend evidence is reused; the desktop delta received the full checks above and new remote CI. Native VoiceOver, real Tauri selection, dark theme and non-Chromium observation were not performed. Browser artifacts exercise real frontend with mocked Tauri/relay boundaries, not an installed release. ## Review and visual evidence Current-head CI and automated review must complete after this update; the old `2997bfb5` green results do not establish this new head. Required human review remains separate from agent approvals. No merge/install/restart authorization. Before/after icon evidence: https://github.com/block/buzz/pull/7338#issuecomment-5544575866 Updated compact-key screenshots are posted below. The editor intentionally retains the full literal address; only readonly chip display is abbreviated. Origin: buzz://message?channel=3355d33a-b72a-423a-b064-a58275f9a8af&id=38b3a27e689f5a9604e273d45f4e3122fceba76081e7bcd3bbdbf439524a5a18 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
5d107836c6 |
Persist video playback speed preference (#7336)
## Summary - persist the selected desktop video playback speed as a device-level preference - apply the shared preference to inline and review video players, including after a reload - cover persistence, validation, cross-window updates, and the end-to-end playback flow ## Verification - `node --test desktop/src/shared/lib/videoPlaybackSpeedPreference.test.mjs` - `cd desktop && pnpm exec tsc --noEmit` - `cd desktop && pnpm check:px-text` - `cd desktop && pnpm test` - `cd desktop && pnpm build:e2e && pnpm exec playwright test --project=smoke video-attachment.spec.ts` - pre-push: desktop check, file-size check, typecheck, and 6,416 desktop tests --------- Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> |
||
|
|
e09f715c9d |
Verify ACP relay events before prompt routing (#7010)
## What changed Verify every Nostr EVENT received by buzz-acp immediately after deserialization. Events with an invalid NIP-01 ID or Schnorr signature are dropped before subscription routing, deduplication, replay-watermark updates, membership handling, or either harness queue. ## Safety Signature verification runs on the blocking pool so cryptographic work does not block the relay task. A verification failure drops only that event and keeps the connection available for subsequent valid traffic. The existing observer-control verification remains as defense in depth. Regression tests cover valid delivery; changed content, ID, signature, pubkey, tags, and timestamp; a forged owner shutdown command with a recomputed ID; forged membership notifications; and forged observer-control events. ## Testing cargo test -p buzz-acp cargo clippy -p buzz-acp --all-targets -- -D warnings just ci --------- Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
b17c0776b7 |
fix(buzz-acp): bound busy-owner hold to prevent cross-channel starvation (#7337)
## Problem #6732 added a busy-owner hold to the ACP harness: when a scope's recorded session owner (`session_owners`) is checked out on **any** turn, `dispatch_pending` holds the scope's batch instead of dispatching it. The hold was added to keep one provider session per thread — but it is unconditional: it applies to `Conversation` scopes too, and it has no time bound. Under the default `session_policy=channel`, every channel collapses to a single `Conversation` scope, so once two channels' sessions land on the same worker (pass 2 of `try_claim` picks the first idle worker by index, so this happens quickly after any restart), channel A's mention starves behind channel B's in-flight turn — for up to the full `max_turn_duration` (7200s by default) — while other workers sit idle. The only signal is a DEBUG-level log, and the 👀 seen-reaction is added at queue admission *before* the hold decision, so the user sees the agent acknowledge the mention and then nothing. Observed in production on the first day of the v0.5.22 rollout: three separate incidents where a mention got 👀 but no turn started until an unrelated channel's turn ended on the shared worker (in the worst case the blocking turn sat in a single tool call for 6+ minutes). ## Fix One new seam, `AgentPool::hold_decision`, replaces the raw `should_hold_for_busy_owner` check in `dispatch_pending` (the predicate itself is unchanged and remains the inner check): - **`Conversation` scopes never hold.** Channel-policy channels and all DMs dispatch immediately; a busy owner means forking onto an idle worker, exactly the pre-#6732 behavior. This removes the cross-channel head-of-line blocking entirely for the default policy. - **`Thread` scopes hold for a bounded window.** `HOLD_BUSY_OWNER_TIMEOUT` (10s) is measured from the first time the batch is held (`held_since` stamp); once elapsed, the batch stops holding and forks a fresh session on an idle worker, rebuilding thread context from the relay. This preserves #6732's session-continuity intent for the momentary-busy case while capping the worst-case wait. No new timer is needed: held batches are requeued with preserved timestamps and re-evaluated on every dispatch trigger (turn end, relay event, 30s maintenance tick), so the effective worst-case re-check gap on a fully silent system is one maintenance tick. - **Holds are observable.** Holding logs at INFO and a hold expiry logs at WARN (previously DEBUG-only), and both emit observer-feed events (`busy_owner_hold`, `busy_owner_hold_forked`) with the scope, owner index, and held duration. `held_since` is derived state and is cleared on every removal path: dispatch/fork (inside `hold_decision`), `invalidate_channel_sessions`, `invalidate_scope_session`, and `switch_idle_agent_model`. ## Accepted trade-offs - A fork after an expired hold leaves the old owner's now-orphaned thread session in its session map until natural rotation/invalidation — benign, and identical to pre-#6732 fork semantics (`loadSession: false`; sessions are worker-pinned, so migration is not an option). - Under sustained pool exhaustion the hold stamp is cleared on the fork attempt and re-stamped next cycle, so the bound is effectively "timeout after a worker frees up," not absolute wall clock. ## Tests - New table test `hold_decision_covers_variant_session_busy_and_timeout` over the full input space (scope variant × idle-session presence × owner busyness × elapsed vs. window). The `Conversation` + busy-owner row is the cross-channel regression guard; the past-window row guards the bound. Both were mutation-checked: removing the variant gate or the timeout branch fails the suite. - `busy_session_owner_holds_batch_instead_of_forking_session` extended with the Hold → ForkAfterHold transition, the `Conversation` dispatch guard, and `held_since` pruning on channel invalidation. - Scope-invalidation and idle-model-switch tests extended to cover `held_since` cleanup alongside the existing `session_owners` assertions. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
e7e29937a1 |
feat(desktop): invite owned agents from standalone forums (#7125)
## Summary In Desktop's standalone Forums, selecting an owned agent from another device could leave a post or reply unsendable if the agent had not joined the forum. This adds **Invite / Cancel** to the send flow so you can resolve membership without leaving your draft. - **Invite** checks response policy and your permission to add members, adds the agent to the forum, waits for refreshed membership, then rechecks authorization before posting to the original destination. Membership is forum-wide, not limited to one post. - **Cancel / Escape** keeps the text, attachments and selected recipients for retry. Unlike chat's **Do nothing / Send anyway**, this dialog has no reference-only send choice. Invite is disabled while pending; Cancel remains available. - Leaving the source post/reply cancels its pending invitation, even if you return. Errors remain visible, focus returns to the initiating editor when appropriate, and late completion cannot resume a cancelled post or interfere with a newer attempt. - A rejected send restores text, uploaded media and exact selected recipients to the source draft only if no newer edit, deletion, upload intent or send supersedes it. Clipboard verification settles before recipient capture, with stale edits/navigation fenced out. ### Related issue Targets `main` after #7124 merged. This PR reuses its publication checks and draft protection; the five forum commits have been replayed unchanged onto the merged parent. Owned-agent discovery (#7122) is already merged. Split from #7114. Forum creation/templates, channel-less Notes and local-agent management are unchanged. Duplicate-name binding from #7133 is already merged and retained by this stack. Inviting does not start a remote agent or promise that it is online or will reply. ### Testing Forum composer lifecycle tests, including clipboard-settlement cases, and TypeScript/changed-file formatting checks passed after the restack. Earlier invitation, focus and transport-recovery browser coverage is retained, not claimed as a fresh full browser run on this head. See [live CI](https://github.com/block/buzz/pull/7125/checks) for current-head results. Browser evidence uses mock IPC; no full local `just ci` pass or native/live-relay validation is claimed. To try it: open a forum post or reply, select an owned nonmember agent and send. Cancel, then retry without reselecting; Invite should add that agent before posting. Deny the add to check the visible error and retained draft. Navigate away/back during a pending invitation or rejected send; no stale publication or overwrite of a newer draft should occur.  *Earlier mock-browser capture, not current-head runtime proof. [Error and successful-post captures](https://github.com/block/buzz/pull/7125#issuecomment-5513883462); no before-state/native capture available.* **Limits:** cancellation cannot undo accepted membership changes or dispatched posts; authorization and publication are not atomic. Recovery is same-window, subject to browser storage limits, and is not a durable in-flight send journal: reload/crash can lose a pending snapshot. Cross-window coordination and in-flight upload custody are unchanged. The parent's legacy member-agent compatibility does not establish ownership for nonmember invitations. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
d595806fc3 |
fix(desktop): authorize remote mentions at publication (#7124)
🤖
## Summary
In Buzz Desktop, you could own an agent running on another device but be
unable to mention it in a channel where it had not yet joined: it was
filtered out before you could invite it. A selected agent could also
disappear from the message's recipients when permissions changed. This
lets you select an eligible agent in the existing **@ menu**, invite it
from the message composer, and send to that agent—or see an error and
keep your draft rather than silently sending without it.
#### Where the experience changes
| Screen / control | Before → after |
| --- | --- |
| A channel's **Message #…** composer, or a message's **Reply in thread
to …** composer | Type `@` (or use the existing @ button), choose your
agent, write the message and press **Send message**. An owned agent not
yet in the channel can now reach the existing **“Mention people outside
this channel?”** dialog when its response settings allow you to address
it. |
| That dialog's **Invite** button | Previously the membership
requirement could block the agent before the invitation. Now Invite
checks permission to add it, adds it as an agent member of the
**channel** (not just the thread), then rechecks membership and response
permission before sending the waiting message. An agent already in the
channel needs no invitation. |
| Existing direct message, or the new-message screen with the **To:**
field | A mention is checked against the conversation the message will
actually enter, including a newly created direct message, rather than
the old or not-yet-created destination. This does not add an Invite
control to direct messages. |
| Editing a message / sending attachments | The selected agent remains
part of the send or edit attempt through attachment upload and the final
permission check. Lost permission produces a visible error instead of
dropping that recipient. |
**Invite is not the only chat choice.** The existing **Do nothing**
button sends the message *without inviting or notifying the nonmembers*;
their names remain references in the text. Where you cannot invite, that
choice is labelled **Send anyway**. To abandon the send instead, dismiss
the dialog with Escape. Invitation actions are disabled while
preparation is pending, preventing duplicate clicks.
**Leaving and returning must not resurrect a cancelled send.** Switching
threads or leaving the composer cancels its pending invitation, even if
you return to the same thread. Cancellation before dispatch sends no
message; an accepted membership change cannot be automatically undone.
An ordinary send without a pending invitation remains bound to its
original destination rather than following you into another
conversation.
**Failed sends must not overwrite your next draft.** If you leave a
thread, return and replace or deliberately clear its draft while an
older send is pending, the older failure cannot restore deleted text,
recipients or files; success cannot erase the newer draft—even if its
text is identical. An untouched draft cleared automatically for sending
remains recoverable on failure. This protection also covers reopening
the composer and starting a newer send.
The channel timeline also keeps its existing **new-messages / Jump to
latest** button available when newer messages are waiting to be
displayed. For example, after sharing a reply to the channel and closing
the thread panel, you can click the catch-up button to reveal buffered
messages. Closing the thread does **not** guarantee the shared row
appears automatically or force you away from reading history.
### Related issue
Built on [#7122](https://github.com/block/buzz/pull/7122), base branch
`split/owned-agent-discovery`, which lets Desktop find and verify owned
agents independently of this device. Current integration head:
`1144465d00273cf74b7c22544ae5a3299bd98560`, built on exact published
root `3a56d17824522580fe04cae463b54f4c7ba66021`. Root #7122 has its own
CI and security gates; this PR must not land ahead of that dependency.
Finding an agent is not channel membership, online status or a promise
of a reply. This PR changes what the existing message controls can do
with those agents; it adds no profile, presence, cloud marker or remote
start/stop UI.
Standalone forum post/reply **Invite / Cancel** is added separately in
[#7125](https://github.com/block/buzz/pull/7125); here those composers
only gain visible authorization errors. Same-name selection/binding
fixes ([#7133](https://github.com/block/buzz/pull/7133)) and mention
spacing ([#7128](https://github.com/block/buzz/pull/7128)) are not
included.
Extracted from [#7114](https://github.com/block/buzz/pull/7114)
(historical source `98fe33ec`). [Behavior and draft-recovery
contract](
|
||
|
|
ce9decb235 |
fix(acp): rename system tag to agent-instructions (#7332)
## Summary Rename the paired persona prompt boundary from `<system>` to `<agent-instructions>` in both modern and legacy ACP delivery paths. Update Desktop diagnostics to label the new boundary as Agent Instructions while keeping archived `<system>` captures readable. `system` is confusing (esp with system role) and agent instructions is what shows up in the UI today <img width="767" height="298" alt="Screenshot 2026-09-04 at 11 37 54 AM" src="https://github.com/user-attachments/assets/4e34ac06-db50-461e-a555-ce17885b0031" /> ### Related issue Follow-up to #6701. No duplicate open issue or PR found. ### Testing - `cargo test -p buzz-acp` - `cargo clippy -p buzz-acp --all-targets -- -D warnings` - `node --import ./desktop/test-loader.mjs --experimental-strip-types --test desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs desktop/src/features/agents/ui/agentSessionTranscript.test.mjs` - `pnpm --dir desktop typecheck` - Pre-push hook: 6,237 Desktop tests, 3,164 Tauri tests, and all 13 Rust unit-test lanes passed Generated with Codex Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
ee883d73fb |
fix(desktop): bind duplicate mention selections to exact recipients (#7133)
## Summary Selecting two people or agents named Scout could replace the first recipient with the second even though the message still looked right. This binds each selection to its exact identity: the first keeps `@Scout`, and a conflicting selection gets `@Scout (<full public key>)`. Removing one no longer removes or redirects the other. - Reuse the existing **@ suggestion list**; team selection and automatic agent addressing reserve and reuse distinct labels too. Typing an ambiguous name manually shows an instruction to use the picker and preserves the draft without publishing—in chat, edits and standalone forums. - Share literal mention matching across recipient extraction, removal, display and editing so a shorter name cannot claim another recipient's longer or qualified label. - Rebuild authored references when a message is edited, and use the latest authorized snapshot when reopening or forwarding. Rendering/editing can recover qualified identities only from the message's recorded references, not from a key typed into its body. Unresolvable historical names remain literal rather than guessed recipients. - Wrap full-key labels within narrow/zoomed layouts while keeping their complete accessible label and ordinary mention icons. Edit activation waits for the action menu's focus cleanup before focusing the editor. ### Related issue Targets `main`; mention spacing (#7128) is already merged. Split from #7114. This is independent of the #7124 → #7125 remote-invitation stack and does not expand agent eligibility or invitation permissions. The separate Enter-selection suffix issue remains tracked in #7253. ### Testing The desktop unit suite and focused mock-Chromium checks passed on the published integration candidate, including pending-paste selection, edit/forwarding, copy and narrow-layout cases; formatting, types and frontend builds passed. On `0b3b18c0`, 64 focused trust/paste/selection unit tests and six mock-Chromium tests passed with zero browser retries, including actual timeline chip copy → fresh channel paste → send and mismatched-key rejection; TypeScript, changed-file Biome and an isolated E2E build passed. The broader browser run had copy failures before the focused repairs and is not claimed as wholly green. See [live CI](https://github.com/block/buzz/pull/7133/checks) for current-head results. No full local `just ci` pass, native/live-relay or cross-browser validation is claimed. To try it: select two same-name recipients, remove one, send, then edit/reopen and forward; only the intended identities should remain. Type an ambiguous name without choosing a suggestion and check that sending retains the draft with an error. Inspect full-key labels in a narrow window at 150% text size, and open Edit and type immediately.   *Earlier mock-browser captures, not current-head runtime proof. No new before-state capture; screenshots alone do not prove recipient delivery.* **Clipboard trust:** generated full-key-qualified mentions now retain their exact recipient after copy/paste when the full key matches the clipboard record and community directory/profile state independently vouches for the base alias, including numeric collision suffixes. A qualifier alone does not establish trust. Arbitrary historical labels still cannot always be reconstructed. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Bad Janet <150b20bdf6130418df9239dd1bd082c71612c8d653b47c277200365b9be215dc@buzz> |
||
|
|
cd02b693aa |
refactor(relay): extract NIP-29 membership authorization (#7285)
## Why NIP-29 membership authorization for kinds 9000, 9001, and 9022 was embedded in a large async handler, requiring Postgres and Redis to test and repeating the last-owner rule across five call sites. This addresses [TheSentinel454/buzz#24](https://github.com/TheSentinel454/buzz/issues/24) without adding the broader state/repository abstraction suggested there because HTTP and WebSocket writes already share ingestion. ## What - Extract pure, typed membership authorization decisions while leaving database reads and mutations in `validate_admin_event`. - Preserve existing client-visible errors and independent database last-owner safeguards. - Collapse five last-owner policy restatements into one predicate and share the identical self-departure policy. - Include the relay decision modules in `just test-unit`, so these tests execute in CI. ## Risk Assessment Medium-low. This touches production relay authorization, but intentionally preserves wire behavior and database defense in depth; exhaustive decision tables and relay-backed tests cover the affected paths. ## Simplification This removes repeated policy from the orchestration path and makes the rule set directly testable without introducing a repository trait or second transport path. ## Verification Verified at `15255a090797f85874921120003c645962fefaed`: - `just test-unit` — all 10 package summaries passed. An initial run hit two unrelated timing-sensitive `buzz-acp` failures; the complete retry passed 905/905 in that package. - `cargo fmt --all -- --check` - `cargo clippy --workspace --all-targets -- -D warnings` - `just file-size-check` — 10/10 policy tests passed. - `just security-review-check` — 13/13 tests passed. - Push preflight — `push-head-scope`, `branch-skew`, `file-size-check`, `rust-tests`, and `desktop-tauri-checks` passed. - Blox Postgres relay lane — 84/84 at the byte-identical source patch; full live `e2e_relay` behavior matched the clean base (45 pass and the same pre-existing kind:9002 failure on each). Generated with Claude Code --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
01bacb8df3 |
chore(release): release Buzz Desktop version 0.5.22 (#7308)
## Buzz Desktop release v0.5.22 - **Frozen main:** `75f101d8b4f5b4b26f9891b73b87c67fedac25a0` - **Reviewed candidate:** `9ceb1f79bbc21785a0a075c40aecb3c058b1ea15` - **Previous desktop release:** `desktop-v0.5.20` - **Proposed immutable tag:** `desktop-v0.5.22` 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 <pfleger.will@gmail.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
b1f6b7ef77 |
feat(desktop): preserve mentions across copy and paste (#7228)
Copying a message out of the timeline lost the mention. The rendered chip drops the `@` for display, so the clipboard carried "John Smith" — two ordinary words no composer could bind back to a pubkey. Pasting into another channel produced dead text, and sending it tagged nobody. ## What changed Every Buzz copy now writes two clipboard flavors in one transaction: - **`text/plain`** — readable anywhere, sigils restored, no pubkeys. This is what TextEdit, Slack, and every other external app receive. - **`text/html`** — the same content with each mention wrapped in a span carrying `data-mention-pubkey` / `-label` / `-kind`. On paste, the composer harvests those records, registers each `name → pubkey` with the existing mention machinery, and inserts the content: the chip re-lights and the send path recovers the identity the author tagged. A marker attribute records what the plain flavor holds, so a Markdown copy pastes through the text pipeline and a rendered copy through the HTML one. **Covered surfaces:** timeline selection copy, thread-panel selection copy, forum post/reply selection copy, "Copy message", and composer copy/cut — plus paste in both the channel and forum composers. ## Trust boundary Clipboard HTML is untrusted, and the branch treats it that way: - Records are capped (50), labels bounded (200 chars), and a pubkey must be 64 hex before it can become a `p` tag. - **Only mentions the paste actually shows are registered.** An empty `<span data-mention-pubkey=… -label="Jane Doe">` would otherwise rebind that display name for the rest of the composer session, so a later hand-written @Jane Doe would chip-light convincingly against the attacker's key. Each branch registers only the records whose label appears in the text *it* inserts, matched with `getMentionOffsets` — the same matcher the send-time extractor uses. - **The visibility gate reads only what ProseMirror will insert.** `DOMParser` hard-drops `script`, `style`, `title`, `noscript`, `object`, and `head` content, so `visible<style>@Jane Doe</style>` beside an empty chip span used to smuggle a binding past the gate. Those elements are stripped before either output is derived. - **A partial chip never gains a sigil.** A selection crossing a chip boundary falls through to the browser's default copy, which serializes the full identity attributes around a slice of the text — pasting that invented "@Smith" out of "John Smith". Paste now leaves a fragment as plain text, tolerating only what a whole chip picks up in transit (restored sigil, author casing, U+00A0 swaps, and the label cap's own ellipsis). Both clipboard sides share one `matchChipTextToLabel` helper, in the module that owns the label attributes, so copy and paste cannot drift on what counts as a whole chip. ## Notes - Mention matching reuses `getMentionOffsets`, so code spans and fences are excluded and the longest display name wins. - The plain flavor inlines chip boxes before reading `innerText`; a chip is a flex container, so the browser's own copy split "@John Smith" onto its own line. - `MarkdownMention` and `MacEmacsTextShortcuts` are extracted verbatim from `markdown.tsx` and `useRichTextEditor.ts` to keep both files under the size gate. ## Testing - ~40 unit tests over the flavor builder/parser, the visibility filter, the ignored-tag sweep, and the chip-match verdicts. - A Playwright spec (`desktop/tests/e2e/mention-clipboard.spec.ts`) driving real copy/cut/paste DOM events: timeline and "Copy message" of a multi-word non-member mention pasted into another channel and sent with the original pubkey in its `p` tag; the forum copy → forum reply round trip; composer copy/cut; plain flavor asserted to contain no 64-hex string; the boundary-crossing drag; and the hidden-record and `<style>` smuggling vectors. - Every regression test is bound to a production seam and fails with its guard removed. - `just ci` / pre-push lanes green (5927 desktop unit tests, typecheck, lint, file-size gate). --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b0744d7f3 | test(desktop): await Bestie drag and profile hover endpoints (#7294) | ||
|
|
2ac0aa1dd1 | Collapse contiguous join messages (#7262) | ||
|
|
75f101d8b4 |
chore(release): release Buzz Desktop version 0.5.21 (#7301)
## Buzz Desktop release v0.5.21 - **Frozen main:** `88687876f7808a2fd742b7eb2e4b9f87d999ad8d` - **Reviewed candidate:** `47e4e0347fb4142df55b03e0e3dc537fe33a5d62` - **Previous desktop release:** `desktop-v0.5.20` - **Proposed immutable tag:** `desktop-v0.5.21` 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 <pfleger.will@gmail.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
8e7b275cb0 |
fix(scripts): copy global-agent-config.json in buzz-adopt-prod-agents (#7303)
## What Extend `scripts/buzz-adopt-prod-agents.sh` to copy `agents/global-agent-config.json` from the prod app-data store to dev as part of the existing atomic bundle commit, and document what the script cannot copy (baked build-time defaults). ## Why `global-agent-config.json` holds the owner's global agent defaults (`env_vars`, `provider`, `model`, `preferred_runtime`). Previously the script carried the dev copy across verbatim — it was absent from the bundle drop list and not in `RECORD_FILES`. After a `just reset`, the dev build inherited stale global defaults (or none at all) instead of mirroring prod, inconsistent with how all other bundle files are handled. The "WHAT THIS DOES NOT DO" header lacked any mention of baked build-time defaults, which are a separate (and common) source of prod/dev parity confusion. ## Changes - **`RECORD_FILES`** — add `global-agent-config.json`. The file has no volatile fields (`GlobalAgentConfig` struct = `env_vars`/`provider`/`model`/`preferred_runtime`), so it joins the verbatim-copy path already used by `teams.json` — no new transform. - **Stage-seed block** — add `global-agent-config.json` to the `rm -rf` drop list so the prod overlay replaces it cleanly; remove its stale mention in the "carried verbatim" comment. - **Docs/comments** — update "4-file bundle" → "5-file bundle" in the DMG concurrent-run warning and the step 2 comment; list the file explicitly in the step 2 header comment. - **"WHAT THIS DOES NOT DO" header** — add a bullet explaining that baked build-time agent defaults (`BUZZ_BUILD_BUZZ_AGENT_PROVIDER` / `BUZZ_BUILD_AGENT_ENV` via `option_env!` in `build.rs`) are compiled into the binary, not stored in app-data. Includes the correct `just production` invocation pattern and a `.env` warning. - **Preflight symlink checks and dry-run output** iterate `RECORD_FILES` and pick up the new entry automatically — no other changes needed. Verified: `bash -n` clean, `shellcheck` clean, `--dry-run --force` shows `[dry-run] stage global-agent-config.json (0600)` in the expected position. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
88687876f7 |
feat(relay): add early startup lifecycle logs (#7258)
## Why Early relay failures can currently appear as a container restart without a trustworthy in-process account of whether crypto, structured logging, configuration, relay identity, or the metrics listener failed. Most of those steps happen before the Prometheus exporter exists, so their chronology belongs in logs rather than metrics. Implements the logs-only early-startup slice of #7238. Post-bind Prometheus exporter supervision is tracked separately in #7284. ## What changed - create a process lifecycle recorder before the Tokio runtime and emit a fixed, versioned JSON schema directly to stderr; - record started and exactly one terminal event for `crypto_init`, `tracing_init`, `config_load`, `key_load`, `metrics_bind`, and the aggregate `process_telemetry` phase; - keep every status and reason bounded and suppress raw errors that could contain credentials, keys, URLs, or other secrets; - return typed metrics-install errors so `metrics_bind` can be classified without logging raw values, while preserving the existing public `metrics::install` API; - document the logs-only evidence contract and add real child-process regressions for success and failure paths. This PR adds **no startup metric families** and no dashboard contract. Existing application metrics remain unchanged. ## Verification Exact head: `8faf7526822a119efa035e58b2b3c59aa67fc81d` - `cargo fmt --all -- --check` - `cargo clippy -p buzz-relay --all-targets -- -D warnings` - relay binary target: 13 passed, 1 PostgreSQL-only test ignored - real relay child-process lifecycle target: 9 passed - full relay package library target: 1,023 passed, 89 ignored; the same six media tests failed at `crates/buzz-relay/src/api/media.rs:1145` with `Sqlx(PoolTimedOut)` because local PostgreSQL is unavailable - three independent exact-head reviews found no correctness, security, compatibility, lifecycle-accounting, logs-only-scope, or test-adequacy finding All exact-head GitHub CI gates are green, including lint, unit tests, PostgreSQL, relay/backend/desktop integration, both Linux server cross-compiles, Windows/macOS builds, and security checks. ## Staging verification - exact multi-architecture image: `dev-sha-8faf7526822a119efa035e58b2b3c59aa67fc81d-run-33708188952-1` - immutable manifest: `sha256:26cad28266a6bb0b0e7081eb6091d374e5489f8bb78c475a4a65737dee86cc67` - image workflow: https://github.com/block/buzz/actions/runs/33708188952 - focused staging deployment: https://github.com/squareup/builderbot-platform-core-infrastructure/pull/314 - replacement ReplicaSet `buzz-d68764bc7` has two Ready pods with zero restarts - Datadog received one complete, contiguous sequence 1-12 from each pod; both end with `process_telemetry/terminal/succeeded` at 3 ms - queries scoped to the replacement ReplicaSet return no data for the removed `buzz_startup_phase_terminal` or `buzz_startup_phase_duration_seconds` families The experimental Row 7 was removed from the Buzz Startup & Rollout Safety dashboard. This logs-only PR deliberately adds no replacement dashboard row. --- **Update Sep 3, 12:26 ET:** Clarified the review boundary: this PR does not close the broader #7238. Later exporter-task termination is pre-existing runtime behavior and is now explicitly tracked in #7284; no production code or staged image changed in this update. Generated with Codex Signed-off-by: Ravneet Arora <rarora@squareup.com> |
||
|
|
752cbfc037 |
docs(nip-fi): add Blossom kind-24242 media possession-proof exception (#7278)
Adds the media/Blossom possession-proof exception section to
\`docs/nips/NIP-FI.md\`.
## What this changes
Encodes the condition set confirmed by Thufir's security review
(2026-09-03) as normative MUSTs in NIP-FI. Kind-24242 Blossom auth
events are admitted as the NIP-FI pairing possession proof for **media
routes only** — a named, bounded exception with an explicit precedent
fence against expansion.
### Scope fence
Kind-24242 proofs are valid only on:
| Proof type (`t` tag) | Valid route | Method |
|---|---|---|
| `upload` | `PUT /upload` (and temporary alias `PUT /media/upload`
until removed) | PUT |
| `get` | `GET /media/{hash…}`, `HEAD /media/{hash…}` | GET, HEAD |
All other protected routes MUST reject kind-24242 proofs.
### Upload proofs
Exactly one `x` tag over consumed body bytes; temporal check precedes
body consumption.
### Read proofs (the relaxation Will approved)
Host-wide MAY: no `x` required. Exactly one `server` tag matching the
resolved tenant host is a MUST. Optional `x` must match parent hash if
present.
Named residual (verbatim in spec): within at most 60 seconds from
minting (plus 5s future-skew), a captured full header set allows reading
any media blob on exactly one tenant host — read-only,
membership-checked, revocable, not state-changing, not cross-tenant.
### Freshness (Thufir option 2)
- `created_at <= now + 5s` (bounded future skew)
- `now - created_at <= 60s`
- Exactly one `expiration`, valid at admission, satisfying `expiration
<= created_at + 60s`
### Transport/cardinality
Exactly one each of `Authorization` (Nostr scheme), `t`, `expiration`,
`server`; `x` at most once; reject any duplicate, malformed, or
conflicting instance.
### Per-request pairing
Full assertion verification, exact key equality between assertion
`nostr_pubkey` and kind-24242 signer, deny-map enforcement on every
request. Stub gap named.
### Compliance note
PR #7264 implementation is explicitly non-compliant until the bounded
hardening task lands (named gaps: multi-tag acceptance, 3600s window,
optional `server`).
## Behavioral oracle
`FI-TRACE-HTTP-INGRESS` extended to cover kind-24242 admission and
denial cases.
## Scope
Docs-only. No code changes. The code hardening is a separate follow-on
task.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
6cf514ed90 |
fix(desktop): wrap message tables within the available pane (#7279)
## Summary - Let Markdown tables use the available message width instead of their maximum-content width, inheriting the renderer's existing `wrap-anywhere` handling for long tokens and links. - Top-align header cells as well as body cells. Give cells a `min-w-24` readability floor so short labels do not collapse into one-letter columns; retain the existing table-local scrollbar when many columns genuinely cannot fit. - Preserve semantic table markup, links, inline code, and surrounding message layout. Add three browser regressions through the real message renderer (channel, narrow thread, and many-column overflow). ### Related issue Fixes #5313. No matching open table-readability PR found. Owner-authorized task channel: `819b36d5-7371-4ed0-bf9b-d461d723779e` Source: buzz://message?channel=819b36d5-7371-4ed0-bf9b-d461d723779e&id=3bf2d1bd5df28fcb481e71159dc1be2b3888918b05f67e270354fbfea65a8c90 ### Testing Original production candidate: `4427f2e6f9ef85a01a267fe9eabd1cf13cacfb78`; base: `7a9a5233d9d755e715be0c585cf7850e935d28cf`. - `pnpm -C desktop check` — passed. - `pnpm -C desktop test` — 6,110 passed, none skipped. - `pnpm -C desktop typecheck` — passed. - `BUZZ_PROTECTED_BUILD_OUTPUT=<isolated-production-directory> pnpm -C desktop build` — passed. - `pnpm -C desktop build:e2e --outDir <isolated-candidate-directory>` — passed. - `just file-size-check` and `git diff --check` — passed. - Chromium / mock Tauri bridge: all 3 new Playwright tests passed against the fixed candidate build. Both wrapping tests fail on the unchanged base build, while the overflow-fallback test passes. Each browser run used a unique output directory and a non-reused local server pinned to its build directory. - At 883px channel message width: table scroll width **1,395 → 883px**. At 292px thread message width: **1,395 → 292px**. All cells top-aligned; long tokens/URLs wrap, link destinations and code text remain intact, short labels stay on one line, and document width remains 1,280px. - Focused fresh-frame review checked actual before/after screenshots and the complete diff. It caught one-letter label wrapping in an early width-only candidate; the final cell-width floor and regression assertion address that finding. Limitations: native Tauri/WebKit and relay-backed integration were not exercised for this CSS-only delta. Repository-wide `just ci` was attempted but exceeded the local command budget during unrelated Rust compilation; it is **not** reported green. The full affected TypeScript package gates above passed on the exact candidate. Production/E2E builds retain existing chunk-size and mixed static/dynamic import warnings. Before/after screenshots are posted below using the repository's screenshot publication script. No merge, production installation, or runtime restart is requested. ### CI-driven test-only follow-up Current head: `925d964b6cf31ba704d74baf73769110774b2789`. Smoke shard 3 exposed an older test in `messaging.spec.ts:457` that still required three columns of ordinary prose to overflow horizontally ([failure log](https://github.com/block/buzz/actions/runs/33771325404/job/100702242706)). This expectation contradicts the intended wrapping change. Updated its name and assertion to require containment; the separate many-column local-scroll test is unchanged. No production code or harness settings changed in this follow-up. - Affected browser validation: **4/4 passed** (updated existing prose/narrow table case plus the three new channel/thread/overflow cases). - Reused the immutable `4427f2e6` E2E build on a fresh non-reused server; production source is unchanged at the current head. The package/build checks above remain evidence for that unchanged production tree, not a claim of rerunning the full suite at the new SHA. - Targeted Biome check, file-size gate, and `git diff --check` passed. - Current-head CI: https://github.com/block/buzz/actions/runs/33773490446 - Prior unrelated Bestie baseline failure and reproduction: https://github.com/block/buzz/pull/7279#issuecomment-5528077181 - Security authorization and review must target the **current** head, not the old screenshot/build SHA. A Block organization member must comment exactly `@buzz-security-review 925d964b6cf31ba704d74baf73769110774b2789`. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> |
||
|
|
7a9a5233d9 |
🤖 fix(desktop): harden smoke E2E tests against Bestie overlay and toast timing (#7270)
## What Two remaining E2E hardening fixes from the Desktop Smoke flake pattern introduced by `ac5a18697` (Bestie — added `VITE_BUZZ_BESTIE=1` to `.env.e2e` and mounted `BestieGlobalOverlay` globally). The toast/DM-retry fix landed independently in main via #7127 (`input.hover()` + bounded `toHaveCount` wait); that hunk is dropped from this PR. ## Fixes ### `agent-control-regressions.spec.ts:240` — Stop does not accept an unconfirmed or foreign-channel result **Cause:** Playwright 1.60.0's `page.clock.install()` fakes all timers including `requestAnimationFrame`. The test called it before opening the settings menu. With RAF frozen, the `DropdownMenuContent`'s `zoom-in-95 duration-150` CSS enter-animation never advances — Playwright's stability check observes a continuously-changing bounding box until the 30s test timeout. **Fix:** Re-sequence so the menu is opened on real time first. After `openAgentActivity`, open the trigger, assert visibility/enabled, call `waitForAnimations(page)` to settle the enter-animation (real `setTimeout`, no fake clock installed yet), then install the clock. The `fastForward(8_001)` correlation timeout still works because it's scheduled after the clock is active. Pointer actionability preserved — normal `stop.click()` (no `force`) fails with pointer-interception under a covering surface. ### `message-feedback-snapshots.spec.ts:97` — profile hover uses the channel hover surface **Cause:** `channel.hover()` triggers a CSS `transition-colors` animation. With the Bestie `LayoutGroup` mounted, `evaluate()` captures a mid-transition background value that never matches the profile card's settled token. **Fix:** `waitForAnimations(page)` after `channel.hover()` and before reading `channelHoverColor`. ## Evidence - Target specs pass: stop-turn 8/8, profile-hover passes. - Full `agent-control-regressions.spec.ts` (7 tests) green. - `just desktop-typecheck` clean at pushed head `0fbfe2a9f`. - No production code changed. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> |
||
|
|
c6ca9d94d9 |
Show status and huddle indicators beside names (#7112)
## Summary - show status and huddle emoji beside names in DMs and message rows - provide accessible tooltips, fallback status emoji, and profile-menu icon replacement - add the desktop status editor with preset durations, a ShadCN calendar, and a capped half-hour time menu ## Validation - desktop checks, typecheck, and file-size guard - 5,802 desktop tests - focused Playwright coverage (3 passed) - E2E build and native Builderlab staging verification Updated visual snapshots are attached in the PR comments. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> |
||
|
|
6f5ec4a731 |
Add mobile voice notes (#7121)
## Summary - add iOS and Android voice-note recording and preview directly in the mobile composer - add waveform playback, scrubbing, speed controls, haptics, and one-shot playback in chat - package recordings in a canonical H.264/AAC MP4 envelope on both platforms so existing relays accept them - preserve the shared composer interaction and attachment-card treatment across mobile platforms Mobile counterpart to #6978. ## Testing - `just ci` - `just mobile-check` - `just mobile-test` (2,026 tests) - Android debug build compiled, installed, launched, and Voice note verified in the attachment menu on Pixel 10 - signed iOS device build installed on iPhone --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> |
||
|
|
40220d5618 |
perf(desktop): publish mention sends before waking agents (#7154)
## Summary Cuts perceived agent-mention send latency by publishing the message first and waking the agent afterwards, instead of blocking the send on a synchronous agent start/deploy round-trip. A send that mentions a stopped or undeployed managed agent now shows the message immediately; the wake runs fire-and-forget after the relay accepts the publish. The already-running-agent send also gets faster via revalidation dedupe and NIP-11 caching. ## Changes ### Publish-first agent wake - Wakes for mentioned managed agents are collected during send preparation and flushed fire-and-forget only after `await send(...)` resolves. No start can fire — and no "your message was sent" toast can appear — for a message the relay never accepted; every abort path (cancel, readiness error, publish rejection, dismissed non-member prompt) simply drops the queue. Persona-create wakes ride the pending draft behind the non-member prompt for the same reason. - Each wake is bound to the tenant scope captured at send time: the new `useDetachedAgentStart` hook passes `expectedRelayUrl` + `expectedSignerPubkey` with every start, so a wake that outlives a community switch fails closed at the backend instead of spawning against the new tenant. A wake whose scope has not resolved yet (identity query still loading, blank stored relay URL) is refused with a recoverable toast rather than fired unscoped. - In-flight wakes are deduped through a module-level map keyed by `(relay URL, pubkey)` — the same tenant pair the backend keys on — so two quick sends or two composers cannot double-spawn a cold agent during the seconds-long start window. Entries are deliberately retained across community switches (the key *is* the tenant scope, so a retained entry can never affect another community, and clearing it let an A→B→A round trip deploy a provider agent twice) and self-clean when the start settles. - Wake-failure toasts are fenced to the community they fired in via a module-level scope mirror: a start that settles after a community switch logs instead of rendering community A's failure over community B's UI, and an A→B→A return re-delivers the warning where it is actionable. - Membership attach and access-policy writes stay synchronous, so the harness's first kind-39002 read still sees the channel. ### Replay floor - The send timestamp travels with the wake as `BUZZ_ACP_REPLAY_FLOOR`, threaded through both local spawns (`spawn_agent_child`) and provider deploys (`deploy_to_provider` injects it into `launch.policy_env`), so the harness's startup watermark replays back past the just-published triggering message no matter how long the spawn takes. `buzz-acp` clamps the floor to `[now − 15min, now]`. - The floor is captured at enqueue time, not flush time — the flush runs post-publish, so a flush-time stamp could exceed the message's `created_at` and skip the very message the floor exists to cover. - On local spawns the caller's floor is asserted *after* the user env layering (and the ambient parent-process value is stripped unconditionally), so a saved persona/global/agent env entry cannot shadow this send's floor — mirroring the shadow-strip the provider path applies to `launch.env`. Both halves share one `REPLAY_FLOOR_ENV_VAR` const. ### Send-path latency reductions (already-running agents) - Mention revalidation is deduped: the publish-boundary pass reuses the pre-side-effect authorization pass unless an awaited round-trip actually separated the two (background upload, link-preview settlement, DM expansion, a real access-policy/membership write, or active-huddle enrollment). This preserves the #5681 authorization boundary while making the common send single-pass. - NIP-11 `self` lookups are cached per relay URL for 5 minutes. Only verified values are cached — non-2xx and malformed responses stay retryable — and URL keying keeps community switches from serving another relay's identity. - `applyReusableAgentAccessPolicy` now reports its relay write explicitly (`{ agent, wrote }`) instead of signalling through object identity, so the revalidation trigger above is load-bearing by construction. ### File splits Four files crossed the repository file-size ratchet during this work; one cohesive unit was extracted from each rather than raising a ceiling — `runtime/setup_payload.rs`, `commands/agents_create_fields.rs`, `app_state_accessors.rs`, and `useEnsureAgentMentionsReady.ts`. The ratchet is green at the tip. ### Review follow-ups The three concrete findings from the first review round are fixed at the tip: the pre-publish wake and its false "your message was sent" toast (fixed by queueing wakes behind the publish), the stale cross-community failure toast (fixed by the scope-mirror fence), and the A→B→A duplicate provider deploy (fixed by retaining the tenant-keyed in-flight entries across switches). The fast-path admission-staleness point is answered in the review thread: deferred paths already re-validate at the publish boundary, and the remaining fast-path window is milliseconds against an irreducible network-transit race. Mid-branch send-perf instrumentation was added to attribute the residual spinner latency and reverted once that analysis concluded — it is net-zero in this diff. ### Deferred follow-ups Durable mention catch-up via `event_mentions` (option 2 step 3) and backend deploy-epoch coalescing for the wake paths that do not funnel through `useDetachedAgentStart` (Agents-panel Start, restore, inbound-persona deploys) are intentionally left for separate changes. ## Testing - `cargo test --lib` on desktop/src-tauri: 3054 passed; clippy `-D warnings` + fmt clean - Desktop unit tests: 5856 passed (the 5 failures are the pre-existing `inboxReopenNavigation` / `useRetainedProjectGitViews` baseline, present on origin/main); `tsc --noEmit` and biome clean - Full mentions (87), channels (89), and community-rail (25) Playwright smoke suites against `pnpm build:e2e` bundles, with 3× stress reruns of each new spec - The load-bearing regression specs were confirmed red on the pre-fix code: publish-failure → zero starts and no false toast, the dedupe hold (1 call vs 2), the fail-closed scope refusal, the rail-switch toast fence, and the A→B→A retention spec (1 deploy vs 2) - New unit coverage pins the queue contract (enqueue-time floors, attach-seam queueing), the scope capture and verbatim relay-URL handoff, the dedupe map's keying and settle-then-repermit behavior, the unscoped refusal, the toast-scope mirror, the `{ agent, wrote }` contract, and the replay-floor env layering on both spawn paths 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5073e075d7 |
fix(desktop): unify owned-agent cloud provenance markers (#7129)
Unify presentation-only cloud provenance across agent identity surfaces. Keep successful local-inventory and verified-ownership gates; preserve main availability and mention spacing behavior. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com> |
||
|
|
d5a73b9f35 |
fix(desktop): derive agent availability from relay presence (#7127)
🤖 ## Requested rebase published — |
||
|
|
44d19f59a5 |
fix(desktop): preserve spacing after multi-word mentions (#7128)
🤖
## Summary
In Buzz Desktop, choosing a multi-word name and immediately continuing a
sentence could swallow the space after the mention: `Hey @Alice
Chenhello`. This keeps the separator, so the same action produces `Hey
@Alice Chen hello` without moving the caret or repairing the name by
hand.
The editor recognizes the complete selected label, including its
internal spaces, and settles the autocomplete caret after the trailing
separator. Deliberately moving left or clicking inside the label still
lets you edit there; this is not a rule that forces every caret to the
end of a mention.
### Related issue
Independent base: `main`. Child:
[#7133](https://github.com/block/buzz/pull/7133), whose disambiguated
labels also contain spaces. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).
[Behavior
contract](
|
||
|
|
c328202cb0 |
docs(nip-fi): document Git smart-HTTP credential exemption (#7268)
Amend NIP-FI HTTP ingress with an explicit Git smart-HTTP credential-helper exemption. The exception covers method binding, endpoint-URL binding, and the `payload` tag requirement for `info/refs`, `git-upload-pack`, and `git-receive-pack`, while preserving per-request NIP-FI assertion, key pairing, and deny-map enforcement. The spec records Git's credential-protocol limitation, the required compensating controls, and the rule that this exception is limited to these endpoints and is superseded by per-request signing. Related: [PR #7264](https://github.com/block/buzz/pull/7264) Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
47d068e210 |
feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259)
## What Two new agent-facing capabilities in `buzz-cli`: ### 1. `buzz gifs` command group (agent KLIPY picker path) Agents can now search and share GIFs via the relay's authenticated KLIPY proxy without holding a provider credential. ```bash buzz gifs search # trending GIFs buzz gifs search --query "celebration" # search GIFs buzz gifs share --slug <slug> # report selection to provider Recents ``` Output is a JSON array of GIF objects. Paste the `cdn_url` field directly into `buzz messages send --content` — sending a GIF is a plain message containing the CDN URL, no special send-path handling. **Implementation details:** - Gates on NIP-11 `supported_extensions` containing `buzz-gif` and `gif.provider == "klipy"` - Uses relay-relative paths from the NIP-11 `gif` descriptor — no hardcoded paths; safe-path validation mirrors `desktop/src/features/gifs/api.ts` - New `post_json_authed` helper in `BuzzClient` handles NIP-98-signed JSON POSTs and 204 No Content responses - `customer_id` derived as `SHA-256(secret_key_bytes || '\0' || relay_url_bytes)[..16]` → 32 hex chars: stable, relay-scoped, not computable from public data, no storage needed - `locale` defaults to `$LANG` (stripped of encoding suffix) or `en_US` ### 2. NIP-30 custom emoji tags on outgoing messages `buzz messages send` now automatically attaches `["emoji", shortcode, url]` tags for any `:shortcode:` patterns in the content that resolve in the workspace palette — identical to the desktop composer behavior. ```bash buzz messages send --channel <uuid> --content "hello 👋 everyone 🎉" # → event carries ["emoji", "wave", "..."] and ["emoji", "tada", "..."] tags ``` **Implementation details:** - Hand-rolled single-pass scanner (no new dependency) implementing `:([a-z0-9_-]+):` case-insensitively with canonical lowercase output — mirrors `desktop/src/shared/lib/customEmojiTags.ts` exactly - Zero extra relay round-trips when content contains no `:` character; one `query` when candidates exist but none match - Palette fetch reuses the existing `union_custom_emoji` logic from `commands/emoji.rs` - `build_message` in `buzz-sdk` gains a new `emoji_tags: &[Vec<String>]` parameter (additive — all existing callers pass `&[]`); NIP-30 tag attachment lives in the SDK alongside `imeta` tags - MCP send path (`buzz-acp`) continues to pass `&[]` and is not affected; the MCP gap is noted in a comment ## Files changed | Crate | File | Change | |-------|------|--------| | `buzz-cli` | `src/commands/gifs.rs` | New — search + share handlers, NIP-11 gating, tests | | `buzz-cli` | `src/commands/mod.rs` | `pub mod gifs` | | `buzz-cli` | `src/lib.rs` | `Gifs(GifsCmd)` variant, dispatch arm, inventory test update | | `buzz-cli` | `src/client.rs` | `post_json_authed` helper | | `buzz-cli` | `src/commands/emoji.rs` | `scan_shortcodes` + `resolve_emoji_tags_for_content` + tests | | `buzz-cli` | `src/commands/messages.rs` | Emoji scan + tag injection in `cmd_send_message` + seam tests | | `buzz-cli` | `README.md` | `buzz gifs` section + emoji-in-messages note | | `buzz-sdk` | `src/builders.rs` | `build_message` gains `emoji_tags` param + tests | | `buzz-acp` | `src/pool.rs` | Update `build_message` call site (`&[]`) | | `buzz-acp` | `src/setup_mode.rs` | Update `build_message` call site (`&[]`) | | `countdown-bot` | `src/main.rs` | Update `build_message` call site (`&[]`) | Relates to: https://buzz.block.builderlab.xyz — buzz-team channel thread on agent GIF/emoji support --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
ac5a18697c |
feat(desktop): add persistent Bestie experience (#7223)
## Summary - add a persistent, community-scoped Bestie designation for local managed agents - surface the designated agent in the sidebar, agent library, profile actions, message toolbar, and draggable floating shortcut - bloom the floating shortcut into a lightweight compact composer that reuses the normal DM timeline, reactions, presence, and send behavior - support message handoff with a bounded snapshot and a full Buzz thread link so the agent can retrieve the complete conversation ## UX details - the floating avatar and expanded panel stay above app chrome and drag as one aligned surface - closing the expanded panel returns it to its top-right anchor - each mini-composer opening starts visually fresh while messages sent during that opening remain conversational - the designated Bestie's duplicate DM entry is hidden from the regular DM list - Bestie actions are suppressed inside the mini timeline to avoid recursive handoff ## Reliability and maintainability - preserve existing retention database paths across upgrades - serialize assignment and deletion, clearing matching assignments across community scopes before an agent is removed - fence async conversation resolution against workspace and assignment changes - validate stale assignments against existing local agents before hiding DMs - share lightweight assignment state across agent cards and keep protected-feature behavior out of the shared timeline API ## Testing - `pnpm --dir desktop test` — 5,886 tests passed - `pnpm --dir desktop exec tsc --noEmit` - `pnpm --dir desktop exec biome check ...` - `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - focused native retention and Bestie assignment/command tests — 34 passed - `VITE_BUZZ_BESTIE=1 pnpm --dir desktop build:e2e` - `VITE_BUZZ_BESTIE=1 pnpm --dir desktop exec playwright test --project=smoke tests/e2e/bestie.spec.ts` - pre-commit and differential pre-push hooks ## Rollout The UI remains gated by the `bestie` build feature. Screenshots covering setup, empty, assigned, floating, and message-handoff states are included in the PR discussion. --------- Signed-off-by: Arjun Mahanti <arjun@squareup.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
6f6093243e |
fix(desktop): harden profile batch and thread-reply fetches against relay slowness (#7188)
Three client gaps turn transient relay failures into permanent UI degradation. Under a slow or rate-limited relay: 1. A cold channel's profile batch exhausts its single retry and leaves raw npubs + broken mention chips until the user manually kicks the channel. 2. A thread opened from a notification trusts a successful-but-empty reply read as authoritative and never retries. 3. A rate-limited `CLOSED` on a history subscription immediately rejects the caller rather than retrying after the rate-limit window. All three are addressed without changing global query defaults or the happy-path behavior. ## Changes **Fix 1 — cold profile batch resilience** (`useUsersBatchQuery`, `desktop/src/features/profile/hooks.ts`) Override `retry: 3` with exponential backoff and error-gated `refetchOnWindowFocus: (query) => query.state.status === "error"`, scoped to this query only. The global defaults (`retry: 1`, `refetchOnWindowFocus: false`) are intentional for other queries and are unchanged. After the retry budget exhausts, a window-focus event (e.g. channel-switch) recovers the query automatically — but only when it is already in an error state, preventing unnecessary refetches for successful batches. **Fix 2 — stale-empty thread reads** (`useThreadReplies.ts`, `ChannelScreen.tsx`) Add optional `expectedEventId` parameter. When a completed paged fetch does not contain the expected event, throw `ThreadExpectedEventMissingError` so React Query's built-in retry machinery handles it rather than caching an authoritative empty. `ChannelScreen` passes `threadScrollTargetId` (the notification-linked reply ID) as `expectedEventId`. When notification routing changes `expectedEventId` while the same thread root is already mounted (same query key), an explicit `invalidateQueries` in a `useEffect` triggers a fresh validation pass. The `useEffect` is declared after `useQuery` so TanStack's internal options-update effect installs the new `queryFn` closure first; the refetch therefore uses the current `expectedEventId` rather than the previous null closure. For the cold-start race (target arrives before the first page returns), the effect detects `fetchStatus === "fetching" && status === "pending"` and calls `cancelQueries().then(invalidateQueries)` so the obsolete in-flight response cannot settle as authoritative before the new target's validation closure is active. The query-fn tracks consecutive fetch attempts per target. On attempt 3, it adds the target to `exhaustedTargetsRef` before calling `loadThreadReplies`. `loadThreadReplies` sees the target in the exhausted set and returns the fetched replies directly rather than throwing — the terminal attempt always resolves to success. No re-entrant scheduling: the resolution is synchronous inside the query function itself. Deleted/moderated targets never lock the thread in a terminal error surface. **Fix 3 — CLOSED recovery for history subscriptions** (`relayClosedRecovery.ts`, `relayClientSession.ts`, `relayClientShared.ts`, `relayGateBoundary.ts`) On a rate-limited `CLOSED` the subscription previously rejected the caller immediately. Store `filter` and `timeoutMs` on `HistorySubscription`, then on rate-limited `CLOSED` re-register under a fresh `subId` and defer `sendReq` until the rate-limit window clears — matching the live-sub recovery design already present in `relayClosedRecovery.ts`. Bounded to 3 attempts; exhausted retries reject immediately so callers are never left waiting indefinitely. A new op-timeout guards the retry REQ against a non-responding relay; when the op-timeout fires it sends `CLOSE` for the rotated `subId` (matching the behavior of the original timeout path) so the relay releases the slot rather than counting it against the per-connection cap. ## Tests - `relayClosedRecovery.test.mjs`: behavioral fake-clock tests for history-sub retry, 3-attempt exhaustion, op-timeout CLOSE send + late-EOSE non-regression, rejecting-`closeSubscription` swallowed without unhandled rejection, wiring source assertion (fails if `relayClientSession.ts` drops the `closeSubscription` callback) — 18 tests - `useThreadReplies.test.mjs`: `loadThreadReplies` unit tests (throw/exhaustion-guard); behavioral hook tests via real `QueryClientProvider` + `renderHook`: exhaustion-resolves-to-data, settled null→target change retries on missing-target page and lands target data (fails if `invalidateQueries` is removed OR if `useQuery` is moved after the `useEffect`), cold-fetch cancel-then-invalidate (gated fetcher — released after rerender, stale empty discarded, replacement fetch settles with target); ChannelScreen wiring source assertion — 9 tests - `profileBatchResilience.test.mjs`: source assertions for `retry: 3`, `retryDelay`, error-gated `refetchOnWindowFocus`, and unchanged global defaults — 2 tests --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
187df22252 |
docs(nip-fi): adopt deny-until-TTL and extend enforcement to HTTP ingress (#7254)
Spec revision following two decisions: Option B admin deny (2026-09-02) and the HTTP ingress ruling (2026-09-02). Revises `docs/nips/NIP-FI.md` only. Follows #7214 (merged spec v2). ## What changed ### Admin disconnect: session-only → deny-until-TTL The disconnect operation proceeds in two steps, in order: 1. Insert a memory-resident deny entry keyed by `(iss, target_pubkey)` with absolute expiry `until` — atomically combined with the `(iss, jti)` replay-identity reservation as one all-or-nothing mutation. If the deny set is at capacity (per-issuer bound), the relay rejects `503`; neither the jti nor the deny entry is recorded, and the caller may safely retry the same signed command. 2. Close all live WebSocket connections for the target pubkey, synchronously. The single atomic admission mutation (jti reservation + deny-entry insertion) lives inside `VerifyCommandJwt` step 7, after all pure authorization checks. The endpoint only closes sessions on success. This ordering ensures a capacity failure leaves no state behind and makes the retry-safe 503 contract implementable. The deny set is RAM-cache only — no durable storage, no schema changes. The same operational posture as the JWKS snapshot. A relay restart MAY forget active entries; the issuer SHOULD re-push still-active deny entries on observed restart (same publish/cache pattern as JWKS). If the issuer stops issuing assertions and re-push completes before any expired-entry reconnection attempt, residual exposure after restart is bounded by `max(0, min(exp, iat + maximum_assertion_age) - now)`. If the issuer continues issuing or re-push does not complete in time, that formula does not apply and access may continue beyond it. **`until` claim:** Required on the disconnect command JWT. Because an assertion accepted at the future-skew boundary (`iat <= now + skew`) remains valid until `iat + maximum_assertion_age`, the latest possible authority deadline is `now + skew + maximum_assertion_age`. The relay enforces `until <= now + skew + maximum_assertion_age`. A value above this ceiling rejects `400`; a past `until` still closes live sessions — absent an active same-key entry it creates no future denial, while an active entry remains unchanged under the merge rule. **Capacity and eviction (per-issuer):** The relay MUST bound the deny set size **per issuer**. Capacity exhaustion under one issuer MUST NOT cause rejection of another issuer's commands; the `503` capacity check is evaluated against the command's own issuer bound. Implementations MUST evict only expired entries; when an issuer's partition is at capacity and all entries are still active, the relay MUST reject the new command `503` without removing any existing entry. There is no LRU eviction of active denies. **Issuer-global deny:** The deny entry applies to admission across all communities served by the relay under that issuer. Identity-level revocation is intentionally not community-partial. **Cross-replica propagation:** In a deployment with multiple relay processes, the deployment MUST propagate both the session-close and the deny entry to every process serving admissions for the issuer's communities. The mechanism is deployment-defined (e.g. the existing inter-process message bus, same posture as JWKS convergence). Propagation is asynchronous with no protocol-level completion bound. The issuer re-push duty is the recovery path for lost propagation, exactly as for relay restart. **Response shape:** A successful disconnect responds `{"disconnected": true}` regardless of how many sessions were closed. No session count is returned; a count would aggregate activity across communities and constitute an information leak. **Admission procedure:** Step 5 registers the session's proven `k` before the deny-set check (new step 6) — ensuring any connection that straddles a concurrent disconnect is caught by one side or the other. `FI-TRACE-DENY-SET` oracle covers the per-issuer capacity rule and the straddling termination requirement. ### HTTP ingress enforcement Without explicit enforcement, a protected HTTP surface (bridge, invites, media, git) with NIP-98-only authorization allows a principal holding an active key to mint fresh NIP-98 events indefinitely — NIP-98 proves key possession only, not identity. Without assertion verification there is no expiry bound; the key remains valid for as long as it is accepted. **Pairing rule:** in enforce mode, a protected HTTP request MUST carry both: ``` Authorization: Nostr <base64-NIP-98-event> Nostr-Federated-Identity: Bearer <compact-JWS> ``` The NIP-98 pubkey MUST equal the assertion `nostr_pubkey` claim. Missing, mismatched, or invalid evidence of either kind denies, fail closed. **Verification:** reuses `VerifyAssertion` unchanged — offline, same JWKS, same claim requirements, same denial classes. **Per-request:** HTTP is sessionless; every request re-verifies. No session lifetime, no cached admission. The cumulative residual bound applies per request. **Deny-set applicability:** the deny-until-TTL entry introduced above is consulted per HTTP request identically to WebSocket admission. **Protected surface:** deployment-configured set of routes, fail-closed default (unclassifiable routes treated as protected). No normative route names in the spec. `FI-TRACE-HTTP-INGRESS` oracle added. Security considerations updated with HTTP ingress bypass analysis. NIP-98 source reference added. ### Other changes - `authorization_denied` rejection table row updated to "active deny-set entry for pubkey". - Discovery: `maximum_residual_upstream_revocation_seconds` remains `null` — the deny-until-TTL model is best-effort RAM state and provides no unconditional finite revocation bound. - Rejection and privacy: explicit sentence for HTTP denial path. - Client-attached transport: opening sentence generalized to cover both WebSocket and HTTP. ## Scope Single file: `docs/nips/NIP-FI.md`. No code changes. References #7214. Channel: buzz-enterprise-identity-spec-v2 (#a6fe0b1c-987a-43c5-a974-71ee36678d78). --------- 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> |