mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
tho/message-embed
517
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f8f2ef0440 |
feat(cli): add --visibility flag to channels update (#5119)
## Why `buzz channels update` could already change name, description, and TTL, but the SDK/relay/DB path for channel visibility was unreachable from the CLI. ## What - Add `--visibility open|private` to `buzz channels update` - Pass the visibility value through to `build_update_channel` - Add guard tests proving empty updates still fail and visibility-only updates are accepted ## Risk Assessment Low — this is limited to the buzz-cli update command and uses existing SDK validation plus existing relay/DB handling. ## References - Spike notes: `RESEARCH/SPIKE_CHANNEL_VISIBILITY_TOGGLE.md` - Local validation: `cargo test -p buzz-cli` Generated with Codex Signed-off-by: Cameron Hotchkies <chotchkies@block.xyz> Co-authored-by: Lazy Joe <dbd8c9941ba6dafebcef0abc015b65e75d52e7452f2ce483c9c3fd4d180f2504@buzz.block.builderlab.xyz> |
||
|
|
2777189d96 |
fix(channels): restore member invitations to private channels (#5493)
## Summary - restore private-channel invitations for every active member - keep owner/admin-only enforcement for elevated role grants, active role changes, and removals - preserve #4612's unrelated Desktop/mobile failure handling and hardening - add relay coverage for the ordinary actor/target role matrix (`member`, `guest`, `bot`) ## Validation - pre-push hook passed on `7de700e17642ad7e10155f9537033168d9249268`: branch skew, Desktop checks/typecheck/tests/Tauri checks, mobile tests, and Rust tests - `cargo test -p buzz-test-client --test e2e_relay --no-run` - `cargo fmt --all -- --check` - `git diff --check` - Donut and Mongo independently reviewed the cross-layer authorization behavior; Donut's role-matrix coverage finding is addressed in this revision --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
3c76f682c3 |
fix(search): surface exact short profile names (#5480)
## Summary - prioritize exact whole-lexeme matches within short kind-0 prefix searches - preserve the existing prefix result set, pagination, community/channel scope, hydration, and authorization path - add a Postgres regression where newer noisy `jm…` profiles saturate the bounded page ## Why Desktop mention autocomplete starts searching after one character. The `jm` profile is indexed and matches both `jm:*` prefix search and standard full-text search, but production prefix search returns a full 50-result page without it. Raw profile JSON supplies enough unrelated `jm…` lexemes that newer equal-rank matches fill the bounded page before the exact short display name. Changing clients would leave deployed Desktop 0.5.8 installations broken. This shared search-layer compatibility fix changes ordering only for `Prefix + kinds:[0] + query length <= 2`; message search, longer profile typeahead, and agent eligibility are untouched. ## Validation At commit `ff88761135d5045139aeb3da14d08cbfba203169` with a clean worktree: - `BUZZ_TEST_DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz cargo test -p buzz-search --tests -- --include-ignored` — 22 passed (3 unit + 19 Postgres integration) - `cargo clippy -p buzz-search --tests -- -D warnings` - `cargo fmt --all -- --check` - mutation check: disabling exact-lexeme priority makes `short_kind0_prefix_prioritizes_exact_lexeme_on_a_noisy_page` fail - mandatory pre-push hooks: branch-skew, Rust tests, and Desktop/Tauri checks passed ## Risk Low. The extra ordering predicate applies only to one- or two-character prefix searches restricted exactly to kind 0. It does not add candidates, bypass filters, or alter access control. Exact matches move ahead of broader prefix matches; all remaining ordering stays relevance, recency, then event ID. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
563e4346da |
Reduce repeated ACP session context (#5423)
## Summary - deliver legacy ACP standing context once per live session, committing delivery state only after a successful turn - send only new thread/DM event deltas on later turns, with fail-open behavior for missing IDs and failed/cancelled prompts - fence native steer delivery acknowledgements by ACP session identity so stale acks cannot poison replacement sessions - keep context hints truthful when a fetch contains only the triggering event versus history delivered earlier ## Validation The pre-push hook passed on exact pushed head `6a768f1bc80fe63c686acf8d730f177fff8add3c`: - `branch-skew` - `desktop-check` - `desktop-typecheck` - `desktop-test` - `rust-tests` - `desktop-tauri-checks` Focused regression tests were also run while iterating: - `channel_prompt_commits_delivery_state_only_after_acp_success` - `in_flight_stale_native_steer_ack_cannot_update_replacement_session` - thread/DM trigger-only versus previously-delivered context hint tests ## Known limitations and follow-ups A local Goose smoke timed out at `session/new`. This diff does not change code that executes at or before `session/new`; its earliest affected runtime behavior is delivery-state insertion after session creation succeeds. The smoke failure is therefore bounded as environmental or pre-existing, but no successful live-provider turn was obtained. Scripted ACP wire/lifecycle tests carry the regression coverage. - #5421 — distinguish post-delta, already-delivered, and fetch-truncated context counts - #5422 — define a standing-context re-delivery policy if a legacy provider compacts it away Durable process-restart/session resume remains out of scope for this slice of #5342. #5386 also remains separate pending upstream adapter support. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
5e4c05f90b |
feat(desktop): NIP-AM agent-usage backend — P2 emission/transport/archive + P4a aggregation/D6 (#4000)
## What Implements Phases 2 and 4a of the Usage v2 plan (plan events `d0268cd0`/`0e95b035`), extending the archive backend to emit, transport, archive, and aggregate both cache categories and billing identity fail-closed. ### P2 — emission, transport, archive **Tri-state accumulators** (`Unseen`/`Exact`/`Unknown`) for cache-read and cache-write in `buzz-agent` turn and session state. Absent field = Unknown (never zero) through the full pipeline. No `unwrap_or(0)` on the cache path. Both cache folds are gated on usage-bearing responses (same gate as the total-state and identity folds) — a response with no usage at all must not poison either accumulator. **Overflow-aware input token parsing and accumulation** — closed end-to-end from parse through wire to ACP: - `sum_usage()` returns `SumUsageResult` (`Exact(u64)` | `Overflow`) — checked arithmetic, never clamps. `anthropic_input_tokens()` returns `Option<SumUsageResult>` since it sums three fields (`input_tokens + cache_read_input_tokens + cache_creation_input_tokens`) that can collectively overflow. Single-field callers (`prompt_tokens`, `completion_tokens`, etc.) convert via `.into_exact()` — their single-field sums cannot overflow. - `LlmResponse.input_tokens_overflowed: bool` propagates the parse-layer signal into the run loop. When set, `input_tokens` is `None` (clamped value discarded), the context-gate baseline (`last_request_input_tokens`) is frozen at its prior reading, and `turn_input_tokens` is poisoned to `TurnIOState::Poisoned` before any emission — including mid-turn `emit_usage_update` calls. A dedicated enum on `LlmResponse.input_tokens` would ripple into ~20 existing test assertions on `r.input_tokens == Some(...)`; the bool flag confines the change to the two call sites that check it. - `TurnIOState` (`Unseen`/`Exact`/`Poisoned`) for input and output: per-round fold uses `checked_add`; overflow poisons permanently at turn and session level, no healing. Absence does not poison (pass-2-cleared contract unchanged). Wire emission omits `accumulatedInputTokens`/`accumulatedOutputTokens` when poisoned — never null, never `u64::MAX`. ACP treats absent = publisher-poisoned: `delta_reliable: false`, null turn fields, null cumulative for that category; session cumulative stays unknown for all subsequent turns once poisoned. **Conditional wire emission** for `accumulatedCachedInputTokens` and new `accumulatedCacheWriteTokens`: fields are omitted when the cumulative is Unseen or Unknown. ACP `_goose/unstable/session/update` contract documented next to the payload with tests for all absence/zero variants. **`PricingIdentity` stamping (publisher-side)**: - `pricing_authority()`: canonical parsed-URL endpoint comparison against the official allowlist — HTTPS only, exact allowlisted host (lookalike-safe), default port (omitted or explicit :443), required API base path, rejects userinfo/query/fragment/path-prefix lookalikes. - Model: the actually-requested `request_model` after mesh/auto resolution (not `effective_model_str`). - Turn discipline: identity retained only while ALL usage in the current turn carries one identical proven identity; any mismatch, unproven-usage-bearing response, or unpaired cumulative snapshot poisons to absent; a later matching notification does not heal a mixed turn. **ACP `UsageTracker` identity fold**: per-in-flight-turn tri-state identity accumulator replacing last-update-wins. Any absent identity on a token-advancing notification or exact mismatch poisons to absent; poison survives later updates; reset in `begin_turn()`/`take()`; reset also when a request fails (baseline cleared so preflight gate cannot stay frozen sub-threshold on retries). **M3 migration**: adds `turn_cache_write_tokens`, `cumulative_cache_write_tokens`, `pricing_authority`, `pricing_model`, `pricing_cache_class` to `agent_metric_index`. Additive, idempotent, guarded per-column by marker. M2 migration also guarded per-column (turn and cumulative cache-read columns checked and added independently; marker commits only after both are present). Fresh-DB schema includes all columns. **First-turn baselines**: `seed_zero_baseline` seeds `last_input: Some(0)`, `last_output: Some(0)`, `last_cached_input: Some(0)`, `last_cache_write: Some(0)`, and `last_total: Some(0)` — all have the known-zero-at-spawn argument. Absent fields from incoming snapshots still produce unknown (tri-state unchanged). Sessions buzz-acp did not spawn (no seed) remain fail-closed on turn one. **`ReportedUsage` TS mirror**: `cacheReadTokens`, `cacheWriteTokens`, `freshInputTokens` added to `tauriArchive.ts` as `UsageField` members, field-for-field with the Rust struct. ### P4a — aggregation layer **Extended S-1 ladder** to cache-read and cache-write via the same `ladder_token` path as the existing token fields. **`freshInputTokens` derivation**: checked arithmetic, fail-closed — absent cache fields produce Unknown (not zero), overflow and `cacheRead+cacheWrite > input` both produce `incomplete: true`. Aggregated as a `UsageField`. **D6 comparator**: `sort_value()` = provider total when known, else `input+output` when both known, else `None` (unknown-last). Replaces the prior total-only comparator for both agent-level and model-level sort. Ships a pinned test vector that the TS render layer (P5) must match. ## Test coverage - `buzz-agent`: 440 lib + 15 integration (golden_transcripts) — includes 13 new `cache_total_state_tests`; 14 new `turn_io_state_tests`; 3 new `sum_usage_*` tests (exact single-field, exact two-field, overflow signals correctly); 3 new `parse_anthropic_*` tests (overflow flag set + value cleared, normal sum no flag, absent usage no flag); end-to-end golden transcript drives real subprocess with Anthropic-shaped `input_tokens: u64::MAX, cache_read: 1` response and asserts `accumulatedInputTokens` absent from the emitted `usage_update` — no logic duplication; 3 wire pin tests; 4 `fold_pricing_identity_*` tests; `pricing_authority()` explicit-:443 acceptance - `buzz-acp`: 700 tests (691 lib + 9 integration) — 4 new usage tests (absent input → unreliable+null; absent output → unreliable+null; goose-shaped both present unchanged; poison mid-session); 3 ACP behavior tests; 7 pool lifecycle tests - Desktop (Rust): 2259+ tests — 14 new P4a pinned tests; 2 M3 round-trip tests; 1 serde key-shape test; 2 M2 partial-schema migration tests; first-turn cache round-trip test ## Related PRs - P1 NIP-AM spec: [#4632](https://github.com/block/buzz/pull/4632) - P3 pricing table: [#4629](https://github.com/block/buzz/pull/4629) - UI (P5): [#4001](https://github.com/block/buzz/pull/4001) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
5bf78671f4 |
fix(agent): retry LLM completion on malformed 2xx JSON body (#5351)
## Problem
A provider can return HTTP 200 with a **truncated JSON body** — cleanly
closed connection, correct framing, content cut off mid-value. Both LLM
HTTP loops treated this as a terminal error on the first attempt:
`AgentError::Llm("json: EOF while parsing a value")`, surfaced as code
-32000 at the ACP boundary, killing the agent turn before it produced
anything.
Observed live in a tb2.1 bench trial (write-compressor, tb21-twins-1):
deepseek via OpenRouter returned a truncated body, the agent died
mid-prompt with 0 turns completed, and the trial scored 0 on a provider
hiccup.
Meanwhile the same loops already retry timeouts, 429s, 5xxs, 499s, and
mid-body stream stalls — a truncated-but-complete body was the one
transient upstream fault that fell through to terminal.
## Fix
In both `post()` and `openrouter_post()`
(`crates/buzz-agent/src/llm.rs`): when the fully-received success body
fails `serde_json::from_slice`, `continue` the **existing** retry loop
instead of returning terminal — same `MAX_RETRIES` (3) bound, same
`backoff_with_jitter`. On exhaustion, the error goes through
`terminal_llm_error` so it carries cumulative duration + attempt count
like every other retried failure (previously the `json:` error carried
neither).
`post_anthropic` routes through `post()`, so
Anthropic/OpenAI/Databricks/mesh and OpenRouter are all covered.
## Why this cannot re-run a tool call
Hard requirement: tool calls are not idempotent, and this change must
not introduce any possibility of replaying one.
1. **The retry lives inside the HTTP POST helper, below the parse
boundary.** Tool calls are only ever extracted from a *successfully
parsed* response value
(`parse_openai`/`parse_anthropic`/`parse_responses`, all downstream of
these helpers' `Ok` return). A malformed body never parses, therefore no
tool call was ever extracted from it, therefore nothing downstream of it
ever dispatched.
2. **What is re-sent is the completion request itself** — the identical
`body_bytes` captured once at function entry. Sending a completion
request executes no tools; it asks the model for the next message.
3. **Same safety class as existing behavior.** The loop already re-sends
this identical request on 429/5xx/timeout/stream-stall; this adds one
more transient-fault arm to the same loop with the same bytes.
## Tests
Three new tests mirroring the existing 499/dropped-connection fixtures
(raw `TcpListener` stubs):
- `post_retries_malformed_json_body_and_succeeds` — truncated 200 body
on attempt 1, valid JSON on attempt 2; asserts success and **exactly 2**
server-side requests
- `post_exhausts_retries_on_persistent_malformed_json` —
always-truncated body; asserts exactly `MAX_RETRIES` attempts and a
terminal error carrying `json:` + cumulative/attempt context
- `openrouter_post_retries_malformed_json_body_and_succeeds` — same
recovery through OpenRouter's separate loop
Full `cargo test -p buzz-agent` green at
|
||
|
|
6e5c462ac5 |
chore(release): release Buzz Relay version 0.2.1 (#2856)
## Buzz Relay release v0.2.1 ### Changes since relay-v0.2.0: - fix(sdk): preserve self-mention p tags in message and forum event builders ([#4975](https://github.com/block/buzz/pull/4975)) ([`78c87ae20e`](https://github.com/block/buzz/commit/78c87ae20e182fffdd99744d6c9ff99df82b159c)) - feat(desktop): adding rich link previews to messages ([#3818](https://github.com/block/buzz/pull/3818)) ([`1922d49cb2`](https://github.com/block/buzz/commit/1922d49cb200a3382a91ec253f530b44dfda5f55)) - feat(relay): accept kind:30179 private managed-agent events at ingest ([#5133](https://github.com/block/buzz/pull/5133)) ([`ad923353a2`](https://github.com/block/buzz/commit/ad923353a24b784df13a7c88757d6b24ebe36299)) - fix(media): require authenticated reads ([#4610](https://github.com/block/buzz/pull/4610)) ([`769ac70b74`](https://github.com/block/buzz/commit/769ac70b741e3ad6809bff14eba29d3dd2cbd318)) - feat(identity): recover desktop identity from a signed-in phone ([#4845](https://github.com/block/buzz/pull/4845)) ([`6eb65919f1`](https://github.com/block/buzz/commit/6eb65919f1eabd46b3850c15eefab31092dd500b)) - ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes ([#3862](https://github.com/block/buzz/pull/3862)) ([`38bf642fcf`](https://github.com/block/buzz/commit/38bf642fcfa7a9fc1e06d6cf87d66ae94da29341)) - relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) ([#4542](https://github.com/block/buzz/pull/4542)) ([`e14fff74d0`](https://github.com/block/buzz/commit/e14fff74d00623acd30945eec5be366e25b0cf09)) - fix(reactions): support max-length custom emoji ([#3833](https://github.com/block/buzz/pull/3833)) ([`2ea9385015`](https://github.com/block/buzz/commit/2ea9385015fb922de2adf0a53e86fc5a21d07b90)) - fix(channels): restrict private-channel invitations ([#4612](https://github.com/block/buzz/pull/4612)) ([`efe1893dd3`](https://github.com/block/buzz/commit/efe1893dd372cfb92ed2e8a3ada2ed7b62c9477a)) - fix(workflow): bind trigger author to the signed event ([#4607](https://github.com/block/buzz/pull/4607)) ([`885bed35ee`](https://github.com/block/buzz/commit/885bed35eee3f933c48d333c8979fdbc038e98b9)) - fix(git): revoke access for banned relay members ([#4608](https://github.com/block/buzz/pull/4608)) ([`997b8caaa4`](https://github.com/block/buzz/commit/997b8caaa4c9e5af69dd8a496b4995d09a69f694)) - Define private managed agent wire protocol ([#4593](https://github.com/block/buzz/pull/4593)) ([`067c085f37`](https://github.com/block/buzz/commit/067c085f37d9dcb2f598b0e2a6b6653903364783)) - perf(relay): index channel-id lookups and skip trace-only reads ([#4647](https://github.com/block/buzz/pull/4647)) ([`bc9e6528a7`](https://github.com/block/buzz/commit/bc9e6528a7ba6007c5a25f6a0aca9c05d72e9d2c)) - Polish mobile inbox and media flows ([#4512](https://github.com/block/buzz/pull/4512)) ([`feccf4eabc`](https://github.com/block/buzz/commit/feccf4eabc23fdba94ce3537a194357ed17b197c)) - fix(git): allow deleting the default branch ([#4297](https://github.com/block/buzz/pull/4297)) ([`fc598f5f8d`](https://github.com/block/buzz/commit/fc598f5f8d70728d11d0712b9fa8e3acc44ea4c3)) - feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) ([#4020](https://github.com/block/buzz/pull/4020)) ([`b7bb15122e`](https://github.com/block/buzz/commit/b7bb15122e8a2053b545dc2210afc167f6c7a626)) - perf(relay): serve relay-membership checks from the read replica ([#4124](https://github.com/block/buzz/pull/4124)) ([`ac4fa13b8e`](https://github.com/block/buzz/commit/ac4fa13b8e4d947071d57deb6918dcf12bf74961)) - fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) ([#3998](https://github.com/block/buzz/pull/3998)) ([`5765fc74b7`](https://github.com/block/buzz/commit/5765fc74b77224f0207ddd4b41736a5ff18d333d)) - feat(relay): accept kind:30621 multi-repo projects at ingest ([#3171](https://github.com/block/buzz/pull/3171)) ([`cb9701cd30`](https://github.com/block/buzz/commit/cb9701cd30fb344bf134585634a09007f3155bfb)) - feat(relay): raise hosted community limit to five ([#3829](https://github.com/block/buzz/pull/3829)) ([`10d5a26414`](https://github.com/block/buzz/commit/10d5a26414dc90dc89fd27de74b21e105d4fa622)) - fix(relay): align NIP-11 max_limit with REQ ceiling ([#3635](https://github.com/block/buzz/pull/3635)) ([`23f0c26b1c`](https://github.com/block/buzz/commit/23f0c26b1ceba8e07bf3c160a1e08c7bda82ccd9)) - feat(relay): gate kind 30178 team-catalog reads behind the shared tag ([#3358](https://github.com/block/buzz/pull/3358)) ([`114d40d9d3`](https://github.com/block/buzz/commit/114d40d9d37f05eff83ee90347ed93fb3da512c5)) - fix(db): isolate usage metrics advisory-lock test on scratch DB ([#3670](https://github.com/block/buzz/pull/3670)) ([`dba97eecd9`](https://github.com/block/buzz/commit/dba97eecd9d8659c9c816cd6666fa6d687b6bca1)) - perf(presence): reduce heartbeat frequency ([#3783](https://github.com/block/buzz/pull/3783)) ([`bf139e8d0b`](https://github.com/block/buzz/commit/bf139e8d0bdba10df9a5adbf16843140e0a78a59)) - feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) ([#3741](https://github.com/block/buzz/pull/3741)) ([`4933672eb4`](https://github.com/block/buzz/commit/4933672eb4589e7208b312829ebddcd10dfa9dd3)) - feat(replica): portable heartbeat-token fence with snapshot-local reader routing ([#3268](https://github.com/block/buzz/pull/3268)) ([`63496cc1d4`](https://github.com/block/buzz/commit/63496cc1d4c6f1b7c613801bdcc694169dcf391a)) - fix(git): channel binding tooling + author remediation for unbound repos ([#3626](https://github.com/block/buzz/pull/3626)) ([`788b3c002b`](https://github.com/block/buzz/commit/788b3c002bd2509455444f57f8a03a054b4b496a)) - feat: configure S3 URL addressing style ([#3400](https://github.com/block/buzz/pull/3400)) ([`7012d86d52`](https://github.com/block/buzz/commit/7012d86d52fd188b27c7beedeaa132d9c1f61fa8)) - feat(tracing): correlate trace IDs in relay logs ([#3608](https://github.com/block/buzz/pull/3608)) ([`005b5b819a`](https://github.com/block/buzz/commit/005b5b819a98ce85d4d80cd81b258fb6f9b8d51e)) - fix(relay): avoid subscription lock inversion ([#3413](https://github.com/block/buzz/pull/3413)) ([`22be8bb351`](https://github.com/block/buzz/commit/22be8bb35177e27efc2dca2534df9a8dd871eae0)) - feat(cli): add users set-status command for NIP-38 profile status ([#3253](https://github.com/block/buzz/pull/3253)) ([`60158fce3e`](https://github.com/block/buzz/commit/60158fce3e670f11bb35d42627857ccaea50ff06)) - feat(relay): make Postgres pool size configurable, default 50 ([#3191](https://github.com/block/buzz/pull/3191)) ([`2ce2d71cc3`](https://github.com/block/buzz/commit/2ce2d71cc38a9657eaf344c10e07f155b8a18615)) - feat(tracing): add datastore tracing plumbing ([#2760](https://github.com/block/buzz/pull/2760)) ([`e94b9aeda0`](https://github.com/block/buzz/commit/e94b9aeda0b2272d36e3744e78680be69295b8b5)) - feat(invites): add use-limited invite links ([#3141](https://github.com/block/buzz/pull/3141)) ([`d500c2d5cf`](https://github.com/block/buzz/commit/d500c2d5cf5d9aabe0ca4ebebfcafdbe5f5b7fd3)) - feat(admin): show reported message content in report detail ([#3149](https://github.com/block/buzz/pull/3149)) ([`f069a85503`](https://github.com/block/buzz/commit/f069a8550373328babe4239ed614fcdf884721e2)) - resolve findings ([#3150](https://github.com/block/buzz/pull/3150)) ([`9b0f744804`](https://github.com/block/buzz/commit/9b0f744804697b802f7afb88947194702765c78d)) - Revert "fix(cli,relay): resolve agents by verified owner" ([#3168](https://github.com/block/buzz/pull/3168)) ([`a041e2d21e`](https://github.com/block/buzz/commit/a041e2d21e292a271fdfc26f0cdcdd0456f815c5)) - fix(cli,relay): resolve agents by verified owner ([#2615](https://github.com/block/buzz/pull/2615)) ([`c3084b36d9`](https://github.com/block/buzz/commit/c3084b36d975259f2dfeee8edc9131b40a8bce83)) - fix(security): enforce durable community ban on NIP-43 relay-admin kinds 9030-9033 ([#3128](https://github.com/block/buzz/pull/3128)) ([`e2e0079101`](https://github.com/block/buzz/commit/e2e007910114ddf7c5a4e93bb03f6afe13552e92)) - fix(security): authorize kind:9000 role changes in both directions ([#3017](https://github.com/block/buzz/pull/3017)) ([`00ecf2cac7`](https://github.com/block/buzz/commit/00ecf2cac7544d986b4eb111ad0a8b1d7560791f)) - feat(desktop): handle project work from Inbox ([#3117](https://github.com/block/buzz/pull/3117)) ([`c5c4f390b6`](https://github.com/block/buzz/commit/c5c4f390b6713256e2efb8394c59823ebad73db6)) - feat(relay): make per-owner community limit configurable via BUZZ_MAX_COMMUNITIES_PER_OWNER ([#2599](https://github.com/block/buzz/pull/2599)) ([`2a051a404d`](https://github.com/block/buzz/commit/2a051a404dcde42dddbff2a0b33f717ffe9cf999)) - feat(relay): add author-only-unless-shared read gate for kind 30175 ([#2768](https://github.com/block/buzz/pull/2768)) ([`ab3af82871`](https://github.com/block/buzz/commit/ab3af828714ab699dfc87644d234014987a4fe6b)) - fix(core): block IPv6 transition SSRF targets ([#2801](https://github.com/block/buzz/pull/2801)) ([`c26bf5945d`](https://github.com/block/buzz/commit/c26bf5945d8f2ef19746a78e80a7c1dae2ef3db9)) - fix(workflow): bypass system proxies for webhooks ([#2800](https://github.com/block/buzz/pull/2800)) ([`60a171b19e`](https://github.com/block/buzz/commit/60a171b19efd515d9213b535d52a2bcbec3ff2fe)) - fix(audit): hash created_at at the precision Postgres stores ([#2638](https://github.com/block/buzz/pull/2638)) ([`264a56a226`](https://github.com/block/buzz/commit/264a56a2260ac87350bfe1f5d3ec3d89615eb47c)) - feat(desktop): make pull request reviews actionable ([#2510](https://github.com/block/buzz/pull/2510)) ([`9081ab0ec9`](https://github.com/block/buzz/commit/9081ab0ec9c5d91548c7f5ff52eba6cca4788dd0)) - fix(relay): decompress gzip-encoded git smart-HTTP request bodies ([#2670](https://github.com/block/buzz/pull/2670)) ([`5ca36e7b91`](https://github.com/block/buzz/commit/5ca36e7b919097733868764d8e0073e99c3206c3)) - fix(sharing): preserve agent/team snapshot tEXt chunks through media sanitization ([#2438](https://github.com/block/buzz/pull/2438)) ([`b096b0a15a`](https://github.com/block/buzz/commit/b096b0a15af4c4566365c5b1efe7f39b700222ed)) - fix(relay): send 1012 restart close to all clients on graceful drain ([#2575](https://github.com/block/buzz/pull/2575)) ([`1911c69aa2`](https://github.com/block/buzz/commit/1911c69aa2912c1408bd6b21759b657458fb43af)) - fix(media): sanitize animated image uploads ([#2524](https://github.com/block/buzz/pull/2524)) ([`8f8f5fa5a4`](https://github.com/block/buzz/commit/8f8f5fa5a4b2463cdc6c2a527acb7086150cdaae)) - fix(channels): strip leading hash prefixes from names ([#2250](https://github.com/block/buzz/pull/2250)) ([`d0ab3fdb05`](https://github.com/block/buzz/commit/d0ab3fdb054e0cfedbf21e4c5143ad6c671c10cc)) - feat(relay): make Redis pool size configurable, default 16 ([#2521](https://github.com/block/buzz/pull/2521)) ([`bcc3e13069`](https://github.com/block/buzz/commit/bcc3e1306946528102bb26be9a7c41299e2f8e00)) - feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool ([#2122](https://github.com/block/buzz/pull/2122)) ([`61cc738ee8`](https://github.com/block/buzz/commit/61cc738ee8991e92563136de4b77e54cb9756420)) - feat(media): add S3-truth per-community storage sweep ([#2044](https://github.com/block/buzz/pull/2044)) ([`bd37a4d584`](https://github.com/block/buzz/commit/bd37a4d584fefc1d13ad8abadf6e890e66183072)) - feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests ([#2206](https://github.com/block/buzz/pull/2206)) ([`7e34bee62c`](https://github.com/block/buzz/commit/7e34bee62cacaa9d8a96c14d5892a471b59a1983)) - Revert "feat(relay): inventory unreachable Git objects" ([#2275](https://github.com/block/buzz/pull/2275)) ([`0fb820f9bf`](https://github.com/block/buzz/commit/0fb820f9bfbd7e19e48f9826e332920c2ee2c229)) - feat(relay): inventory unreachable Git objects ([#2264](https://github.com/block/buzz/pull/2264)) ([`3afc9dae15`](https://github.com/block/buzz/commit/3afc9dae159262220c4149e9c8add50772869318)) - relay: add author_type label to buzz_events_stored_total ([#2243](https://github.com/block/buzz/pull/2243)) ([`b9f54c43fe`](https://github.com/block/buzz/commit/b9f54c43fe2bcd0eb8fb3b76914e9aa0c31f6927)) - fix(git): make project branch workflows reliable ([#2213](https://github.com/block/buzz/pull/2213)) ([`166f27be4b`](https://github.com/block/buzz/commit/166f27be4bc1abf2d465493bf2137353045399dc)) - feat(cli): manage repository protection rules ([#2193](https://github.com/block/buzz/pull/2193)) ([`f94324598d`](https://github.com/block/buzz/commit/f94324598d84b2db9a05a3fa1f855970c4c5b575)) - feat(cli): add agents archive/unarchive/archived subcommands ([#2173](https://github.com/block/buzz/pull/2173)) ([`7d7992067b`](https://github.com/block/buzz/commit/7d7992067b2914b582b7e6d31a6174603b480b4b)) - fix(mobile): sanitize Android image uploads ([#2188](https://github.com/block/buzz/pull/2188)) ([`ee21da90bd`](https://github.com/block/buzz/commit/ee21da90bd6b1da6bfaaf22ba00749398aaa9640)) - fix(cli): paginate channel directory queries ([#2181](https://github.com/block/buzz/pull/2181)) ([`03fe19d603`](https://github.com/block/buzz/commit/03fe19d6033094ae2ec4c89c26eb23174ef53daa)) - fix(mobile): image upload fails due to unstripped metadata ([#2185](https://github.com/block/buzz/pull/2185)) ([`37f15b2001`](https://github.com/block/buzz/commit/37f15b20019169363b697aee41c99573b7bc3f24)) - perf(relay): compact Git packs before manifest limits ([#2172](https://github.com/block/buzz/pull/2172)) ([`80e0ab16b0`](https://github.com/block/buzz/commit/80e0ab16b03c656ec8def18bedc27eaf29c02867)) - perf(relay): cache Git pack hydration ([#2169](https://github.com/block/buzz/pull/2169)) ([`a4d82ec722`](https://github.com/block/buzz/commit/a4d82ec7226e685a933dbd829b6bb8bce0787b4e)) - fix(relay): bound and observe Git read operations ([#2167](https://github.com/block/buzz/pull/2167)) ([`5f7c93d9c1`](https://github.com/block/buzz/commit/5f7c93d9c12ce7894288ae47f3fe223fcff2dce3)) - relay: gate push enqueue on live leases; batch matcher pipeline (T1b/T1a-repair/T2b) ([#2145](https://github.com/block/buzz/pull/2145)) ([`e43b2d5aac`](https://github.com/block/buzz/commit/e43b2d5aac0d1f2b6b623b04f7af5a51f77da8c6)) - relay: add audit logging disable switch ([#2134](https://github.com/block/buzz/pull/2134)) ([`bf5acabdde`](https://github.com/block/buzz/commit/bf5acabdde44aa133bdcbafcf9e1a4ff752c3302)) - relay: skip TTL deadline bump for known-permanent channels (T1a write-amp) ([#2125](https://github.com/block/buzz/pull/2125)) ([`2e936d439c`](https://github.com/block/buzz/commit/2e936d439ce29182b48086f9f8a7a3ffe3b9b345)) - fix(git): carry NIP-OA delegation in auth event ([#2120](https://github.com/block/buzz/pull/2120)) ([`c12257d57a`](https://github.com/block/buzz/commit/c12257d57a54d5c1e16435440b02beb5d1c057b8)) - Route lag-tolerant reads to an optional Postgres read replica ([#2084](https://github.com/block/buzz/pull/2084)) ([`29c48883d3`](https://github.com/block/buzz/commit/29c48883d30e6feed75e33490571ca96082c6282)) - fix: recover community access visibility ([#2074](https://github.com/block/buzz/pull/2074)) ([`ca384d082d`](https://github.com/block/buzz/commit/ca384d082d9804ec53a3fd12ccbf4a0846b21d92)) - feat: proxy feedback-scoped admin attachments ([#2059](https://github.com/block/buzz/pull/2059)) ([`d7f918e3cb`](https://github.com/block/buzz/commit/d7f918e3cbcc4d30f222d0f4ae836de808f859a2)) - feat: add read-only deployment moderation dashboard ([#1999](https://github.com/block/buzz/pull/1999)) ([`68e670e001`](https://github.com/block/buzz/commit/68e670e001d2bed2cf141095926feaf482c3bed8)) - Bug-bash round 2: table scroll, Goose instructions, workflow mention wake ([#2034](https://github.com/block/buzz/pull/2034)) ([`64b8fea6dc`](https://github.com/block/buzz/commit/64b8fea6dce3be684aa0bac5dbd701e46dc7e432)) - Strip media metadata on clients and reject it at the relay ([#2006](https://github.com/block/buzz/pull/2006)) ([`5cfd69cb0c`](https://github.com/block/buzz/commit/5cfd69cb0cf1dc63d718454defe3b8a8aaf5f15b)) - [codex] Hold Git concurrency permits through streaming (BUZZ-SEC-018) ([#1916](https://github.com/block/buzz/pull/1916)) ([`7baea42abb`](https://github.com/block/buzz/commit/7baea42abbbb794e6e5ab0e9df11e2d1b0550d0b)) - [codex] Enforce shared relay admission limits (BUZZ-SEC-019) ([#1917](https://github.com/block/buzz/pull/1917)) ([`73fc0ec6cf`](https://github.com/block/buzz/commit/73fc0ec6cf58a79bfc65e42faba457bf49c2d232)) - [codex] Block banned actors from moderation commands (BUZZ-SEC-007) ([#1915](https://github.com/block/buzz/pull/1915)) ([`caa195ca58`](https://github.com/block/buzz/commit/caa195ca58ea49cf8ed9c3ede55d6a2e4ed37096)) - [codex] Fix relay WebSocket admission limits ([#1682](https://github.com/block/buzz/pull/1682)) ([`d3ce971fc7`](https://github.com/block/buzz/commit/d3ce971fc75a34162d5498c27ac4a1c30236630a)) - feat: add invite QR and mobile direct join ([#1957](https://github.com/block/buzz/pull/1957)) ([`648cbf3610`](https://github.com/block/buzz/commit/648cbf36109d97be6bd8530e77073d1c7e6008a0)) - fix(join-policy): require legal consent on hosted invites ([#1987](https://github.com/block/buzz/pull/1987)) ([`2e1577f76f`](https://github.com/block/buzz/commit/2e1577f76f5105ddacda7be884518574ca8d6b96)) - [codex] Prevent actor-tag UI impersonation ([#1931](https://github.com/block/buzz/pull/1931)) ([`c540ec9678`](https://github.com/block/buzz/commit/c540ec967869ef0f4eef90439bf70929fc74f7f6)) - Scope relay runtime state by community ([#1658](https://github.com/block/buzz/pull/1658)) ([`d52dedb06f`](https://github.com/block/buzz/commit/d52dedb06fc2c7692c6d9225c7a08b41a509633a)) - Apply optional relay join policy across join flows ([#1894](https://github.com/block/buzz/pull/1894)) ([`6c2d667575`](https://github.com/block/buzz/commit/6c2d667575cbc372ba42d26134448660fb1d2ee9)) - feat(media): require auth for relay media reads ([#1926](https://github.com/block/buzz/pull/1926)) ([`f308762852`](https://github.com/block/buzz/commit/f3087628524951de91028c9d263bcd0d0a727fab)) - feat(relay): add community unarchive endpoint ([#1908](https://github.com/block/buzz/pull/1908)) ([`6b9641db2b`](https://github.com/block/buzz/commit/6b9641db2b4709b71622b7ef0b799117a0605ca6)) - feat(relay): gate Git web GUI separately ([#1901](https://github.com/block/buzz/pull/1901)) ([`34dc7dec75`](https://github.com/block/buzz/commit/34dc7dec75285ab6f2107ce5cad170b80b92206a)) - mesh: upgrade runtime, enforce membership, add shared compute provider ([#1656](https://github.com/block/buzz/pull/1656)) ([`54638ff4bb`](https://github.com/block/buzz/commit/54638ff4bb5af2d3d3759b44118b43052f814bb1)) - Route Git scratch through configured volume ([#1884](https://github.com/block/buzz/pull/1884)) ([`2318b3096c`](https://github.com/block/buzz/commit/2318b3096c585f8d31bd43e27dc5d6305c5fe20d)) - feat(relay): gate usage metrics behind stable leader ([#1814](https://github.com/block/buzz/pull/1814)) ([`59e9821503`](https://github.com/block/buzz/commit/59e9821503a2fe23fc4630aa0f36bb252ae4566f)) - Relay mesh: cross-pod tunnel + huddle transport (buzz-relay-mesh) ([#1670](https://github.com/block/buzz/pull/1670)) ([`ccb021d713`](https://github.com/block/buzz/commit/ccb021d71339009aabedc383c8f3d8e5c23e1e42)) - feat(push): deliver accepted relay events as wakes ([#1866](https://github.com/block/buzz/pull/1866)) ([`bffbc5f22c`](https://github.com/block/buzz/commit/bffbc5f22cc80e9a07dedc622798347d598a215c)) - fix(db): resolve duplicate migration version ([#1863](https://github.com/block/buzz/pull/1863)) ([`08ad38a07f`](https://github.com/block/buzz/commit/08ad38a07f0c49bb3f20b775b8f534f1cfa529c3)) - Add private product feedback sidecar ([#1857](https://github.com/block/buzz/pull/1857)) ([`af190c93e1`](https://github.com/block/buzz/commit/af190c93e1048af64c3fbfb3831c689cb703997c)) - feat(relay): add durable community archival ([#1834](https://github.com/block/buzz/pull/1834)) ([`2b15a72675`](https://github.com/block/buzz/commit/2b15a726750dbd7437711050cd3241b679dff317)) - feat(push): add public APNs gateway ([#1770](https://github.com/block/buzz/pull/1770)) ([`1c006822e4`](https://github.com/block/buzz/commit/1c006822e4484d68e33fce14f9139c2f70ce9d66)) - feat(relay): add atomic community ownership transfer ([#1845](https://github.com/block/buzz/pull/1845)) ([`52e42ccb9f`](https://github.com/block/buzz/commit/52e42ccb9fc85445814614c72d40f346e986152b)) - Bound NIP-RS retention and search indexing ([#1771](https://github.com/block/buzz/pull/1771)) ([`1b4703021d`](https://github.com/block/buzz/commit/1b4703021dbfd37dc31845223dba9ba182e4647f)) - Add optional standalone pairing relay to Helm chart ([#1799](https://github.com/block/buzz/pull/1799)) ([`9b47c8548f`](https://github.com/block/buzz/commit/9b47c8548fd061fbb806ea8b9ddee831c19cf80e)) - fix(relay): publish membership snapshot on provisioning ([#1761](https://github.com/block/buzz/pull/1761)) ([`0950d392b7`](https://github.com/block/buzz/commit/0950d392b7a862694c95cbea1cec45985ee42996)) - feat(relay): per-community usage metrics ([#1723](https://github.com/block/buzz/pull/1723)) ([`620822899a`](https://github.com/block/buzz/commit/620822899a6373fa3a17a87815cd7cade25ed332)) - refactor(desktop): remove vestigial MCP toolsets config ([#1776](https://github.com/block/buzz/pull/1776)) ([`dfec75b3c0`](https://github.com/block/buzz/commit/dfec75b3c0b8080529e4d9089d4ed80e3902aaed)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
261c460761 |
fix(buzz-agent): recover from 400-shaped image rejections; unbound benchmark agent rounds (#5318)
## Problem
Two failure modes from the `tb21-glm52-crusoe-1` benchmark run (GLM-5.2
solo, TB2.1) wedged or killed 13 of 89 trials without the model being at
fault:
1. **Conversation poisoning on text-only endpoints.** Crusoe's
serverless `crusoeai/GLM-5.2-NVFP4` rejects any request whose history
contains an image with `400: ... is not a multimodal model`. The
recovery machinery for exactly this case already exists —
`AgentError::UnsupportedImageInput` → `replace_unsupported_images()`
strips the image blocks, marks the tool result as an error, substitutes
a text placeholder, and continues the turn. But classification only
matched OpenRouter's 404 body (`no endpoints found that support image
input`) and was only consulted on the 404 arms. The Crusoe 400 fell
through to terminal `AgentError::Llm`: the image stayed in history,
every subsequent call failed identically, buzz-acp rode its 10-retry
ladder (~40 min), and the trial idled to budget death. Measured blast
radius: **8 trials wedged, 12.7h aggregate idle-after-poison.**
2. **Bounded agent rounds in benchmark trials.** The harness default
`DEFAULT_MAX_AGENT_ROUNDS = 32` ended solo trials mid-work when turns
rotated (thinking-heavy models hit max_tokens rotation fast; 4 trials
died this way). Benchmark trials already have a wall-clock budget as the
real limit — the round cap only converts recoverable rotation into trial
death.
## Fix
- `is_unsupported_image_input_error()` also matches the verbatim `is not
a multimodal model` body. Matcher stays deliberately tight (same
doctrine as `is_context_length_error`): misclassifying a generic 400 as
recoverable would mutate history for an error that removing images
cannot fix.
- Both status ladders — shared `post()` and `openrouter_post()` —
consult it on their 400 arms and return the typed
`UnsupportedImageInput` (OpenAI-compatible providers report this as 400;
a BYOK/passthrough upstream can surface the provider's own 400 through
OpenRouter).
- Harness `DEFAULT_MAX_AGENT_ROUNDS` → `0` (unbounded —
`BUZZ_AGENT_MAX_ROUNDS=0` is the agent config's documented unbounded
value). Per-agent `budget.max_calls` in manifests still overrides.
## Acceptance
- A 400 with the image-rejection body reaches the existing image-strip
recovery path instead of wedging the session — asserted through
`complete()` (covers the return path into the convergence mapper) and at
the `openrouter_post` terminal, both proving single-attempt (a
deterministic capability rejection must never be retried).
- Ordinary 400s stay terminal `AgentError::Llm` (existing negative tests
unchanged).
- Benchmark trials run unbounded rounds by default; python tests updated
for 0-is-legal with a negative arm at -1.
## Verification
- `cargo test -p buzz-agent`: 427 + 18 + 20 + 15 + 8 + 1 + 48 passed, 0
failed (full package, 3 consecutive clean runs)
- `cargo clippy -p buzz-agent --all-targets`, `cargo fmt --check`: clean
- `uv run --extra dev pytest tests/` in harbor-buzz-orchestra: 35 passed
- Pre-push hooks (full workspace rust-tests + desktop-tauri-checks)
green on rustc 1.95.0 at head
|
||
|
|
6a17d035f7 |
Revert "fix(acp): reject unattended permission requests" (#5323)
Reverts block/buzz#4609 |
||
|
|
c7b663680a |
fix(buzz-agent): budget summarizer reasoning separately so it cannot starve the handoff summary (#5248)
## Problem
The handoff summarizer sends `max_tokens: 8192`
(`HANDOFF_MAX_OUTPUT_TOKENS`) with no reasoning budget separation. On
reasoning models, thinking tokens count against that cap: the model can
spend the entire budget reasoning, length-stop with empty `content`, and
`summarize()` — which only reads `content` — reports an empty summary.
The handoff then degrades to lossy history truncation.
Observed on deepseek-v4-flash during a terminal-bench 2.1 run
(tb21-solo-3, 89 tasks): **13 consecutive handoff attempts across 5
trials failed exactly this way** (`handoff returned empty summary;
truncating`), each burning ~3 minutes of full-cap reasoning, before a
stochastically-short reasoning run finally fit. circuit-fibsqrt alone: 5
failures, 5 truncations, then success on attempt 6. video-processing
failed its task by one frame after 3 context truncations.
## Fix
`openrouter_summary_body` now grants reasoning its own equal-sized
budget and excludes it from the response:
- `reasoning.max_tokens = max_output_tokens` — thinking gets a dedicated
budget instead of competing with the summary text
- `reasoning.exclude = true` — reasoning is never in the response body;
`summarize()` only reads `content`
- `max_tokens = max_output_tokens * 2` — the total cap covers both
budgets, so the text budget the caller asked for is actually available
for text
Non-reasoning endpoints ignore the `reasoning` object. Deliberately not
paired with `provider.require_parameters`, for the reasons documented at
`apply_openrouter_mutations` (it hard-404s valid model ids).
The prior test
`openrouter_summary_carries_neither_reasoning_nor_provider` asserted
`reasoning` absent from the summary body — that assertion guarded
against *effort-based* reasoning leaking in from config (the body is
built independently of `cfg`, which is still true and still tested:
`reasoning.effort` stays unset). Replaced with
`openrouter_summary_budgets_reasoning_separately_and_carries_no_provider`.
## Verification
- `cargo test -p buzz-agent`: 422 unit + 110 integration tests pass at
|
||
|
|
2b873cf208 |
Recover from max-token response truncation (#5223)
## Summary - treat provider `max_tokens` as an interrupted assistant response and continue the same turn with actionable feedback - discard tool calls from truncated responses, including malformed partial arguments, so they are neither executed nor replayed with invalid tool-result pairing - bound recovery to two retries while preserving normal finite `max_rounds` accounting ## Verification - `cargo fmt --all -- --check` - `cargo test -p buzz-agent` (422 unit tests plus all package integration/doc suites passed) - `cargo clippy -p buzz-agent --all-targets -- -D warnings` ## Notes The pre-push repository-wide hook also ran. Its Rust tests passed (2,270 passed, 14 ignored), but its `buzz-db` unit-test build was blocked because local rustc 1.89 is below sqlx 0.9's rustc 1.94 requirement. The affected package suite above is green on the exact pushed commit. Originating Buzz channel: `c3252dd2-0142-4e01-88c7-a2183c3960a5` Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |
||
|
|
78c87ae20e |
fix(sdk): preserve self-mention p tags in message and forum event builders (#4975)
## What users saw `buzz messages send` silently removed an explicitly supplied self-mention. The caller passed `--mention <sender-pubkey>` and received `accepted:true`, but the signed event had no matching `p` tag and `mention_pubkeys` was empty. ## Why it happened `nostr` 0.44 strips `p` tags matching the signer's pubkey by default. The codebase already opts out with `.allow_self_tagging()` for identity archive and unarchive requests, but the message and forum builders that accept mentions did not. The library therefore removed the tag during signing after the CLI had validated the explicit mention. ## What changed Added `.allow_self_tagging()` to all three event builders that accept mention tags: - `build_message` (kind 9) - `build_forum_post` (kind 45001) - `build_forum_comment` (kind 45003) An explicit mention now survives signing even when it matches the sender. ## How this was tested Added one regression test per builder. Each test signs with the same key included in the mention list and asserts that the resulting event preserves the self-referential `p` tag. Validation at `cd0f30bca`: ```text ./bin/cargo fmt --all -- --check cargo test -p buzz-sdk --lib cargo test -p buzz-cli --lib cargo clippy -p buzz-sdk -p buzz-cli --all-targets -- -D warnings ``` All 257 `buzz-sdk` tests and all 321 `buzz-cli` tests passed, and formatting and strict Clippy checks completed successfully. ## Scope and non-goals - Does not change mention validation, deduplication, or channel-member checks. - Does not change `normalize_mention_pubkeys`, which is not used by the messages-send path. - Does not add a dropped-mentions output field because the explicit tags are now preserved. Closes #4906. --------- Signed-off-by: Brad Groux <bradgroux@hotmail.com> Signed-off-by: npub17q2gdupkvswvk5kprwc7plergm4gn295uw6fe4mjyjv53ahuhtnq02jd3f <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz> Signed-off-by: Brad Groux <3053586+BradGroux@users.noreply.github.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: npub17q2gdupkvswvk5kprwc7plergm4gn295uw6fe4mjyjv53ahuhtnq02jd3f <f01486f036641ccb52c11bb1e0ff2346ea89a8b4e3b49cd772249948f6fcbae6@digitalmeld.communities.buzz.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
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>
|
||
|
|
b2ac66cde8 |
refactor(cli): replace probe/decider/detail split with single typed extractor (#5191)
Replaces the four-helper auth resolution path with two focused functions and adds production async tests that count relay round-trips. **Before:** `resolve_auth` called `resolve_auth_from_profile` (warn-emitting probe into a throwaway sink) → `resolve_auth_deciding` (re-classified the same profile) → `handle_auth_failure` → `auth_failure_detail` (third classification). `Option<Option<&Value>>` encoded a sentinel for unreachable state; tests exercised only the pure sync helper, not the actual fetch count. **After:** - `extract_auth(profile, target, signer) -> Result<[String;4], AuthFailure>` — pure typed extractor; `AuthFailure` now covers `NoProfile` and `NoTagsArray` inline, no separate helper needed - `resolve_auth()` is now the linear state machine: self-check → fetch + extract → on failure: fetch again → route final `Err` to `CliError::Usage` (default) or one admin warning (`--admin`). No throwaway sinks, no duplicate classification, no sentinel type. - Five async tests drive the production resolver through a counted Axum test server on `POST /query` and assert on both return value and exact fetch count: first success (1), retry success (2), double failure / no `--admin` (2 + `Err`), double failure / `--admin` (2 + `Ok(None)` + one warning), self path (0). Two parser tests pin `--admin` on both `archive` and `unarchive`. - `--admin` short help text corrected to describe when the flag takes effect (after extraction fails, not unconditionally). 341 tests passing, clippy clean, fmt clean. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
346ae8cadc |
fix(buzz-agent): escalate LLM timeouts per retry and log per-call latency (#5130)
Non-streaming LLM calls (`"stream": false`) through slow model/provider combinations routinely take longer than the fixed `BUZZ_AGENT_LLM_TIMEOUT_SECS` window (default 240 s) to return their first response byte. The retry loop then re-ran the identical 240 s bet three times, failed the turn, and the ACP harness requeued the whole turn from scratch: agents spent 30+ minutes producing nothing while every attempt died at the same wall. And because the LLM path only logged WARN lines on failure, a healthy-but-slow call was indistinguishable from a wedged one. ### Timeout handling - **Per-attempt escalation**: the per-request budget doubles after each timeout failure (`base × 2^n`, capped at `max(1200 s, base)` — `escalated_timeout()` in `llm.rs`), shared by the main `post()` loop and `openrouter_post()`. Non-timeout retryables (429/5xx/connect) do not escalate. A call that needs six minutes now succeeds on a later attempt instead of never. - **Per-request total timeouts**: enforcement moved from the client-level `read_timeout` to `RequestBuilder::timeout()` on each LLM request, so escalated budgets aren't silently floored by the shared client and each attempt's bound covers connect through body completion. Timeout error messages were updated to match the new semantics and still point at `BUZZ_AGENT_LLM_TIMEOUT_SECS`. ### Observability - One INFO line per completed LLM call: model, provider, `duration_ms`, `input_tokens`, `cached_input_tokens`, `output_tokens`. Slowness and prompt-cache effectiveness are now visible in harness logs without waiting for a failure, and `None` vs `0` token reports stay distinguishable. Handoff summarization calls log the same line with duration only. - The agent main loop wraps the call in a `session_id` tracing span so each line is attributable to a session. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
c293b3cd40 |
fix(agent): resolve oauth cache home cross-platform (#5151)
## Summary - resolve the OAuth cache home directory with the repository's platform-aware `dirs` convention - preserve the existing `.config/buzz-agent/oauth` cache layout on macOS and Linux - make the cache-path regression assertion portable across path separators ## Problem `buzz-agent` read only `$HOME` when constructing the OAuth token cache path. Packaged Windows processes do not guarantee that Unix variable, so OAuth source construction failed with `oauth cache: $HOME not set` even though Windows had a valid user profile. ## Validation Independent reviewers validated exact commit `836d820483b141b7291170cb33535ac7cb49b2eb` on Windows/MSVC with `HOME` unset and `USERPROFILE` present: - `cargo +1.94.1 clippy -p buzz-agent --all-targets --locked -- -D warnings` - `cargo +1.94.1 fmt --all -- --check` - `git diff --check f53bbd1..836d820` - `auth::` tests: 11/11 passed with `HOME` unset - full package lib target: 396 passed / 2 failed; identical-base controls classified both as pre-existing Windows failures The changed regression fails on the base with `$HOME not set` and passes on this branch. `dirs 6.0.0` was already locked by other workspace crates; the lockfile change adds only the `buzz-agent` dependency edge. Signed-off-by: Kalvin C <kalvinnchau@users.noreply.github.com> |
||
|
|
ee9690a93c |
fix(cli): emit structured JSON warning when archive/unarchive owner-auth extraction fails (#4824)
emit structured JSON diagnostics when NIP-OA owner-auth extraction fails
during `buzz agents archive`/`unarchive`
## Problem
When owner-auth extraction returned `None`, the CLI silently sent a bare
request. The relay replied with `400: missing auth tag` and the caller
had no way to know why extraction failed.
## Solution
Extract `resolve_auth_from_profile` — a sync function that owns all
three warning branches and the success path. `resolve_auth` reduces to:
self-check → fetch kind:0 → delegate.
- **Four distinct diagnostics**: no kind:0 profile / no tags array /
`classify_owner_auth_tag` failure (typed `AuthFailure` enum:
`NoAuthTag`, `AmbiguousAuthTag`, `WrongArity`, `NonStringElement`,
`InvalidOwnerHex`, `InvalidSigHex`, `OwnerMismatch`)
- **JSON format**: each fallback emits exactly one `{"warning":"..."}`
line to stderr, matching the CLI's documented structured-stderr contract
and the precedent in `channels.rs:597`
- **Relay-supplied values** (target pubkey, actual owner pubkey) pass
through `serde_json` serialization — no unescaped text
- **Admin bare path preserved**: request is always sent after the
warning; bare non-self requests are legitimate for relay admins
- **Self path unchanged**: silent, no relay query
## Boundary tests
Tests call `resolve_auth_from_profile` directly with `&mut Vec<u8>`.
Each of the three production `writeln!` calls is covered: deleting any
one fails at least one test. Success path asserts zero bytes written.
## Changes
`crates/buzz-cli/src/commands/agents.rs` only:
- `AuthFailure` enum with `message()` formatter
- `classify_owner_auth_tag` returning `Result<[String;4], AuthFailure>`
- `extract_owner_auth_tag` reduced to `#[cfg(test)]` `.ok()` wrapper
- `resolve_auth_from_profile` sync helper (testable without
`BuzzClient`)
- `resolve_auth` reduced to self-check + fetch + delegate
- 9 new boundary tests replacing the prior test-local helper
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
ad923353a2 |
feat(relay): accept kind:30179 private managed-agent events at ingest (#5133)
## What Relay-only carve-out of the ingest half of #4999: generic EVENT ingest now accepts kind:30179 (NIP-PMA private managed-agent config). One file, `crates/buzz-relay/src/handlers/ingest.rs`, 16 insertions / 15 deletions; **two semantic lines**, byte-identical to the ingest hunk of #4999 at `6f486e88`: 1. `required_scope_for_kind`: 30179 requires `Scope::UsersWrite` — same arm as its public sibling 30177 and the other owner-authored NIP-AP kinds. 2. `is_global_only_kind`: 30179 is owner-global, keyed `(pubkey, kind, d-tag)`; a stray `h` tag must not channel-scope it. The rest is import reflow plus replacing the guard test with a positive one (`private_managed_agent_kind_is_owner_scoped_global_user_data`: asserts UsersWrite scope, global-only, no h-channel scope). ## Why the guard test can be retired The removed test (`private_managed_agent_kind_remains_rejected_until_atomic_ingest_exists`) pinned a stated precondition: *"must not enter generic EVENT ingest before privacy and aggregate CAS deploy."* Both halves are resolved: - **Privacy** — the author-only read gates for 30179 shipped to main with #4593: `AUTHOR_ONLY_KINDS` membership, `req.rs` pre-filter + result gates, `count.rs`, `event.rs` fanout, and the bridge pre-filter (`bridge.rs:999-1000` returns `restricted: author-only kinds require authors=[self]` / 403). Only the author can read the event back. - **Aggregate CAS** — #4999 settled generation as **advisory**: the `g` tag is shape-validated, never relay-enforced. Last-write-wins per coordinate is the contract of record (see the kind:30179 contract blurb in #4999), so no CAS mechanism is pending on the relay side. ## Why this is inert to existing relays and clients - No production desktop code on main authors kind:30179 — the codec (`private_managed_agent.rs`) has zero non-test callers. This PR accepts a kind nobody can produce yet. - Content is opaque NIP-44 ciphertext to the relay; the relay never decrypts it. - Reads remain author-only via the already-shipped gates above. - Storage is the standard parameterized-replaceable path already exercised by kinds 30175–30178. No schema, config, or migration changes. ## Testing - Full `buzz-relay` package suite at this commit: 859 passed, 1 failed — `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` (504 vs 200), which **reproduces identically on clean main `769ac70b`** with this change stashed; pre-existing/environmental, not introduced here. - New positive ingest test passes. - Pre-push hooks green (branch-skew, rust-tests, desktop-tauri-checks). ## Relationship to #4999 #4999 (relay-primary agent config, desktop half) stays DO-NOT-MERGE pending live relay receipts + real CI; once this lands and deploys, its live test simplifies to plain `desktop-standalone` against the real relay, and #4999 rebases to drop its now-duplicate ingest hunk (identical bytes → trivial rebase). Originating thread: buzz://message?channel=06f13ed3-0557-4ac2-922c-1545dd00bf97&id=2a43b3b4933a2ea78b77088619251c061355f9b7b6dc29ea0d702193f2344149 ## Brownfield FTS note (review findings, operator-ruled non-blocking for this PR) Max and Sami independently identified that the FTS privacy skip-set is regime-dependent: migration 0008 installs the positive allowlist (`kind IN (0, 9, 40002, 45001, 45003)`) **only on an empty events table**; an already-populated database keeps the 0001/0005 negative skip-list (wrapped by 0014 to add 30350), which omits 30179 — so on such an installation this PR admits 30179 rows whose NIP-44 ciphertext gets indexed by `to_tsvector`. Sami measured both regimes against real Postgres (brownfield: 30179 INDEXED; fresh: NULL) and demonstrated the existing drift test only exercises the fresh regime. `schema/schema.sql:222`'s canonical literal is also the negative list and omits 30179. Migration dates put any relay deployed with data before 0008 landed (2026-07-13) in the brownfield class. **Scope of exposure (Sami's trace):** not a content leak — `event_visible_to_reader` / `is_author_only_event` gates hold on both search surfaces (`req.rs:725`, `bridge.rs:1770`), so foreign readers receive nothing. Lost is the storage-level NULL-tsv backstop plus FTS page budget burned on post-filtered hits. **Operator ruling (Tyler, events `1472e5b6`, `cbd368ed`):** ship this PR without an exclusion migration. Safety argument that makes this sound rather than merely accepted: main has **zero non-test 30179 writers** until #4999's desktop half deploys — no 30179 rows can exist, so nothing can be indexed in any regime while this PR is the only half live. **Additional review characterizations (Sami, non-blocking, on the record):** - *Behavioral delta enumerated:* routing triple (`required_scope_for_kind` / `is_global_only_kind` / `requires_h_channel_scope`) compared for all 65,536 kinds at base `769ac70b` vs head `77eeba6e` — exactly one row differs (30179). No other kind or client changes behavior. - *"SQL visibility before LIMIT" (NIP-PMA step 2):* no `AUTHOR_ONLY_KINDS` pushdown clause exists in `buzz-db` (only `SHARED_GATED_KINDS` has one). Author-only kinds are protected by the pre-filter (`author_only_filters_authorized`) plus post-filter omission; mixed-kind filters can burn candidate-page budget on discarded rows. Pre-existing and identical for 30300/30350 — not introduced here; noted so the NIP's step-2 checkbox is not read as fully ticked. - *Envelope validation gap:* 30179 is the only parameterized-replaceable kind at ingest with no per-kind envelope validator (codec grammar checks run in the desktop writer, not the relay). Generic limits only (256 KiB, ±15 min, pubkey==identity, d-tag bound). Self-inflicted footgun bounded to the author's own coordinate — candidate companion to the exclusion migration in the #4999 rebase, deliberately not added here. **Bound follow-up (required before/with the #4999 desktop half):** a 0014-shape additive migration (`pg_get_expr` capture + `CASE WHEN kind = 30179 THEN NULL ELSE (<existing>) END` wrap), add 30179 to the `schema/schema.sql:221` literal, and a brownfield-regime variant of the FTS drift test, per Sami's finding. Deploy-time spot check if ever wanted: `SELECT pg_get_expr(d.adbin, d.adrelid) FROM pg_attrdef d JOIN pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE d.adrelid = 'events'::regclass AND a.attname = 'search_tsv';` Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.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> |
||
|
|
bd2fdf4a2f |
fix(buzz-agent): classify read timeouts distinctly in LLM error messages (#4959)
## Problem
When `buzz-agent` exhausts retries on a stalled LLM call, the error
message reads:
```
transport: error sending request for url (...) (cumulative 721s, 3 attempts)
```
That text is reqwest's generic pre-response failure string — identical
whether the cause is a TLS abort, a reset connection, or a
`read_timeout` fire. An operator reading the log cannot tell whether
something broke or whether the LLM generation legitimately took longer
than the configured timeout.
## Root cause (probe-confirmed)
A live probe against `goose-claude-fable-5` with a 900s client timeout
completed in **370s** — well past the default
`BUZZ_AGENT_LLM_TIMEOUT_SECS=240`. Extended-thinking models emit zero
bytes on non-streaming calls until generation is complete, so reqwest's
`read_timeout` fires on byte-silence regardless of whether the server is
healthy. The 46× exact-721s stall signatures in production logs (3 ×
240s + backoff) are deterministic self-inflicted timeouts, not network
faults.
## Fix
### Pure classifier over `{is_connect, llm_timeout, phase}`
A new `timeout_message(is_connect: bool, llm_timeout: Duration, phase:
TimeoutPhase)` pure function produces factual messages with the
configured duration value embedded verbatim. Two thin wrappers
(`classify_transport_error`, `classify_body_read_error`) extract the
reqwest flags and delegate. The duration reaches the classifiers through
a new `read_timeout: Duration` parameter on `post()` and
`openrouter_post()`; callers pass `cfg.llm_timeout`.
### Messages emitted
| Case | Message |
|---|---|
| Connect-phase timeout (`is_connect && is_timeout`) | `connect timeout:
no connection established within 10s` |
| Transport read-timeout | `read timeout: no response bytes received
within 240s (consider raising BUZZ_AGENT_LLM_TIMEOUT_SECS)` |
| Body-read timeout | `read timeout: no further response bytes received
within 240s (consider raising BUZZ_AGENT_LLM_TIMEOUT_SECS)` |
| Non-timeout | `transport: {reqwest text}` / `body read: {reqwest
text}` (unchanged) |
`LLM_CONNECT_TIMEOUT` is now a named `const` (was inline
`from_secs(10)`).
**Out of scope by explicit decision:** streaming support, changes to
`MAX_RETRIES` or backoff.
## Files changed
- `crates/buzz-agent/src/llm.rs` — `timeout_message` pure fn +
`TimeoutPhase` enum + `LLM_CONNECT_TIMEOUT` const; two classifier
wrappers updated; `post()` and `openrouter_post()` gain `read_timeout`
param; tests replaced.
## Tests
`cargo test -p buzz-agent`: **397 passed, 0 failed** at `294ce5897`.
**Pure-function tests (no network):**
- `timeout_message_connect_true_shows_connect_timeout` —
`is_connect=true` → connect-flavored text with 10s value; both phases
checked
- `timeout_message_transport_phase_shows_read_timeout_and_duration` —
transport phase includes 240s and config knob
- `timeout_message_body_read_phase_says_no_further_bytes_and_duration` —
body phase says "no further", shows 300s
- `timeout_message_duration_is_not_hardcoded` — 600s supplied → 600s in
output, not 240s
**Loopback reqwest integration tests:**
- `classify_transport_error_read_timeout_is_loopback_verified` — TCP
connect succeeds, server sends no bytes; verifies reqwest sets
`is_timeout && !is_connect` and message contains 50ms value
- `classify_transport_error_non_timeout_preserves_reqwest_text` —
controlled accept-then-close on an owned loopback listener → non-timeout
error; asserts exact `transport: {err}` output equality
- `classify_body_read_error_timeout_says_no_further_bytes` — loopback
server sends headers + 4 bytes of a declared-1024-byte body, then holds;
verifies `is_timeout`, "no further", 100ms value, config knob
No test performs egress beyond loopback (`127.0.0.1`). The TEST-NET-3
dial is deleted.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
38bf642fcf |
ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes (#3862)
## Summary
CI now proves the full Buzz shared-compute join story end to end: a
member can discover another member's served model **through the Buzz
relay alone** and run inference over the mesh, while a non-member gets
nothing — the relay rejects its auth, and the mesh refuses to route for
it even holding a leaked endpoint address.
This is deliberately different from mesh-llm's own CI smokes (which
bootstrap two nodes with a hand-carried invite token / mdns): here the
**relay is the control plane**, exactly like the desktop app:
1. **Membership** — identities A and B are added via `buzz-admin`
(kind:13534 NIP-43 roster); C is not.
2. **Advertise** — each member publishes a client-signed kind:30003
discovery note carrying its MeshLLM owner binding and (for the serve
node) `serveTargets[].endpointAddr`, covered by an endpoint-binding
signature — the exact payload shape the desktop coordinator publishes.
3. **Trust** — the serve node derives its admission allowlist from the
relay (statuses ∩ roster) and requires the **exact expected {A, B}
owner-id set** before starting with `TrustPolicy::Allowlist`.
4. **Join** — the client verifies owner + endpoint bindings and
membership, then dials the relay-discovered endpoint (the desktop
join-watcher's `dial_endpoint_addr` step). No out-of-band token.
5. **Infer** — a chat completion against the client's local OpenAI
endpoint routes over QUIC to the serve node's model (CPU, SmolLM2-135M,
~105MB).
6. **Deny (differential)** — the stranger's NIP-42 auth must fail with
the relay's own membership rejection (`restricted: not a relay member` —
successful auth or any unrelated connect error fails the run), and
dialing the leaked endpoint must not produce a routed inference —
**while the trusted client re-proves inference immediately afterwards**,
so a dead serve node can't masquerade as an admission denial.
## What's in the PR
- `crates/buzz-relay/examples/mesh_relay_lifecycle_smoke.rs` — the
harness. One process per node (mesh-llm keeps process-global state under
`~/.mesh-llm`), orchestrator + serve/client/stranger roles,
byte-identical binding payloads to
`desktop/src-tauri/src/mesh_llm/identity.rs` (called out with
keep-in-sync comments). Child stdout is pumped through a reader thread
so every wait has a hard deadline; timed-out children are killed; exit
statuses are checked.
- `scripts/ci-mesh-lifecycle-smoke.sh` — provisions a membership-gated
relay (throwaway owner + signing identities via `buzz-admin
generate-key`), runs the harness, cleans up. Fails fast if :3000 is
already occupied (a stale open relay would mask gating).
- `scripts/start-relay-for-tests.sh` — gains opt-in NIP-43 membership
env passthrough (`BUZZ_REQUIRE_RELAY_MEMBERSHIP` + `RELAY_OWNER_PUBKEY`
+ `BUZZ_RELAY_PRIVATE_KEY`). Default behavior unchanged.
- `.github/workflows/mesh-lifecycle.yml` — separate, path-filtered,
non-required workflow (mesh paths, the harness's dependency crates,
`Cargo.lock`, dispatch), pinned to `ubuntu-24.04`. Caches the mesh
native runtime + HF model keyed on the lockfile hash, so a mesh pin bump
rolls the runtime cache. Uploads relay + harness logs on failure.
## Scope
This is an **independent protocol harness**: it speaks the same wire
protocol and payload shapes as the desktop but re-implements the
binding/verification logic (the desktop crate is outside the workspace).
Regressions inside the desktop's own discovery filtering are the desktop
unit tests' job; what this smoke proves is that the relay + mesh-llm SDK
+ admission stack support the lifecycle end to end.
## Relationship to mesh-llm's CI
Follows the shape mesh-llm's own CI proved stable (tiny CPU model, one
runner, multiple real mesh-llm processes over real QUIC — cf. their
`ci-two-node-client-serving-smoke.sh`), but swaps the token bootstrap
for the relay-driven lifecycle, which is the part only Buzz can test.
## Validation
Green on GitHub Actions (ubuntu-24.04) across three runs, including
after rebases onto the mesh v0.74 upgrade (#3467) and latest main:
```
PASS 1/6: relay-derived allowlist is exactly {A, B}
PASS 2/6: serve member ready + advertised model: jc-builds/SmolLM2-135M-Instruct-Q4_K_M-GGUF:Q4_K_M
PASS 3/6: client member discovered + joined via relay
PASS 4/6: inference routed over the mesh: "PONG"
PASS 5/6: relay rejected the stranger's NIP-42 auth (membership gate)
PASS 6/6: stranger denied (gossip visible, inference rejected: 503 all tunnels failed) while trusted inference still routes
PASS: full relay-driven mesh lifecycle verified
```
Also validated locally on macOS. `cargo fmt --all --check` and `cargo
clippy -p buzz-relay --all-targets -- -D warnings` pass.
## Notes
- The harness follows the repo's mesh `[dev-dependencies]` pin
automatically, so it doubles as a canary for future mesh upgrades (it
already caught the v0.73.1 → v0.74.0 bump during development).
- The stranger "deny" accepts either shape mesh-llm exhibits: no model
visibility at all, or gossip visibility with inference refused —
mesh-llm applies the receiving node's owner policy after the gossip
handshake, so admission gates *routing*, not gossip. The differential
trusted-inference re-check (PASS 6/6) is what makes that a real denial
rather than a dead server.
- Model-visibility windows are tunable via `MESH_CLIENT_WINDOW_SECS` /
`MESH_STRANGER_WINDOW_SECS` if shared runners prove slow — pin a longer
window in the workflow env rather than re-running the job.
---------
Signed-off-by: Michael Neale <michael.neale@gmail.com>
|
||
|
|
4da7264d90 |
fix(acp): pace observer telemetry at 1/s with per-channel batch envelopes (#4917)
## Problem Observer telemetry is the noisiest client of the relay: the old pacer (167ms spacing + 90/min rolling cap) let a busy session bill up to 6 events/second against the owner's message quota, and the rolling cap silently *dropped* frames once exceeded. Ruling from the rate-limiting investigation thread (channel `826fc99b-1472-40e7-a529-6b9db8943b8c`): pace at 1/s, always emit, minimal PR. **Review round 1 (Max, Sami)** found the first cut wrong in three ways — tick burst (all pending frames per tick), startup burst (`interval` fires at t=0), and per-channel quota arithmetic. All fixed and mutation-verified in round 1. **Review round 2 (Sami, Max)** found two more against the round-1 head: 1. **Drain-rate collapse (Sami, blocker):** front-run-only packing meant a frame held ONE event whenever channels interleaved — measured 275 B/s vs 63.5 KB/s, so an ordinary 2-channel session fell minutes behind with zero drops and no warning. Silent unbounded latency. 2. **Coalescer byte-cap bypass (Max):** chunks pending in `ObserverChunkCoalescer` were unbounded and outside the 4 MiB cap — 500 distinct-`messageId` 50KB chunks retained ~48MB with `pending_bytes == 0` and zero drops. **Review round 3 (Max)** found the drop accounting undercounted merged chunks: a coalescer entry that merged N same-`messageId` chunks counted as **1** in `dropped_events` when evicted (50 merged 1KB chunks evicted → counter read 1, 49 generated events unaccounted). Fixed: accounting is now denominated in **source (generated) observer events** end to end — each pending entry tracks how many chunks it absorbed, eviction charges that count, and the count survives flush into the publish FIFO. **Review round 4 (Sami, Max)** found three more against the round-3 head: 1. **Coalescer byte undercount (both, independently):** a pending merged entry retains its first chunk's text **twice** until flush — once inside the serialized event skeleton and once in the extracted text accumulator — but was charged only `serialized_len`, so true retention overshot the 4 MiB cap ~2× (measured 8.3 MB). Fixed: `push_pending` charges `serialized_len(&event) + text.len()`. 2. **Cap regressions asserted the accumulator against itself,** which is how the undercount hid. All three cap tests now assert on independently **walked** retained bytes (`serialized_len` per FIFO entry + `serialized_len + text.len()` per coalescer entry), with a secondary `accumulator >= walked` sanity check. Reverting the fix makes them fail at exactly 8,328,386 / 8,328,272 bytes. 3. **FIFO-arm source accounting was implemented but untested (Sami M13; Max reproduced at `cc9333b7c` with 102/151):** the round-3 regression only evicted a merged entry while still in the coalescer. New test forces a merged entry (50×1KB, `source_events=50`) through flush into the publish FIFO, then evicts it from there — mutating the FIFO eviction to `dropped += 1` fails with the reviewers' exact numbers (102 vs 151). **Review round 5 (Sami 9/9/9, Max 9/9/9)** — production judged merge-safe by both; remaining items are tests only, all landed at `63d821620`: 1. **The walker instrument was itself unverified (Sami M17–M20; Max independently confirmed the `return 0` mutant survives):** every cap test asks `walked_retained_bytes()` only for `<= CAP`, so a blinded walker passes everything — and paired with a reverted `push_pending` fix the two mutations cancel, hiding exactly the 8.3 MB overshoot it exists to detect. New two-sided pin: the walker must SEE the first chunk's text twice, and must agree with the accumulator EXACTLY while both stores are non-empty. Kills M17, M18, M19, M20. 2. **Two pre-existing snapshot-clone siblings (Sami D5b/D5c; byte-identical at merge-base `7334ad1e1` — not this PR's regression, but the PR made the class visible):** aliasing the inner turns map leaks a post-save turn into the snapshot; aliasing the inner tombstones map leaks a post-save terminal that blocks a legitimate post-restore resurrection. Two isolation tests with in-test controls — all three inner-map clones in `saveActiveAgentTurnsForCommunity` are now pinned. ## Change **Harness (`crates/buzz-acp`)** - **Global pacer: AT MOST ONE relay frame per second**, regardless of channel count or backlog size. `interval_at(now + 1s)` restores the no-startup-burst property; `MissedTickBehavior::Skip` is now pinned by a paused-time test (a stalled tick arm fires one catch-up frame, not one per missed deadline). At 1 frame/s telemetry spends ≤60/min of the shared 120/min quota; `OBSERVER_PUBLISH_TICK` documents the tradeoff as the knob. - **`ObserverPublishQueue` with gather-packing:** events wait as byte-accounted events (FIFO). `next_frame()` packs the front event's channel **gathered queue-wide in FIFO order** — frames never mix channels, and each channel's events keep their FIFO order, but cross-channel frame order MAY differ from arrival order. That is what keeps the drain rate in **bytes per slot** (one ~64KB frame/s) instead of front-run-length events per slot. **Null-channel events (`agent_panic`-class) are packing barriers** nothing gathers across, so causally-global events keep exact order against every channel. - **One byte cap over BOTH stores:** the event FIFO and the coalescer's pending chunk buffer count against the 4 MiB budget together; eviction is oldest-first across both (queue front, then coalescer front — structural age order) with accounting (warn + counter). A high-cardinality chunk flood is bounded exactly like a plain event flood. Coalescer entries are charged their **true** retention (`serialized_len + text.len()` — the first chunk's text lives in both the serialized skeleton and the extracted accumulator until flush). - **Shutdown is not a burst bypass:** paced one-frame-per-tick drain until empty. **Desktop** - `unwrapObserverBatch` expands envelopes on the live relay path and archive-ingest seam (round 1, unchanged). - **`activeAgentTurnsStore` watermark re-keyed per (agent, channel)** with a dedicated null-channel bucket: the per-agent `(timestamp, seq)` gate would silently skip a delayed channel's frames as stale under gather-packing's intentional cross-channel reorder. Safe because every turn-mutating path is channel-scoped by the event's own `channelId` (endTurn's null-turnId fallback matches `turn.channelId`; resurrectTurn keys on `event.channelId`), so per-channel serialization preserves each guard the per-agent gate provided. The tombstone-cap justification is rewritten for the new keying (worst case for an evicted tombstone is a ghost badge the prune reaps — bounded cosmetic staleness, not corruption). Community-switch save/restore deep-clones the nested map. Other per-agent maps stay agent-keyed: the clock offset is a running minimum (order-insensitive); turns/tombstones mutate only through channel-scoped paths. ## Version skew — old desktop + new harness Gather-packing *intentionally* emits cross-channel-reordered frames. An **old desktop** (per-agent watermark) against a **new harness** will silently skip a delayed channel's turn-state events as stale — working badges on that channel can go stale/missing until its next fresh event. Transcript and archive are unaffected (the transcript store sorts + rebuilds on out-of-order arrival; the archive is per-channel by construction). Ship desktop and harness together; skew degrades badges only, not data at rest. ## Throughput ceiling — "lossless" is qualified Sustained lossless rate is what fits in one ~64KB frame per second, now genuinely in bytes under interleaving: | event payload | events per frame | sustained ceiling | |---|---|---| | 100 B | 250 | 250 ev/s | | 500 B | 99 | 99 ev/s | | 2 KB | 30 | 30 ev/s | | 10 KB | 6 | 6 ev/s | With C channels producing concurrently, publish slots round-robin between them: per-channel drain is ~64KB/C per second and the 4 MiB burst budget (~64s single-channel) shortens accordingly. Beyond budget, oldest-first drops **with accounting** — visible, designed loss. **Accounting semantics:** `dropped_events` counts SOURCE (generated) observer events, not retained entries — evicting a coalesced entry that merged N chunks charges N. On the published side, a merged entry ships all N sources' text in ONE event, so the reconciliation invariant is `ingested == dropped_events + Σ source_events over published events` (for unmerged events, source_events = 1). ## Verification At `63d821620d3513505e8766ac691a8002f9d4a96f` (this head; `git rev-parse HEAD` matched in the same shell as every run), rustc 1.95.0: - `cargo test -p buzz-acp`: **689 lib + 9 integration, 0 failed** — regressions: interleaved 2-channel drain packs into ≤4 frames not 200 slots; null-channel barrier; queue-wide gather with within-channel FIFO; distinct-key 50KB chunk flood bounded by the cap with event-level accounting (published + dropped == ingested, survivors newest); paused-time `MissedTickBehavior::Skip` pin (verified to fail under `Burst`: 3 frames vs 1); merged-key eviction accounts every absorbed source chunk in BOTH arms — coalescer-side (Max's round-3 probe) and post-flush FIFO-side (Sami M13 / Max's round-4 probe: fails 102 vs 151 under `+= 1`). All three cap tests assert on independently walked retained bytes, not the accumulator (verified to fail without the `+text.len()` fix: 8,328,386 / 8,328,272 vs 4 MiB); the walker itself is pinned two-sided against the accumulator (all four blinding mutants M17–M20 verified to fail it, including the walker+fix cancellation pair). - `cargo clippy -p buzz-acp --all-targets -- -D warnings` clean, `cargo fmt --check` clean - Desktop: `tsc --noEmit` clean; node tests **4366 passed, 0 failed** — snapshot-clone family fully pinned: watermark aliasing (round 4), turns aliasing and tombstone aliasing (round 5, pre-existing gaps; each mutant verified to fail exactly its target test with an in-test control). Prior rounds: cross-channel reorder processed, cross-channel-delayed null-turnId `turn_error` evicts only its own channel's turn, null-bucket replay idempotency, same-channel stale/duplicate still skipped, watermark survives community-switch save/restore - All pre-push hooks green at the pushed commit (branch-skew, desktop-check, desktop-test, rust-tests, desktop-tauri-checks) Part of the rate-limiting fix stack; independent of `eva/rate-limit-fixes` by design (separate minimal PR per Tyler's ruling). --------- Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> |
||
|
|
e14fff74d0 |
relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542)
## Problem On SIGTERM the relay sends every live WebSocket a **1012 Service Restart** close frame via `ConnectionManager::drain_all()` — all in the same instant (`main.rs` shutdown task → `state.rs::drain_all`). On a pod holding thousands of sessions, that makes every client reconnect simultaneously: the thundering-herd reconnect behind the DB pool-timeout bursts observed on each rolling deploy. Client-side jitter can't fix this — the desktop client *resets* its backoff to base on a 1012 and reconnects with only ±25% jitter (`relayClientSession.ts`), so the spread has to come from the server. ## Change Add `BUZZ_DRAIN_JITTER_MS` (default `0` = unchanged behavior). The two paths are kept **deliberately separate** so the default is byte-for-byte the previously shipped shutdown: - **Jitter off (`0`/unset, the default):** the original synchronous, all-at-once `drain_all()` runs unchanged — queue the 1012 on each connection's control channel, cancel, return. No new machinery on the default path. - **Jitter on (`> 0`):** a separate async `drain_all_jittered(jitter_ms)` spreads each connection's restart close over an independent uniform delay in **`[1, jitter_ms]`**. Each delayed close travels a dedicated `RestartClose` channel; the writer flushes the 1012 frame and **acknowledges the flush over a oneshot**, so drain waits for confirmed delivery (up to `RESTART_CLOSE_ACK_TIMEOUT` = 5s) rather than assuming it, falling back to cancellation if the channel is full/closed or the ack times out. The drain future is **owned and awaited** by the shutdown task, and the 30s hard-drain backstop is aborted only after a clean drain — so a clean roll exits `0`. The two methods can be unified and the old one dropped later once the jittered path is proven for all cases. - **`config.rs`** — `drain_jitter_ms`: non-negative parse, clamped to `MAX_DRAIN_JITTER_MS` = **20s** (leaving 10s of the 30s budget for flush). Junk fails loudly at startup; **empty/whitespace-only is treated as unset (jitter off)** so a `BUZZ_DRAIN_JITTER_MS=""` kill switch does not crashloop the relay (matches the sibling env vars in this file). - **`state.rs`** — `drain_all()` (unchanged synchronous default) + `drain_all_jittered()` (jittered + flush-ack). Both set the sticky `draining` flag before the first await. A registration that lands mid-shutdown always self-signals via the **immediate** control-frame + cancel path — jitter smears already-established sockets, not late arrivals. - **`main.rs`** — shutdown task dispatches: `drain_jitter_ms == 0` → `drain_all()`, else `drain_all_jittered(...).await`. ## Safety - **Default off is the currently-committed path.** With jitter unset/0 the shutdown runs the original synchronous `drain_all()` — no restart channel, no ack wait. Safe to deploy dark and dial up. - **Shutdown-boundary race preserved.** Sticky flag set before any await; a late registration self-signals its close with no jitter. - **Owned + backstopped.** The jittered drain future is awaited; the 30s hard-drain `process::exit(1)` remains the ceiling. `MAX_DRAIN_JITTER_MS` (20s) + `RESTART_CLOSE_ACK_TIMEOUT` (5s) = 25s, inside the 30s budget; 5s pre-sleep + 25s = 30s against `terminationGracePeriodSeconds: 60`. ## Known behavior to note (not a blocker, flagged from review) On a **successful** flush the jittered path deliberately does not cancel the connection token — teardown then depends on the client echoing our Close, or on process exit. Compliant clients echo; a silent client rides to the 30s hard exit. The default (jitter-off) path cancels deterministically as before. ## Tests - `config::tests::drain_jitter_defaults_off_and_rejects_junk` — default off, `20000`, clamp `60000`→`20000`, explicit `0`, junk `"soon"` fails, **empty `""` and whitespace-only treated as off**. - `state::tests::drain_all_is_immediate` — default path queues frame + cancels synchronously. - `state::tests::drain_all_sends_restart_close_and_cancels_every_conn`, `drain_all_full_control_buffer_still_cancels`, `register_after_drain_self_signals_restart_close_and_cancel`. - `state::tests::drain_all_jittered_defers_close_until_within_jitter_window` (paused time). - `state::tests::drain_all_jittered_waits_for_writer_acknowledgement_without_cancelling`. - `state::tests::drain_all_jittered_cancels_when_restart_channel_is_full_or_closed`. - `state::tests::drain_all_jittered_cancels_when_flush_ack_times_out` (paused time — the 5s ack-timeout fallback). Validation at `46c690940`: `cargo fmt -p buzz-relay --check`, `cargo clippy -p buzz-relay --all-targets -- -D warnings`, and the drain/config unit suite all clean. Local live SIGTERM test with a real relay process + 200 NIP-42-authenticated sockets — see the PR comment for the before/after distribution and exit codes. ## Rollout Ship with default `0`, then set `BUZZ_DRAIN_JITTER_MS` (e.g. 10000–20000) on bb-block first, watch the roll-window pool-timeout metric, then bb-public. `""` is a safe kill switch. Complements the preStop `sleep` (stops routing before close). --------- Signed-off-by: npub1srl70fhzyu3fsnahl06vw2czvqc2w3ds37hyzvjnk8ve8f03ngcqg9le2w <80ffe7a6e22722984fb7fbf4c72b026030a745b08fae413253b1d993a5f19a30@buzz.block.builderlab.xyz> Signed-off-by: npub128x7j3pwgm4vs8yra3c42fcgcwcvh94g3luwzkqa376du2q6l0esqcrwch <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz> Signed-off-by: Brad Seiler <seiler@squareup.com> Co-authored-by: npub1srl70fhzyu3fsnahl06vw2czvqc2w3ds37hyzvjnk8ve8f03ngcqg9le2w <80ffe7a6e22722984fb7fbf4c72b026030a745b08fae413253b1d993a5f19a30@buzz.block.builderlab.xyz> Co-authored-by: npub128x7j3pwgm4vs8yra3c42fcgcwcvh94g3luwzkqa376du2q6l0esqcrwch <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz> |
||
|
|
2ea9385015 |
fix(reactions): support max-length custom emoji (#3833)
**Category:** fix **User Impact:** Custom emoji with valid 64-character names can now be used as reactions without errors. **Problem:** Buzz accepted 64-character custom emoji names during registration, but rejected them as reactions after the required surrounding colons made the payload 66 characters. Validation also differed between desktop, SDK, relay, and storage boundaries. <img width="554" height="47" alt="image" src="https://github.com/user-attachments/assets/4013452f-210e-4dd3-9003-f45ff3b28dc8" /> **Solution:** Keep the product limit at 64 ASCII characters for custom emoji names, enforce it consistently when emoji sets are registered, and allow only valid matching custom reaction payloads up to 66 characters. Widen the reaction projection to preserve the wrapped payload while retaining the existing 64-character limit for ordinary reactions. <details> <summary>File changes</summary> **crates/buzz-sdk/src/builders.rs** Defines the shared custom emoji boundaries and covers accepted 64-character and rejected 65-character shortcodes. **crates/buzz-relay/src/handlers/ingest.rs** Validates emoji-set shortcodes and permits 66-character reactions only when they are valid colon-wrapped custom emoji with a matching tag. **crates/buzz-db/src/event.rs** Adds storage regression coverage for maximum-length custom emoji reactions. **crates/buzz-db/src/migration.rs** Verifies the reaction column migration is applied correctly. **desktop/src/shared/api/customEmoji.ts** Enforces the existing 64-character shortcode maximum during desktop normalization and registration/import. **desktop/src/shared/api/customEmoji.test.mjs** Covers the desktop shortcode boundary. **migrations/0027_long_reaction_payloads.sql** Widens stored reaction payloads to 66 characters for the two required surrounding colons. **schema/schema.sql** Keeps the desired schema aligned with the migration. </details> ## Reproduction Steps 1. Register or import a custom emoji whose ASCII shortcode is exactly 64 characters. 2. Select that emoji as a reaction to a message. 3. Confirm the reaction publishes, persists, and renders without an error. 4. Attempt to register a 65-character shortcode and confirm it is rejected. 5. Publish an ordinary or malformed reaction over 64 characters and confirm the relay rejects it. ## Verification - `cargo test -p buzz-sdk`: 243 passed - `cargo test -p buzz-db`: 94 passed, 152 Postgres-required tests ignored - `pnpm test` in `desktop`: 3,859 passed - `cargo test -p buzz-relay`: 795 passed, 9 existing Postgres-unavailable failures, 35 ignored; new reaction boundary tests pass directly - `cargo fmt --all -- --check` - `git diff --check` Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f` --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
ed4b3e7afa |
fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
Provider `context_length_exceeded` 400s permanently wedged agent sessions: the turn errored, the oversized history persisted in the in-memory session, and the usage baseline stayed frozen at the last successful sub-threshold reading (failed requests report no usage), so the preflight handoff gate never fired again — every later prompt failed identically until an agent restart. The byte-truncation fallback never intervened because it is a request-body limiter (`estimated_bytes`), not a context-window defence; at context-window scale it is a measured no-op. This adds the reactive recovery path: - **Typed classification.** `AgentError::LlmContextExceeded` is classified at both non-success provider terminals — the shared `post()` (Anthropic, OpenAI, Databricks) and `openrouter_post()` — on `status == 400` plus a context-window body match, so ordinary 400s stay terminal. - **Forced handoff.** A context-400 forces a summarize-handoff that bypasses `should_handoff()` and `BUZZ_AGENT_MAX_HANDOFFS`, bounded by its own per-turn budget (`MAX_CONTEXT_RECOVERIES_PER_RUN = 3`). - **Shrink ladder.** The summarize prompt budget halves from the observed rejected history size — not from `max_context_tokens`, the number the provider just contradicted — rung to rung, with a 4096-byte floor. A summarize call rejected for the same reason takes the next rung instead of re-sticking. At the floor (overflow dominated by unshrinkable frame: system prompt, tool schemas, live prompt) recovery is refused and the provider error surfaces clearly instead of self-healing. - **Baseline reset.** The stale usage baseline is cleared when a request fails, so the preflight gate cannot stay frozen sub-threshold on retries. Named behavior changes: 1. **Anthropic and OpenRouter errors now carry the `(model)` stamp.** Provider arms return their `Result` into the central error mapper instead of early-returning past it, making the code match its documented single-convergence contract at that mapper. 2. **`max_rounds` now counts completions the loop acts on.** A request rejected with a context-400 that is then successfully recovered refunds its round before the retry, paired 1:1 with a consumed recovery rung, so the round cap is neither weakened nor able to drop a recovered turn unanswered. Related: #4805 — the complementary proactive fix (per-session handoff-cap kill switch that let sessions grow to the provider wall). #4805 prevents reaching the wall; this PR recovers at it. --------- Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
ccdaa16161 |
docs(persona-pack): fix stale desktop import instructions (#4500)
## Summary The desktop app no longer imports persona packs the way the docs described. `PERSONA_PACK_SPEC.md` and the `meadow-core` example still pointed users at a `.zip` import through "My Teams / My Agents → Import" and a future "Install Pack" button — none of that exists anymore. The app only imports agent/team **snapshots** (`.agent.json`/`.agent.png`, `.team.json`/`.team.png`), and a persona-pack `.zip` is explicitly rejected. ## Fix Updated both docs to describe the current import paths (Agents / Agent teams sections, snapshot files only) and added a note that persona packs and desktop snapshots are separate, non-interchangeable formats today. Fixes #4468 --------- Signed-off-by: SomSamantray <92726151+SomSamantray@users.noreply.github.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> |
||
|
|
6df7eba24d |
fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805)
`BUZZ_AGENT_MAX_HANDOFFS` compared against the session-cumulative `handoff_count` (persisted across prompts). After N handoffs a long-lived session hit the cap permanently: `maybe_handoff()` returned `Skipped` on every subsequent prompt, the 16 MiB byte-truncation fallback never bound before a 1M-token provider wall, and the session wedged on the first 400 with no recovery path. Thufir's session log shows 8 days of cap-forced truncation before the first `context_length_exceeded` 400. The fix replaces the session-level cap comparison with a local `handoff_attempts` counter constructed at the start of `run()` and passed into `maybe_handoff()`. The counter resets on every `session/prompt` turn so `BUZZ_AGENT_MAX_HANDOFFS` caps compaction loops within a single turn while allowing unbounded compactions across a session's lifetime. The session-cumulative `handoff_count` is retained for log context only and is not reset. Steer-driven rounds share the per-turn budget automatically since steers inject into the running `run()` loop, not a new call. - Move `handoff_attempts` increment to before `summarize()` so failed, empty, and cancelled summarize calls each consume one budget slot — the cap cannot be bypassed by a repeatedly-failing summarizer - Upgrade cap-forced `Skipped` from `INFO` to `WARN`; add structured fields for `session_id`, attempt count, projected tokens, and threshold so the cap→wall pairing is attributable per session - Document `max_handoffs` in `config.rs` as a per-`session/prompt`-turn bound - Three new behavioral regression tests: per-turn reset proven across two separate turns; within-turn cap proven via multi-round tool-call turn; failed summarize proven to burn the attempt budget Note: this is the proactive half of the context-window fix. The reactive `context_length_exceeded` 400 recovery path is owned by Sami's branch (`buzz-ctxfix-sami`, Tyler's crew); this PR is intended to land after that one. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
efe1893dd3 |
fix(channels): restrict private-channel invitations (#4612)
This change requires an active owner or administrator for third-party additions to private channels. The relay validator and transactional database authority enforce the same rule, including removed-member reactivation and role-change paths. Idempotent self-target behavior remains available, while ordinary members can no longer extend private-channel access to another identity. ## Testing - `git diff --check origin/main...codex/security-private-channel-invite-authority` - 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: Eli Foster <efoster@squareup.com> Co-authored-by: Eli Foster <efoster@squareup.com> |
||
|
|
ad538bfb1e |
fix(acp): reject unattended permission requests (#4609)
This change removes the ACP permission-bypass mode, defaults managed sessions to `dontAsk`, and answers permission requests with `reject_once` or cancellation in both ACP read loops. Unattended operations that require interactive approval now fail closed instead of being silently authorized. Explicit non-interactive modes that do not bypass a permission request remain available. Both layers have to change together: `apply_permission_mode` treats an unsupported mode and a failed `set_config_option` as non-fatal by design, so a request can still reach the harness even in a non-interactive mode. Removing `bypassPermissions` from the enum rather than only changing the default means the mode cannot be restored by configuration alone. The scope of the guarantee is that `buzz-acp` never grants approval. An agent that pre-authorizes tools in its own configuration (for example Claude Code's `settings.json`) still runs them without asking, which is outside this harness. ## Testing - `env -u BUZZ_ACP_LAZY_POOL bin/cargo test -p buzz-acp` at `16fff4d`: 671 library tests and 9 integration tests passed - `cargo clippy -p buzz-acp --all-targets -- -D warnings` and `cargo fmt -p buzz-acp -- --check`: clean - `git diff --check origin/main...codex/security-acp-shell-auto-approval` The permission tests previously re-implemented the `reject_once` lookup in the test body instead of calling the code under test, so they would have passed unchanged if the harness went back to selecting `allow_once`. They could not call it directly, because `handle_permission_request` is a method on `AcpClient`, which owns a live `Child` and its stdio pipes. The choice is now a free function, `permission_denial_response`, and the tests exercise it: `reject_once` preferred over offered allow options, the cancelled fallback when no `reject_once` exists, an empty option list, and a `reject_once` missing its `optionId`. The cancelled fallback had no coverage before despite being the fail-closed backstop. ## Operator notes - `BUZZ_ACP_PERMISSION_MODE=bypassPermissions` no longer parses, so a process configured with it fails to start rather than silently downgrading. - Desktop managed agents do not set a permission mode, so they inherit `dontAsk`. The desktop has no permission prompt, so operations needing approval now fail with no in-app way to approve them. Originating Buzz thread: `buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1` --------- Signed-off-by: Jordan Mecom <jm@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> |
||
|
|
885bed35ee |
fix(workflow): bind trigger author to the signed event (#4607)
This change derives `trigger_author` exclusively from the signed event pubkey. Actor tags remain available as event data but cannot override the identity used by author-sensitive workflow conditions. This removes the impersonation path without changing workflow definitions or requiring stored-data migration. ## Testing - `bin/cargo test -p buzz-workflow` at `78819df`: 154 passed, 2 Postgres-dependent tests ignored - `git diff --check origin/main...codex/security-workflow-trigger-author` Originating Buzz thread: `buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1` Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
997b8caaa4 |
fix(git): revoke access for banned relay members (#4608)
This change rechecks the durable community ban in the shared Git HTTP authentication path for advertise, fetch, and push requests. A banned member is denied even if repository-channel membership still exists, and restriction lookup errors fail closed. The additional database lookup happens on every Git HTTP request so access revocation does not depend on stale session state. The check also cascades to the NIP-OA owner. Git accepts NIP-OA attestations on the NIP-98 token, so an agent key can act for its owner — without the cascade, a banned human would keep clone and push access through any agent key. This mirrors the NIP-42 gate in `handlers::auth`: either principal's ban denies the request. The check runs inside the `GitAuth` extractor, so all three Git routes inherit it. ## Testing - `git diff --check origin/main...codex/security-ban-revokes-git` - Rebased onto `origin/main` at `5c98932` - `cargo test -p buzz-relay --lib sec005_read_gate_tests`: 8 passed, 7 ignored (Postgres) - `cargo clippy -p buzz-relay --all-targets -- -D warnings` and `cargo fmt --check`: clean Pure tests cover the decision table (agent ban, inherited owner ban, no attestation). Postgres-gated tests cover the wiring: the real ban row, a live `compute_auth_tag` attestation, and the 503 fail-closed path. **Not yet verified:** the three Postgres-gated tests compile and skip but have not been run — no local Postgres, and CI does not run `--ignored`. They need `cargo test -p buzz-relay --lib sec005_read_gate_tests -- --ignored` against a migrated dev database. Originating Buzz thread: `buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1` --------- Signed-off-by: Jordan Mecom <jm@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> |
||
|
|
8a7eb8d3d7 |
fix(agent): recover from unsupported image input instead of poisoning the turn (#4896)
## Problem
`buzz-dev-mcp` advertises `view_image` to every agent regardless of
whether the session's model accepts images. When a text-only model (e.g.
DeepSeek V4 Flash) takes the bait, the image lands in session history
and every subsequent LLM request 404s with `No endpoints found that
support image input`. The error was classified as `LlmModelNotFound` and
propagated fatally out of the turn loop — history stays poisoned,
buzz-acp retries the batch with exponential backoff, and the session
burns its entire clock doing no work. In a recent trial run, **all 57
trials that called `view_image` on a text-only model died this way; none
recovered.**
## Fix
Capability-gating the advertised tool isn't reliable — there is no
image-capability metadata at the agent layer across providers. Instead,
recover at the turn loop:
- **Typed error**: new `AgentError::UnsupportedImageInput`, classified
narrowly on the exact provider phrase `No endpoints found that support
image input` on both the generic 404 path and OpenRouter's 404 path.
Unknown-model 404s and OpenRouter parameter-routing 404s keep their
existing classifications. No deterministic retry.
- **In-turn recovery**: on this error, `RunCtx::run` strips every image
block from history — keeping the tool result (and therefore
tool-call/result pairing) intact — marks the result `is_error`, appends
actionable model-facing guidance ("The current model does not support
image input. The image was removed from conversation history so this
turn can continue. Use a text-based inspection tool…"), and continues
the same turn. Base64 never replays again.
- **Loop guard**: recovery only fires when at least one image was
removed; if the provider says "image" and history has none, the error
propagates as before.
## Tests
- Unit: phrase classification (typed, not retried; unknown-model 404
unaffected), idempotent image-to-error history mutation preserving call
IDs and text.
- End-to-end (`fake_llm.rs` + `fake_mcp.rs`): tool call → MCP image
result → 404 unsupported-image → same-turn recovery. Captured requests
prove round 2 carried the image, round 3 replays no image, carries the
guidance text, preserves pairing, and ends `end_turn`.
- Loop guard: typed unsupported-image error with **no** image in history
fails after exactly one provider request instead of spinning —
mutation-testing showed deleting the `removed == 0` guard survived the
suite, and `max_rounds` defaults to unlimited in production, so this
branch needed direct coverage.
Verified at `a210305019b33d5f56677b4c82bab79e4ac52d24`: `cargo test -p
buzz-agent` (full package, 381 unit + all integration suites) green;
`clippy --all-targets -D warnings` green; `fmt --check` green; pre-push
hooks (rust-tests, desktop-tauri-checks, branch-skew) green.
**Scope of the classification guarantee**: the classifier runs in the
shared `post()` (which Anthropic and OpenAI paths route through) and in
`openrouter_post()` — i.e., every 404 path in `llm.rs`. It only runs on
404 responses; providers that reject images with a different status
(e.g. a 400) are out of scope for this PR — see the review-comment
discussion for why broadening the phrase list alone would not cover
them.
Authored by Wren, loop-guard test by Sami, reviewed by Eva.
---------
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
|
||
|
|
067c085f37 |
Define private managed agent wire protocol (#4593)
## Summary - reserve kind `30179` for owner-private managed-agent aggregates - define the fail-closed owner-self NIP-44 v2 envelope and versioned payload codec - bind runnable identity/configuration to complete signed `30175`/`30177` recovery projections - validate NIP-OA owner→agent attestations and reject self-attestation - document NIP-PMA authority, migration prerequisites, privacy, and deployment order - keep generic relay ingest closed until private storage and atomic aggregate CAS exist ## Safety boundary This is the inert protocol/codec slice only. It does not publish secrets, change agent authority, migrate local records, or enable kind `30179` ingestion. The relay regression test proves generic EVENT ingest still rejects the kind. The finalized migration plan adds later prerequisites for relay-private storage/CAS, runtime lease/fencing, Desktop cutover, and harness authentication. Those belong in staged follow-up PRs rather than expanding this inert foundation. ## Validation At commit `67f0ea4ebb8d3ccba3a3eb9374e89a7178913f74`: - `cargo test -p buzz-core` — 246 unit + 2 doc tests passed - `cargo test -p buzz-relay private_managed_agent_kind_remains_rejected_until_atomic_ingest_exists` — passed - push hooks: Rust tests and desktop checks passed (`2145` desktop tests passed, `14` ignored) - `cargo fmt --all -- --check` - `git diff --check` ## Review Princess Donut cleared security/data integrity with no remaining high/medium findings. Mongo cleared migration compatibility and wire grammar. The later runtime lease/fencing protocol was also adversarially cleared as a plan; implementation slices still require independent evidence before activation. Deterministic plaintext/signed-projection/auth-tag interoperability vectors remain a valuable follow-up, not an S0 merge gate; random NIP-44 ciphertext is intentionally not snapshotted. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@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> |
||
|
|
a1d78f2959 |
feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues (#4695)
## Summary
Gives Buzz-hosted git entities the same "GitHub-style" chat experience
GitHub links already get: rich preview cards, real titles, and
click-through — except clicks navigate **in-app** to the Projects view
instead of a browser.
- **Spec**: `docs/buzz-entity-links.md` — link scheme, slices, and
deferred work (`buzz://project`, OS deep links, web routes).
- **Canonical `buzz://` deep links**: new
`desktop/src/shared/lib/entityLink.ts` with builders + strict parser for
`buzz://pr?id=…&owner=…&d=…`, `buzz://issue?…`, and
`buzz://repo?owner=…&d=…`, mirrored by a Rust module
(`crates/buzz-cli/src/links.rs`) with a shared golden-format test so the
two implementations can't drift.
- **Preview cards**: `linkPreview.ts` recognizes `buzz://` entity links
*and* HTTPS relay clone URLs (`{origin}/git/<pubkey>/<repo>`, the shape
agents paste today). Both normalize onto the canonical `buzz://` href,
so the two spellings of a repo dedupe to one `Buzz`-provider card
(`BuzzMark` logo) rendered by `link-preview-attachment.tsx`.
- **Title enrichment**: PR/issue cards fetch the real subject from the
relay event (`subject` tag or first content line) via
`useResolvedLinkPreviews.ts`; the cache is community-scoped and reset in
`resetCommunityState()`.
- **In-app navigation**: clicking a card or inline anchor (including
HTTPS relay clone URLs whose origin matches the active relay) routes to
the canonical `30617:<owner>:<d>` coordinate via `goProject()`
(`markdown/entityLinks.tsx`). **Merge dependency: #4671 must merge
first** — route resolution for `30617:` coordinates is implemented on
that branch (`feat/multi-repository-projects`). Entity-link and
external-anchor logic were extracted out of `markdown.tsx` to stay under
the file-size ratchet.
- **Agent side**: `buzz pr open`, `buzz issues create`, and `buzz repos
create` now return a ready-made `link` field (omitted when the relay
returns `accepted: false`), and `base_prompt.md` instructs agents to
paste it verbatim when announcing work.
## Test plan
- [x] Desktop unit tests: pass, including new `entityLink.test.mjs` and
`linkPreview.test.mjs` coverage (golden formats, malformed-link
rejection, clone-URL/`buzz://` dedupe, origin-gated anchor behavior,
label-must-win invariant, cache epoch)
- [x] Rust: `cargo test -p buzz-cli` golden-format test +
accepted/rejected link guard assertions, clippy + fmt clean
- [x] Biome + `tsc --noEmit` clean; pre-push hooks
(desktop-tauri-checks, rust-tests, desktop-test) pass
- [ ] Manual: paste a relay clone URL and a `buzz://pr` link in a
channel — verify one card each, real PR title, and in-app navigation to
the Projects view
Related: [#4671](https://github.com/block/buzz/pull/4671)
---------
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
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> |
||
|
|
bc9e6528a7 |
perf(relay): index channel-id lookups and skip trace-only reads (#4647)
## Problem
`SELECT id, community_id FROM channels WHERE id = ANY($1) AND deleted_at
IS NULL` is the top **Load by waits (AAS)** on the Buzz Postgres writer.
Two independent causes compound, and both are fixed here.
### 1. No index can serve it
`channels` is `PRIMARY KEY (community_id, id)`, and every secondary
index leads with `community_id`:
| Index | Columns |
|---|---|
| *(primary key)* | `(community_id, id)` |
| `idx_channels_nip29_group` | `(community_id, nip29_group_id)` |
| `idx_channels_dm_hash` | `(community_id, participant_hash)` |
| `idx_channels_community_type` | `(community_id, channel_type)` |
| `idx_channels_community_visibility` | `(community_id, visibility)` |
| `idx_channels_created_by` | `(community_id, created_by)` |
| `idx_channels_ttl_expiry` | `(ttl_deadline)` *(partial)* |
The two tenant-independent lookups carry **no `community_id` predicate**
— deliberately:
- `Db::communities_of_channels` — `WHERE id = ANY($1) AND deleted_at IS
NULL`
- `Db::community_of_channel` — `WHERE id = $1 AND deleted_at IS NULL`
That independence is load-bearing, not an oversight: projecting a row's
*true* owning community regardless of the fetch query's `WHERE` clause
is what makes `Inv_NonInterference` non-vacuous. If the fetch ever
dropped its tenant scoping, this lookup would still report the real
label and the checker would catch the mismatch.
But a composite btree is only usable when its leading column is
constrained, so neither query can use the primary key, and nothing else
leads with `id`. **Both sequentially scan `channels` on every call.**
### 2. In production the result is discarded
Both call sites feed `record_read_message_rows` /
`record_read_by_id_rows`, which call `tracer.record(...)`. Production
binds `NoopTracer` (`crates/buzz-relay/src/state.rs`), whose `record`
body is empty.
The existing guard tests `trace_state`, which is `Some` for every
well-formed request — it only goes `None` on malformed pubkey bytes. So
the scan ran on the hot read path and its output was dropped. This is
the classic eager-argument bug: `log.debug("..." + expensiveCall())`
with no `isDebugEnabled()` check.
### 3. Multiplied per filter
The non-search call site sits **inside the phase-3 per-filter loop**, so
a `REQ` carrying N filters performed N sequential scans of `channels`
before responding.
## Changes
**`Tracer::enabled()`** — a capability check on the trait (the
`isDebugEnabled()` of this seam), defaulting to `true`. `NoopTracer`
overrides it to `false`, and both emitters in `req.rs` now gate on it,
skipping the trace-only DB read entirely in production.
**`migrations/0027_channels_id_lookup_index.sql`**
```sql
CREATE INDEX IF NOT EXISTS idx_channels_id_live
ON channels (id) INCLUDE (community_id)
WHERE deleted_at IS NULL;
```
- `INCLUDE (community_id)` — both queries select exactly `(id,
community_id)`, so this is covering and can be served index-only.
- Partial on `deleted_at IS NULL` — matches both predicates exactly,
excludes soft-deleted history, and lets Postgres skip the recheck.
- **Not `UNIQUE`.** `id` alone is *not* unique in this table —
`command_executor.rs` documents that `community_of_channel(channel_id)`
is ambiguous because the same channel id can appear under more than one
community. A unique index would encode a false constraint and fail to
build on any database already holding such a pair.
Worth keeping the index even though fix #1 removes the production
caller: it still runs under conformance, and `community_of_channel` has
the same problem on its own paths.
**`schema/schema.sql`** — mirrored, since a test asserts desired-state
parity.
## Conformance is unchanged
This is the part worth reviewing closely. Under a real tracer
`enabled()` returns `true` and **every emit happens exactly as before**
— the gate only skips *building* emit inputs when nothing observes them,
never an emit that would otherwise have been made. The coverage-breach
guard stays non-vacuous.
`CountingTracer` forwards `enabled()` to its inner tracer rather than
inheriting the `true` default. Both directions matter and both fail
silently:
- inheriting `true` over a `NoopTracer` would keep the overhead this PR
removes;
- hardcoding `false` over a live tracer would suppress the emits whose
absence `EmitGuard` reports as `ImplBug` — masking real breaches behind
expected ones.
Covered by a new regression test,
`counting_tracer_delegates_enabled_to_inner`, which asserts delegation
in both directions.
## Verification
- `cargo check -p buzz-conformance -p buzz-relay` — clean
- `cargo clippy --all-targets` — clean, zero warnings
- `cargo test -p buzz-conformance` — 6/6
- `cargo test -p buzz-relay --lib conformance` — 11/11
- `cargo test -p buzz-db --lib migration` — 7/7
- `just test-unit` (pre-push) — green
Migration-count assertions in `crates/buzz-db/src/migration.rs` were
bumped 26 → 27, with content assertions for 0027 following the existing
per-migration pattern (including a guard that it never becomes
`UNIQUE`).
## Open questions for reviewers
1. **Lock strategy.** Built *without* `CONCURRENTLY`, following
migration 0004's precedent, because sqlx runs each migration inside a
transaction and `CREATE INDEX CONCURRENTLY` cannot run in one. This
takes a brief `SHARE` lock on `channels` (blocks writes, not reads) —
small relative to `events`, but an operator preferring zero
write-blocking can pre-build it by hand and `IF NOT EXISTS` makes the
migration a no-op. I could not confirm whether sqlx 0.9 supports a `--
no-transaction` directive; if it does, that may be preferable.
2. **Diagnosis is static.** This comes from reading the source, not from
`EXPLAIN` against the live database. Worth confirming with `EXPLAIN
(ANALYZE, BUFFERS)` on the writer before/after — that also sizes the win
by revealing the real table size and row counts.
3. **Expected impact** scales with average filters-per-`REQ`, which I
did not measure. `pg_stat_statements` ordered by `total_exec_time` would
confirm this query drops off the top and show whether anything else is
scanning the same way.
Signed-off-by: Jemiah Westerman <jemiah@squareup.com>
|
||
|
|
56003ebf98 |
docs(acp): explain per-channel session model in base prompt (#4729)
## Overview Agents running in Buzz have no built-in awareness that each channel is an isolated conversation context. When a human mentions work "you" are doing in another channel, the current session can misread this as its own active context and try to coordinate, re-plan, or take ownership of it — causing confusion and wasted turns. ## What changed Added a `## Session Model` section to `crates/buzz-acp/src/base_prompt.md`, inserted immediately after the opening paragraph and before `## Buzz CLI`. The section explains: - Each channel is a separate session; multiple sessions of the same agent identity may be active simultaneously. - Sessions share core memory, workspace, and relay — but not conversation context or in-flight reasoning. - Cross-channel work belongs to the owning session by default; the current session may take it over only when the human explicitly requests it. No runtime code changes. Base prompt only. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
feccf4eabc |
Polish mobile inbox and media flows (#4512)
## Summary - make mobile unread state visible with bold channel names, an animated Inbox badge, and swipe-to-toggle Inbox rows - add directional transitions for top-level mobile navigation - let mobile send while media uploads, with cancellable progress UI - normalize iOS and Android video uploads, attach poster frames, and improve native video playback ## Validation - `just mobile-check` - `just mobile-test` - `cargo test -p buzz-media` - Pixel smoke test - iPhone smoke test Desktop background uploads moved to #4522 so the two platforms can be reviewed independently. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz> |
||
|
|
09c86c56e5 |
fix: report agent usage per provider round, not once per turn (#4545)
## The bug buzz-agent emitted its `usage_update` notification in exactly one place: after `ctx.run()` returned. Until that moment a turn's token counters lived only in the prompt task's stack frame. **A turn killed mid-flight reported nothing at all** — the provider had already billed every round it completed, and no consumer ever saw any of it. That is not a corner case for anything that ends a turn on a clock. It is the normal case for a long-horizon benchmark run that relaunches its agent between phases. ## How big Measured against a provider's own billing ledger over one run's window: | | provider ledger | what we recorded | |---|---|---| | the relaunched lead seat | $485 / 348M tok | $98.99 / 90.3M tok | | the two seats that were not relaunched | $29.90 / 856M | $25.81 / 765M — reconciles | 97% of that run's usage rows came back all zeros, against 1–4% for comparable runs that never relaunch. In one 450-phase trial exactly 7 phases recorded any usage — and each of those carries 177k–437k input tokens, a whole session's worth landing in the one phase that happened to end gracefully. Worth being precise about what was *not* wrong, since both were plausible and both were checked: - **Not pricing.** The rates were verified against the provider's endpoints API and match what we charge. - **Not a truncation bug.** The usage files were intact and internally consistent. The tokens were never captured in the first place. ## The fix The run loop now emits a session-cumulative `usage_update` after every usage-bearing provider response, so an interrupted turn has reported everything but its single in-flight request. - **Emitting more than once per turn is already part of the contract.** buzz-acp's `UsageTracker` advances its committed baseline only at publish time, and goose behaves the same way — which is why the tracker was written to tolerate it. - **The turn-start session baseline is snapshotted into `RunCtx`** so the mid-turn figure stays *session*-cumulative. A turn-local number would be discarded by a high-water-mark consumer and lose the turn entirely; there is a test for exactly that. - **Snapshot by value, not a session handle.** The loop reports once per round, and taking the sessions lock on each would serialise concurrent sessions behind one another's provider round-trips. Nothing else advances those counters while the turn holds `busy`, so it cannot go stale. - **One shared `wire::usage_update_payload`** for both call sites, so the mid-turn and end-of-turn shapes cannot drift. A drift there would present as tokens silently vanishing, which is the failure this reporting exists to prevent. ## Why not a SIGTERM handler That was the obvious shape and it does not work. At signal time the counters are not sitting anywhere a handler could reach — they are in the turn's stack frame, and the value the handler would need has not been folded into the session yet. Making usage durable *during* the turn is what actually fixes it; once it is, a handler adds nothing beyond the in-flight request, whose cost is unknown until its response lands. ## Tests - `usage_is_reported_after_each_round_not_only_at_turn_end` — two rounds; asserts the **first** notification carries round 1's counts alone, proving it went out before round 2 returned. - `mid_turn_usage_includes_earlier_turns` — a mid-turn report must be session-cumulative, not turn-local. buzz-agent 18/18 on the `fake_llm` suite, 382 unit. `cargo fmt` / `clippy` / `cargo check --workspace --all-targets` clean. ## Scope Agent-side only, against `main`. The matching harness change — settling usage on the timeout path, which was skipped on the reasoning that an incomplete turn has nothing to flush — is **#4553**, against the benchmark branch, since that harness does not exist on `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Atish Patel <atish@squareup.com> Co-authored-by: Claude Code <noreply@anthropic.com> |
||
|
|
7ff5fc3189 |
feat(acp): deliver system prompt via _meta.systemPrompt for claude-agent-acp (#4395)
`claude-agent-acp` (since v0.6.0 / PR #91) accepts `_meta.systemPrompt: {append: text}` on `session/new` to append to the adapter's native preset while keeping its tool-use prompt intact — the same non-standard extension pattern as `_session/steering` was before it was standardised. ## What changes **Rust (`crates/buzz-acp/`)** - Adds `SystemPromptTransport` enum to `acp.rs`: `Field(&str)` (ACP protocol v2, unchanged) vs `ClaudeMeta(&str)` (new `_meta.systemPrompt: {append: text}`). When both `ClaudeMeta` and `session_title` are present the two `_meta` members are merged into one object so neither clobbers the other. - Gates on exact adapter identity `@agentclientprotocol/claude-agent-acp` in `pool.rs`: `session_new_system_prompt()` routes that name to `ClaudeMeta` regardless of reported `protocolVersion` (CC declares v1). `has_system_prompt_support()` gains the same name check so user-message `[Base]`/`[System]` framing is suppressed for CC sessions. - All other paths — goose post-hoc method, protocol-v2 `Field`, legacy user-message framing — are byte-identical to before. **Desktop (`desktop/src/features/agents/ui/`)** - `agentSessionTranscript.ts`: the `session/new` extractor now checks `params._meta.systemPrompt.append` as a fallback when bare `params.systemPrompt` is absent. Bare field takes precedence. Net line count stays at 1173 (ratchet limit). - `agentSessionTranscript.test.mjs`: two new tests — one verifying the `_meta` transport produces the identical standalone card (same five sections, same `turnId: null`, same placement before the first turn) as the bare-field transport; one proving bare field wins when both transports are present. ## Gate claim `@agentclientprotocol/claude-agent-acp` implies `_meta.systemPrompt` support because the feature landed in v0.6.0 (Oct 2025, commit `ea796f3`) before the `@zed-industries/claude-code-acp` → `@agentclientprotocol/claude-agent-acp` package rename (Mar 2026, commit `b409782`). The new name is therefore a reliable capability gate; the old name falls through to the protocol-version gate (status quo, no regression). ## Tests - Rust: Claude append serialization; `_meta` coexistence with `sessionTitle`; protocol-v2 bare field byte-identical; codex/old-zed omission; claude-name support/suppression gate; old `@zed-industries` name falls through to protocol-version gate. - Desktop: `_meta` transport → identical standalone card; bare field wins over `_meta` when both present. ## Pre-existing failures `just mobile-check` and `just mobile-test` fail identically on clean `origin/main` (5 `compose_bar` / `channels_page` tests + 3 Flutter lint warnings) — not caused by this change. All other `just ci` jobs are green. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
318fbf896e |
fix(security): bump nostr crates for RUSTSEC-2026-0225..0232 + default sprig image to published digest (#4392)
## What Two changes, both fallout/follow-up from #4289 landing: ### 1. Fix the Security job failing on main (lockfile-only) Eight RUSTSEC advisories published today against the nostr stack turned `cargo-deny check` advisories red on main ([failing run](https://github.com/block/buzz/actions/runs/30761611723/job/91533106673)). Not introduced by #4289 — the advisories landed upstream and any push to main today would have tripped them. - **RUSTSEC-2026-0225..0230** → `nostr` 0.44.6 → **0.44.7** (Debug output exposing NIP-46/NIP-60 credentials; wallet parsers accepting unauthenticated events; NIP-44/NIP-04/NIP-98 resource exhaustion; NIP-50 empty-filter panic) - **RUSTSEC-2026-0231..0232** → `nostr-relay-pool` 0.44.2 (root) / 0.44.1 (tauri) → **0.44.3** (auth-challenge memory exhaustion; processing of unverified relay events) Both workspace lockfiles bumped (`Cargo.lock`, `desktop/src-tauri/Cargo.lock`). No manifest changes. ### 2. Default the desktop GUI's sprig image to the published `ghcr.io/block/buzz-sprig` The first main-push after #4289 published the image publicly (package created 18:44Z, visibility `public`). The `config_schema()`'s `image` property now carries a `default`: ``` ghcr.io/block/buzz-sprig:sha-6530b58@sha256:17facfc7608d8ddb33bc056c9aaba1098f4ef6abe5655702fbfd7584d1f74d76 ``` **Why tag+digest, not tag:** the backend deliberately rejects tag-only references — the pod runs with the agent's nsec and tags are mutable pointers (`image.rs` §Image). The tag+digest form keeps the human-traceable `sha-6530b58` while the digest does the pinning; `image::parse` already normalizes it to the tagless canonical form, so create-intent fingerprints are identical to the bare-digest spelling. The digest is the **multi-arch manifest-list digest** (amd64+arm64), resolved via `docker buildx imagetools inspect`. **This is a UI prefill, not a baked fallback:** `image` stays in the schema's `required` list, an empty value still fails closed with a named field, and the desktop submits the value explicitly in `provider_config` (the `WhereToRunSection` probe seeds `providerConfig` from schema defaults) — so deploy fingerprints never depend on compiled-in provider state, and the spec's §K8s pod-reconciliation concern about baked-default divergence is not engaged. Module prose that said "no published image exists yet" is updated to match reality. No desktop code changes needed: the form already prefills from `properties[*].default` and submits seeded defaults. ## Testing - `cargo-deny check` at head: **advisories ok, bans ok, licenses ok, sources ok** (was: advisories FAILED) - `cargo test -p buzz-backend-kubernetes`: **158 passed** (154 lib + 4 wire), including new `schema_default_image_round_trips_through_parse` pinning the constant + its normalization, and the wire `info` test now asserting the default is present in the provider's real stdout response - Live provider probe: `{"op":"info"}` against the built binary returns the default in `config_schema.properties.image.default` with `required` unchanged (`["namespace","image"]`) - Full workspace test suite via pre-push hook: green (earlier direct `cargo test --workspace` run: sole failure was `api::mesh_demo::demo_join_forwarded_arm_round_trips_echo`, the documented pre-existing main flake — unrelated, fails on base) - Image existence verified against GHCR: `docker buildx imagetools inspect ghcr.io/block/buzz-sprig:sha-6530b58` resolves to the pinned manifest-list digest with linux/amd64 + linux/arm64 manifests --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
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 @ |
||
|
|
fc598f5f8d |
fix(git): allow deleting the default branch (#4297)
Tal here, human. Trying to help. This bug bugged me... ## Summary A repository's first branch becomes its symbolic `HEAD`, and Git's bare-repository default rejects deleting that branch even when another branch survives. This change: - sets `receive.denyDeleteCurrent=ignore` only for the ephemeral `git receive-pack` process - preserves the existing server-side `core.hooksPath` override and authorization hook - lets the existing CAS publication logic select a surviving branch as the next manifest `HEAD` - adds regression coverage using a real stateless `git receive-pack` request and a manifest HEAD-selection test This lets users replace an accidental default branch without deleting the object-storage manifest pointer. ### Related issue Fixes #3572 ### Testing - `cargo test -p buzz-relay api::git::` (128 passed, 5 ignored) - `just ci` - live E2E roundtrip against a release relay with PostgreSQL, Redis, and MinIO: - created a repository through signed Nostr events - verified authorized pushes and rejected unauthorized clone/push - pushed a surviving `master` branch - deleted the active `main` branch over authenticated Smart HTTP - freshly cloned the repository and verified `master` became HEAD, `origin/main` was absent, and repository content remained intact Signed-off-by: Tal Weiss <major.tal@gmail.com> |
||
|
|
b7bb15122e |
feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) (#4020)
Implements the `buzz projects` command group — the NIP-MP Phase 2 write path for kind:30621 multi-repo projects. The relay accepted kind:30621 in #3171; this adds the two-layer Rust builder in `buzz-sdk` and the seven CLI commands. ## What this adds ### `crates/buzz-sdk/src/builders.rs` — two-layer builder **Layer A (protocol):** - `validate_project_envelope(tags, content)` — 8 NIP-MP rules in relay order: `d`-cardinality, `d`-empty/length, member-cap (≤64 `a` tags, checked before per-tag parse), member-tag-arity (2–3 elements), member-coordinate grammar (first-two-colons split, literal `30617`, lowercase 64-hex owner, non-empty remainder), member-duplicate (coordinate only, hint ignored), singleton metadata cardinality, byte bounds (`name` ≤256 / `description` ≤2048 / `buzz-channel` ≤256 / `buzz-visibility` ≤256). - `build_project_with_tags(content, tags)` — raw Layer A builder; RMW mutations path. - `ProjectMemberCoord` — `30617:<owner-hex>:<repo-d>` + optional opaque relay hint; equality/Hash by coordinate only. **Layer B (writer policy):** - `build_project(slug, name, description, members, channel, visibility)` — constructs `d` tag, enforces UUID channel and `listed|unlisted` visibility, forces empty content; composes onto Layer A. This is the `create` path. **Shared:** - `build_delete_addressable(kind, pubkey, d)` — generic NIP-09 kind:5 coordinate delete; `build_workflow_delete` now delegates to this. - All 31 `NIP-MP.fixtures.json` cases exercised through `build_project_with_tags`; count assertion guards against omissions. ### `crates/buzz-cli/` — seven commands ``` buzz projects create <slug> --repo <coord> [--name] [--description] [--channel <uuid>] [--visibility listed|unlisted] buzz projects get <slug> [--owner <pubkey>] buzz projects list [--owner <pubkey>] [--limit <n>] buzz projects add-repo <slug> --repo <coord> [--repo <coord>]... buzz projects remove-repo <slug> --repo <coord> [--repo <coord>]... buzz projects update <slug> [--name|--clear-name] [--description|--clear-description] [--channel <uuid>|--clear-channel] [--visibility listed|unlisted|--clear-visibility] buzz projects delete <slug> ``` Command semantics: - **`create`**: all local validation (slug, repos, channel, visibility, name length) fires before the collision preflight — invalid input returns `Usage` without a network call. Routes through Layer B (`build_project`). - **`update`**: at least one setter/clearer required — enforced by a clap `ArgGroup` with `required(true).multiple(true)`, with a runtime backstop for programmatic callers; setter + own clearer are mutually exclusive per clap conflicts. - **`add-repo`/`remove-repo`**: coordinate expansion and dedup fire before head fetch — malformed or duplicate `--repo` values return `Usage` without touching the relay. - **`delete`**: head-based tombstone at `created_at = head + 1`; post-submit re-query verifies tombstone landed. - All mutations: strip `auth`, re-validate full envelope through Layer A; `created_at` advances from observed head, never wall-clock. - Relay hints on existing member tags preserved verbatim through RMW. ## Limitations (recorded, not in scope) - **No relay-hint authoring**: `--repo` carries a coordinate only; existing hinted `a` tags survive RMW unchanged. - **Signer-self delete only**: NIP-OA owner-delete extension not exposed; `delete` targets the signer's own coordinate. - **Deletion durability**: watermark carry-over applies; `delete` is best-effort against a later-arriving replacement. ## Live round-trip 21-step transcript executed against a relay built from `origin/main` `b1b283cd4`, covering create, get, multi-field update (name + description + channel in one call), channel set/clear, add-repo, remove-repo, delete (tombstone verified at `head+1`, repeated delete → `NotFound`). Delta transcript confirmed multi-field update, channel set/clear, no-op add-repo → `Conflict` exit 5, empty update and setter+own-clearer both rejected at parse time. Duplicate create → `Conflict`. Cross-owner `add-repo` with full coordinate exercised. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
89bf03c05d |
fix(nip-oa): accept raw Nostr tag form in parse_json_array (#4203)
## What `BUZZ_AUTH_TAG` stored in the **raw Nostr tag form** `[auth,hex,,hex]` (unquoted, comma-delimited — how an `auth` tag serializes inside a Nostr event and how `.env` files commonly store it) was rejected by the CLI: ``` BUZZ_AUTH_TAG is malformed: invalid JSON: expected value at line 1 column 2 ``` …and even when the CLI *could* parse it, it forwarded the raw string as the `x-auth-tag` header, so the relay's `verify_auth_tag` (which expects JSON) rejected it with `403 relay_membership_required`. Two commits close both gaps. ## Commits ### 1. `fix(nip-oa): accept raw Nostr tag form in parse_json_array` `parse_json_array` (`crates/buzz-sdk/src/nip_oa.rs`) only accepted well-formed JSON arrays. Added a fallback: when strict JSON parsing fails *and* the trimmed input is bracket-delimited, split on `,` and treat each field as a string (empty field `,,` → empty string, matching `["auth","hex","","hex"]`). All consumers (`parse_auth_tag`, `verify_auth_tag`, the CLI, `buzz-acp`) benefit from one change at the lowest layer. ### 2. `fix(cli): canonicalize BUZZ_AUTH_TAG to JSON before sending x-auth-tag header` The CLI stored the raw input string and sent it verbatim as the `x-auth-tag` header (`client.rs:618`). Added `canonicalize_auth_tag` in `buzz-sdk`: parse either form, re-serialize to canonical JSON. The CLI now canonicalizes before storing as `auth_tag_json`, so the header is always valid JSON regardless of input form. Together: local parse + wire canonicalization means the raw form works end-to-end. ## Why The raw form `[auth,hex,,hex]` is exactly how an `auth` tag serializes inside a Nostr event. That shape leaks into `.env` files and shell variables because there's no canonical "stored form" outside an event. The SDK + CLI should accept it rather than push quoting/conversion logic onto every consumer (harnesses, agent shells, external tools). ## Security Both changes are purely syntactic — they only change how a 4-element string array is extracted and containerized. All downstream validation is unchanged: - `parse_auth_tag`: still checks exactly 4 elements, `"auth"` label, 64-char lowercase-hex pubkey, 128-char signature. - `verify_auth_tag`: still reconstructs the preimage and verifies the BIP-340 Schnorr signature against the owner pubkey. No new attack surface — a malformed or forged tag is still rejected at the same validation points. ## Tests 4 new tests in `nip_oa::tests`: - `test_parse_auth_tag_raw_nostr_form` — raw form with conditions + empty conditions - `test_parse_auth_tag_raw_form_with_whitespace` — raw form with surrounding whitespace - `test_canonicalize_auth_tag_raw_to_json` — raw→JSON and JSON→JSON normalization All 25 `nip_oa` tests pass (21 existing + 4 new). `cargo fmt --check` and `cargo clippy -p buzz-sdk -p buzz-cli` clean. ## Verification Confirmed end-to-end against a live community relay (`wss://hermesagent.communities.buzz.xyz`): - **Before:** raw `BUZZ_AUTH_TAG` → CLI parse error, or `403 relay_membership_required` if somehow parsed. - **After:** raw `BUZZ_AUTH_TAG` → CLI parses it, canonicalizes to JSON for the header, relay accepts via NIP-OA owner delegation, `buzz channels members` returns the full roster. ## Context Originated from a community investigation where agent-side relay access was failing because the harness-exported `BUZZ_AUTH_TAG` (raw Nostr form) was rejected by the CLI (expecting JSON). This removes the impedance mismatch at the source. --------- Signed-off-by: amanning3390 <adam.manning@pro-serveinc.com> Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
ac4fa13b8e |
perf(relay): serve relay-membership checks from the read replica (#4124)
## Summary Route `Db::is_relay_member` — the membership check that runs on every authenticated HTTP request and WS AUTH — through the standard `route_read` machinery on the bounded arm, instead of adding a bespoke cache (replaces #3844). - `crates/buzz-db/src/relay_members.rs`: add `is_relay_member_on(&mut PgConnection, ...)` executor seam; the pool version delegates to it. - `crates/buzz-db/src/lib.rs`: `Db::is_relay_member` now routes via `route_read("relay_membership", RoutePredicate::Bounded)` — replica only on a proved fresh session, writer on any route rejection, writer re-run on replica query error. Exactly the shape of every other routed read. This is the one permission read served from the replica, by explicit product decision (Tyler accepted ≤1s bounded staleness on reads we choose): the fleet-wide fence guarantee (`BUZZ_REPLICA_READ_MAX_AGE_MS`, deploy target 1s) is an order of magnitude tighter than the 10s TTL proposed in #3844 and needs no invalidation machinery. Staleness is symmetric for admits and revokes. `BUZZ_REPLICA_READ_MAX_AGE_MS` unset = writer-only = kill switch. It is not precedent for routing other permission reads. ## Validation At this exact commit (`git rev-parse HEAD` confirmed in the same shell, rustc 1.95): - `cargo test -p buzz-db` — 94 passed, 0 failed - PG-gated suite single-threaded — **151 passed, 2 failed**; the 2 failures are the per-owner-limit tests broken on main by #3829 (limit 3→5, tests still seed 3) — they fail identically at base `19d57b0d4` in a pristine control checkout; separate trivial fix to follow - New PG-gated test `is_relay_member_is_bounded_routed_and_fails_closed` — divergent writer/replica fixtures prove: budget unset ⇒ writer; budget set + fresh proof ⇒ replica; over-budget entry ⇒ writer - clippy `-D warnings` + fmt clean; pre-push hooks green (desktop check/test, rust tests, tauri checks) - **Live-local pass** (TESTING.md, release binary, `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`, fresh DB): - writer-only (no `READ_DATABASE_URL`): member accepted, outsider 403 `relay_membership_required`; metrics `route_decision{path="relay_membership",decision="writer",reason="disabled"}` - replica configured + `BUZZ_REPLICA_READ_MAX_AGE_MS=1000`: member accepted / outsider denied via `decision="replica",reason="fresh"`; admit visible to the routed check within ~1.2s; revoke enforced within ~1.2s - reader outage mid-flight (TCP proxy killed): member send still succeeds in <200ms via `decision="writer",reason="reader_acquire_timeout"`; outsider still denied — fails closed, no availability loss Reviewed by Wren: 9/10 minimalness, 9/10 elegance, 9.5/10 correctness at this SHA. Supersedes the 10s-cache approach in PR 3844, which should be closed unmerged once this lands. Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
5765fc74b7 |
fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) (#3998)
## Problem The desktop deliberately shows the workspace icon editor on open relays (#2640, gate: `canEditIcon` in `desktop/src/features/communities/ui/EditCommunityDialog.tsx`) and defers to the relay-side kind:9033 check — which required an admin/owner row in `relay_members`. For a community with **no admin/owner row at all** (the `ensure_configured_community` path, which never writes an owner), every 9033 was refused and the icon was permanently unsettable. **Correction from review (thanks @Dawn):** the original version of this PR claimed nobody holds a role on an open relay. That's false — `main.rs` bootstraps `RELAY_OWNER_PUBKEY` as owner regardless of `BUZZ_REQUIRE_RELAY_MEMBERSHIP`, so a production open relay like bb-block *does* have an owner row, and the old gate was refusing everyone except that owner. The first revision of this diff would have silently widened that owner-only control to any NIP-42-authenticated sender. ## Fix — steward-wins `may_set_workspace_profile(sender_role, membership_enforced, community_has_steward)`: | Relay mode | Community has admin/owner row? | Who may set the icon | |---|---|---| | Closed (`require_relay_membership=true`) | any | admin or owner (unchanged) | | Open | yes (e.g. bb-block) | admin or owner (unchanged posture) | | Open | no (genuinely rosterless) | any NIP-42-authenticated sender | - New DB helper `has_admin_or_owner(community)` (`crates/buzz-db/src/relay_members.rs`); the call site only queries it on open relays. - The rosterless admit logs a `warn!` with the sender pubkey — 9033 writes no audit row and publishes no announcement event (unlike 9030/9031), so this is the only durable attribution. - Kinds 9030–9032, NIP-42 auth, `AdminUsers` scope, ban gate, and icon validation are all untouched. - Doc comment fixed: cited nonexistent `canEditCommunityProfile`; real symbol is `canEditIcon`. ## Test coverage — closing the mutation gap Dawn's mutation testing showed the original unit tests pinned only the helper's truth table: inverting the flag at the call site or deleting the gate entirely survived the full suite. - Unit tests now cover the 3-arg truth table (closed steward-independent, open-with-steward stays steward-only, rosterless-open admits). - Two `#[ignore]`d Postgres integration tests drive `handle_relay_admin_event` with a real `AppState` (open rosterless admit → steward appears → roleless refused again; closed relay member refused). Wired into the Backend Integration CI job as a dedicated nextest step. - **Both of Dawn's mutants verified killed** at this head: flag inversion fails 1 unit test; gate deletion fails both integration tests (`Ok(())` where `Rejected` expected). ## CI wrinkle found and fixed: pre-existing schema drift The first Backend Integration run of the new 9033 tests failed with `column "icon" of relation "communities" does not exist` — migration `0003_community_icon.sql` added the column, but `schema/schema.sql` (the desired-state file that CI job applies via pgschema) was never updated. Pre-existing drift, invisible until a test in that job actually wrote the column. Fixed in `297148f62` (3-line addition to `schema/schema.sql`). ## Receipts (at `1b4b52db8` code / `297148f62` head) - `cargo test -p buzz-relay`: 835 pass, 1 fail — `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo`, pre-existing (fails identically at the old base and on clean main); `telemetry::trace_context_lookup_does_not_enable_callsites` is a known order-dependent flake, passes in isolation. - `cargo test -p buzz-db`: 94 pass. - Both ignored integration tests pass live against local Postgres. - `cargo fmt --all -- --check`: clean. - Live-local pass per TESTING.md at this head (release build, relay on :3199, real WS + NIP-42 via nak): - open rosterless: roleless key sets icon → NIP-11 serves it; `warn!` with sender pubkey in the relay log - open + owner row inserted: fresh roleless key refused ("must be admin or owner"); owner sets icon - closed relay (owner bootstrapped, `BUZZ_RELAY_PRIVATE_KEY` set): plain member refused, owner sets icon, `javascript:` URL rejected, empty icon clears (NIP-11 → null) --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |