mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix/e2e-origin-bootstrap
340
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
edc4a09aaa |
feat(workflows): add responsive library card actions (#6008)
**Category:** improvement **User Impact:** Users can scan what each workflow does and trigger, edit, duplicate, enable, disable, or delete it directly from the library. **Problem:** The workflow list buried common actions and did not expose each automation's trigger-to-action shape at a glance. **Solution:** Add a responsive workflow library with a persistent create tile, compact trigger/action diagrams, prominent workflow titles with supporting descriptions, and shared card actions while preserving existing detail, editor, and run-history entry points. Card toggles refresh both list and open-detail caches so status and definition stay consistent. <details> <summary>File changes</summary> **desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx** Adds a shared card menu for trigger, edit, duplicate, enable/disable, and delete actions. **desktop/src/features/workflows/ui/WorkflowCard.tsx** Reworks cards around the prototype's visual hierarchy: color-coded trigger, action flow, sentence-case eyebrow, prominent title, supporting description, status, channel, and update date without a footer clock icon. **desktop/src/features/workflows/ui/WorkflowsView.tsx** Adds the responsive grid, create tile, mutation wiring, and list/detail cache invalidation. Container breakpoints keep cards two-across at medium widths and three-across in the 1280px desktop layout. **desktop/src/features/workflows/ui/workflowDefinition.ts** Adds immutable enabled-state updates plus narrow trigger and first-action readers used only to select card icons. **desktop/src/features/workflows/ui/workflowDefinition.test.mjs** Covers neutral icon selection, enabled-state immutability, and status presentation. **desktop/tests/e2e/workflows.spec.ts** Covers the create tile, title/description hierarchy, selected-card enable/disable consistency, and deterministic narrow/medium/wide captures while retaining existing action coverage. </details> ## Reproduction steps 1. Open **Workflows** and confirm the create tile stays first as cards flow from one to three columns with available width. 2. Confirm each card shows a sentence-case trigger eyebrow, prominent workflow title, supporting description when present, status, channel, and update date without a clock icon. 3. Open a card's overflow menu and trigger, edit, duplicate, enable/disable, or delete the workflow. 4. Leave the detail panel open while toggling and confirm its badge and JSON definition update with the card. ## Screenshots Real built E2E UI with representative workflow data at three viewport sizes. ### Narrow — 800 × 720  ### Medium — 1024 × 720  ### Wide — 1280 × 720  ### Card actions  --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
f716eef437 |
fix(desktop): enforce shared agent access across devices (#6086)
## Summary - discover shared managed agents from authenticated relay directory records instead of treating channel membership as sufficient proof - publish and refresh access-policy changes immediately so running clients converge across machines without a restart or five-minute poll - route profile edits through the exact managed instance and stop/restart runtimes around access changes so unrelated edits cannot silently widen access - keep mention send-time revalidation and Block owner-only build enforcement fail closed - explain invalid custom provider/model configuration instead of leaving Save silently disabled ### Related issue Fixes #3204 ### Known residuals - a brand-new remote agent's first policy record can wait for the bounded directory poll when no authenticated directory coordinate exists yet; send-time mention revalidation remains fail closed - a failed remote-provider policy redeploy is recorded but cannot undeploy the older provider instance until the provider protocol gains the destructor tracked by #5570 ### Testing - full Desktop unit suite: 4,961 tests passed - focused profile editor Playwright workflow passed, including Customize access edits and prompt-only edits after tightening an instance - Desktop TypeScript, Biome formatting, file-size ratchet, Tauri checks, and pre-push suites passed - independently reviewed for authenticated directory trust, live subscription teardown, runtime revocation ordering, fail-open edit paths, and per-agent provider deployment serialization --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz> Co-authored-by: diegorumo <diegorumo@gmail.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz> |
||
|
|
122a8b8988 |
Projects v3: unify sharing, discussions, and issue ownership (#5792)
## Summary Projects v3 makes repository work shareable, discussion-aware, and easier to scan in one coherent workspace. People can copy canonical links, reopen the exact workspace tab, understand issue and pull-request context at a glance, find related channel conversations, and assign or unassign issues across Desktop and CLI. - **Unified workspace** — top-level sections sit above repository controls in one rounded workspace, with navigation positioned close to the page heading. README and Files retain branch selection; every section has a labeled icon header, and Issues and Pull Requests expose creation from a consistent right-aligned action. - **Repository management** — the repository selector is always available, including single-repository projects. Its integrated add flow lets project owners create a repository manually or select an existing repository without a separate toolbar button. - **Readable work-item lists** — issue and pull-request rows use plain-language context instead of opaque metadata. Files, commits, issues, pull requests, channels, and contributors share consistent row density and right-aligned timestamps, while deterministic fallback-avatar colors keep participants distinct on light backgrounds. Inbox pull-request metadata wraps between complete phrases and truncates long channel names instead of compressing copy into narrow columns. - **Reliable entity links** — projects, repositories, issues, pull requests, and commits have canonical `buzz://` links, preview cards, OS deep-link routing, and tab-aware navigation. Reopening the same link re-applies its destination instead of leaving the user on a locally selected tab. - **Related conversations** — repository and work-item views surface channels discussing the current entity, including participants, channel navigation, message context, and an explicit notice when discovery reaches its 500-result cap. - **Reversible issue ownership** — trusted assignment and unassignment events work across Desktop, Tauri, `buzz-sdk`, and `buzz issues`. Assignees appear in project views and the assigned inbox, while authorized users can remove assignments directly from the assignee row. Assignment state is derived chronologically from labeled Nostr notes. Issue authors and repository owners may change any assignee; other users may only assign or unassign themselves. Shared golden fixtures keep entity-link grammar and validation aligned across TypeScript and Rust. The branch also updates `webbrowser` to the patched release for RUSTSEC-2026-0257. ### Related issue N/A. ### Testing - [x] `just ci` — formatting, lint, typechecking, unit tests, and builds passed - [x] Full pre-push suite — organization, branch-skew, Desktop checks, typechecking, and tests passed on the latest push - [x] `cargo test -p buzz-cli` and focused `buzz-sdk` assignment tests passed - [x] Focused Tauri recipient-note and 500-result search-limit tests passed - [x] Desktop entity-link and issue-assignment unit tests passed - [x] Playwright smoke coverage passed for assignment, repeated entity-link navigation, repository create/select flows, section headers and actions, timestamp alignment, timeline icons, sentence-style issue/PR metadata, header spacing, avatar contrast, and Inbox metadata at stacked and side-rail breakpoints - [ ] Manual staging pass: link round-trips, Channels tab, assignment flows, and inbox routing ### Screenshots Pull requests explain who opened the request, where it lives, and which branch it comes from; fallback avatars remain visually distinct.  Issues use the same sentence-style hierarchy while keeping status and recency easy to scan.  The wide Inbox detail keeps author, timestamp, and origin context readable beside its metadata rail.  [View the complete six-state Projects v3 screenshot set](https://github.com/block/buzz/pull/5624#issuecomment-5268039672) and [the compact/wide Inbox comparison](https://github.com/block/buzz/pull/5624#issuecomment-5268614585). --- > Supersedes #5624, whose head commit accumulated permanently-queued required check suites (block-dco-check et al.) that GitHub never dispatched. History flattened into a single signed-off commit on latest main; tree verified byte-identical (`git merge-tree`) to merging the original branch into main. --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Co-authored-by: Wintermute <3f1797424fd9ad6653a83665c660517777cd7f8c228c0d5907f49e01537f3ca5@buzz.block.builderlab.xyz> |
||
|
|
f086eb6544 |
fix(link-previews): send while previews finish in background (#5697)
**Category:** fix **User Impact:** Messages send immediately after submission while link previews finish in the background, with an option to skip delayed preview preparation. **Problem:** Waiting for link-preview metadata or snapshot uploads kept the composer occupied after users pressed Send, while races between completion, timeout, and cancellation risked inconsistent payloads. **Solution:** Freeze and promote speculative preview work into a bounded background send task, clear the composer immediately, and publish exactly once with prepared previews or gracefully without them when skipped, failed, or timed out. https://github.com/user-attachments/assets/987d2f2c-679f-473a-965f-dfb279951e52 <details> <summary>File changes</summary> **desktop/src/features/communities/useCommunityInit.ts** Resets pending link-preview preparation when community context changes so work cannot cross community boundaries. **desktop/src/features/messages/lib/linkPreviewPreparationStore.ts** Adds the coordinator-owned preparation state machine, bounded fallback, Skip behavior, and exactly-once terminal publication handling. **desktop/src/features/messages/ui/ComposerUploadProgressOverlay.tsx** Extends floating background progress UI to include link-preview preparation. **desktop/src/features/messages/ui/ComposerUploadProgressPill.tsx** Adds the preparing-link-preview label and Skip action to the progress pill. **desktop/src/features/messages/ui/MessageComposer.tsx** Hands submitted preview work to the background coordinator and clears the composer immediately. **desktop/src/features/messages/ui/messageComposerAutoSubmit.test.mjs** Updates auto-submit unit coverage for coordinator-owned preview preparation. **desktop/src/features/messages/ui/messageComposerAutoSubmit.ts** Allows submit to promote unfinished preview work instead of blocking composer submission. **desktop/src/features/messages/ui/useComposerLinkPreviews.tsx** Starts preview work speculatively and exposes frozen preparation jobs for adoption by the send flow. **desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts** Carries prepared preview tags through the mention and media payload helpers. **desktop/src/features/messages/ui/useMentionSendFlow.ts** Integrates prepared preview tags into final message publication. **desktop/src/shared/lib/useResolvedLinkPreviews.ts** Exposes the in-flight metadata promise so promoted work can be adopted rather than restarted. **desktop/tests/e2e/messaging.spec.ts** Covers immediate submit, upload handoff, Skip/completion races, failure fallback, auto-send, and exactly-once publication. </details> ## Reproduction steps 1. Enter a supported link and press Send while preview metadata or snapshot upload is still pending. 2. Confirm the composer clears immediately and the floating progress UI shows **Preparing link preview · Skip**. 3. Let preparation finish and confirm one message is published with its preview. 4. Repeat and choose **Skip**; confirm one message is published without waiting for the preview. 5. Simulate preview failure or timeout and confirm the message still publishes once without preview tags. ## Validation - TypeScript, Biome/format, file-size, px-text, and pubkey checks - Full desktop unit suite: 4,734 passed - Focused Playwright messaging suite: 5 passed - Push hooks at `86c0aa7de2ff81b79286c99bf23db12345adc6ca`: desktop check, typecheck, and tests passed --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
34a7f2fb91 |
Unify agent profile content (#5788)
## Summary - remove synthetic preview runtime and configuration data so profiles show only real agent content - simplify model settings to the effective values and restore bare section icons - make owned-agent profiles resolve to the same current persona instance from every entry point ## Why Agent profiles opened from DMs or channels could fall back to a partial declared-owner view instead of the full managed-agent profile shown on the Agents page. Test preview content and configuration provenance also remained visible after the redesign. ## User impact Owned agent profiles now expose the same actions, runtime, channels, memories, and configuration regardless of where they are opened. Profiles no longer synthesize preview data, and model settings use the same simple title/value hierarchy as the rest of the panel. ## Validation - `pnpm --dir desktop check` - `pnpm --dir desktop build:e2e` - focused unit tests: 10 passed - profile entry-point integration tests: 2 passed - configuration screenshot suite: 7 passed, with six visually distinct captures Snapshots are attached in a PR comment. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz> |
||
|
|
2693e0db1f |
Make workflow run history authoritative in Desktop (#5780)
## Summary - persist stable workflow run `error_code` values separately from human diagnostics - expose NIP-98 authenticated, channel-authorized run history and approval reads with stable keyset pagination - connect Desktop to those authoritative reads and return the relay-created run ID on trigger - show truthful loading, failure, and pending-trace states, and do not render approval actions from non-actionable stored hashes ## Validation - pre-push `branch-skew`, `desktop-typecheck`, `desktop-test`, `rust-tests`, `desktop-tauri-checks`, and `desktop-check` all passed on `a097dbe5f` - Desktop tests: 4,761 passed, 0 failed - `cargo check -p buzz-relay` - `git diff --check` ## Remaining gate This does not claim a relay-backed Playwright workflow journey. The browser relay bridge still routes workflow invokes through in-memory handlers; that production-shaped acceptance gate remains follow-up work before Workflows can leave preview. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz> |
||
|
|
a96af89526 |
Harden shared agent instruction review (#4220)
## Summary - render shared-agent instructions as literal text so Markdown cannot conceal spoiler contents, link destinations, or image sources - reject non-reviewable Unicode controls at every agent-definition boundary while preserving legitimate rendered emoji sequences - verify shared catalog event IDs and signatures before trusting authorship, coordinates, pagination, or executable content - preserve the exact system-prompt bytes between review and execution instead of silently stripping or normalizing content ## Security rationale Shared system prompts are executable configuration. Previously, catalog prompts were projected through the chat Markdown renderer, which could hide text, replace link destinations with benign labels, and turn image syntax into remote loads. Zero-width and bidirectional controls could also make reviewed text differ from what the agent executes. This change establishes a review invariant: the prompt a user sees is the prompt the agent executes. Definitions that cannot be reviewed faithfully are rejected rather than rewritten. Catalog events must also pass Nostr ID/signature verification before they can claim a publisher, coordinate, or cursor. ## What changed - catalog instructions render as exact literal text rather than rich Markdown - catalog relay events are verified on a fresh wire-shaped object before paging, coordinate selection, attribution, or projection - forged content, pubkeys, signatures, and invalid newer heads are ignored and cannot shadow a valid signed definition - TypeScript catalog parsing rejects unsafe remote definitions before they reach the UI - shared Rust validation covers persona create/update/import, inbound relay sync, definition-less managed-agent sync, and catalog publication paths - definition-less managed agents now fail closed on local create, local update, and publication before persistence or relay retention - linked managed agents validate their local name while treating the persona definition as authoritative; their inert record-level prompt is not executed or published - names reject layout controls; prompts retain ordinary newlines and tabs - legitimate emoji composition is supported, including contextual VS16, ZWJ, skin-tone, family, flag, and keycap sequences - detached selectors/joiners, bidirectional controls, tag characters, zero-width concealment, and other default-ignorables remain rejected - names are bounded to 128 characters and prompts to 64 KiB - contributor guidance documents the byte-for-byte review requirement for future sharing paths Validation reports the offending code point and never silently removes it. ## E2E recording [buzz-shared-agent-security-e2e.webm](https://github.com/user-attachments/assets/44d6b75f-0877-490f-bda4-a716fae3f700) The recording demonstrates: - a safe definition remains visible - a prompt containing zero-width `U+200B` is rejected - a name containing bidi override `U+202E` is rejected - the prompt is preserved exactly - spoiler, link, and image syntax remains literal and does not render or load ## Verification Passed locally: - `just test`: all 10 unit and Docker-backed integration stages - desktop frontend unit suite: 4,295 tests - persona catalog relay unit suite: 32 tests, including forged-event and cursor-shadowing cases - focused Rust definition-validation coverage: 3 local create/update tests and 6 publication-filtered tests - complete desktop Tauri library suite after rebase: 2,263 passed, 14 ignored, 0 failed - desktop Tauri clippy with warnings denied and Rust formatting - complete agent Playwright spec: 34 tests - the exact formerly failing `inbox-edit` immediate-attachment smoke test after rebase: 1 test - focused shared-agent publish, literal-review, hidden-control, signature, and cross-member import Playwright coverage - desktop E2E production build and TypeScript typecheck - changed-file formatting/lint and file-size ratchet - pre-commit secret scan and DCO signoff The branch was rebased onto current `main`, which includes the upstream attachment-button label fix. Fresh post-rebase GitHub CI is green for every required and selected check: Desktop Core, all four Desktop Smoke E2E shards, both Desktop E2E Integration shards and their aggregate, Desktop E2E Relay, Desktop Build (macOS), Windows Rust, Rust Lint, DCO, security scanners, and Desktop Release Candidate. The previously failing `Desktop Smoke E2E (3)` shard now passes. The repository-wide desktop check also reports existing CSS formatting/`!important` findings in `components.css` and `terminal.css`; neither file is changed by this PR. GitHub's Desktop Core lint and format stage passes on the rebased branch. --------- Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> |
||
|
|
1ff98fa685 |
fix(desktop): launch Databricks OAuth from passive model discovery (#5607)
When a user's agent runtime is `buzz-agent` with no cached Databricks OAuth token, the desktop app's passive model-discovery surfaces were forbidden from launching interactive auth. Discovery failed silently, so the model dropdown showed only built-in fallback models behind a vague "Could not load live models for `databricks_v2`" note (reported internally by Nick and Jose). ## What changed Both discovery surfaces — the passive draft-form discovery and the explicit saved-model picker — now launch the browser OAuth flow, matching goose's behavior. The only behavioral difference between them is cooldown handling: - **Passive draft discovery** fires on every form-state change, so a failed, cancelled, or timed-out sign-in records a per-host cooldown (5 min) that suppresses re-popping the browser on the next keystroke. While the cooldown is active it returns the "sign-in required" guidance instead of relaunching. - **The explicit model picker** is a deliberate user action, so it always launches and clears any stale cooldown first. Safety rails: - A 150s hard timeout (`AUTH_FLOW_TIMEOUT`) bounds the whole interactive flow so an abandoned SSO tab fails discovery cleanly rather than wedging the dropdown. Success clears the cooldown; failure and timeout both record it. - `AuthCooldown` recovers from a poisoned lock rather than wedging every future sign-in on one panic. The frontend maps the terminal Databricks sign-in states to typed, actionable copy in `formatModelDiscoveryErrorStatus`: "sign-in required" is a muted note pointing at the picker and `buzz-agent auth databricks`; a failed or timed-out sign-in is a warning pointing at the explicit retry. Other Databricks failures fall through to the existing generic notice. ## Scope Changes are confined to Databricks discovery and its frontend status formatter — no `agent_models.rs` call sites are touched. The interactive-auth helper takes an injected timeout so the timeout/cooldown policy is unit-testable without a live browser. ## Deferred Cooldown keys use the raw trimmed `DATABRICKS_HOST`, while the catalog and OAuth cache normalize trailing slashes (`crates/buzz-agent/src/catalog.rs:96`, `crates/buzz-agent/src/llm.rs:2046`). So `https://workspace/` and `https://workspace` share credentials but get separate cooldown entries — an equivalent-spelling change to the host field mid-cooldown can re-pop passive OAuth once within the 5-minute window. Self-limiting (one extra browser launch, never auth corruption). Follow-up: a `trim_end_matches('/')` on the cooldown key plus an equivalent-host test, picked up with the coordinator migration if [#5545](https://github.com/block/buzz/pull/5545) ever merges. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
f35930104b |
fix(desktop): remove 0.5.9+ perf regressions, speed up get_channels (#5599)
Desktop input latency regressed sharply for users on v0.5.9 and worsened on latest main: multi-second stalls when clicking back into the app, slow fresh boots, intermittent lockups, and scroll/mouse degradation. Reverting to `119a84897` (pre-0.5.9) was confirmed to resolve it, isolating the regression to that range. Profiling a live production renderer plus a commit-level audit of the range found three independent, additive causes — fixed here — plus a long-standing `get_channels` cost that made every remaining refetch expensive, also addressed here. ## 1. Focus-return refetch storm (`refetchOnWindowFocus`) #5490 wired TanStack's `focusManager` to app focus and flipped ~20 query sites to `refetchOnWindowFocus: true`. A focus return after >60s away fires them all within milliseconds — and a click into an unfocused window *is* a focus return, so the burst runs before the click is processed. That is the "click into the composer, wait 5 seconds" symptom, and it also explains why mouse input feels worse than keyboard (clicks arrive with focus transitions; typing happens while already focused). A 5-second `sample` of a live production renderer caught a single window activity-state transition consuming ~1.25s of main-thread time, dominated by `JSON.parse` in the focus listener's microtask drain. #5535 already established the fix pattern but applied it to only two families (channels, home-feed). This PR extends the same 5-minute `staleTime` discipline to the remaining families: pulse (×5), workflows (×4), agents (×4), forum (×2), presence, user-status, custom-emoji, channel-templates, and the persona catalog. Polling cadences and push-invalidation paths are untouched — interval refetches and `invalidateQueries` both bypass `staleTime`, so live-update behavior is unchanged. Each gated family exports its focus-refetch policy as an options object that the production hook spreads into `useQuery`, and a `focusRefetchPolicy.test.mjs` drives a `QueryObserver` with that same production object — locking the policy behaviorally (fresh focus return → 0 fetches; stale → refetch) and failing if a hook's `staleTime`/`refetchOnWindowFocus` wiring drifts. Four families deliberately keep tighter freshness, all surfaces where the 5-minute gate would suppress the only refresh path and none of which feed the app-wide storm: `repo-sync-status` keeps its fresh focus refetch (its inline comment documents the "committed in a terminal, switched back to the app" flow as intended); the workflow-runs list stale-gates at 10s because a remotely-started run has no push invalidation and its conditional 1s poll is off while the cache shows no active runs; the workflow list queries (`useChannelWorkflowsQuery` and the all-channels aggregate) stale-gate at 10s because they have no poll and no relay subscription, and mutation-driven invalidation only covers this renderer — remote workflow creates/edits/deletes surface only via focus refetch; and the managed-agent log stale-gates at one poll tick (30s) so returning to a live agent log refreshes immediately. Run approvals keep the 5-minute gate under `RUN_APPROVALS_FOCUS_STALE_TIME_MS` — their focused 10s poll already covers freshness. ## 2. Synchronous localStorage sweep on the boot/focus path #5453's stale-cache sweep synchronously `getItem` + `JSON.parse`s every whitelisted localStorage entry on the main thread (multi-MB on seasoned profiles), scheduled with a `requestIdleCallback` timeout of 1.5s that guaranteed it landed mid-boot, and re-armed on every hidden→visible transition — stacking it onto the exact moment the focus storm fires. #5454's `trimSelfProfileCaches()` additionally scanned every localStorage key on every `writeSelfProfileCache()` call (which fires per relay self-profile delivery at boot). Now: the first sweep waits `BOOT_SWEEP_FLOOR_MS` (30s) after startup, the scan is time-sliced across idle callbacks, and the visibility trigger is removed — boot-delayed plus hourly still covers the 14-day TTL contract. The sliced sweep re-checks staleness immediately before each removal (a key rewritten fresh mid-sweep survives), isolates per-key storage errors so one bad entry can't strand the rest of the snapshot, defers oversized values once rather than parsing them on a zero-budget slice, guarantees forward progress on timeout-fired callbacks, and cancels its scheduled slice when stopped. The profile trim keeps a lazily-initialized memoized key count so the common under-cap write is O(1); the full parse scan runs only when the count exceeds a cap, resyncs if external deletions made it stale, and a failed scan skips the trim instead of aborting the write. Sweep semantics (rules, TTLs, eviction) are unchanged, and tests cover the scheduling, slice-progress, error-isolation, defer-once, and trim short-circuit behaviors. ## 3. The macOS window was never opaque #5478's glass appearance is correctly opt-in at the CSS layer, but the compositor cost was baked in deeper than its native `on_webview_ready` transparency call: the main window is declared `"transparent": true` in `tauri.conf.json` (added for the original glass work in #1671), which makes tao call `NSWindow.setOpaque(false)` at creation and resolve every later `set_background_color(None)` to `clearColor` — and no runtime `setOpaque(true)` path exists through tauri, while wry's runtime background setter can only force the WKWebView's `drawsBackground` off, never back on. So "restore the platform default" was unreachable: every launch, glass or not, ran with a non-opaque NSWindow, defeating WindowServer's opaque-window compositing fast path and forcing full window compositing every frame — compounded by the existing `backdrop-blur` chrome overlapping the scrolling timeline. This matches the compositor-shaped symptoms (scroll and pointer input degrading first). The window is now created opaque (`"transparent": false`) and the NSWindow layer is never made transparent at runtime. Glass never needed a transparent window: behind-window `NSVisualEffectView` vibrancy renders inside opaque windows (this is how Finder and Notes draw vibrant sidebars); it only requires a transparent WKWebView canvas, which the `set_window_vibrancy` enable path already establishes at runtime (`macos-private-api` compiles that in independent of the window flag). Enabling glass installs the vibrancy layer and then makes only the webview canvas see-through; disabling clears the vibrancy layer — the canvas may stay non-drawing afterwards (wry's flag is one-way at runtime), which is harmless because glass-off CSS paints fully opaque above an always-opaque NSWindow. The boot-path first-frame backing writes touch only the NSWindow backing color and are therefore inert to glass state regardless of how they order against the `ThemeProvider`'s vibrancy call on a persisted-glass-on cold boot. Glass-off users (the default) get an end-to-end opaque window from boot for the first time. ## 4. `get_channels`: serial round-trips and a multi-MB payload on every refetch The stale gates in (1) cut refetch frequency; this cuts the cost of the refetches that legitimately remain (boot, and focus returns after more than 5 minutes away — previously still a multi-second stall). `get_channels` made ~8 fully serial relay round-trips (~3.2–3.6s at 1,100+ channels), then shipped the full `ChannelInfo` list — including every channel's member pubkeys — across IPC, where the renderer's `JSON.parse` of the multi-MB payload froze the main thread (the ~1.25s stall captured in the live sample). - **Concurrent stages**: the membership chain, the open-channel directory scan, and the hidden-DM snapshot run concurrently, as do the member-count and last-message queries that follow. The critical path drops from ~8 sequential round-trips to 2 phases. Filters, limits, pagination, and merge semantics are unchanged. - **Not-modified short-circuit**: the command now takes a client-supplied content hash (FNV-1a 64 over the channel list, canonicalized by id and excluding `last_message_at`) and omits the channel list from the response when nothing else changed. Last-message timestamps — which change on nearly every message anywhere — ship as a small separate map that the client overlays onto its cached list with reference preservation, so React Query's structural sharing also skips downstream re-renders. On a typical refocus the renderer parses kilobytes instead of megabytes. The hash is stored in the query cache itself, tying its lifecycle to the data it describes so a community switch can never leak a stale hash. The E2E mock bridge speaks the new payload shape — including the complete `last_messages` map the client treats as authoritative — and hash canonicalization plus overlay reference-preservation are unit-tested on both sides. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
b0795a10ea |
Add Send to channel for thread messages (#5305)
## Summary - Share eligible self-authored or owned-agent thread messages into the parent channel as new top-level messages. - Link the shared message back to the exact root thread with a semantic channel label and excerpt. - Add a dedicated channel-arrow icon plus ownership and navigation coverage. ## Validation - Desktop lint, size, and text guards - Desktop TypeScript build and all 4,543 unit tests - Focused Playwright send-to-channel and thread-link navigation tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
bba3e06386 |
Fix macOS attachment picker lifecycle and allow inert HTML downloads (#5569)
## Problem Canceling the native macOS file chooser leaves the composer's temporary, detached `<input type="file">` without a `change` event or an explicit cleanup path. Opening Finder again immediately creates a second detached input while WebKit may still be unwinding the first picker. The newly selected files can therefore fail to reach the upload pipeline. Drag and drop is unaffected because it bypasses this picker lifecycle. This does **not** add an automatic retry mechanism. “Retry” means the user's next attachment attempt after canceling or after a prior selection. ## Fix - give each composer hook one hidden, body-mounted file input for its lifetime instead of creating a detached one per click - reset and reconfigure that input before every open, replace its handler rather than stacking handlers, and remove it cleanly on unmount - preserve normal selection, cancel then reopen, selecting the same file again, and multi-select behavior - accept canonical `text/html` attachments while continuing to serve and render them strictly as inert downloads - keep XHTML, SVG, JavaScript, and executable MIME types blocked The picker change fixes the ownership/lifecycle bug at its source; it does not retry failed uploads, add delays, or mask errors. ## Testing - mandatory pre-push gate: branch-skew, desktop typecheck/tests/check, Rust tests, and desktop Tauri checks passed on `ea5a97adf957803935b28d63d32f9f332cf65287` - `cargo test -p buzz-media --lib` (110 passed) - `pnpm --dir desktop typecheck` - focused Biome check for the three picker files - picker Playwright regression: cancel/no selection then reopen, select the same file again, and multiple selection (run on the source commit before integration) - HTML live-relay response regression added as ignored E2E because it requires the S3-backed relay harness ## Manual verification Playwright models cancellation with Chromium's `FileChooser.setFiles([])`; it cannot exercise the native macOS Finder panel/WebKit presentation lifecycle. Before merge, manually verify in the built macOS app: 1. select a PNG normally 2. cancel, then immediately reopen and select a PNG 3. select the same PNG on a subsequent attempt 4. multi-select two PNGs --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz> Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
240cdd3ea1 |
chore: mesh upgrade, clean up legacy special case code, simplify model selection for mesh (#5289)
Shared compute now has exactly two model choices: MeshLLM's virtual `mesh` model, or a model you name. Buzz picks between them in one place, and buzz-agent no longer knows meshes exist. ## What changed - **MeshLLM v0.74.0 → v0.75.1.** v0.75.0 added `degrade_to_single_model`, so a `model=mesh` request is answered by one served model when there is no committee to form, instead of failing. v0.75.1 adds Mesh-LLM#1196, which skips stale pre-0.75 runtime cache entries rather than aborting startup on them — without it, anyone who had run mesh on 0.73/0.74 could not start. - **Deleted the client-side mesh catalog probe.** buzz-agent used to poll `/v1/models` (5s TTL, 30s cooldown, two-observation debounce) to decide whether `mesh` was safe to send. MeshLLM now decides per request, so the polling, its hysteresis, and its 503 fallback are gone. - **One mapping point.** `relay_mesh_wire_model()` turns the stored value into a wire name: `auto` becomes `mesh`, a named model passes through. The spawn env, the ACP harness, and the readiness probe all use it, so they cannot disagree — previously `BUZZ_ACP_MODEL` and the probe both said `auto`, a name the mesh does not advertise. - **Removed the `nostr-relay-pool` advisory exception.** #5404 allowed RUSTSEC-2026-0243 "after mesh-llm migrates to nostr-sdk >= 0.45". v0.75.1 does, so the retired crate is gone from both lockfiles and the exception would only mask a future advisory for it. - **Deleted `scripts/ensure-mesh-native-runtime.sh`** and its six justfile call sites. It built llama.cpp from source into the runtime cache; the app already downloads the signed release runtime itself, and CI never called it. ## Why it is better **−639 lines of Rust.** Availability is decided by the node that knows the answer, per request, instead of by a client cache that could be stale for up to 30 seconds. A second worker joining now takes effect on the next request rather than after two confirming probes. ## Behaviour change A 503 on an explicit `mesh` request takes the ordinary transport retry under the same model instead of failing over to a second one — there is no second model to fail over to now. MoA repairs partial committee results internally before it reaches that point. ## Validation `crates/buzz-relay/examples/mesh_agent_e2e.rs` now sends `mesh` where it previously sent `auto` or the physical model id, so no leg was covering what Buzz actually puts on the wire. 4/4 on gemma-4-E4B, gemma-4-26B-A4B, and Qwen3-8B — including a real ACP tool call through `mesh` into buzz-dev-mcp, asserted by reading the written file back off disk. Hand-tested in the desktop app on both gemma-4 sizes: picked Auto, agent logged `model_id=mesh`, replied in channel. ## Not covered A committee that forms and then loses a worker returns 502, and that needs two workers to reproduce — not testable on one machine. --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
7eb8cc5a5f |
fix(link-preview): resolve YouTube videos through oEmbed (#5520)
**Category:** fix **User Impact:** YouTube video links now resolve into reliable previews instead of intermittently appearing as bare links. **Problem:** Buzz intentionally reads at most **256 KiB** of page HTML when building a generic link preview. The YouTube response that exposed this bug was roughly **1.3 MiB**, with its Open Graph metadata beginning around **686 KiB**—well beyond Buzz's bounded read—so extraction returned no usable preview. YouTube can move that metadata between responses, which explains why the same link may appear to work in one build or request and fail in another; raising the generic cap would increase bandwidth and allocation for every site while still scraping an unstable application document. **Solution:** Route recognized YouTube video URLs through YouTube's structured oEmbed endpoint instead of parsing raw watch-page HTML. The provider response is capped at **64 KiB** and retains Buzz's existing HTTPS validation, pinned DNS/SSRF protection, disabled redirects, timeouts, metadata bounds, and thumbnail sanitization. Provider failures return no preview rather than falling back to fragile HTML scraping, and embed URLs are canonicalized safely, including percent-encoded video IDs. <details> <summary>File changes</summary> **desktop/src-tauri/src/commands/link_preview.rs** Recognizes supported YouTube URL forms, fetches bounded JSON metadata from YouTube oEmbed, canonicalizes embed links, and adds response, URL-boundary, malformed-data, resource-limit, and encoded-ID regressions. **desktop/src-tauri/Cargo.toml** Declares percent decoding as a direct desktop dependency for safe embed-ID canonicalization. **desktop/src-tauri/Cargo.lock** Records the direct dependency in the desktop package lock entry. </details> ## Reproduction Steps 1. On the base branch, paste a YouTube URL whose Open Graph metadata falls beyond the first 256 KiB of the raw watch-page response and observe that no preview is produced. 2. Run this branch and paste a YouTube watch, mobile, music, `youtu.be`, Shorts, live, or embed URL into the composer. 3. Confirm the preview resolves with the video's title, creator, and sanitized thumbnail without downloading the full watch-page HTML. 4. Try an embed URL with a percent-encoded ID, such as `https://www.youtube.com/embed/%64Qw4w9WgXcQ`, and confirm it resolves to the same video. 5. Try a YouTube lookalike domain or an embed ID containing encoded separators and confirm it is not routed through the provider path. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
1922d49cb2 |
feat(desktop): adding rich link previews to messages (#3818)
## Overview **Category:** improvement **User impact:** Link previews appear in the composer and travel as privacy-safe sender-authored snapshots, so recipients never contact the linked site merely by opening a conversation. **Problem:** Cold-cache link paste could freeze the composer before the URL painted; recipient-side unfurling leaked visits; invalid or unresolved preview work could interfere with sending or leave dead cards behind. **Solution:** Paint pasted links before starting cold resolver work, resolve only in the sender's composer, attach only complete validated snapshots at Send, and render authored snapshots without recipient fallback fetching. ## Behavior - **Cold paste stays responsive:** bare and angle-bracket URL paste paths commit the visible link before resolver work begins. - **Sender-only fetching:** metadata is resolved while composing; recipients render only the sender-authored snapshot. - **Send never waits:** pending, failed, invalid, and unsendable previews are omitted. They do not block or cancel the message. - **Terminal misses disappear:** failed, timed-out, or 404 resolver results remove the composer card while preserving visible link text. - **Display-text links work:** Markdown links such as `[review the pull request](…)` produce and send the same snapshots as bare URLs. - **Compact and Rich presentation:** Compact remains the default; Rich preserves source description line breaks and paragraphs. - **Immediate draft-wide dismissal:** clicking × immediately hides all previews for the draft, suppresses links pasted later, and emits only `["link-preview", "none"]`. No confirmation detour. Suppression resets after send or clearing the draft. - **Zero recipient fallback:** missing, stale, malformed, off-relay, unsupported, or suppressed snapshots remain ordinary visible links; recipients never regenerate them. ## Implementation - Resolve previews from deferred composer URL state so paste can paint first. - Upload finished preview media to the active community relay and snapshot only valid, sendable media references. - Atomically capture ready snapshots at submit time; never append a late preview after send. - Validate snapshot and suppression tags in desktop/native and relay ingestion, rejecting duplicate or mixed forms. - Render composer previews as stable 55px attachment cards at desktop and narrow widths. - Add deterministic E2E coverage for cold paste, ready/pending/failed/invalid previews, display-text links, multiline Rich descriptions, immediate dismissal, later-pasted links, and suppression reset. ## Validation Validated head: `9807ba8952f190e76153834abf8ab61dd40be5e2` - Push hooks passed: `check-push-org`, branch skew, desktop check, mobile tests, desktop tests, Rust tests, and desktop Tauri checks. - Focused screenshot E2E at the validated head: 5/5 passed across Compact/Rich composer and recipient states, 800px/420px geometry, display-text links, multiline descriptions, and immediate dismissal. - PR CI was triggered for this exact head and is currently running; completed checks are green at the time of this update. - Worktree is clean and both PR head and validated branch resolve to `9807ba895…`. ## Screenshots ### Compact composer | Loading | Ready | |---|---| |  |  | ### Rich composer | Loading | Ready | |---|---| |  |  | ### Responsive composer | 800px loading | 800px ready | |---|---| |  |  | | 420px loading | 420px ready | |---|---| |  |  | ### Recipient presentation | Compact | Rich | |---|---| |  |  | ### Display-text Markdown link | Composer | Recipient | |---|---| |  |  | ### Rich multiline description  ### Immediate dismissal | Before × | Immediately after × | |---|---| |  |  | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
742e8d1197 |
fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
Three pre-existing gaps in the buzz-agent observer feed fixed together
per Will's ruling ("all 3 in the current PR"):
1. **OpenAI/DBv2-GPT route** — `responses_body` never requested
`reasoning.summary`; GPT-family models billed thinking tokens but
returned `summary: []`.
2. **Anthropic/DBv2-Claude route** — `anthropic_thinking_config()` never
sent `thinking.display`; newest Claude models (Opus 5, Sonnet 5, Fable
5, Mythos 5, Opus 4.7/4.8, Mythos Preview) default to
`display:"omitted"`, returning thinking blocks with an empty `thinking`
field — observer rendered nothing.
3. **ACP v2 compliance** — buzz-agent negotiates ACP v2 but emitted
`agent_thought_chunk` and `agent_message_chunk` without `messageId`,
which ACP v2's `ContentChunk` requires (`messageId` + `content` both
required at schema head `d13d1baa`).
## Changes
**`crates/buzz-agent/src/config.rs`**
- New `ThinkingSummary` enum (`Auto`/`Concise`/`Detailed`) with
`BUZZ_AGENT_THINKING_SUMMARY` env var (default `Auto`); mirrors
`BUZZ_AGENT_THINKING_EFFORT` pattern
- `anthropic_thinking_config()` now emits `"display": "summarized"` in
both the adaptive shape and the manual-budget shape whenever thinking is
enabled
- Rewrote `is_adaptive_thinking_model` and `anthropic_thinking_config`
doc comments to match Anthropic's exact three-way per-model terminology
(doc:
https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models):
- Opus 4.6/4.7/4.8, Sonnet 4.6: **Off** — thinking OFF by default;
`type:"adaptive"` required to enable
- Opus 5, Sonnet 5: **On** — thinking on by default, can be disabled; we
still send `type:"adaptive"` to activate `output_config.effort`
- Fable 5, Mythos 5, Mythos Preview: **Always on** — thinking cannot be
disabled; we still send `type:"adaptive"` to activate
`output_config.effort`
**`crates/buzz-agent/src/llm.rs`**
- `responses_body` emits `reasoning.summary` alongside
`reasoning.effort` when effort is set (gated — no bare
`reasoning:{summary}` without effort)
- Covers both the pure-OpenAI Responses path and the DBv2 GPT-family
Responses path
**`crates/buzz-agent/src/agent.rs`**
- `agent_thought_chunk` carries `"messageId":
format!("{run_id}-thought-{round}")`
- `agent_message_chunk` carries `"messageId":
format!("{run_id}-message-{round}")`
- The two IDs are distinct (thought and assistant are two logical
messages per the ACP v2 Message ID RFD)
- `run_id` is a fresh random token per `session/prompt` invocation so
IDs are session-unique across multiple prompts
**`crates/buzz-agent/src/lib.rs`**
- `run_id` plumbed into `RunCtx` (was already generated in `run_prompt`,
just not threaded through)
**`crates/buzz-agent/tests/golden_transcripts.rs`**
- `test_acp_v2_chunks_carry_message_id` — negotiates v2, drives two
consecutive `session/prompt` calls, asserts: both chunk types carry
non-empty `messageId`; thought and message IDs are **distinct**; IDs do
**not** recur across the two prompts in the same ACP session
**`desktop/src-tauri/src/managed_agents/env_vars.rs`**
- `BUZZ_AGENT_THINKING_SUMMARY` added to `is_safe_to_reveal` allowlist
**`desktop/src-tauri/src/commands/agent_config_tests.rs`**
- Tests for `BUZZ_AGENT_THINKING_SUMMARY` allowlist entry
(case-insensitive)
## Tests added
- `parse_thinking_summary_round_trips_all_values`
- `parse_thinking_summary_unset_and_empty_yield_auto`
- `parse_thinking_summary_is_case_insensitive`
- `parse_thinking_summary_rejects_unknown_value`
- `thinking_summary_as_str_mapping`
- `responses_body_summary_present_iff_effort_set`
- `responses_body_emits_configured_summary_mode`
- `responses_body_concise_summary_mode`
- `anthropic_thinking_config_adaptive_emits_display_summarized`
- `anthropic_thinking_config_manual_budget_emits_display_summarized`
- `test_acp_v2_chunks_carry_message_id` (integration test — two-prompt
cross-session case)
## Notes
- **DBv2 gateway parity for `display`**: unverified — the DBv2 Claude
route proxies Anthropic Messages shape, but whether the gateway passes
`thinking.display` through is not confirmed. Flagged here rather than
blocking on it.
- buzz-acp and Desktop TS are unchanged — they already parse `messageId`
as optional and will pick it up from the wire automatically.
- Chat Completions and OpenRouter paths: untouched.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
769ac70b74 |
fix(media): require authenticated reads (#4610)
This change requires a valid signed Blossom authorization request and current relay membership for every media GET and HEAD request. It removes the unauthenticated compatibility path and updates desktop reads to send the required authorization. This blocks anonymous retrieval and access after relay-membership revocation. It does not yet bind a blob to its originating channel, so someone removed from a private channel can still read a known blob while remaining a relay member. That channel-ACL follow-up remains required before closing the full finding. ## Testing - `git diff --check origin/main...codex/security-media-read-auth` - Rebased onto `origin/main` at `5c98932` - Full CI pending Originating Buzz thread: `buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1` --------- Signed-off-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> Signed-off-by: Eli Foster <efoster@squareup.com> Co-authored-by: Eli Foster <efoster@squareup.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6eb65919f1 |
feat(identity): recover desktop identity from a signed-in phone (#4845)
**Category:** new-feature **User Impact:** People who lose a desktop identity can securely restore it from a signed-in Buzz phone without creating a replacement identity. **Problem:** A fresh or identity-lost desktop could not recover its existing full Buzz identity from an already-authorized phone. **Solution:** Add a SAS-confirmed reverse NIP-AB transfer, durable desktop import, a dedicated mobile recovery entry point, and clearer desktop recovery dialogs with tested loading, drag-and-drop, and failure states. https://github.com/user-attachments/assets/e9215c9c-80d0-462f-9161-0fa184ca2f74 <details> <summary>File changes</summary> **crates/buzz-core/src/pairing/session.rs** Adds the reverse encrypted payload and source-completion state transitions used for phone-to-desktop recovery. **desktop/src-tauri/src/commands/identity.rs** Exposes the existing guarded identity commit path for recovery imports. **desktop/src-tauri/src/commands/pairing.rs** Adds recovery-mode pairing, durable nsec import, start serialization, stale-task protection, and explicit rejection of unsupported recovery payloads. **desktop/src-tauri/src/lib.rs** Registers the recovery pairing command. **desktop/src/app/App.tsx** Refreshes the recovered identity before continuing onboarding. **desktop/src/features/onboarding/machineOnboarding.ts** Adds recovery transitions to the onboarding state machine. **desktop/src/features/onboarding/ui/BackupPasswordTimeline.tsx** Adds the visual backup-to-password-to-unlock progression. **desktop/src/features/onboarding/ui/IdentityRecoveryPairing.tsx** Implements QR generation, copy fallback, SAS confirmation, cancellation, expiry, and completion UI. **desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx** Connects private-key, phone, and backup recovery paths to the onboarding flow. **desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx** Polishes recovery dialogs, backup drag-and-drop, loading stability, and security copy. **desktop/src/shared/api/tauri.ts** Keeps the existing pairing API surface focused on standard desktop-to-mobile pairing. **desktop/src/shared/api/tauriPairing.ts** Adds the recovery pairing invoke without growing the ratcheted shared API file. **desktop/src/testing/e2eBridge.ts** Mocks recovery pairing commands and lifecycle events for browser tests. **desktop/tests/e2e/identity-lost.spec.ts** Covers lost-identity entry, QR/copy recovery, SAS, cancellation, expiry, success, errors, backup import, drag-and-drop, and screenshots. **desktop/tests/e2e/onboarding.spec.ts** Verifies recovered identities continue through harness setup without replacement-key side effects. **mobile/lib/features/pairing/pairing_page.dart** Adds recovery-only scanning and explicit identity-handoff warnings. **mobile/lib/features/pairing/pairing_provider.dart** Recognizes recovery codes, returns the signed-in nsec after mutual SAS approval, and waits for desktop completion. **mobile/lib/features/settings/settings_page.dart** Accepts the recovery route builder at the app composition boundary to preserve feature isolation. **mobile/lib/features/settings/settings_page/connection_section.dart** Adds the signed-in “Send identity to desktop” settings action. **mobile/test/features/pairing/pairing_page_test.dart** Covers recovery-only validation and handoff messaging. **mobile/test/features/pairing/pairing_provider_test.dart** Covers reverse payload encryption, confirmation ordering, success, failure, timeout, and cleanup. </details> ## Reproduction steps 1. Launch Buzz Desktop with identity-lost state and choose **Recover from your phone**. 2. Confirm the QR and persistent **Copy pairing code** fallback appear without layout shift. 3. On a signed-in phone, open **Settings → Send identity to desktop**, scan or paste the recovery code, and compare the six-digit SAS on both devices. 4. Confirm on both sides and verify Desktop restores the identity and continues to harness setup. 5. Repeat from identity-lost state with **Recover from a backup file**; verify picker and drag-and-drop both advance to password entry and restore the encrypted backup. 6. Exercise cancellation, mismatched/unsupported codes, expired sessions, and an invalid backup; verify each returns actionable, non-stuck UI. ## Screenshots ### Desktop phone recovery — complete flow | Recovery entry | Pairing QR | Code match | Receiving identity | |---|---|---|---| |  |  |  |  | ### iOS Simulator — complete handoff flow | Settings entry | Recovery scanner | Manual recovery code | Code confirmation | |---|---|---|---| |  |  |  |  | ### Encrypted backup recovery — adjusted file flow | File picker | Drag-and-drop target | Password step | |---|---|---| |  |  |  | ## Verification - `cargo test -p buzz-core pairing` — 71 passed - `just mobile-test` — 1,169 passed - `pnpm build:e2e && pnpm exec playwright test identity-lost.spec.ts --project=smoke` — 15 passed - Full pre-push gates — desktop checks, desktop unit tests, Rust tests, Tauri checks, and mobile tests passed --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
16cc3de6d6 |
fix(desktop): enforce owner-only access in internal builds (#4053)
## Problem Managed agents in internal Buzz builds should answer only their owner. Previously, an agent could keep a broader access setting and respond to other people, which did not match the access policy for internal builds. This PR makes owner-only access effective for every managed agent in internal builds and makes that restriction clear in the Desktop UI. Open source builds remain configurable. ## Changes - Enforce owner-only access when any managed agent starts or is deployed from an internal build. - Show the agent access control as locked to **Only me** in Desktop, with an explanation of why it cannot be changed. - Keep Welcome teammates working under the same rule without triggering unnecessary restarts. - Leave open source build behavior unchanged. This changes effective runtime access without rewriting stored or relay-advertised settings. The companion [#4064](https://github.com/block/buzz/pull/4064) explains the restriction in-thread when someone without access mentions an agent. The enforcement will remain inactive in shipped builds until [squareup/buzz-releases#74](https://github.com/squareup/buzz-releases/pull/74) marks internal releases during the build. ## Screenshots | Before | After | | --- | --- | |  |  | ## Tests Added coverage for: - Runtime enforcement for [locally run agents](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src-tauri/src/managed_agents/runtime/tests.rs#L196) and [deployed agents](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src-tauri/src/commands/agents_tests.rs#L510). - The [current-build deployment path](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src-tauri/src/commands/agents_tests.rs#L455), [invalid stored access](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src-tauri/src/managed_agents/access_policy.rs#L98), and the [local startup guard](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src-tauri/src/managed_agents/env_vars/tests.rs#L149). - Consistent enforcement across [both agent backends](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src-tauri/src/managed_agents/access_policy.rs#L112). - Welcome teammates created as [locally run](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src/features/onboarding/welcomeGuide.test.mjs#L384) or [deployed](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src/features/onboarding/welcomeGuide.test.mjs#L393) agents, including [access-only](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src/features/onboarding/welcomeKickoff.test.mjs#L202) and [runtime-related](https://github.com/block/buzz/blob/e0165f52b52741a74184c9899e2b51eeec40c939/desktop/src/features/onboarding/welcomeKickoff.test.mjs#L225) restart behavior. The full Desktop Rust and JavaScript suites, type checks, formatting, clippy, and file-size checks passed. Playwright E2E was not run. --- Originated from Buzz channel [buzz-agent-control](buzz://channel?id=cf5dada7-e26a-4887-ae41-b3bd5f42d3b2). Supersedes #2537. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Signed-off-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz> Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz> Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
719f9730d4 |
feat(desktop): allow leaving your final community (#3621)
**Category:** improvement **User Impact:** People can leave their final Buzz community and return to **Join or create a community** without losing their signed-in identity. **Problem:** Buzz Desktop blocked people from leaving when only one community remained. Its existing remove action also changed local configuration without ending relay membership. **Solution:** Allow the final community to be left. Buzz now asks the relay to end membership, removes the community locally only after acceptance, and returns the person to the community selector while keeping their identity signed in. If other communities remain, Buzz switches to one of them. Relay rejection or timeout keeps the community in place and shows an actionable retry error. <details> <summary>File changes</summary> **desktop/src/features/communities/leaveCommunity.ts** Adds signed kind 28936 publishing for active and inactive community relays with actionable timeout handling. **desktop/src/features/communities/leaveCommunity.test.mjs** Covers event shape, relay selection, acceptance gating, rejection, timeout messaging, and cleanup. **desktop/src/features/communities/useCommunities.tsx** Allows final-community removal and clears community-specific storage without touching identity. **desktop/src/features/communities/resolveCommunityRemoval.test.mjs** Covers final, active, and inactive community removal state transitions. **desktop/src/app/useCommunityNavigationTransitions.ts** Gates local removal on relay acceptance and routes to a fallback community or setup selector. **desktop/src/app/AppShell.tsx** Passes the asynchronous leave operation through shell entry points. **desktop/src/features/communities/ui/EditCommunityDialog.tsx** Replaces the local-only remove action with a pending-aware Leave Community action that retains actionable errors. **desktop/src/features/communities/ui/CommunitySwitcher.tsx** Enables leaving the final community and carries the asynchronous callback. **desktop/src/features/sidebar/ui/AppSidebar.tsx** Carries the asynchronous leave callback through sidebar props. **desktop/src/features/sidebar/ui/CommunityRail.tsx** Enables leaving the final community from rail settings. **desktop/src/features/sidebar/ui/SidebarProfileCard.tsx** Carries the asynchronous leave callback through profile community settings. **desktop/src/testing/e2eBridge.ts** Teaches the mock relay to accept NIP-43 leave events. **desktop/tests/e2e/community-rail.spec.ts** Updates leave interactions and verifies final-community setup navigation, storage cleanup, and identity preservation. </details> ### Reproduction steps 1. Run Buzz Desktop with a signed-in identity and one joined community. 2. Open Community settings and choose **Leave Community**. 3. Confirm the app shows **Join or create a community** and the existing identity remains signed in. 4. Repeat with two communities and confirm leaving the active one switches cleanly to the remaining community. 5. Reject or withhold the relay `OK` response and confirm the community remains configured with an actionable error in the dialog. ### Test plan - `pnpm check` - `pnpm build` - `pnpm test` (3,913 passing) - `pnpm build:e2e && pnpm exec playwright test tests/e2e/community-rail.spec.ts --grep "final community"` <img width="557" height="316" alt="image" src="https://github.com/user-attachments/assets/b628182f-cba5-451d-ae4b-bee8d8dd19aa" /> --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
7334ad1e16 |
fix(desktop): route macos notification clicks (#4799)
## Summary - deliver macOS notifications through `UNUserNotificationCenter` - route notification clicks to the referenced channel or thread through the existing frontend activation path - preserve click targets across cold startup and frontend remounts with a small process-wide activation queue - keep Linux notification activation behavior unchanged ## Architecture A single `UNUserNotificationCenterDelegate` is installed during Tauri setup. Each notification stores its navigation target in `userInfo`. On click, Rust queues the target before emitting a wake-up event; the frontend atomically drains the queue and dispatches the existing notification action. The queue is the source of truth, which prevents cold-start loss and duplicate delivery. ## Validation Verified at `a81241611d617becf7640bee6fe56b5cdb4d0fab`: - Biome format/check and lint - TypeScript typecheck - repository and desktop-Tauri `cargo fmt --check` - repository and desktop-Tauri Clippy with `-D warnings` - full pre-push desktop tests and Tauri workspace checks/tests - desktop production build Manual macOS validation passed: after explicitly ad-hoc signing the local bundle with `xyz.block.buzz.app`, the operator confirmed real Notification Center delivery and click navigation. <details> <summary>Local macOS test procedure</summary> Tauri's generated ad-hoc signing identifier is not accepted by `UNUserNotificationCenter`. Re-sign the local bundle with its bundle identifier and keep other Buzz copies closed: ```bash just desktop-release-build APP="$HOME/.cache/cargo-target/aarch64-apple-darwin/release/bundle/macos/Buzz.app" codesign --force --deep --sign - \ --identifier xyz.block.buzz.app \ --entitlements desktop/src-tauri/Entitlements.plist \ "$APP" codesign --verify --deep --strict --verbose=2 "$APP" pkill -x buzz-desktop || true open -n "$APP" ``` </details> Buzz channel: `55e2bfca-1b38-48fb-9dc2-584d400501f3` --------- Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
6c40ce394f |
feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
OpenClaw connects to a single shared Gateway daemon. Spawning the
default 10 ACP workers per agent is both resource-expensive and
architecturally wrong — each worker opens a separate gateway connection.
Tyler's ruling: cap at 5, lower if needed.
## Contract
Store the requested value (1–32) verbatim at every persistence and wire
boundary. Apply `effective = min(requested, harness_cap)` only at the
four enforcement points:
| Boundary | Implementation |
|---|---|
| Local spawn | `BUZZ_ACP_AGENTS` env var in child `Command` |
| Remote deploy | `launch.policy_env["BUZZ_ACP_AGENTS"]` + legacy
`parallelism` field |
| Restart badge | `SpawnConfigSnapshot.parallelism` stores effective
value; the diff surface displays what actually runs |
| UI copy | Amber hint when requested > cap; no `max` attribute, no
save-path clamp |
`BUZZ_ACP_AGENTS` is added to `RESERVED_ENV_KEYS` — the Desktop resolves
the effective value into `policy_env`; a user-supplied override in `env`
would bypass the cap and is silently stripped.
## Changes
**`managed_agents/parallelism.rs`** (new) — policy core:
- `OPENCLAW_MAX_PARALLELISM = 5`
- `harness_max_parallelism(command)` — keyed on
`normalize_command_identity` so path prefixes, `.exe` suffixes, and
other cosmetic differences are ignored
- `effective_parallelism(command, value)` — identity for uncapped
harnesses
- `acp_agents_value(command, parallelism)` — `env("BUZZ_ACP_AGENTS", …)`
helper
**`runtime.rs`** — spawn clamp: `BUZZ_ACP_AGENTS =
acp_agents_value(effective_command, record.parallelism)`
**`agents_deploy.rs`** — deploy egress clamp: `build_deploy_payload`
resolves `effective_parallelism` once from `descriptor.command`; both
`launch.policy_env["BUZZ_ACP_AGENTS"]` and the legacy top-level
`parallelism` field use that value — the two are always consistent
regardless of stale `record.agent_command` pins
**`spawn_snapshot.rs`** — `from_inputs` stores
`effective_parallelism(&descriptor.command, record.parallelism)` in the
`parallelism` field. Over-cap edits that don't change the pool (e.g. 10
→ 8, both clamp to 5 on OpenClaw) produce equal snapshots; cap crossings
(8 → 3) produce different snapshots.
**`AcpRuntimeCatalogEntry.max_parallelism: Option<u32>`** — derived from
the static definition command, not the probed `entry.command` (which may
be `null` for unavailable entries), so unavailable OpenClaw entries
still carry the cap. Propagated through all four catalog constructors
(builtin discovery, preset catalog construction, custom discovery,
custom-save response), IPC types
(`RawAcpRuntimeCatalogEntry.max_parallelism`), and the frontend catalog
type.
**UI** — `EditAgentAdvancedFields` and `PersonaAdvancedFields` show an
amber hint when `selectedRuntime.maxParallelism` is set and the current
value exceeds it. Cap and label come from the catalog entry — no
hardcoded 5 in TS. No `max` attribute on inputs; the input stays
`type="text"` with 1–32 copy.
**Docs** — `docs/remote-agents.md`: `BUZZ_ACP_AGENTS` moved from the
deliberately-non-reserved section to reserved; new contract documented.
`desktop/src/features/agents/AGENTS.md`: command-keyed execution policy
documented as the sanctioned second metadata source feeding the catalog
projection.
## Tests
**Rust** (`parallelism.rs`):
- `policy_table` — `harness_max_parallelism` and `effective_parallelism`
across all openclaw variants and uncapped harnesses
- `acp_agents_value_openclaw_above_cap_is_capped` — spawn-env seam
- `override_direction_*` — both override directions (openclaw runtime +
goose override; goose runtime + openclaw override)
- `summary_persona_inherited_*` — live persona wins over stale
`agent_command`
- `snapshot_export_carries_requested_definition_parallelism` — requested
value travels wire/sync unchanged
**Rust** (`spawn_snapshot/tests.rs`):
- `openclaw_above_cap_parallelism_snapshots_equal` — stored 10 vs 8,
both clamp to 5 → snapshots equal
- `openclaw_cap_crossing_parallelism_snapshots_differ` — 8 (clamps to 5)
vs 3 → snapshots differ
**Rust** (`discovery/presets.rs`):
- `openclaw_preset_unavailable_carries_max_parallelism` /
`openclaw_preset_available_carries_max_parallelism` — catalog metadata
present with `command: null` and with a resolved path
**Rust** (`agents_deploy.rs`):
- `launch_block_openclaw_over_cap_policy_env_is_capped` — direct
`launch.policy_env` seam
-
`deploy_payload_json_stale_goose_record_live_openclaw_descriptor_both_capped`
— stale `record.agent_command=goose`, live descriptor=openclaw: both
fields cap to 5
-
`deploy_payload_json_stale_openclaw_record_live_goose_descriptor_both_uncapped`
— stale `record.agent_command=openclaw`, live descriptor=goose: both
fields pass through requested
- `deploy_payload_json_explicit_openclaw_override_both_capped` —
explicit `agent_command_override=openclaw`: both fields cap to 5
**Rust** (`persona_events/stale_pin_tests.rs`):
- `apply_persona_snapshot_goose_to_custom_harness_drops_stale_goose_pin`
— custom-direction stale-pin drop (builtin pin → loaded custom harness
via `update_loaded_harness_registry`)
**TypeScript** (`agentParallelism.test.mjs`):
- `parallelismCapHint` — at/below cap (null), above cap (hint includes
label and cap value), singular form for cap=1, uncapped harness (null)
**TypeScript** (`tauri.test.mjs`):
- `fromRawAcpRuntimeCatalogEntry` round-trips `max_parallelism` →
`maxParallelism`; absent when `undefined`
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
25a9cf1be6 |
feat: paste composer text without formatting (#4801)
## Summary - handle Cmd+Shift+V on macOS and Ctrl+Shift+V on Windows/Linux in the message composer - read plain text through the native Tauri/arboard clipboard path in packaged builds, with a browser-only Clipboard API fallback - re-enter ProseMirror's paste pipeline with populated `text/plain` clipboard data so selection, undo, multiline behavior, and paste observers remain intact - cover both platform mappings with rendered composer E2E tests that assert the native command path ## Testing - `pnpm test` — 4,286 passed - `pnpm check` - `pnpm typecheck` - `pnpm exec playwright test composer-selection-formatting.spec.ts --project=smoke` — 26 passed - `cargo check --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets --target aarch64-apple-darwin` - `just desktop-tauri-test` — 2,206 core tests plus integration and doc-test groups passed - full pre-push hooks passed ## Manual verification Physical packaged-app clipboard verification remains recommended on macOS, Windows, and Linux. The automated E2E uses mocked Tauri IPC but asserts the native `read_clipboard_text` command is invoked. Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
4a2305170e |
fix: reauthenticate databricks model discovery (#4008)
## Summary - preserve Databricks catalog 401 responses as authentication failures and retry discovery exactly once after silently refreshing the rejected bearer - preserve runtime OAuth recovery: when discovery has no usable OAuth credential, `session/new` succeeds with only the trimmed configured model so the first `session/prompt` can run the existing browser PKCE flow - reject a rejected configured `DATABRICKS_TOKEN` with actionable, non-interactive guidance; static credentials cannot recover through PKCE - use the configured-model fallback for non-auth discovery failures without caching failed or fallback catalogs, so later sessions retry discovery - keep known Databricks v2 models only for authenticated empty-catalog responses and mark their provenance - resolve discovery before MCP spawn or session registration, preventing failed discovery from leaking resources or consuming session capacity - permit serialized interactive PKCE only from the explicit saved-agent model picker; passive draft discovery never opens a browser ## Runtime flow 1. OAuth discovery attempts cached credentials and silent refresh without opening a browser. 2. If no usable OAuth bearer exists, `session/new` advertises only the configured model and succeeds. 3. The first `session/prompt` uses `TokenSource::bearer()`, which may launch browser PKCE. 4. A later session retries discovery and caches only the authenticated catalog. ## Regression coverage - rejected-but-locally-fresh OAuth bearer performs one refresh and one catalog retry - OAuth mode with no cached token allows `session/new` and returns exactly the trimmed configured model - the OAuth fallback is not cached; a later authenticated session retries discovery and caches the returned catalog - rejected static tokens still reject `session/new` - failed discovery does not consume the sole session slot or spawn the supplied MCP process - Desktop interactive/passive auth intent, static-token redaction, and authenticated empty-catalog provenance ## Verification - `cargo test -p buzz-agent` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib commands::agent_models` - `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - `cargo fmt --all -- --check` - `git diff --check` - full pre-push hooks ## Review Adversarial review found and drove fixes for session/MCP resource leakage, duplicate concurrent PKCE flows, sensitive error propagation, incorrect 403 reauthentication, missing discovery-level coverage, passive browser launch, and the Desktop file-size ratchet. The final follow-up preserves the existing prompt-time OAuth flow while retaining static-token rejection and pre-allocation discovery ordering. --------- Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
e30db7028f |
feat(projects): support multiple repositories (#4671)
## Summary - adopt the finalized NIP-MP project model so one project can enumerate and switch between multiple NIP-34 repositories - add project and repository navigation, activity summaries, existing-repository attachment, and repository access-channel management - preserve privacy-safe activation provenance for agent-authored patches, pull requests, issues, and associated commits ## Test plan - [x] Run desktop typecheck and unit tests - [x] Run focused NIP-MP, repository access, and provenance tests - [x] Run Rust formatting and desktop lint checks - [x] Run the complete pre-push suite after merging current `main` - [ ] Manually verify project creation, repository attachment, switching, and access repair on staging - [ ] Manually verify public-channel and private-agent origin labels on newly created Git activity Related: [#4695](https://github.com/block/buzz/pull/4695) --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
5179726737 |
fix(local-archive): default both archive settings to enabled (#4750)
## Overview
Both local archive settings — "Archive my agents' observer frames" (kind
24200) and "Archive my agents' turn metrics" (kind 44200) — previously
defaulted to OFF in OSS builds, controlled by build-time env vars. This
had an irreversible cost: observer frames are ephemeral (not stored by
the relay), so any missed events are permanently unrecoverable. This PR
makes both settings default to enabled for all builds and removes the
build-time flag machinery entirely.
## What changed
### Rust
- `observer_archive_default_enabled()` — returns `true` unconditionally;
removed `option_env!("BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT")`
check and `nest_is_dev()` runtime fallback.
- `agent_metric_archive_default_enabled()` — returns `true`
unconditionally; removed
`option_env!("BUZZ_DESKTOP_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT")` check
and its OSS-build test.
- `build.rs` — removed both `rerun-if-env-changed` declarations
(`BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT`,
`BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT`) and the two baked-env
emitting blocks.
### Build / CI
- `Justfile` — removed `desktop-tauri-test-compiled-flags` recipe (the
dual-compile test machinery).
- `.github/workflows/ci.yml` — removed the "Desktop Tauri compiled-flag
verification" CI step.
### TypeScript
- `useObserverArchiveSeed.ts` — removed `observerArchiveDefaultEnabled`
dep from `ObserverArchiveSeedDeps` and the `policyOn` gate in
`reconcileObserverArchive`; the function now unconditionally calls
`mergeSaveSubscriptionKinds`.
- `useAgentMetricArchiveSeed.ts` — removed
`agentMetricArchiveDefaultEnabled` dep from `AgentMetricArchiveSeedDeps`
and the `defaultOn` flag-check path in `maybeSeed`; the
`hasExplicitChoice` guard is preserved as the sole gate against
re-seeding.
- `LocalArchiveSettingsCard.tsx` — removed `policy` prop,
`observerPolicy` state, and `observerArchiveDefaultEnabled` fetch from
`ObserverArchiveSection`; toggle is now always enabled (just `toggling`
disables it); removed the stale "Always on for internal builds" copy
branch; removed the `observerPolicy !== false` guard from
`handleObserverToggle`.
- `tauriArchive.ts` — updated JSDoc on both default-enabled functions to
reflect always-true.
- `e2eBridge.ts` — changed both mock defaults from `?? false` to `??
true` so E2E tests without an explicit mock override exercise the real
default behavior.
### Tests
- `useObserverArchiveSeed.test.mjs` — replaced `policyOn` dep with
direct merge dep; updated `test_oss_policy_off_no_merge` →
`test_reconcile_always_seeds_24200`; all cancellation, identity-switch,
and ordering tests adapted.
- `useAgentMetricArchiveSeed.test.mjs` — removed `defaultOn` dep and
`test_oss_build_does_not_seed`; updated
`test_internal_build_unset_seeds_*` → `test_default_enabled_*`;
`hasExplicitChoice` guard tests unchanged.
## Preservation of explicit opt-outs
Users who have previously toggled the setting off are unaffected:
- `useAgentMetricArchiveSeed` skips seeding when
`hasExplicitChoice(pubkey)` returns true (localStorage-persisted per
identity).
- Observer archive reconciliation now unconditionally calls
`mergeSaveSubscriptionKinds`, but a user who already deleted the
subscription can turn it off via the Settings toggle, which calls
`removeSaveSubscriptionKind` — this is the existing explicit opt-out
path, and the toggle is now always enabled (not locked by a policy
flag).
## Result
- No `BUZZ_BUILD_*_ARCHIVE_DEFAULT` /
`BUZZ_DESKTOP_BUILD_*_ARCHIVE_DEFAULT` references remain outside
CHANGELOG/history.
- Desktop node tests: 4168 pass, 0 fail.
- `just desktop-tauri-check`: clean.
- `just desktop-tauri-test`: all pass.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
f86dfc5883 |
feat(desktop): surface config diff in restart-required badge (#3637)
The "Restart required" badge reports that an agent's running config has
drifted from its spawn-time config, but never says what changed. This
ships the full feature: a typed Rust diff engine and a TS/UI layer that
renders it at every badge site.
## Rust core (spawn-snapshot diff engine)
Replaces the lossy `u64` `spawn_config_hash` with a typed
`SpawnConfigSnapshot`. The snapshot is stamped from the already-resolved
command/env/config values immediately before `spawn()`, closing the race
window where a mid-spawn config edit would suppress the badge.
`SpawnConfigSnapshot::canonical()` is the single JSON projection shared
by the badge and the diff. Drift is `to_value(stamped) !=
to_value(current)`; the diff is a generic leaf walk over those same two
values, so badge-on and diff-non-empty are structurally guaranteed.
Adding a snapshot field reaches the UI with no code change to the diff
engine — `mutation_table_covers_every_serialized_field` fails CI if a
new field arrives without a mutation row.
`eligible_restart_diff(persona_orphaned, Option<TrackedSpawnState>)`
returns the final vector — snapshot walk entries plus a synthetic
`adapter_availability` entry. It returns empty for an orphaned instance
(spawning one would fail) and for agents with no tracked spawn state
(never stamped, can never have drifted). `needs_restart =
!restart_diff.is_empty()` derives from that vector and nothing else.
Redaction policy (`policy_for(path)`) is shared by the wire diff and the
snapshot's manual `Debug` via `is_safe_to_reveal()` from
`managed_agents::env_vars` as the single authority for env-key masking:
| Policy | Paths | Rendering |
|---|---|---|
| `Text` | `system_prompt`, `team_instructions` | character counts only
|
| `MaskedBare` | `args`, `relay_url` | `••••`, no suffix |
| `MaskedSuffix` | non-allowlisted `env.*` | `••••` + last 4 chars when
longer than 8 |
| `Plain` | allowlisted `env.*` (`BUZZ_AGENT_THINKING_EFFORT`,
`BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL`, `DATABRICKS_HOST/MODEL`) and
everything else | verbatim |
Default-deny: every env key not in the explicit allowlist stays masked.
`is_safe_to_reveal()` is the single allowlist authority for both the
baked-env display and the diff.
`restart_diff` is omitted from the wire when empty
(`skip_serializing_if`).
## TypeScript / UI layer
New `restartDiff.ts` module defines `RestartDiffEntry`, `RestartChange`,
`JsonValue`; `tauri.ts` and `types.ts` re-export and add `restart_diff`
/ `restartDiff` fields (Rust omission → `restartDiff: []`).
**`RestartDiffBadge`** — hover tooltip capped at 6 entries + "and N
more", `asChild` span trigger (never inside a `<button>`), auto-restart
blurb below the diff list (on/off variant from `autoRestartEnabled`
prop; same `AUTO_RESTART_ON_BLURB` / `AUTO_RESTART_OFF_BLURB` constants
shared with the Runtime-tab banner). **`RestartDiffList`** renders the
full uncapped list for the Runtime-tab banner with `tooltip`/`inline`
presentation variants for correct foreground in both surfaces.
**`ManagedAgentRow` B4 fix** — badge moved to a sibling `div` of the row
expansion button; tooltip trigger has no `button` ancestor.
**`UnifiedAgentsSection`** — both badge sites render
`<RestartDiffBadge>` instead of a raw `<Badge>`, with
`autoRestartEnabled` threaded from `agent.autoRestartOnConfigChange`.
**Side-panel fix** — `RestartDiffBadge` rendered tab-independently in
the `ProfileSummaryView` hero area (was Runtime-tab only — root cause of
the ~50% inconsistency Will reported). Hero badge is `self-center` in
the flex column. `ProfileRuntimeTabContent` early-return checks
`needsRestart` so the banner is never dropped when all other content is
empty. Auto-restart blurb in the Runtime-tab banner uses the shared
constants.
## Wire shape
```jsonc
"restart_diff": [
{ "field": "model", "change": { "kind": "value", "before": "gpt-5", "after": "claude-4" } },
{ "field": "system_prompt", "change": { "kind": "text", "before_chars": 1234, "after_chars": 1410 } },
{ "field": "env.OPENAI_API_KEY", "change": { "kind": "masked", "before": "••••bc12", "after": "••••xyz9" } },
{ "field": "env.BUZZ_AGENT_THINKING_EFFORT", "change": { "kind": "value", "before": "medium", "after": "high" } }
]
```
`added`/`removed` occur only for dynamic-map keys; nullable struct
fields always serialize as `null`; arrays are atomic leaves (`args`,
never `args.0`).
## Tests
**Rust** — 1902 passing: snapshot mutation coverage, diff entry
serialization, allowlist-aware env masking
(`allowlisted_env_key_shows_plain_value`,
`allowlisted_env_key_is_case_insensitive`,
`non_allowlisted_env_key_stays_masked`),
`unstamped_agent_yields_no_badge_and_no_entries` (both orphan values),
`summary_without_drift_omits_restart_diff_from_the_wire`,
`unstamped_availability_is_not_drift`. Clippy clean, fmt clean.
**TypeScript** — `needs-restart-screenshots.spec.ts`: 11 E2E cases
registered in the smoke project — all three badge sites, tooltip +
keyboard focus, DOM no-button-ancestor assertion, 6+1 truncation,
uncapped Runtime list, unknown field humanisation, side-panel badge on
default Info tab, inactive/friendly-error Runtime opening path.
Consolidates [#3652](https://github.com/block/buzz/pull/3652)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
f18a9cb106 |
Defer desktop media uploads until send (#4522)
## Summary - send desktop messages immediately while media uploads continue in background state across channel navigation - show immediate progress above the composer and keep Jump to latest above it - report the real media stages as Preparing, Processing, Converting, Uploading, and Finishing - use Buzz's shared spinner during local media work, then switch to the real percentage when byte transfer begins - animate phase-label and status-suffix changes without overlap or layout jumps - keep cancel, progress fill, message publication, and community-reset behavior coordinated with the background task - use raw Tauri IPC for large browser files so renderer-side byte serialization does not block initial feedback ## Why Desktop previously blocked sending while attachments uploaded in the composer. Large videos could also pause the renderer before progress appeared, and the progress pill said Uploading while native media processing was still underway. This makes the initial response immediate and describes the work actually happening. ## Validation - `cd desktop && pnpm check` - `cd desktop && pnpm typecheck` - `cd desktop && pnpm test` (3,931 passed) - `cd desktop && pnpm exec vite build --mode e2e` - `cd desktop && pnpm exec playwright test tests/e2e/file-attachment.spec.ts --project=smoke` (11 passed) - focused native media tests (80 passed) - native Clippy with all targets and features - pre-push native suite (2,107 passed, 14 ignored; 3 diagnostics passed) Updated phase snapshots are included in the PR comments. Split from #4512 so the desktop and mobile changes can be reviewed independently. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
985cdcc6ea |
feat(agents): model-tuning parity in global Agent Defaults editor (#4578)
## Overview
The global Agent Defaults surface (Settings card, defaults modal,
onboarding) exposed structured controls for Effort but left Max Output
Tokens, Context Limit, and Max Rounds as raw env vars. Per-agent dialogs
had structured numeric fields but only for `isBuzzAgentRuntime` —
incorrectly excluding Goose. This PR unifies numeric-tuning capability
across all surfaces, fixes a pre-existing dual-editor defect, and adds
full test coverage.
## What changed
### Phase 1 — Catalog projection
- Add `max_rounds_env_var` to `KnownAcpRuntime` in `runtime_metadata.rs`
(`Some("BUZZ_AGENT_MAX_ROUNDS")` for buzz-agent, `None` elsewhere).
- Project all three numeric env-var fields (`max_tokens_env_var`,
`context_limit_env_var`, `max_rounds_env_var`) end-to-end:
`AcpRuntimeCatalogEntry` Rust struct, TS `types.ts`,
`RawAcpRuntimeCatalogEntry` + `fromRawAcpRuntimeCatalogEntry` in
`tauri.ts`, and the e2e mock bridge (`withMockRuntimeConfigMetadata`).
### Phase 2 — Field model
- `deriveAgentConfigFieldModel` now derives `maxOutputTokens` /
`contextLimit` / `maxRounds` descriptors from catalog-projected fields.
- `structuredEnvKeys(descriptors)` — exported helper that takes the
**rendered** descriptor set (not the whole model). Hidden keys follow
what is actually rendered per surface: global hides effort + all three
numeric keys for buzz-agent / two for Goose; per-agent buzz-agent hides
effort + three numeric keys; per-agent Goose hides only its two numeric
keys. `BUZZ_AGENT_THINKING_EFFORT` stays a visible generic env row
per-agent because no effort control renders there.
### Phase 3 — UI
- Extract `NumericTuningFields` from `buzzAgentModelTuningFields.tsx` as
a shared descriptor-driven component (`descriptors`, `envVars`,
`inheritedEnvVars`, `onEnvVarChange`). Kind-specific minima:
`NUMERIC_KIND_MIN` map (`maxOutputTokens`/`contextLimit`: 1,
`maxRounds`: 0) applied to `<input min>`.
- **Global surface** (`AgentConfigFields.tsx`): deduplicate the
previously duplicated Advanced env-editor block; render
`NumericTuningFields` below the env editor when descriptors exist;
`hiddenKeys` and `bakedGenericRows` exclusions use `structuredEnvKeys`
so structured keys are never double-rendered. Under 1000 lines.
- **Per-agent surfaces** (`EditAgentAdvancedFields`,
`PersonaAdvancedFields`): replace `isBuzzAgentRuntime` as the
numeric-field gate with `deriveNumericDescriptors(selectedRuntime)` from
`agentConfigCore`; hidden keys come from
`structuredEnvKeys(numericDescriptors)` — the same rendered descriptor
set, no local rebuilding (fixes pre-existing dual-editor defect).
Catalog status carried as `RuntimeCatalogStatus` (`loading | ready |
error`); both error and loading withhold structured controls and leave
saved values visible as generic rows, making error distinguishable from
"runtime not capable" (`ready` + no runtime).
- **Dialogs** (`AgentDefinitionDialog`, `AgentInstanceEditDialog`,
callers): `AgentDefinitionDialog` accepts `runtimeCatalogStatus?:
"loading" | "ready" | "error"` (replaces separate
`runtimesLoading`/`runtimesError` booleans); all call sites —
`AgentManagementDialogs`, `AgentsView`, `RequestedAgentCreateDialogs`,
`UserProfilePersonaDialogs` — compute and pass the status.
### Phase 4 — Tests
- `buildRecord` exported from `EnvVarsEditor.tsx` as a pure `(nextRows,
value, requiredKeys, hiddenKeys) => Record<string, string>` helper for
isolation testing.
- **17 new node tests** in `agentConfigCore.test.mjs`:
`deriveNumericDescriptors` (all three fields, partial, undefined
runtime, matches field-model subset); `structuredEnvKeys` per surface
including discriminating Goose per-agent effort-key invariant;
`NUMERIC_KIND_MIN` values.
- **4 new node tests** in `EnvVarsEditor.test.mjs`: hidden tuning key
preserved through generic row edits; runtime-switch then generic edit
(derives both descriptor sets, asserts new-runtime hidden key survives
`buildRecord` via `hiddenKeys` and old-runtime key survives via generic
rows); baked numeric key excluded via `filterBakedGenericRows` with
`numericTuningPlaceholder` assertion; clearing a structured override —
`numericTuningPlaceholder` verifies placeholder text.
- **5 new Playwright tests** in `agent-numeric-tuning.spec.ts` (added to
smoke project `testMatch`): global numeric fields visible for
buzz-agent; global: non-capable runtime hides numeric controls; Goose
per-agent shows `Inherit (16384)` after saving global value through the
UI; delayed catalog: saved values visible as generic rows while loading
then structured controls appear after settle; failed catalog: saved
values remain visible as generic rows (never the "unsupported" empty
state).
## Result
- buzz-agent global defaults: Max output tokens, Context limit, Max
rounds as structured inputs with `Inherit (N)` placeholders from baked
env.
- Goose global defaults: Max output tokens, Context limit as structured
inputs.
- A Goose global value surfaces as `Inherit (<value>)` in the per-agent
Goose edit dialog.
- No structured key is editable in two places on any surface; no
persisted key has zero editors.
- No `runtime.id === "buzz-agent"` comparison decides numeric-field
visibility anywhere — capability flows catalog →
`AcpRuntimeCatalogEntry` → field model → UI.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
80315ac1a6 |
fix(desktop): harden Windows installs against Defender block and orphaned Node (#4382)
This PR fixes two Windows-specific install failures: Windows Defender blocking the bare `irm|iex` PowerShell install command, and managed Node shims pointing at a version-bumped (now-absent) Node directory. The Defender block (Trojan:Win32/Commando.A!ml) fires before PowerShell runs and is not clearable via Allow. The Node orphaning means shims in the managed npm prefix resolve but fail at runtime with 'node not recognized' because they reference the deleted old Node path. - Replace all three Windows CLI install commands (Goose, Claude, Codex) with a two-step shape — `Invoke-RestMethod` to a named temp file, then execute — to eliminate the dropper signature; a new `windows_install_command!` macro in `discovery/windows_install.rs` generates all three strings at compile time so the shape cannot drift between runtimes - `$ErrorActionPreference='Stop'` aborts on download failure instead of falling through to a missing-file exit-0; `exit $LASTEXITCODE` propagates the vendor script's own exit code - Add `probe_node(executable, expected_version, timeout)` as a bounded seam: stdout goes to a temp file (not a pipe) so no exit path can block on an inherited handle; the child runs in its own process group on Unix so an unconditional group SIGKILL on every exit path terminates all descendants; on Windows `taskkill /T /F` provides the same tree-wide cleanup; `managed_node_runtime_ready()` is a thin wrapper that resolves the managed Node path and calls the seam - Add `resolve_adapter_path()` in `managed_node.rs`: resolves the candidate first, then calls `should_invalidate_adapter()` — a pure predicate that returns `true` only when the resolved path is under `buzz_managed_npm_bin_dir()` AND the managed Node runtime is orphaned; external adapters outside the managed prefix are always preserved Note: CI cannot reproduce the Defender block (no live Defender ML classifier). Proof of fix is structural — the command shape no longer matches the dropper signature. Canary validation on a real Windows machine with Defender enabled is the definitive check. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
01c80aa9b3 |
fix(desktop): save key backups to authorized path (#4022)
**Category:** fix **User Impact:** Users can save password-protected identity backups directly to protected macOS folders such as Downloads. **Problem:** Signed macOS builds could not save a portable `.ncryptsec` backup to Downloads because the atomic writer created an unauthorized sibling temporary file. This surfaced as an “Operation not permitted” error after the user completed backup creation. **Solution:** Portable exports now write only to the exact path authorized by the native Save panel, sync and verify the saved bytes, and refuse to truncate an existing backup. Buzz’s app-managed backup retains its atomic writer and durability guarantees. <details> <summary>File changes</summary> **desktop/src-tauri/src/commands/export_util.rs** Clarifies that secret exports use a dedicated writer compatible with native Save-panel authorization. **desktop/src-tauri/src/commands/identity.rs** Routes portable NIP-49 exports through the Save-panel-compatible writer while preserving canonical app state. **desktop/src-tauri/src/key_backup.rs** Adds an exclusive-create portable writer with owner-only permissions, disk sync, byte verification, and cleanup on failure. Keeps the existing atomic writer for app-managed backups. **desktop/src-tauri/src/key_backup_tests.rs** Covers portable export permissions, absence of sibling files, and preservation of existing backups. </details> ## Reproduction steps 1. Install a signed macOS build containing this change. 2. Open **Settings → Profile → Private key → Create backup** and complete backup creation. 3. Save a fresh `identity.ncryptsec` file into `~/Downloads` and confirm Buzz reports success. 4. Open and verify the saved backup with its password. 5. Repeat the save using an existing filename and confirm Buzz preserves the existing file and asks for a new filename. ## Verification - Full desktop Tauri suite: 2,049 passed, 14 ignored - Diagnostic suite: 3 passed - Focused backup coverage: 30 passed - Tauri clippy (`--all-targets -D warnings`), Rust formatting, and `git diff --check`: passed - Push hooks: org safety, branch skew, and desktop Tauri checks passed Signed-production Downloads smoke remains required after merge because the signing workflow is restricted to `main`. Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
f810a2f49e |
fix(desktop): make OpenAI key re-enterable after first save in card mint dialog (#4140)
Fixes a write-once dead-end in the card mint dialog where a user with an
expired OpenAI key had no way to replace it.
**Source-aware key status (Rust + TypeScript).** `card_mint_key_status`
returns a layer discriminant (`"none" | "global" | "persona" | "agent" |
"process"`) instead of a boolean. A pure `resolve_key_layer()` helper in
`card.rs` owns the classification logic; `card_mint_key_status`
delegates to it, so the production path is under direct test with no
duplicate logic.
**Mint form always reachable.** The key panel replaces the mint form
only for `none` (first-time setup) or when the user explicitly opens the
edit panel (`editingKey`). Keys from agent/persona/process layers show
an inline provenance row on the mint form with a "Why?" affordance;
clicking it shows the read-only redirect in a panel with a Cancel button
that returns to the mint form — never a terminal state.
**Precise auth-error matching.** The 401 handling in `cardMintStore.ts`
matches `startsWith("Card mint failed (HTTP 401 ")` plus the specific
`Incorrect API key` text, so avatar-fetch 401 errors pass through
unchanged.
**Tri-state key status row.** "Using your saved OpenAI key · Update"
renders only when `keyLayer === "global"` (confirmed writable key).
Query pending or errored hides the row without asserting key existence.
**Real tests.** Panel visibility derivations live in
`cardMintKeyUtils.ts`, which `AgentCardMintDialog.tsx` imports directly.
Tests cover all layers including the mint-reachability invariant (Mint
reachable for every resolved layer; only `none` gates setup).
- `card.rs` — new `resolve_key_layer()` pure helper;
`card_mint_key_status` delegates to it; 999 lines (under the 1000-line
ratchet)
- `card/tests.rs` — precedence test calls `resolve_key_layer()` directly
(no test-local closure); adds process-layer and blank-value cases
- `tauriPersonas.ts` — `CardMintKeyLayer` type; updated
`cardMintKeyStatus` signature
- `cardMintKeyUtils.ts` — `showKeyPanel`, `showReadOnlyRow`,
`showCancelButton`, `keyPanelTitle`, and helpers; component imports all
of them
- `AgentCardMintDialog.tsx` — inline provenance rows for all key
sources; key panel only for setup/edit; no unused variables
- `cardMintStore.ts` — precise 401 prefix matching
- `e2eBridge.ts` — `card_mint_key_status` stub returns `"global"` (not
boolean)
- Tests: 3959 JS passing, 2089 Rust passing, `tsc --noEmit` clean
Related: [block/buzz#4406](https://github.com/block/buzz/pull/4406)
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg <9a232143a0336e34814a6a0f4947e11bc50bc641f21572c886fd0ae1f7a4e867@buzz.block.builderlab.xyz>
|
||
|
|
be95a8a986 |
fix(config-bridge): add harness-definition env tier and fix equal-value model override (#3580)
All seven normalized config fields resolve through sanitized
`InheritedConfigTiers` passed wholesale to `read_config_surface`. The
reader's precedence tiers now match spawn's Layer 2b exactly — including
harness-definition env — and the equal-value model-override regression
is fixed.
## Changes
**`config_bridge/types.rs`** — add `InheritedConfigTiers`: persona env,
global env, harness definition env, structured model/provider/prompt for
both tiers. Add `HarnessDefault` `ConfigOrigin` variant for
harness-definition env values.
**`commands/agent_config.rs`** — `build_inherited_tiers` now resolves
the harness definition env using the same lookup path as spawn
(`record.runtime` → `persona.runtime` → empty string) and applies
`sanitize_inherited_env` to it. `resolve_config_surface` is unchanged in
shape — tiers passed to the reader now include `definition_env`.
**`config_bridge/reader.rs`** — `env_candidates` extended to 4-element
return (record, persona, global, definition). All five field builders
that use env candidates now include the definition-env slot below global
env and above the structured block, matching spawn Layer 2b. Magic
`configured[..6]` slice replaced with `configured[..configured.len()-1]`
(named split: all non-file candidates). Equal-value model-override arm
falls through to the normal resolve path instead of early-returning
`RuntimeOverride`, so the panel shows the baseline origin (e.g.
`BuzzExplicit`) rather than a spurious "Live override" label for a no-op
switch.
**`config_bridge/reader_tests_ext.rs`** — three new Layer 2b tests:
definition env beats structured persona model, global env beats
definition env, reserved-key-absent fallthrough.
**`commands/agent_config_tests.rs`** —
`genuine_explicit_live_switch_to_same_model_yields_clean_field` updated
to assert `origin == BuzzExplicit` (not `RuntimeOverride`); wrapped in
`with_no_goose_config` for hermeticity. New
`reserved_key_in_definition_env_shaped_map_is_stripped_by_sanitize` test
pins the shared sanitization contract.
**`AgentConfigPanel.tsx` / `types.ts`** — `HarnessDefault` origin
variant wired end-to-end: TS union type and provenance sentence
("Inherited from harness definition").
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
6530b58a61 |
feat(k8s): Kubernetes backend plugin + desktop deploy path (#4289)
# Kubernetes backend plugin (crates/buzz-backend-kubernetes) + desktop deploy path Implements docs/remote-agents.md (merged @ |
||
|
|
fa1a5b1a79 |
fix(mesh): stop restarting a busy or loading shared-compute node (#3909)
## Problem Sharing compute with a large model (e.g. `gemma-4-26B`) put the desktop app into a **restart loop**: toggle Share → app appears to "download" / stall → the whole app restarts → repeat. Small models (E4B) were unaffected, which made it look model-specific and flaky. It is not model-specific and not flaky. It is a **false-positive liveness check**. ## Root cause (proven by black-box measurement) A `serve` node's OpenAI ingress (`:9337`) serializes **all** HTTP — including the `/v1/models` liveness probe — behind the current in-flight inference. It is *also* HTTP-unresponsive during model load and package-layer download. In every one of those phases the node is alive and progressing, but it cannot answer an HTTP probe. Measured on a standalone `gemma-4-26B` node (randomized ~30k-token prompt, cache-miss): | during one ~30s inference | result | |---|---| | concurrent `GET /v1/models` | **27.0s**, then 200 | | concurrent small `/chat/completions` | **28.8s**, then 200 | | `tcp_connect(:9337)` throughout | **~0ms** | Both HTTP calls simply queued behind the turn; TCP kept accepting instantly. A probe with any timeout shorter than the turn reads the node as dead. Buzz then acted on that false "dead" reading in two places, **both restart paths added in #2823**: 1. **Ingress watchdog** — after 2 consecutive `/v1/models` timeouts, evicts the node; for a serve node eviction means `app.request_restart()`. Two dead probes landing inside a prefill window → restart loop. 2. **Start / restore paths** — on a `wait_for_mesh_inference` timeout, `stop()` the node and (fresh start) `request_restart()` the app "to guarantee cleanup" — even though the node was still loading weights or downloading layers. This is the exact line in the incident log: `started node failed inference readiness … Buzz is restarting`. ## Fix Treat a **bound TCP port as alive**. Death has exactly one unambiguous signal: a *closed* port. - **Watchdog** (`recovery.rs`): only `PortClosed` may evict. A bound-but-HTTP-unresponsive `Unhealthy` port is never evicted, at any probe streak or urgency. Closed-port eviction is unchanged. - **Start / restore** (`commands/mesh_llm.rs`): install the runtime **before** probing readiness (so it is always tracked by `AppState` and can never be orphaned — which is what the restart was guarding against), and on a readiness timeout **leave it warming up** instead of stopping/restarting. Launch-restoration stays disarmed until real inference is confirmed, so a genuinely broken start is retried next launch rather than silently disabling Share Compute. ### What this deliberately does *not* do Detecting a node that is bound-but-internally-wedged needs a liveness signal that bypasses the inference lock. There is none today, so this fix cannot distinguish "wedged" from "busy" and errs toward not restarting. That gap is a mesh-llm bug, filed upstream: **Mesh-LLM/mesh-llm#1126** (lock-free `/live`+`/ready` on the ingress). A follow-up here can consume it once it lands. ## Tests - Watchdog never evicts a bound/busy port at any probe streak or urgency (the regression). - Closed-port eviction still fires (dead listener still reclaimed). - Black-box: a listener that accepts TCP then stalls HTTP classifies as `Unhealthy`, not `PortClosed`. - **Mutation-proven**: reverting the eviction rule to the old count-based logic fails the busy-node test. `cargo test` (desktop, `--features mesh-llm`) green, fmt + clippy clean. ## Not covered here The intermittent nature means I could not force the live loop deterministically on a warm machine; the proof is the measured serialization + the mutation-proven unit/black-box tests. Live behaviour (app no longer restarts while a 26B node loads/serves) still merits a manual check before merge. --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> |
||
|
|
eb049ddf81 |
feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock (#3278)
## Agent Trading Cards "Create Agent Card" action in the agent panel that mints an AI-generated trading card PNG which **is** the agent: the card carries the `buzz_agent_snapshot` tEXt chunk and is drag-in importable like any snapshot PNG. ### What's in here - **Mint pipeline (Rust):** one OpenAI Responses call — `gpt-5.6-sol` as card designer with `gpt-image-2` via the `image_generation` tool (~2–3 min). New `mint_agent_card` / `save_agent_card` commands; preview with reroll; save or send as `.agent.png` with round-trip verification before any bytes leave the app. - **Snapshot/chunk work stays in Rust,** reusing the existing encoder/decoder seams (byte-compat golden vector proves the plain path is identical to the pre-envelope encoder for placeholder, PNG-injection, and JPEG-transcode paths). - **Locked cards (NIP-44):** optional `buzz-agent-snapshot-encrypted` envelope encrypted to the (owner, agent) pair. `parse_canonical_pubkey` performs lift-x curve validation before any API spend; wrong-key decrypt returns a fixed refusal; the plain decoder refuses locked cards. - **Guardrails:** 10 MiB ceiling on final bytes, memory structurally `none` in the snapshot, full-manifest import disclosure, API-key hygiene via env layering (record > persona > global > process), fail-early validation ordering (all key/lock/NIP-44-cap checks before Responses spend). - **Import side:** full-manifest disclosure dialog, locked-card import disclosure, bounded avatar fetch. ### Review Code reviewed by Wren across the full arc; final locked-card cross-review **APPROVED 9/9/9** at exactly this head (`64f819dc8`), with independent same-SHA verification: Rust lib 1,843/1,843, clippy `--all-targets -D warnings`, desktop file-size gate. ### Live-mint evidence (real API, shipping seams, this SHA) - **Plain (Honey):** 188s, 1500x2250, 5,101,503 bytes (< 10 MiB); decoded manifest == built manifest; memory=none. - **Locked (Fizz):** 176s, 4,670,184 bytes; owner-key and agent-key decrypt both verified via logical manifest compare; wrong-key refusal exact; plain decoder refuses. - **Live finding:** built-in agents' ~171 KB inline avatars exceed the NIP-44 65,535-byte plaintext cap and the fail-early guard fires before API spend — clean error path, noted as a UX follow-up for large-avatar agents choosing lock. Full evidence (cards + dialog screenshots) posted in the originating thread. --------- Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |
||
|
|
468647a51f |
feat(desktop): locally stored NIP-49 encrypted key backup (#2937)
## Summary
Adds a locally stored **NIP-49 encrypted key backup** (`ncryptsec`) to
the desktop app, per the plan reviewed in buzz-development (Rev 3,
approved 9/10 by Wren; implementation also reviewed and approved 9/10).
**Two-artifact design — canonical bytes originate entirely in Rust:**
- `create_ncryptsec_backup` runs under the `identity_mutation` lock:
encrypt → decrypt-verify against the live pubkey → atomic `0o600` write
to `{app_data_dir}/identity.ncryptsec` → reread/byte-compare → return
the exact persisted bytes. The frontend never re-derives or re-encrypts.
- `save_ncryptsec_copy` writes a portable copy via the save dialog
(parse-gated, secret-file semantics) and never mutates canonical state.
- `generate_backup_passphrase`: 6 words from the EFF short wordlist via
`OsRng` (custom passphrases min 12 chars).
- Import accepts `ncryptsec1` with optional password; the raw-`nsec`
path is untouched. Different-pubkey import and sign-out wipe the
app-managed backup (post-commit, best-effort — a failed import can never
destroy the still-live identity's backup; regression-tested).
**Never-relay guarantee (egress guard + tripwires):**
- `egress_guard.rs` fail-closed at all 8 `/events` submission boundaries
(relay submit funnel, 3× `relay.rs`, huddle STT, both engram submitters,
native WS choke point), rejecting `ncryptsec1`/`NCRYPTSEC1` in text and
binary frames. Scope is deliberately ncryptsec-only: pairing
intentionally carries raw nsec inside its encrypted session.
- Site-granular `/events` inventory tripwire: per-file (`/events` count,
guard-call count) pairs; unlisted files expect zero. Mutation-style
tests prove a ninth site in an existing file, a removed guard, and a new
unlisted file all fail the scan.
- ncryptsec source-allowlist scans in **both** trees (Rust + TS).
**Frontend:** onboarding `BackupStep` is encrypted-by-default — the
default path never invokes `get_nsec` (e2e asserts the command log).
Raw-nsec export stays behind an explicit click with prior semantics.
Shared `EncryptedBackupCreator` powers onboarding + a new settings row;
the import form auto-switches to encrypted mode on `ncryptsec1` paste
(case-insensitive HRP).
**Open product call for @tlongwell-block:** onboarding default is
*encrypted* in this PR; flipping to raw-default is a small change either
way (documented in the plan).
Review history: plan Rev 3 and the implementation were both iterated
with Wren to 9/10 (two blockers from round 1 — import ordering,
inventory granularity — plus an uppercase-bech32 hardening gap, all
fixed in `dde37183e`). Thread: buzz-development.
### Related issue
Follow-up to the direction explored in #385 (NIP-PB, closed) — this
ships local NIP-49 (the standard) instead of a new NIP. No open
duplicate found.
### Testing
All at exactly `dde37183e` (same shell, HEAD verified):
- `cargo test` — 1680 passed / 0 failed / 14 ignored (includes a
deliberate ~70s log_n-18 NIP-49 round trip, spec vector, wrong-password,
NFKC, uppercase-vector decrypt, injection test per egress boundary,
inventory mutation tests, import-ordering regression tests)
- `cargo clippy --all-targets -- -D warnings` — clean; `cargo fmt
--check` — clean
- `pnpm typecheck` — clean; JS unit suite 3529/3529; biome (repo-pinned
2.4.16) clean
- Playwright `onboarding-backup` / `onboarding` /
`onboarding-agent-defaults` / `profile-nsec-reveal` — 86 passed, 1 known
avatar-reservation flake (passed on rerun; untouched by this diff).
`passThroughBackupStep` now exercises the encrypted default, so every
downstream onboarding spec covers the new path.
- Note: browser e2e fakes the crypto via the mock bridge (fixed
spec-vector blob); decryption correctness is proven in the Rust tests.
## Latest onboarding integration
The current head adds an additive `IdentityInfo.storage` field
(`ephemeral`, `system-keyring`, `local-file`, or `environment`) so
onboarding can accurately explain where the active identity is
protected. It surfaces storage metadata only—never key material—and
leaves the existing lost/keyring-locked recovery behavior intact.
---------
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
|
||
|
|
114d40d9d3 |
feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
Team catalog projections (`kind:30178`) embed every member's system
prompt, so they need the same read gate personas already have: only the
author sees an unshared event. The gate was hardcoded to `kind:30175` at
six read surfaces plus the SQL pushdown, so rather than adding a second
special case it becomes kind-generic over `SHARED_GATED_KINDS = {30175,
30178}`.
## Kind 30178
New parameterized-replaceable kind, addressed by `(pubkey_o, 30178,
team_id)`. It embeds sanitized member projections instead of referencing
`kind:30175` heads — a foreign reader of a shared team could not
otherwise hydrate members whose own persona events are unshared or, for
built-ins, absent entirely. `kind:30176`'s wire body is untouched, so
device sync keeps its contract.
## Kind-generic shared gate
`buzz_core::kind` replaces `is_persona_shared_kind` /
`is_unshared_persona_event` / `persona_event_is_shared` with
`SHARED_GATED_KINDS` and the kind-agnostic `is_shared_gated_kind` /
`is_unshared_gated_event` / `event_is_shared`. Every read surface
consults the set:
| Surface | File |
|---|---|
| REQ historical delivery + `ids` lookup |
`crates/buzz-relay/src/handlers/req.rs` |
| Live fan-out | `crates/buzz-relay/src/handlers/event.rs` |
| COUNT fallback | `crates/buzz-relay/src/handlers/count.rs` |
| NIP-98 HTTP `/query`, `/count`, `/search` |
`crates/buzz-relay/src/api/bridge.rs` |
| Pre-`LIMIT` SQL pushdown | `crates/buzz-db/src/event.rs` |
The SQL clause generalizes from `kind != 30175` to `kind NOT IN (...)`
bound from `SHARED_GATED_KINDS`, still applied before `ORDER BY … LIMIT`
so a page of newer private events cannot starve an older shared one off
the candidate set. `EventQuery::persona_reader` is renamed
`shared_gated_reader` and `needs_persona_filtering` to
`needs_shared_gate_filtering` to match.
Because the `buzz-core` rename has consumers outside the relay, the four
desktop call sites of `persona_event_is_shared` travel with it:
`desktop/src-tauri/src/commands/personas/pending.rs`,
`desktop/src-tauri/src/event_sync.rs`, and two in
`desktop/src-tauri/src/managed_agents/persona_events.rs`. Each call is
unchanged apart from the name — the persona `shared` projection behaves
exactly as before.
## Ingest validation
`validate_persona_envelope` splits into two reusable pieces —
`validate_shared_tag` (exactly-two-element `["shared","true"]`, at most
one occurrence) and `single_bounded_d_tag` (exactly one `d` tag,
non-empty, `<=64` chars, no ASCII control characters or whitespace).
`validate_team_catalog_envelope` composes both; personas additionally
keep the slug grammar `^[a-z0-9][a-z0-9_-]{0,63}$`.
`kind:30178` deliberately does **not** get the slug grammar. Team ids
are UUIDs or built-in identifiers such as `builtin-team:welcome`, and
the colon is not slug-legal; rewriting ids to fit would break NIP-33
addressing against the team's own `kind:30176` head. The non-empty and
exactly-one checks are load-bearing regardless — without them generic
NIP-33 storage maps a missing `d` onto `(pubkey_o, 30178, "")` and every
team overwrites its predecessor.
The exact two-element `shared` shape is enforced because the SQL
visibility clause is JSONB containment (`tags @>
'[["shared","true"]]'`), which would match a three-element superset such
as `["shared","true","extra"]`.
`kind:30178` is also added to the `Scope::UsersWrite` allowlist and to
`is_global_only_kind`, so a stray `h` tag cannot channel-scope an
owner-authored definition.
## Deferred
`kind:30176` is deliberately not a gate member. Its writers never emit
`shared`, so catalog opt-in semantics do not describe it — it needs
owner-private reads driven by an authenticated principal set, tracked as
a separate follow-up.
## Tests
- 19 new `ingest.rs` unit tests covering the 30178 envelope (UUID and
colon `d` tags, 64-char boundary, non-ASCII bound,
empty/valueless/duplicate/missing `d`, embedded newline, `shared`
false/three-element/duplicate, scope and global-only membership).
- Persona regressions for the valueless `["d"]` shapes, since the
`d`-tag helper is shared by both validators.
- Existing `kind.rs` gate tests generalized and extended to assert the
gate applies to 30178 as it does to 30175.
- New `crates/buzz-test-client/tests/e2e_team_catalog.rs`: 9 WS-level
tests over a live relay covering author reads of unshared heads, foreign
omission from REQ, `ids`-lookup denial, COUNT existence-leak, share and
unshare transitions, and the mixed-kind filter case.
- `.github/workflows/ci.yml` adds `--test e2e_team_catalog` to the Relay
E2E job so the new suite runs.
## Docs
`docs/nips/NIP-AP.md` gains a "Team catalog projection: kind:30178"
section and an "Ingest validation: kind:30178" subsection, records the
gate as kind-generic, documents 30178 deletion vs. unshare semantics,
and adds a security note that sharing a team exposes every member's
instructions even when that member's own `kind:30175` head is unshared.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
d40a33290e |
feat(desktop): raise the install ceiling and make installs observable (#3368)
Windows installs of Goose and other harnesses failed at exactly five minutes with an empty error (#2401). The 300s ceiling was killing installs that were working, just slowly — the Goose step pulls a ~79MB release asset, and Windows Defender scans every file npm extracts. When the ceiling fired it discarded the output it had already read, so the user got a bare timeout string and no way to tell a hang from a large download. ## The ceiling `INSTALL_TIMEOUT` is 900s, and the error names the limit: `install command exceeded the 15m ceiling and was terminated`. It stays a pure wall-clock ceiling with no inactivity kill — nothing observable distinguishes a hung installer from one silently transferring a large artifact, so silence alone never kills an install. A ceiling kill remains non-retryable; re-running a command that already burned 15 minutes costs the user more time with no plausible path to success. The child's exit and both stream drains fold into one resumable settle governed by a single deadline. Waiting on the drains outside that deadline would let a descendant that outlived the install shell hold the output pipes — and the per-runtime install guard behind them — open with no bound, which is the failure the ceiling exists to prevent. So the deadline path terminates the process group on the normal-exit branch too: a leader that exited with a real status still gets its stragglers killed, and the guard cannot stick either way. Whether the leader had already exited only decides the verdict — its real status outranks a timeout. The install shell is a session leader and its descendants inherit the output pipes, so signalling only the leader left them running and the drains blocked on a pipe nobody would close. Escalation keys off the *group's* liveness rather than the leader's, since a descendant that ignores SIGTERM outlives the leader and would otherwise never receive the group SIGKILL. Reaping the killed child and finishing the drains share one bounded grace, so a termination that failed outright cannot extend the ceiling that just fired. ## Output capture Each stream drains into a bounded capture that is *shared* with the reader rather than returned by it, so whatever arrived before a stall is readable at the ceiling — exactly when the output matters most. Output of any size costs a fixed amount of memory. One capture holds two independently bounded views of the same bytes: | View | Head / tail | Cut marker | |------|-------------|------------| | UI (`InstallStepResult`) | 512 B / 1024 B | `... (N bytes omitted) ...` | | Log file | 128 KiB / 128 KiB | `... [N bytes omitted at cap] ...` | The UI budget is screen space; the log's is disk. Both markers are inline, so neither ever implies completeness it does not have. Both ends are cut at arbitrary byte offsets, so a partial character is trimmed and the partial token each cut left behind is dropped — the marker's byte count includes both trims. ## Install log `steps` carries only the last attempt of each step, truncated for display. Everything else — earlier retries, the prerequisite step that actually broke, the managed-Node bootstrap — used to be discarded. `InstallReporter` now appends one self-contained record per attempt of per step to `install-<runtime-id>.log` beside the agent logs, and `InstallRuntimeResult.log_path` carries the file to the UI, where a failure message ends with `Full log: <path>`. Each record is bounded independently by the log-scale capture that produced it, so a first attempt that printed megabytes cannot push out the later record explaining the failure; the run's total is bounded by steps × attempts × per-record cap. Every early return builds its result through one `InstallReporter::failed` helper, so no failure path can omit the log pointer, and synthesized steps go through `record_step` — a step that reaches the UI without passing it would be invisible in the file. Install output can echo a registry token or proxy credential from the environment it ran in, and the file is written unattended. Redaction keys off the *names* of the environment variables the install inherited, snapshotted once per run, rather than a list of known secret value prefixes: a credential with no recognisable shape is exactly the one a prefix match misses. Three name rules apply, because the variables need different treatment: | Rule | Variables | Redacted | |------|-----------|----------| | URL userinfo | `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NPM_CONFIG_PROXY`, `NPM_CONFIG_HTTPS_PROXY`, `NPM_CONFIG_REGISTRY` | `user:password` only | | Exact name | `NPM_CONFIG_KEY`, `NPM_CONFIG__AUTH`, `NPM_CONFIG_OTP` | whole value | | Marker substring | `*TOKEN*`, `*SECRET*`, `*PASSWORD*`, `*_PAT`, … | whole value, 8-byte floor | A proxy or registry keeps its host and port, because an install that fails behind one is diagnosable only if the record still says which one it went through, and a bare `user@` with no password is not treated as a credential. npm's own settings are listed by exact name rather than matched on `KEY` or `AUTH` substrings — both occur throughout an ordinary environment on values that are paths and people's names — and they bypass the 8-byte floor, since a six-digit one-time password is a credential at that length. Matching is case-insensitive, which is what npm's lowercase `npm_config_*` spelling needs. `0o600` is set by the create rather than a later `chmod`, which would leave a window where the umask decides. A runtime id that cannot safely be a filename yields no log rather than a sanitized one — a rewritten id could collide with another runtime's log. The file holds exactly one run. A run opens its own session after the runtime id has been canonically resolved — the previous file rotates to `.1` and any older `.1` is removed before the rename, since a rename that will not replace its destination would otherwise wedge rotation permanently on Windows. The session writes a header naming the runtime, the app version (`app.package_info().version` on the Rust side — cannot be mocked or fail), the OS (`std::env::consts::OS`), and the start time: a Windows failure and a macOS one on the same runtime are different bugs, and a stale app version explains a failure that no longer reproduces. Each record carries its attempt's elapsed time. ## Live output line A 15-minute ceiling with nothing behind it but a spinner is indistinguishable from a hang. The same drain seam feeds an `acp-install-output` event carrying the newest complete line, and the three install entry points — Doctor harness rows, the harness catalog dialog, and onboarding runtime cards — render it under the spinner with `aria-live="polite"`. Ordering is keyed on a `seq` monotonic across the whole install, not on the attempt number, which restarts at 1 for every step: keyed on attempt, one step succeeding on attempt 2 would make the next step's attempt-1 output look stale and freeze the display for the rest of the install. Each executed attempt begins with an unthrottled `line: null` clear signal, so a stale failure line cannot sit under the spinner while the retry runs. Events are otherwise throttled to four per second, and the throttle *retains* the newest pending line and flushes it when the window reopens rather than dropping it — at an attempt boundary a drop would silently eat the new attempt's first line. The subscription is mounted for the runtime's whole lifetime rather than started when the install begins. The install command is invoked from the click handler, so the clear and a fast command's first lines can be emitted before React has committed the pending state, and nothing replays them — a subscription that waited for that state would lose the entire output of a short install. The run boundary resets the ordering key when the install settles, since `seq` restarts for the next run, and the line renders only while installing, so a straggler from a finishing drain cannot appear under a fresh Install button. The 15-minute ceiling deliberately stops waiting on stuck drain threads — a hung installer must not freeze the app. That means a drain thread can outlive its `InstallReporter`. Without a generation guard, a drain that calls `offer` after the run settles would publish an event with the run's high `seq`, poison the permanent listener's React state, and cause the next install's restarted `seq=0` events to be rejected. `Live` now carries a `lifecycle: Arc<RwLock<bool>>`; drain threads hold a **shared read guard** from the admission check through the `(self.emit)(...)` call, making the check-then-emit pair atomic with respect to shutdown. `InstallReporter::drop` takes the **exclusive write guard** and stores `false` — this blocks until every in-flight drain publication releases its read guard, then prevents any new admission. Deactivation is bounded: the write lock holds only for the flag store, so it can block at most for the duration of one emit call (microseconds to low milliseconds). Rust drops locals in reverse-declaration order, so `reporter` drops before `_guard`, ensuring the exclusive write completes before the per-runtime concurrency guard releases and a new install can start. ## Also Install result types move to `desktop/src/shared/api/installTypes.ts`, following the existing `searchTypes.ts` / `workflowTypes.ts` convention, and are re-exported from `tauri.ts` and `types.ts` — both already over the file-size cap, so neither can grow to carry them. Two comments described `AdapterOutdated` as applying only to the deprecated package; it also covers a version below the supported floor. Report: #2401 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
bd0bff24bf |
feat(desktop): add password-protected backups in settings (#3701)
**Category:** new-feature **User Impact:** Users can create, download, and verify a password-protected backup of their private identity from desktop Settings. **Problem:** Buzz does not currently give signed-in users a Settings-based path to protect or validate their private identity independently of onboarding. **Solution:** Add a focused backup menu to the private-key row, keep encryption and verification local in Rust, and preserve completed encrypted backups briefly so native saves can be retried without repeating encryption. <details> <summary>File changes</summary> **desktop/src/features/settings/** Adds the background backup lifecycle, create and test dialogs, private-key menu integration, password handling, and focused unit coverage. **desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx** Extends the masked private-key display with reusable overflow-menu actions used by Settings. **desktop/src/app/App.tsx** Mounts the backup provider at app scope so encryption and save work survive closing Settings or the modal. **desktop/src/shared/api/tauriIdentity.ts** Adds typed desktop bindings for local backup creation, save, selection, and verification. **desktop/src-tauri/src/key_backup.rs and desktop/src-tauri/src/commands/identity.rs** Implements local NIP-49 encryption, password generation, file handling, and public-identity-only verification results. **desktop/src-tauri/src/egress_guard.rs and guarded call sites** Blocks encrypted secret material from relay, websocket, snapshot, sharing, and huddle egress paths. **desktop/src-tauri tests and fixtures** Covers encryption, verification, file behavior, and fail-closed no-egress protections. **desktop/src/testing/e2eBridge.ts, desktop/tests/, and desktop/playwright.config.ts** Expands the mock native bridge and browser coverage across create, retry, expiry, and current/different-identity verification states. **desktop/src-tauri/Cargo.toml, Cargo.lock, and assets** Adds the local cryptography/password-generation dependencies and embedded short-word list. </details> ## Reproduction steps 1. Run the desktop app and open **Settings → Profile → Identity**. 2. Open the private-key overflow menu and choose **Create backup**. 3. Enter or generate a valid password, submit, and confirm progress continues if the dialog or Settings is closed. 4. Save the resulting `.ncryptsec` file; cancel and retry to confirm the temporary download remains available. 5. Choose **Test backup**, select the file, enter a wrong password, then retry with the correct password. 6. Confirm success identifies whether the backup matches the current identity and displays only the public `npub`. ## Screenshots | Settings identity | Private-key menu | Create backup | |---|---|---| | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/981e391b-6829-4081-95ca-ca75a369de71" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/7972c68e-7635-47d8-b0ad-9639390d3e6c" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/4709c8f7-cf02-46f1-bec9-b3f98fe56fb2" /> | | Encrypting | Download available | Test success | |---|---|---| | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/1ac3e934-2b4b-4135-bae6-126c715c8c59" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/cb6f07ee-a16f-44a5-b9a0-6b9fe0e4d40d" /> | <img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/ea58b1b1-966c-46aa-8d59-92c9f06a25bd" /> | Visual review and additional states: [Buzz thread](buzz://message?channel=50ca7ef1-201e-4159-9499-40de3964b7c3&id=87eceb5f0f82fd50c32e560de3d35be48e293760f6620718aafdcef289d475fe) --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
4933672eb4 |
feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
## Summary This is **part 1 of 2** split out from #3467 (per Tyler's request), carrying only the mesh-scoped changes. The agent/ACP response-behavior changes and the new `send_message` tool stay in #3467 as part 2. All commits are @michaelneale's work, cherry-picked with authorship preserved. - Upgrade embedded Mesh to v0.74.0 (tag-pinned instead of commit rev) and use canonical Gemma model IDs. - Keep shared compute serving through member joins, roster changes, app recovery, and community switching. - Wait for actual model readiness and avoid resuming incomplete downloads after quit. - Leave `BUZZ_AGENT_THINKING_EFFORT` unset by default so each model's chat template picks its own thinking default (`none` suppressed Gemma tool-calling entirely; pinning `low` made Qwen3 burn ~4x output budget). Explicit agent/persona/global values still win. ## Relationship to #3467 Contains the mesh commits from #3467 (`2cd640b23`, `0ad81c341`, `ad13ed841`) rebased onto current main, with one deliberate exclusion: the `crates/buzz-agent/src/llm.rs` reasoning→text parser change from `2cd640b23` is **not** here. That change unconditionally affects every OpenAI-compat/Responses provider, so it belongs with the reply-behavior work in part 2, where it can be reviewed as what it is. Not included (remaining in #3467 / part 2): - typed `send_message` tool in dev-mcp + `BUZZ_ACP_SEND_MESSAGE_TOOL` gating - plain-reply delivery fallback in buzz-acp (`BUZZ_ACP_DELIVER_PLAIN_REPLIES`) - the mesh_agent_e2e P5/P6 rewrite (exists to prove the reply path) - the two `env.insert` preset opt-ins in `relay_mesh.rs` for the flags above - the llm.rs parser change This PR is independently mergeable; part 2's flags are all off by default so it can land before or after. ## Testing - `cargo test -p buzz-relay --locked` — 780 passed (one telemetry test is order-sensitive under parallel default settings; passes in the pre-push suite and standalone, unrelated to this diff — files untouched here). - `just desktop-tauri-test` (default features) — 1877 passed. - `cargo test --locked --features mesh-llm` in `desktop/src-tauri` — 1961 passed, including the new relay-mesh preset and coordinator/recovery tests. - Both `Cargo.lock`s resolve with `--locked` against the v0.74.0 tag. - Full pre-push hook suite green (rust-tests, desktop-check/test, tauri checks). Live validation of the mesh v0.74 upgrade itself is documented on #3467 (two-Mac cross-version test). --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> |
||
|
|
788b3c002b |
fix(git): channel binding tooling + author remediation for unbound repos (#3626)
Closes #3527. Repos announced via vanilla NIP-34 (kind:30617 without a `buzz-channel` tag) 404 forever: the SEC-005 read gate requires a channel-membership ACL, and nothing tells the author why or how to fix it. Per the ruling in the originating thread, this ships **bind/rebind tooling plus a narrow author-only remediation carve-out** — the shelved owner-circle approach is intentionally absent. ## Relay - **`api/git/binding.rs` (new):** shared tri-state binding resolver — `Bound(uuid)` / `NotBound` / `Broken`. First-tag, fail-closed: a malformed `buzz-channel` tag is `Broken`, never conflated with "no tag". Both gates use it. - **Read gate (`transport.rs`):** a **never-bound** repo read by **its own announcement author** still returns 404 (status byte-identical to the generic denial) but the body carries remediation: `run: buzz repos bind --id <repo> --channel <channel-uuid> — …`. This leaks nothing — the author announced the repo, and only the author can rebind (30617 is keyed by `(author, d)`). `Broken` bindings stay generic-denial for everyone, including the author (revocation shape). Bound-to-nonexistent-channel stays generic (phase 1; ingest validation is phase 2). - **Push gate (`policy.rs`):** unbound denial now returns `GIT_NO_CHANNEL_BINDING_BODY`. A deploy-skew test pins that the body carries both the new token (`no_channel_binding`) and the legacy phrase (`"no channel binding"`) so already-shipped desktops keep matching. **(Review r1, blocker 2)** `Broken` no longer collapses into "unbound": it denies 403 `invalid channel binding` for *everyone — including the announcement owner —* **before** the owner short-circuit, matching the read gate's fail-closed posture. The remediation token stays NotBound-only. - **`ingest.rs`:** side-effect failure `warn!` → `error!` — prod runs `RUST_LOG=error`, so these failures were invisible during triage. ## Contract - **`buzz-core/git_perms.rs`:** `GIT_NO_CHANNEL_BINDING_TOKEN` / `GIT_NO_CHANNEL_BINDING_BODY` consts as the declared cross-component contract; relay tests and desktop matcher both build on them. ## CLI - **`buzz repos bind --id <repo> --channel <uuid>`** — rebinds an existing announcement, preserving other tags. - **(Review r1, blocker 1)** **`--channel` on `buzz repos create`** — optional; injects exactly one shape-validated `buzz-channel` tag at creation via a pure `build_create_announcement` builder, so the primary create command stops producing repos the relay 404s. UUID existence/membership stays the relay's authority at git-access time (same TOCTOU posture as `repos bind`). Overlaps with #3594 (open, head 6bbe38459) — happy to reconcile whichever lands first; this branch also carries the bind path and tag preservation. ## Desktop - **Rust:** new `commands/project_git_merge_error.rs` (extracted from `project_git_workflow.rs` to respect the 1000-line ratchet); maps the token to a structured `no_channel_binding` error carrying the bind command. - **TS:** new `features/projects/lib/projectBranchErrors.ts` + tests — dual matcher (new token AND legacy spaced phrase); `ProjectBranchDialogs.tsx` uses it. ## Tests / verification (at head |
||
|
|
7012d86d52 |
feat: configure S3 URL addressing style (#3400)
## Summary - add one strict `BUZZ_S3_ADDRESSING_STYLE=path|virtual` setting shared by media and Git/CAS storage - preserve path-style defaults for bundled Compose/Helm MinIO while supporting Railway's virtual-hosted bucket contract - fail startup on invalid or non-Unicode values before dependency connection, and validate the Helm value with the same two choices - document operator mappings and why endpoint and bucket remain separate for routing and SigV4 signing ## Best-practice rationale AWS documents both URL forms and favors virtual-hosted addressing for S3, while compatibility endpoints such as the bundled MinIO deployment can require path style. `rust-s3` defaults to virtual/subdomain addressing and provides `with_path_style()` for the explicit compatibility case. Some providers buckets only support as virtual-hosted bucket styles. This PR therefore uses one explicit, provider-neutral switch rather than endpoint heuristics or fallback behavior, while retaining `path` as Buzz's backward-compatible default. Sources: - https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html - https://docs.rs/rust-s3/0.37.0/s3/bucket/struct.Bucket.html - https://docs.railway.com/storage-buckets#url-style - https://github.com/minio/minio/blob/master/docs/config/README.md#domain ## Validation - `cargo fmt --all` - `cargo check --workspace --all-targets` - targeted `buzz-media` and `buzz-relay` parsing/client-construction tests for defaults, strict errors, and both URL styles - Helm unittest: 45/45 passed - Compose config/render validation passed - local MinIO path-mode relay startup passed the Git A3 conformance probe and became ready - unreachable object storage failed startup and readiness never opened - push hooks completed the broader Rust and desktop suites successfully --------- Signed-off-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Signed-off-by: Kalvin Chau <kalvin@block.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> |
||
|
|
ab55fee818 |
feat: add first-class OpenRouter provider support (#1975)
## Summary
First-class `Provider::OpenRouter` support joining the existing
anthropic/openai/databricks providers. Reuses the Chat Completions path
with targeted mutations for OpenRouter's routing contract.
**Core (`crates/buzz-agent`):**
- `Provider::OpenRouter` enum variant with `OPENROUTER_API_KEY`,
`BUZZ_AGENT_MODEL` → `OPENROUTER_MODEL` fallback, `OPENROUTER_BASE_URL`
env convention
- Body mutator: `reasoning: {effort}` when effort is configured, and
`max_completion_tokens` translated to OpenRouter's `max_tokens`
spelling; no `provider.require_parameters` filter (it routes only to
endpoints advertising every parameter in the body, which hard-404s a
valid model id); summaries get neither. `openai_body` is always called
with `effort=None` on the OpenRouter path — the `reasoning` object is
added by the mutator directly, so `reasoning_effort` is structurally
absent.
- Attribution headers: `HTTP-Referer: https://github.com/block/buzz`,
`X-OpenRouter-Title: Buzz`
- Error-inside-200 check in shared `parse_openai` (`finish_reason ==
"error"`)
- 401 auth handling: static API keys (`refresh_now` returns the same
token) fail terminal immediately with one wire request; PKCE/minting
sources get one retry with the fresh token.
- Status+`error_type` retry matrix (4-arm collapsed form): 429 (honor
`Retry-After`), 502 (retry), 503/`provider_overloaded` (honor
`Retry-After`), everything else including untyped 503 (bounded retries →
actionable routing message). 499 included matching shared `post()`
(#2175) for turn-timeout stall surfacing. Terminal failures wrapped in
`terminal_llm_error` for duration+attempt-count context.
- `anthropic/*` `cache_control` injection (model-gated, mixed-content
safe)
- Provider-agnostic `reasoning_details` opaque round-trip on
`HistoryItem::Assistant` for tool-call continuations — captured verbatim
in `parse_openai_with_reasoning_details`, replayed verbatim in
`openai_body`, byte-accounting charged. `provider_extra` passthrough
from `make_tool_call` composes independently.
**Desktop:**
- Readiness arms checking `OPENROUTER_API_KEY` + `OPENROUTER_MODEL`
- Model discovery via `{OPENROUTER_BASE_URL}/models` filtered on
`supported_parameters` contains `tools`
- Picker entry, credential config, effort table 3-file sync
**`desktop/src/features/agents/AGENTS.md`: no rules changed** — the
scoped rule requiring an explicit note is satisfied here.
Implements the gate-cleared plan from
`PLANS/OPENROUTER_PROVIDER_PLAN.md` (rev 3).
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
324bd6b464 |
Fix shared agent avatar import profiles (#3578)
> Carl is updating this pull request on Wes's behalf. ## Summary - upload an embedded raster avatar through the existing authenticated media pipeline before minting or persisting an imported shared agent - store and publish only the resulting hosted URL so agent kind:0 profiles remain within content limits ## Root cause Snapshot import recovered raster avatar pixels as a large inline base64 data URL. That value was persisted and placed into the agent's kind:0 profile. The relay rejected the oversized profile, so other clients could not resolve the imported agent's avatar. ## Scope This is intentionally the forward fix only. It changes two Desktop files and does **not** add migration or reconciliation behavior for previously imported agents. Existing affected imports must be re-imported or fixed manually. ## Validation - successful pre-push Desktop suite: 1,863 passed, 14 ignored, 0 failed - all pre-push Rust/Desktop gates green, including all-target clippy - valid >256 KiB PNG import → production MIME detection/sanitization → bounded signed kind:0 containing only the hosted URL - upload failure, malformed data, and URL-only avatar cases covered - independent fresh review by Princess Donut: clean, no blocking findings Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
294c8c821d |
perf(desktop): move observer-feed archive and decrypt commands off main thread (#3415)
Opening the agent observer feed could beachball the app. In Tauri 2, a sync (`pub fn`) command body runs on the **main thread** — only `async fn` commands run on the runtime pool. Five commands on the observer-feed open path were sync, so panel open ran SQLite I/O and secp256k1 work on the macOS main thread: | Command | Main-thread work | |---|---| | `decrypt_observer_event` | Schnorr ID + signature verify, then NIP-44 decrypt — once per frame | | `read_archived_observer_events_for_channel` | Opens the archive DB, runs the channel-index JOIN, returns up to 200 raw JSON blobs per page | | `read_unindexed_observer_rows` | Opens the DB, returns **all** not-yet-indexed kind-24200 rows in one shot | | `index_observer_channel_id` | Opens the DB, loops N upserts | | `delete_save_subscription` | Opens the DB, one delete | Eager hydration loads up to 10 pages × 200 frames on panel open, so that's up to 10 main-thread DB reads plus up to 2,000 sequential verify+decrypt calls before any scrolling. The one-shot backfill makes it worse on the first open after history accumulates: one read of every unindexed row, a decrypt per row, then a batch upsert — all on the main thread, and all proportional to archive size. The four archive commands now route their DB work through the existing `run_archive_db_task` helper (`spawn_blocking` + `open_db`), matching `list_save_subscriptions`, `read_archived_events`, and `archive_events` directly around them. `decrypt_observer_event` becomes `async fn` + `tauri::async_runtime::spawn_blocking`, with `state.signing_keys()` extracted before the spawn since `State` is not `Send` — the same pattern `sign_event` uses from #1222. No frontend changes: `invoke` is already promise-based, so the TS wrappers in `tauriArchive.ts` and `tauriObserver.ts` are unchanged. This removes the freeze, not the work. Eager hydration still takes the same wall time — the feed shows a loading state instead of blocking the UI. Batching the per-frame decrypt IPC (2,000 round-trips into one command) would cut the latency itself; that's deliberately out of scope here. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
f7a3988ba1 |
fix(desktop): preserve shared agent fidelity (#3553)
## Summary Fixes two distinct fidelity failures in direct agent sharing: - The sender now puts the same effective avatar shown on the agent card into People-share and file-export snapshot PNGs, including profile/kind:0 fallback avatars. - The importer now persists the visible PNG body as the portable avatar instead of ignoring it in favor of sender-local manifest references. - Export materializes inherited runtime, provider, and model identifiers verbatim, while preserving explicit definition values. It does not translate or substitute configuration for a different recipient setup. - Sharing waits for a profile-only fallback avatar query, preventing an early-click race. The PNG import path keeps the existing safety invariant: decode is capped at 2048×2048 / 32 MiB and re-encoded avatars above the 2 MiB inline limit fall back to the manifest reference. The exact transparent 1×1 no-avatar placeholder is ignored. The original Tyler↔Wes screenshot demonstrates both stages: Wren's attachment had an avatar that disappeared after **Add agent** (receiver/import failure), while Pinky's attachment was already blank (sender/projection failure). ### Related issue N/A — reported and traced in the linked Buzz conversation. ### Testing - `cargo test --manifest-path desktop/src-tauri/Cargo.toml commands::personas::snapshot` — 57 passed - `pnpm exec tsc --noEmit` - Biome check on changed frontend/E2E files - Pre-push hooks: - desktop check - desktop tests - desktop Tauri tests — 1853 passed, 14 ignored - file-size ratchet The People-share E2E regression asserts that a profile-only avatar reaches `avatarPngDataUrl` in the real encode command payload. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
a35771fc44 |
feat(desktop): refine agent catalog sharing (#2439)
## Summary - add custom-agent catalog sharing and hide built-ins from discovery - let owners publish later catalog updates from Share or while saving edits — the save always persists locally, and the publish reports `published` or `queued` (flushed automatically once the relay is reachable again) - preserve agent type, model, and runtime across snapshot import/export - simplify agent and team entry points and tighten catalog layout - migrate the legacy global retention queue into the owner's active scope so pending catalog publishes survive the upgrade - keep the agent list and edits usable in recovery mode by degrading to unshared projections when scope resolution or the retention DB fails - track catalog provenance on copied personas, so adding an already-added foreign agent resolves to the existing copy instead of creating a duplicate - scope inbound persona events to the community relay they arrived on - page the catalog read past the relay's 1,000-row query clamp - unify the share dialog's memory-level choice into a single "What's included" selector that drives both DM-send and copy-link delivery (all six combinations preserved), group the delivery rows above the option rows, and label the catalog toggle "Not shared" / "Shared" - keep emoji avatars on catalog entries — they persist as inline percent-encoded SVG, which the catalog projection's http(s)-only URL guard used to drop, so a shared agent showed initials instead of its avatar - drop the "Active in communities" card from Agents settings, superseded by the per-channel runtime controls in the members sidebar ## Screenshots ### Agent actions  ### Team avatar stack  ### Catalog sharing  ### Publish while editing  ### Publish from Share  ### Catalog details  --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
4e3998f36e |
fix(desktop): gate codex-acp on a minimum supported version (#3254)
The codex adapter version gate accepted any `major >= 1`, so a 1.x `codex-acp` older than the version that fixes outbound relay access for `buzz` CLI subprocesses classified as `Available` and was never offered a reinstall. Only the 0.16.x `@zed-industries/codex-acp` adapter — which fails `--version` outright — was caught. `probe_codex_acp_version` now returns the full `(major, minor, patch)` triple and `codex_adapter_availability` compares it against a new `MIN_CODEX_ACP_VERSION` floor of `1.1.7`, the current npm latest. An adapter below the floor classifies as `AdapterOutdated`, which routes it through the existing uninstall-then-install reinstall plan. The parse requires exactly three numeric dot-separated components. Partial versions (`1.2`) and prerelease tags (`1.2.0-rc1`) return `None` and therefore classify as `AdapterOutdated` — a version Buzz cannot compare against the floor fails closed, offering a reinstall rather than running an adapter of unknown vintage. Both the floor's bump policy and the strict-parse behavior are stated in doc comments rather than left implicit. Supersedes [#3097](https://github.com/block/buzz/pull/3097) by @Bharathchinneni, whose semver floor and behavior tests this carries. That PR could not land as written: the two `probe_codex_acp_major_version` compatibility wrappers it kept had no non-test callers, which is a hard `clippy -D warnings` failure. The wrappers are deleted here and their call sites collapsed onto `probe_codex_acp_version`. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
94675e0d25 |
refactor(desktop): extract install command execution into install_exec (#3251)
`commands/agent_discovery.rs` was pinned at its 2167-line file-size ceiling with zero headroom, blocking the install-supervision and install-log work queued behind it. Install command *execution* is a clean seam and moves into `commands/agent_discovery/install_exec.rs` together with its tests, matching the existing `managed_node.rs` / `post_install_verification.rs` split under the same module. Moved: `INSTALL_MAX_ATTEMPTS`, `run_install_command_with_retry`, `run_install_with_retry`, `install_failure_is_retryable`, `install_retry_backoff`, `annotate_retry_attempts`, `run_install_command`, `truncate_output`, `floor_char_boundary`, and the install-retry test block. Command *construction* (`install_shell_command`, `install_powershell_command`, `build_install_command`) stays in the parent — the new module owns only what happens once a `Command` exists. Public surface is exactly one `pub(super) fn run_install_command_with_retry`. The extraction is behavior-preserving, verified by diffing the moved text against the original line ranges: the parent is original-minus-cuts plus the intended edits, and the moved code is byte-identical except for the `pub(super)` marker, the `build_install_command` → `prepare_install_command` call site, and the new function described below. Two parent imports (`std::io::Read`, `InstallStepResult`) became unused and were dropped. ### Install working directory (#2245) Absorbed from #3090. A packaged desktop launch inherits `/` as its working directory, so installers that write relative to the CWD fail on a read-only root. The new `prepare_install_command` builds the command and applies `default_agent_workdir()`, and it is the only builder `run_install_command` calls — so no spawn path can bypass the workdir. This differs from #3090 in the test: that version spawned `pwd` through the real install shell and deleted `test_install_shell_command_returns_ok_on_unix` to make room. Here the prepared `Command` is asserted directly via `get_current_dir()` — hermetic, no shell spawn — and the existing test is kept. ### Tests Four new, on top of the moved retry block: - `test_prepared_install_command_uses_default_workdir` — every install child carries `default_agent_workdir()`. - `test_truncate_output_leaves_short_output_untouched` — under the cap, byte-for-byte passthrough. - `test_truncate_output_keeps_head_and_tail_with_marker` — over the cap, both ends survive and the marker names the omitted byte count. - `test_truncate_output_does_not_split_multibyte_characters` — the boundary floor prevents a mid-codepoint cut. `truncate_output` had no coverage anywhere before this. ### File-size gate `check-file-sizes.mjs` override for `agent_discovery.rs` moves 2167 → 1808, the exact post-`cargo fmt` gate count — verified both directions (1808 passes, 1807 fails). `install_exec.rs` is 458 lines and needs no override; the default 1000-line limit covers it. Related: #3090, #2245 Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
98a7b13348 |
fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor (#3218)
## Problem User report: ``` Buzz Node mismatch: Buzz supplies Node 24.14.0; OpenClaw requires >=24.15.0. All 10 ACP workers immediately crash. ``` Buzz supplies Node to agent processes from two places, and both were below OpenClaw's floor: | Supply path | Was | Now | |---|---|---| | hermit dev env (`bin/.node-*.pkg`) — the 24.14.0 in the report | 24.14.0 | **24.15.0** (newest hermit publishes; satisfies `>=24.15.0`) | | Desktop managed runtime (`managed_node.rs` / `managed_node_paths.rs`) | v24.11.0 | **v24.18.0** (current latest v24) | The managed runtime sits **first** on the worker PATH (`managed_agents/runtime/path.rs`), so a user-installed newer Node can't mask a stale managed one — the pin itself has to move. ## Verification - SHA-256 digests for all six platform artifacts taken from `https://nodejs.org/dist/v24.18.0/SHASUMS256.txt`; darwin-arm64 independently re-verified by downloading the tarball (hash match), extracting, and running `bin/node --version` → `v24.18.0`. - All artifacts within `MANAGED_NODE_MAX_BYTES` (largest linux-x64 at 57 MB < 90 MB cap); tar.gz layout keeps the `node-vX-platform/bin/node` shape `verify_node_tree` expects. - `cargo test --lib` in `desktop/src-tauri`: **1801 passed, 0 failed** at this commit; `cargo fmt --check` + `cargo clippy --lib -D warnings` clean. - Existing readiness check (`node --version == MANAGED_NODE_VERSION`) makes upgrade automatic: installed v24.11.0 trees fail readiness and the installer stages v24.18.0 atomically (rename with `.old` rollback — existing logic, unchanged). Note: CI `node-version: 24.14.1` pins in `release.yml`/`windows-canary.yml` are build-env only (already `>=` nothing OpenClaw touches) and left alone to keep this minimal. Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> |