Commit Graph

161 Commits

Author SHA1 Message Date
Bradley Axen
12670bd0f0 feat(relay): implement NIP-AR channel artifacts (#7919)
## Summary

Implements NIP-AR channel artifacts (spec merged in #7791) on the relay,
after a simplicity pass over the earlier two-agent implementation.

An artifact is an editable record (kind 45010) with a stable identity
(`d`), one home channel (`h`), and full-snapshot revisions chained by
`prev`:

- **Writes:** one transaction per write. It locks the artifact's head
row, requires `prev` to name the current revision, records the revision
in a ledger that survives retention, and advances the head. Two
competing edits can't both win. A stale edit gets `conflict: artifact
head changed`. Resubmitting an accepted event succeeds without applying
it twice.
- **Permissions:** writes pass the same gates as a kind 9 message in
`h`. A move also passes them in the source channel. The move stores the
destination snapshot and a relay-signed kind 45011 removal marker for
the source in the same transaction, so both channels recover by replay.
The marker never names the destination.
- **Moderation:** redaction is the ordinary kind 9005 removal. The
ledger keeps the ID so `prev` checks still work. Redacting the current
revision retires the artifact.
- **Reads:** HTTP `/query` and `/count` accept explicit `artifact:
current|history` filters with exact multi-character tag matching (e.g.
`#assignee`). Generic REQ and search return only current revisions. Kind
5 against artifacts is rejected. NIP-11 advertises the limits.
- **Spec:** NIP-AR updated to match: kind 9 permission checks, 9005
redaction and retirement, no creator-only delete rule.

A separate small commit routes `insert_channel_head_checked` through the
metered writer pool (`acquire_writer`), like every other event write in
that module.

### Related issue

Follows #7791. No separate issue found.

### Testing

- Clippy clean.
- Unit lane passes. Failures seen along the way came from agent env
leaking into `buzz-acp` tests (they pass in a clean env) and known
timing flakes (`keepalive_resets_idle_past_deadline`, two `buzz-agent`
databricks tests that passed on retry). This diff doesn't touch those
crates.
- Postgres lane passes, including new `artifact_postgres_tests` in
`buzz-db` and `buzz-relay`. Three presence tests need `REDIS_URL` set.
- Docker smoke against a fresh local relay, 29/29: create and idempotent
resubmit, update and stale conflict, current, `#assignee` and history
queries, generic REQ, kind 5 rejection, delete and restore, move with
source marker, private-channel isolation, and 9005 retirement.
- A pasteable curl walkthrough of the same flows was shared with the
reviewer.

Draft until the human hands-on check (AGENTS.md step 3) is confirmed.
The pre-push hook was skipped because every lane above had already run
on this tree. CI is the gate.

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

---------

Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz>
Signed-off-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz>
Co-authored-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
2026-09-28 16:52:09 -06:00
Will Pfleger
6410e685a8 feat(relay): enforce NIP-FI assertion+NIP-98 pairing on HTTP ingress (#7264)
Implements NIP-FI HTTP ingress enforcement per the merged spec (PR
#7254). Every protected HTTP surface in enforce mode requires a valid
`Nostr-Federated-Identity: Bearer <JWS>` assertion alongside its NIP-98
authorization, bound to the same key. The NIP-98 pubkey (proven actor)
must equal the assertion's `nostr_pubkey`; absent, mismatched, or
unverifiable assertions are denied fail-closed with byte-exact
text/plain responses per the spec's rejection table.

This is S5 of the `buzz-enterprise-identity` program. WS enforcement
(S3) is separate.

## What changed

**New `nip_fi_http` module:** `admit_nip_fi_http()` covers the full
admission sequence — `Nostr-Federated-Identity` extraction, offline JWKS
verification, NIP-98 key pairing [FI-INV-05], issuer-qualified deny-map
check via `(iss, pubkey, now)` [FI-INV-14], and byte-exact denial for
all four `DenialClass` variants. `admit_nip_fi_http_on_state()` is the
single call-site wrapper for all surfaces [FI-TRACE-AUTHORITY-UNIFORM].

**Protected surfaces** (all gated via the uniform authority):
- HTTP bridge: `POST /events`, `/query`, `/count`
- GIF proxy: `POST /gifs/search`, `/gifs/share`
- Workflow reads: `GET /workflows/{id}/runs`, `GET
/workflows/{id}/runs/{id}/approvals`
- Moderation reads: `GET /moderation/reports`, `/moderation/audit`,
`/moderation/restricted`
- Media/Blossom: `PUT /upload`, `/media/upload`
- Git smart-HTTP: `info/refs`, `upload-pack`, `receive-pack`
- Git settings control plane: `GET`/`POST
/git/:owner/:repo/default-branch` (now routed through
`admit_nip_fi_http_on_state` for full assertion key-pairing)
- Invites: `POST /api/invites` (mint only; claim exempt as
pre-membership enrollment door)

**NIP-98 strictness in NIP-FI mode:**
- When NIP-FI is not Off, bridge POST handlers force `require_auth_token
= true` — the `X-Pubkey` dev-mode fallback cannot satisfy the pairing
requirement [NIP-FI.md:547-567]
- POST bodies on `/events`, `/query`, `/count` require a matching
`payload` tag in enforce mode [NIP-FI.md:579-597]
- A payload tag present with no content (one-element `["payload"]` or
empty string) is rejected as a structurally invalid claim — NIP-98
requires the content to be a valid 64-char lowercase hex SHA-256 digest.
Malformed payload diagnostics use a static error message (no
attacker-controlled content in logs).
- Off mode preserves pre-NIP-FI behavior per [FI-INV-15]

**Off-mode compatibility changes:** Admission now runs before request
validation on every protected surface, so Off mode differs from
pre-NIP-FI behavior in these ways:
- **GIF routes:** NIP-98 authentication happens before the upstream
provider lookup. An unauthenticated request on a tenant with no GIF
provider now returns 401 instead of 404. The 401 goes through the legacy
`api_error()` path, so the body is JSON `{"error":"..."}` with no
`WWW-Authenticate` header.
- **Workflow runs** (`GET /workflows/{id}/runs`): the query string is
parsed after admission. A request with a missing credential and a
malformed query (for example `?limit=abc`) now returns 401 instead of
400.
- **Authorized malformed query:** once admitted, a malformed query
returns a JSON 400 `{"error":"invalid query: ..."}` from
`parse_query_or_400` instead of axum's `text/plain` `Query` rejection.
- **Moderation reads** (`/moderation/reports`, `/moderation/audit`):
same extraction change as workflow runs. Admission comes first, then a
JSON 400 for a malformed query.
- **Malformed `payload` tag:** shared NIP-98 verification now rejects a
malformed `payload` tag in every mode, including on bodyless requests.
That means a one-element `["payload"]`, an empty digest, or anything
other than 64 lowercase hex characters. Before this change, a bodyless
request ignored the tag.

**Auth header cardinality:** Enforce mode rejects requests with more
than one `Authorization` header with 403 `evidence rejected\n`.
DenyProtected returns 503 `authorization unavailable\n` before this
check (and before NIP-98 or assertion verification) runs. The
cardinality gate is enforced at the NIP-FI admission boundary
(`admit_nip_fi_http`), not via legacy auth boolean flags, so it applies
uniformly to all Enforce-mode surfaces including Git. Off mode passes
through the first header value unchanged, preserving pre-NIP-FI behavior
[FI-INV-15].

**Denial-class remapping:** In Enforce mode, NIP-98
extraction/verification failures from the closure are mapped to the
appropriate `DenialClass`: `MissingEvidence` when no `Authorization`
header is present, `EvidenceRejected` when the header is present but
invalid. Off mode propagates legacy responses unchanged [FI-INV-15]; for
example, the Git missing-Authorization and wrong-scheme 401s keep their
legacy bytes with no `content-type` header.

**JWKS refresh scheduling:** The refresh timer's `*last` checkpoint is
set after the fetch completes, not from the pre-fetch timestamp,
preventing interval drift under network latency. The loop is extracted
to `nip_fi_jwks_refresh_loop` for unit-testability.

**Log hygiene [NIP-FI.md:777-779]:** Issuer identity is never written to
logs or error messages. All startup/refresh diagnostics use an ordinal
index (`issuer_index = N`). Config parse errors for malformed issuers
use classify()/line/column codes rather than raw config values.

**Admin SPA document routes:** `/reports`, `/reports/{id}`, and
`/feedback` are exempt from the NIP-FI assertion guard only on the admin
host (host-qualified via `is_admin_host`). These paths are not added to
the broad `NIP_FI_EXEMPT_PREFIXES` list — tenant hosts remain fully
protected.

**Deny-map seam (`HttpDenyMap` trait):** signature is `(issuer, pubkey,
now)` matching `NipFiDenyMap::is_denied` from S4 (PR #7265) so
integration is a one-liner. Stub is `AlwaysAdmitStubDenyMap` — named to
accurately describe stub-phase semantics (fail-open intentionally
pending S4).

**Route inventory:** every authenticated HTTP route is classified as
Protected or Exempt with justification. Documented in the F4 seam-test
comment in `bridge.rs`.

**Outer-guard tests:** `#[ignore = "requires Postgres"]` tests in
`bridge.rs` drive the full router (`build_router`) in Enforce mode with
valid NIP-98 but no `Nostr-Federated-Identity` header. These exercise
the **outer `nip_fi_assertion_guard` middleware** in `router.rs` — the
layer that fires before any handler runs. They prove the guard's exact
response bytes (401 `authentication required\n`, `text/plain;
charset=utf-8`, `WWW-Authenticate: Nostr`) for each protected route.
Per-handler `admit_nip_fi_http_on_state` wiring is proven separately by
the cardinality test and the media/settings/git handler-level suites.

## Seams

**S3 seam:** `nip_fi_config.rs` is duplicated (S3 not landed;
session-deadline parts stripped). Removed when S5 rebases onto S3.

**S4 seam:** `AlwaysAdmitStubDenyMap` until S4 provides the real map.
Integration is one line at the `admit_nip_fi_http_on_state` wrapper.

## Evidence

At head `ee338ea9c` (branch `hayt/nip-fi-http-enforcement`), hosted CI
run
[36167450853](https://github.com/block/buzz/actions/runs/36167450853):
- **Rust / Unit Tests** ([job
108179338458](https://github.com/block/buzz/actions/runs/36167450853/job/108179338458)):
passed. The `buzz-relay` `test-unit` step ran 171 tests with 171 passes.
Its PASS lines include all 57 non-ignored tests this PR adds to the
relay: `nip_fi_http::tests` (25), `nip_fi_config::tests` (8),
`router::tests` (8), `api::parse_query_tests` (5),
`api::git::transport::off_mode_precedence_tests` (5),
`tests::jwks_refresh_*` (3), and `composition_tests` (3). The same job
ran the `buzz-auth` suite, which includes
`nip_fi::jwks::tests::key_set_serves_live_snapshot_under_reader_and_refresh_overlap`.
- **Rust / Rust Lint**: passed.
- **Relay and PostgreSQL / Backend Integration (relay e2e)**: passed.
- **PostgreSQL Domain / PostgreSQL Tests** ([job
108183303436](https://github.com/block/buzz/actions/runs/36167450853/job/108183303436)):
passed. 542 tests run, 542 passed.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Signed-off-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-25 21:01:31 +00:00
Luke Tornquist
8f6b66f9ff Select one feature flag provider at compile time (#7677)
## Why

Buzz server components need one typed feature-flag contract without
forcing the open-source build to depend on LaunchDarkly. A staging or
production relay must not be able to choose the wrong provider through
runtime configuration.

## What

- Adds typed boolean and signed `i64` flags with static, environment,
and compile-gated LaunchDarkly evaluators.
- Defines an executable relay artifact contract that compiles exactly
one provider.
- Rejects zero or multiple relay provider features at compile time.
- Runs the LaunchDarkly feature tests and the provider compile matrix in
the unit CI lane and fallback test script.

## How

`buzz-feature-flags` stays provider-neutral. The future relay
composition root will expose one build path per artifact feature:
`static-feature-flags`, `environment-feature-flags`, or
`launchdarkly-feature-flags`. Runtime configuration carries only
settings for the selected provider. It never carries a provider name,
fallback, or precedence rule.

The crate still has no relay consumer. A locked compile fixture proves
each singleton builds and every zero or multi-provider combination fails
until the relay adopts this contract.

This removes the proposed runtime `ProviderMode` and leaves one
provider, one construction path, and one evaluator per artifact.

## Risk

This PR still has no relay or app consumer, so it changes no
user-visible behavior. Risk is limited to the new feature-flag crate,
its compile contract, and its unit-test lane.

## Testing

An external Rust consumer at `30cf968bfe85e82980fda7e63f5bebe649ca939f`
exercised six environment-evaluator scenarios and 59 assertions. The
compile-time correction adds no live consumer to exercise manually.

## Bigger picture

When `buzz-relay` adopts this crate, its build manifests must map each
distribution to one provider feature. That consumer PR must verify live
Relay Proxy reachability, secret ingestion, lifecycle ownership, and
user-visible behavior.

Generated with Codex

---------

Signed-off-by: tornquist <tornquist@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 15:31:08 -04:00
Logan Johnson
ea1e97e65f feat(acp): run one prepared task from a file or stdin (#7851)
🤖
## Summary
Scripts, scheduled jobs, and other automation need to give an agent one
prepared task and wait for a result without starting a long-running chat
listener. This adds `buzz-acp run`: it reads a task from a local file or
stdin, runs one fresh agent session with the configured tools and
instructions, cleans up, and exits with a machine-readable status.

```sh
buzz-acp run --task ./task.json
cat task.json | buzz-acp run --task -
```

- The versioned JSON task specifies a task ID, intended agent public
key, prepared prompt, and execution time limit. Invalid input or an
identity mismatch is rejected before agent launch. Credentials,
executable, tools, and permissions remain controlled by trusted launch
settings, not the task document.
- Each invocation uses the normal agent equipment: base/persona/team
instructions, enabled memory, tools, model and effort settings,
permissions, and Git identity, credentials, and signing. Both task and
conversational modes obtain these capabilities from one shared runtime
owner; new shared setup belongs there rather than in separate entry
points.
- Launch configuration reads have a separate ten-second bound and can be
cancelled even when a prompt file blocks. A deadline covers adapter
startup through the task turn. Interrupts and termination signals
trigger bounded cleanup. Stdout contains one terminal JSON record;
diagnostics stay on stderr. A normal turn ending is not proof that the
task achieved its goal.
- Normal invocation remains the conversational service. Task mode does
not start chat subscriptions, presence, heartbeat turns, or a persistent
agent pool. Tasks are trusted executable instructions, not sandboxed
input. Reusing a task ID does not prevent duplicate execution, and
uncertain outcomes must not be blindly retried.

See [the task contract](crates/buzz-acp/TASKS.md) for the document
format, exit codes, limits, and examples. URL retrieval, a task server,
and a persistent execution ledger are outside this change.

### Related issue
None found in the existing issue and PR search.

### Testing
- At `4e86bbc97`: 964 unit tests and 22 integration tests passed, with
three existing ignored tests. The package suite ran serially with
inherited harness/Git options cleared. A final test-only shutdown-result
assertion was then checked with the focused memory test and all-target
Clippy.
- All eight real-binary task tests independently passed on the exact
candidate, including blocked configuration reads, file/stdin validation,
session equipment, deadlines, cancellation, and cleanup. The memory test
passed 20 repeated runs. Removing the oversized-input guard fails the
new error assertion; restoring it passes.
- Package all-target Clippy, formatting, file-size checks, and pre-push
Rust/Tauri checks passed. All executed hosted checks passed on the
updated head (54 successful, 28 skipped). The separate Codex review
comment still requests authorization for this exact range.
- Earlier revisions passed the release build and a real Goose 1.51.0
tool-use smoke test. No new release artifact or installed Janet runtime
is claimed for this feedback revision.
- Repository-wide `just ci` was previously attempted; workspace Clippy
passed, then the desktop dependency check stopped on a local pnpm fetch
failure. Hosted checks cover the applicable PR lanes.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-24 10:58:15 -04:00
Mic Neale
4544ba1713 chore(mesh): upgrade MeshLLM to 0.76.2 (#7559)
## Summary

Upgrade Buzz from MeshLLM `v0.76.0-rc9` to the latest stable [v0.76.2
release](https://github.com/Mesh-LLM/mesh-llm/releases/tag/v0.76.2) (via
v0.76.1).

- Update all eight Mesh git dependencies across desktop and relay smoke
examples; both lockfiles resolve all 47 Mesh packages to
`a0c1e66b0ac037dd56544b9e2d94969ea43d694f` (tag `v0.76.2`). All non-Mesh
lock records are unchanged by the 0.76.1 → 0.76.2 bump.
- Match Mesh's `ed25519-dalek = 3.0.0` and `rusqlite = 0.40`
dependencies.
- Explicitly enable rusqlite's `fallible_uint` feature, preserving
Buzz's existing `u64` SQL conversions when **mesh is disabled**.
Mesh-enabled builds previously supplied this transitively, so testing
both configurations matters.
- Temporarily pin **only** the optional `sqlx-sqlite` package to the
merged upstream compatibility fix
[transact-rs/sqlx#4402](https://github.com/transact-rs/sqlx/pull/4402),
revision `94aafe3a68884d923b0798a767c8d7f6cfda89d2`. SQLx 0.9.0 caps
`libsqlite3-sys` below 0.38, while Mesh 0.76 requires 0.38. Cargo
resolves the conflict even with SQLite disabled. The fix raises that
upper bound; remove the patch once released. Buzz's active PostgreSQL
driver stays on crates.io SQLx 0.9.0.
- Preserve the explicit Linux keyring 3 `crypto-rust` feature so Secret
Service sessions retain DH encryption independently of Mesh keyring 4.
- Preserve the dedicated examples-only `buzz-mesh-smoke` crate and
lifecycle workflow wiring, keeping Mesh/native-runtime dev dependencies
out of relay CI builds.
- v0.76.1 → v0.76.2 upstream delta: self-updater fix for composed
product-v2 bundles (Mesh-LLM/mesh-llm#1844) plus release plumbing — no
API changes affecting Buzz.
- Merged `origin/main` (`7c789dee0`) without rewriting shared PR
history.

### Validation at `ae4adba15` (local macOS ARM64, Hermit toolchain)

- **Passed:** `cargo test --locked -p buzz-mesh-smoke --all-targets`
(all six example harnesses compile; no live harness execution).
- **Passed:** desktop Tauri workspace tests, both default and
`--features mesh-llm` graphs (3,296 tests with mesh enabled, 0
failures).
- **Passed:** `just desktop-tauri-clippy` (both feature graphs, `-D
warnings`), root workspace clippy `-D warnings`, `cargo fmt --all
--check`.
- **Passed:** `--locked` metadata resolution for both workspaces;
lockfile diff audited to touch only Mesh package records.
- **Passed:** full pre-push hook suite on the final push (branch-skew,
rust-tests, desktop-tauri-checks, file-size-check) — no hooks bypassed.
- **Not run:** live Mesh inference/admission smoke or installed-app UI
exercise. CI lifecycle smoke coverage applies.

Originating Buzz conversation:
buzz://message?channel=dffbc117-845a-4c02-bc3f-eaeeefc4deaa&id=670f9c262e1ec0a4df5600b3173a0c5de2abe17e08ba9ccfdd8eff109fadf1a8

---------

Signed-off-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Galadriel <b81c841ed20a8e17caf4e6fac2f6f3bc61d304c84b8394720f6d00f37538c47e@meshllm.communities.buzz.xyz>
Co-authored-by: Gu <gu@buzz>
2026-09-24 07:32:07 -06:00
Kalvin C
5a349db91c feat(relay): add opt-in newest-first thread windows (#7823)
## Summary

Add an opt-in, newest-first thread window to the authenticated HTTP
query path.

A filter with `thread_window: true` returns:

- reply rows ordered by `(created_at DESC, id ASC)` with an exact
composite cursor;
- optional bounded reactions, edits, deletions, and delete-of-aux
closure;
- one relay-signed `kind:39007` bounds event bound to the normalized
host, authenticated reader, channel, root, request cursor, kinds, depth,
limit, and aux choice.

Filters without the flag keep the existing oldest-first thread behavior.
Channel windows and `kind:39006` are unchanged. No client opts into this
mode in this PR.

### Why extend NIP-CW

NIP-CW already defines relay-computed, cursor-paged conversation views
over extended NIP-01 filters. Thread windows use the same protocol
ideas: ordinary signed rows, composite keyset pagination, optional aux
closure, and relay-signed bounds. Adding a thread mode keeps those
related wire contracts together instead of creating a second NIP with
duplicated terminology and compatibility rules.

Extending a draft, optional NIP with an additive mode is normal protocol
evolution. Existing fields and meanings are not reinterpreted:

- `top_level: true` remains channel mode;
- `kind:39006` remains channel-window bounds;
- `thread_window: true` selects the new mode;
- `kind:39007` has a distinct identity and root/request binding;
- extension-unaware clients never send the selector;
- legacy thread queries remain unchanged.

NIP-CW documents only the wire contract and relay semantics. Database
plans, replica internals, and rollout procedures remain implementation
concerns.

### Why thread windows use independent modules

The implementation deliberately keeps `thread_window.rs` separate in
`buzz-core`, `buzz-db`, and `buzz-relay` rather than adding branches
throughout the existing channel-window path.

The modes share pagination concepts, but their invariants differ:

- channel mode selects top-level rows; thread mode requires one root and
depths `1..=depth_limit`;
- thread mode accepts only conversation row kinds and rejects
unknown/conflicting fields;
- thread bounds bind the root and authenticated reader because
cross-channel aux visibility can differ by reader;
- thread aux includes the root and requires complete two-hop closure;
- thread mode refreshes writer authorization before selection and before
signing;
- historical cursor pages may use a proved replica transaction, but
fallback must restart both aux hops on the writer;
- legacy thread traversal is oldest-first and must not inherit
descending cursor semantics.

Forcing these rules into channel-window code would create a mode flag
across parsing, SQL predicates, bounds construction, authorization,
replica fallback, and tests. That abstraction would not remove
complexity. It would distribute it. Separate modules keep each contract
local and leave the established channel and legacy paths untouched.

### Technical design

- Strict request parsing normalizes UUIDs, event IDs, kinds, defaults,
and cursors before computing the response binding.
- SQL applies community, root, channel, depth, kind, deletion, and
cursor predicates before `limit + 1`.
- Ordered metadata scanning plus exact event-key lookup avoids the
stale-statistics plan that previously exceeded the four-second SQL
budget on a 10k-reply thread.
- Aux work is bounded by target batches, query count, raw candidate
count, response bytes, SQL timeouts, and one shared HTTP deadline.
- Fresh authorization-set comparison catches both grants and revocations
during closure. The relay returns retryable `503` rather than signing an
incomplete page.
- Replica failure permanently degrades the request to the writer and
restarts closure from its original targets without resetting budgets.
- Migration `0049_thread_window_index.sql` adds the mixed-direction
index used by newest-first scans. It validates a prebuilt index and
bounds startup lock/build time.

### Tradeoffs

- The stale-statistics-safe plan is intentionally less clever. Measured
warm 100k deep-cursor execution increased from roughly 0.30 ms to 4.80
ms, plus about 8 ms planning, in exchange for avoiding a reproduced
multi-second timeout after rapid 10k-thread insertion.
- Bounds are authoritative only for the served page. There is no
cross-page snapshot guarantee, and replica insertion coverage does not
imply latest edit/deletion/aux visibility.
- Strict parsing rejects unsupported constraints instead of silently
serving a different row set.
- Required aux closure fails closed. The relay does not omit aux and
sign plausible-but-incomplete bounds.
- This PR ships relay capability only. Client rendering, retry UX,
live-history overlap, and compatibility fallback remain separate work.

### Related issue

No existing issue found. This work follows the thread-loading design and
review discussion.

### Testing

- `cargo test -p buzz-core`: 258 unit tests and 2 doctests passed.
- `cargo check --workspace --all-targets`: passed at the rebased
implementation head.
- `cargo test -p buzz-db`: 131 non-PostgreSQL tests passed; 275
infrastructure tests were ignored as declared.
- Focused PostgreSQL migration/runtime regressions: 3/3 passed against
an isolated PostgreSQL container, including both corrected
migration-ledger tests.
- `cargo fmt --all --check`: passed.
- Pre-rebase feature validation passed 398/398 PostgreSQL tests,
signed-wire multi-tenant/auth/pagination/aux/revocation probes, clean
Compose boot/reboot, and full core tests.
- Independent HA validation exercised three relay pods and a physical
PostgreSQL standby on the same runtime logic before the final
documentation-only cleanup.
- Independent final-delta review confirmed exactly two changed files,
byte-identical executable migration SQL after comments/diagnostic
normalization, matching prebuild/catalog/recovery instructions, and `git
diff --check`; no runtime receipts were relabeled to the new SHA.
- The pre-push hook ran repository checks but its Rust unit lane exited
100 without exposing an individual failed test in the captured
multiplexed output. The independently run affected-package checks above
passed; current-head repository CI remains required.

Known unrelated baseline behavior remains: the mesh-demo echo test can
return 504, and same-second archive → unarchive can deduplicate its
membership notification. Neither path is changed here.

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-09-23 12:01:45 -07:00
Salman Mohammed
5621006bcf refactor: move agent Git bootstrap into ACP harness (#7819)
discovered in
https://github.com/block/buzz/issues/7742#issuecomment-5783512985

## Summary

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

Before:

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

After:

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

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

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

### Related issue

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

### Testing

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

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-23 14:41:00 -04:00
Tom Brow
7e96f91229 feat(mobile-infra): support development App Attest (#7744)
Development-signed iOS apps cannot enroll with the current gateway
because their Apple App Attest AAGUID is rejected.

Add an explicit personal development build feature and environment
setting so a development gateway can use a developer's own application
identity and sandbox APNs credentials.

Ordinary gateway builds remain production-only. Special builds also
default to production, enforce exactly the selected signed AAGUID, and
retain certificate-chain, pinned-root, nonce, app-ID, key,
credential-ID, and counter verification.

Document the matching mobile signing configuration and expose the
feature through an optional Docker build argument.

### Validation

The development image deployed successfully to a personal Railway stack;
private readiness, enrollment challenges, authenticated relay access,
and APNs sandbox TLS/HTTP2 connectivity passed.

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-09-22 22:53:46 +00:00
Salman Mohammed
4d08194ea9 Deduplicate thread context by ACP session delivery (#7620)
## Summary

Buzz can repeat a thread reply in later prompts after the provider
already received that event as context. The old fix treated every
message signed by the agent key as retained by the current provider
session. That could hide an unseen heartbeat reply because independent
sessions share the same signing key.

This PR now removes only exact event IDs already delivered to the live
ACP session. It uses bounded overfetch so removed events do not shrink
the new-context window, and it preserves unseen same-key replies from
other sessions.

The context header reports `truncated=true` whenever session history was
omitted. The harness calls `/count` only when the relay query finds a
sentinel beyond its fetch window.

This deliberately does not parse event IDs from shell output. A reply
that the agent just published may appear once in the next context delta;
after that event reaches the provider as context, later prompts omit it.

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-15 10:24:27 -04:00
Tom Brow
7c789dee09 Fix Apple Silicon iOS simulator builds (#7646)
Apple Silicon simulator builds fail because MLImage and MLKit contain
device-labelled ARM64 objects.

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

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

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-09-14 22:14:06 +00:00
Wes
4cd82f5132 feat(buzz-agent): add opt-in workspace-scoped Databricks reuse (#7606)
Posted by Brain, Wes Billman’s AI assistant, on behalf of `wesbillman`.

## Summary

Add an opt-in `databricks::DatabricksConnection` over the existing
PKCE/coordinator and v2 catalog machinery. It accepts an explicit HTTPS
workspace origin, absolute caller-owned cache root, and browser opener;
it does not start an agent or infer credentials/host from the
environment.

- Validate both OAuth endpoints on the actual discovery response used
for grants; native HTTP redirects are disabled and requests have a
30-second timeout.
- Keep strict cache/single-flight state in a separate namespace. Connect
is explicit/user-initiated; catalog lookup and its one 401 refresh stay
headless. Tokens are not exposed by the connection API.
- Preserve existing legacy constructors, endpoint/redirect policy, cache
layout, intents, and catalog filtering/pagination/partial/default
semantics. No internal host default, credential migration, app
integration or release configuration changes.

### Intentional shared changes

OAuth success/discovery JSON is capped at 1 MiB; error JSON at 16 KiB.
Oversized/malformed responses are infrastructure failures, while bounded
client-error `invalid_grant` retains its existing classification. OAuth
diagnostics omit raw bodies/URLs/opener/callback details. Overflowing
token expiry returns an infrastructure failure. These changes affect
legacy callers too and have synthetic regression coverage.

See [the reuse contract](crates/buzz-agent/DATABRICKS_REUSE.md) for API,
compatibility and caller obligations. Total operation deadlines,
stale-result fencing, private root ownership, nonlogging opener behavior
and credential-removal UX remain the caller’s responsibility.

### Related issue

Related discovery work: #6918. Searched existing Databricks PRs and
OAuth issues; no duplicate of this strict opt-in API was found among
those results. This is a new reuse boundary, not a claim of a live
credential leak.

### Testing

Published head: `cf33eb2f4f267d5808e068e35127d5504f0bc674`, based on
main `78618804ec86a014524ad7d1fb55928e8f5c3edf`.
Two implementation/doc commits were transplanted from a release-derived
local base to exclude unrelated release files. `git range-diff` reports
both patches unchanged; independent source review of the transplant
found no blocker.

**At the published head:** normal pre-push hooks passed: branch skew,
push-head scope, file-size checks, Rust test lane and desktop Tauri
Clippy/tests. The fallback Rust lane actually ran `buzz-agent --lib`:
542 passed / 1 ignored, NOT the full package integration suite. Hosted
full package/repository CI remains pending. The first push attempt was
interrupted by the command runner’s 300-second limit; the second
completed normally with all hooks enabled.

**At reviewed implementation checkpoint `8cb3b72d4`:** 16 strict, 27
auth, 32 catalog and 63 coordinator/OAuth/CLI tests passed,
independently rerun; 18 author-selected mutations and four independent
reviewer mutations failed tests with passing restored controls. The
subsequent `539319411` delta changed only two doc-comment blocks. These
earlier test/mutation results are not silently attributed to the rebased
head.

Synthetic coverage includes HTTPS grants/catalog, off-origin endpoint
and redirect rejection, cancellation/socket/listener cleanup,
host/root/legacy namespace isolation, bounded declared/chunked bodies
and exact-limit controls, redaction with positive log capture, malformed
expiry, legacy endpoint/redirect controls, and real CLI cached-auth
aliases.

Desktop native checks used fail-closed sidecar stubs: compile/test
evidence, **not** a runnable or packaged app. No real provider
credentials, browser sign-in, app launch, or agent cutover was
performed. Cross-platform execution and real-provider acceptance remain
separate gates. No merge or downstream adoption is requested by this PR.

---------

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
2026-09-13 08:04:56 -06:00
Mic Neale
44316ff72f feat(mesh): upgrade to mesh-llm 0.76.0-rc8 and recommend Qwen3.8 27B (#6189)
Upgrades Buzz's mesh-llm dependencies through the released `v0.76.0-rc8`
tag (`2040765d`), including the Qwen3.8 curated recommendation and rc8's
scheduler/runtime improvements.

**Scope note:** the earlier open-relay/unenforced-admission mode has
been removed from this PR at Mic's direction — it is not a product mode
we want. Mesh admission remains roster/allowlist driven, exactly as on
`main`: on a relay with no NIP-43 membership snapshot the mesh runs
self-only. No NIP-11 mode probing, no mode-transition restarts. A future
perimeter/admission strategy for open relays will be designed
separately.

This PR also:
- seeds `BUZZ_AGENT_LLM_TIMEOUT_SECS=660` for mesh agents, above
MeshLLM's 600-second backend timeout;
- makes `desktop-tauri-clippy` lint both default and `mesh-llm` cfg
graphs;
- runs the feature-enabled desktop test suite in CI;
- recommends Qwen3.8 27B Q4_K_M for 64 GB-and-larger machines, then
ladders down through Gemma 4 E4B and Qwen 9B for smaller machines;
- keeps stored shared-compute `auto` translated to MeshLLM's supported
wire model `mesh`.

RC8 verification:
- `just ci` passed locally at
`92ecc7ec933bdd4df804cc9f28a2b51efa5313c5`.
- Pre-push differential gates passed, including both desktop Tauri cfg
graphs and package tests.
- A prior isolated runtime smoke used the RC8 binary's OpenAI endpoint
for a Buzz-shaped system/user/tool/tool-result/final-response loop; all
assertions passed and the isolated process was shut down.

Perf previously measured on M5 Metal, Qwen3.8-27B-Q4_K_M: TTFT 0.22–0.32
s, ~25 tok/s streaming; agent-shaped turns ~1 s to first token after the
first (prefix cache).

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Signed-off-by: Alessandro Joabar <sandro@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz>
Co-authored-by: Mic Neale <305999590+micspiral@users.noreply.github.com>
Co-authored-by: Alessandro Joabar <sandro@squareup.com>
2026-09-08 07:30:45 -07:00
Jordan Mecom
4d447b9c20 Add generic information-flow control core (#7293)
Adds a dependency-free `ifc-core` crate implementing reader-set
confidentiality labels and monotonic per-computation flow state.

- Defines flow ordering, join, and meet over caller-supplied principal
universes.
- Fails closed for unknown or cross-universe input and checks reader
widening at egress.
- Includes property tests for lattice laws and monotonic taint, plus the
design paper that motivates the broker integration.

The crate deliberately contains no Buzz, Nostr, channel, membership, or
grant policy; those remain in the Buzz adapter.

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
2026-09-04 22:26:44 +00:00
Will Pfleger
5aed49b505 feat(buzz-agent): add DatabricksAuthCoordinator single-flight OAuth (#5545)
## What

Consolidates all Databricks OAuth acquisition behind one coordinator on
`PkceOAuthTokenSource`. Every entry point — the four `TokenSource`
methods (`bearer`, `bearer_no_browser`, `refresh_now`,
`interactive_login`) plus the public `acquire_with_intent` — routes
through a single `acquire()`/`acquire_locked()` core that owns browser
and cooldown policy.

Before this, acquisition logic was scattered across those methods with
no coordination: concurrent callers (Desktop discovery, the saved-agent
model picker, managed-runtime inference) could each pop their own
browser, and a just-denied attempt would immediately re-prompt on the
next passive read.

## How

- **Intent policy.** `AuthIntent::{Auto, UserInitiated, Headless}`
decides whether a caller may open a browser and whether it honors the
cooldown. `Headless` never browses; `Auto` browses but honors an
unexpired cooldown; `UserInitiated` browses and bypasses+clears the
cooldown.
- **Two-layer single-flight per cache key.** An in-process registry
(`INFLIGHT`) coalesces same-key, same-intent callers onto one leader's
attempt before the file lock. The slot key is `(lock_path, AuthIntent)`,
so a `UserInitiated` sign-in never inherits an `Auto` leader's result. A
joined result is revalidated against the waiter's own contract. Across
processes, callers serialize on a `flock`-based advisory lock and share
success through the on-disk cache. RAII `Drop` releases both lock and
leader slot.
- **Joiner credential-state reconciliation.** `SlotPublish` carries the
full `CachedToken` on success. Each joiner reconciles its own
independent `state` cell under `state.lock().await` before returning:
adopt when absent, expired, or matching the joiner's rejected
credential; preserve any distinct newer usable credential. On a matching
shared failure, neutralize the joiner's in-memory rejected entry under
lock via `expire_rejected_memory` — durable disk mutation is reserved
for `acquire_locked` under the cross-process file lock. Without this, a
joining source's state remains stale or empty and subsequent plain
`bearer()` calls resurface the rejected or absent credential.
- **Validate-before-persist boundary.** `finish()` is the
candidate-token persistence boundary for refresh and browser results.
Before a token is written to cache or the cooldown is cleared, a bearer
equal to the caller's rejected bytes yields a typed failure.
- **Token neutralization.** When `acquire_locked` enters with `rejected
= Some(bytes)`, it calls `expire_rejected()` under the state lock before
any cache check.
- **Cross-process failure single-flight.** An `AttemptRecord` sidecar
records a monotonically-increasing generation, intent, result code, and
SHA-256 digest of the completing caller's rejected token. Adoption is
temporal (pre-queue snapshot predates current generation) and
digest-matched.
- **Typed outcomes.** `AuthError` with stable `code()`/`from_code()`
replaces display-text matching.
- **Durable cooldown sidecar.** Every failed browser attempt is recorded
next to the cache key. 5-minute expiry.
- **Windows disk persistence disabled.** On non-Unix platforms,
`persist()` is a no-op. Lock, cooldown, and attempt sidecars are active
on all platforms. Tests that seed or assert on the on-disk token cache
are `#[cfg(unix)]`-gated.
- **Injected browser opener** invoked while the localhost callback
listener is live.

## Tests

- `crates/buzz-agent/tests/databricks_auth_coordinator.rs`:
browser/cooldown/classification acceptance matrix with a scripted
`BrowserOpener` and stub OIDC provider. P1 regressions exercise the full
`finish()` → `acquire_locked()` → `acquire_leader()` →
`LeaderGuard::complete()` → joiner wiring:
- `test_inprocess_joiner_reconciles_stale_state_after_shared_success`
(Unix): two real sources both loaded locally-fresh-but-rejected X; after
shared success Y, subsequent plain `bearer()` on both returns Y, not X.
-
`test_inprocess_joiner_neutralizes_rejected_on_matching_shared_failure`
(Unix): B's matching rejected X is force-expired in memory after shared
`RefreshRejected`; subsequent read cannot return X.
- `test_inprocess_joiner_populates_empty_state_no_second_acquisition`
(non-Unix): empty A/B join a browser success; B's subsequent headless
read returns Y without a second browser (no disk fallback on non-Unix
exposes the regression).
- `test_crossprocess_userinitiated_waiter_adopts_predecessor_denial`
(snapshot-marker barrier replacing an earlier sleep for deterministic
generation ordering).
- `auth.rs` in-crate tests: lock-primitive edges, disk-recheck on shared
failure (`#[cfg(unix)]`), and:
- `test_joiner_reconciliation_blocked_until_state_lock_released`:
deterministic direct-poll proof that awaited reconciliation requires
`state.lock().await`. The test task holds B's state mutex and manually
polls a pinned real `acquire()` future — Poll 2 (slot published, mutex
still held) must return `Pending` because `lock().await` blocks; with
`try_lock` instead, Poll 2 returns `Ready`, failing the assertion.
- `test_joiner_preserve_distinct_newer_credential`: deterministic direct
polling parks B at `slot.wait()`, then writes Z directly into B's state
in the same task, then publishes Y and awaits completion. B must return
Y but leave state == Z. Mutation check: unconditional adoption
overwrites Z with Y, failing the state assertion.
- `test_joiner_shared_failure_recovers_disk_replacement` (Unix): the
matching-failure joiner enters the recovery branch — after
`expire_rejected_memory` (in-memory, empty state no-op) it reads a
sibling-written disk replacement via `usable_from_disk` and returns it.
Mutation check: removing the `usable_from_disk` recovery branch returns
`Err(RefreshRejected)`.
- `test_joiner_failure_does_not_write_disk` (Unix): byte-for-byte
disk-invariance regression — a matching-failure joiner calls
`expire_rejected_memory` and must not touch the on-disk cache. An
independent process C may write a valid replacement between A's failure
and B's reconciliation; this guard ensures B's unfenced in-memory
neutralization cannot overwrite C's concurrent disk write. Mutation
check: reverting to `expire_rejected` rewrites the file (`expires_at =
0`), changing the bytes and failing the assertion.
- `test_lock_timeout_leaves_cooldown_sidecar_byte_for_byte_untouched`: a
waiter past its deadline returns `LockTimeout` before entering
`acquire_locked`; the cooldown sidecar bytes are unchanged. Documents a
pre-lock limitation: `LockTimeout` callers do not neutralize state or
sidecars.

## Scope / follow-ups

- **Runtime 401 handling is deferred.** This PR owns acquisition
single-flight and policy.
- **Desktop wiring is Phase 2** (not in this PR's boundary). Confined to
`crates/buzz-agent/`.
- **Windows DACL** is a follow-up once the `windows-sys` binding is
available.

## Stack

Built on [#5534](https://github.com/block/buzz/pull/5534)
(`hayt/databricks-oauth-cache-hardening`), now merged. Retargeted to
`main`.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-01 15:59:21 -04:00
Will Pfleger
70895b355f feat(buzz-auth): add production NIP-FI federated assertion runtime (#7109)
Depends on #6994 (merged).

Stack: PR 2 #6994 (merged) → this PR (#7109) → PR 4 #7148 → PR 5

## What

Production assertion runtime for NIP-FI Phase A: JWKS caching layer,
SSRF-hardened HTTP fetcher, startup validation gate, NIP-11 discovery
serialization, federated assertion verifier with sealed key-source
authority, and supporting invariant tests.

## Changes

### `crates/buzz-auth/src/nip_fi/jwks/`

`ProductionJwksSource<F>` implements the sealed `IssuerKeySource` trait:

- `HttpJwksFetcher`: reqwest-backed fetch with SSRF protection — URI
validation (HTTPS, no credentials, no fragment, no host matched by the
shared enumerated deny policy), per-fetch DNS resolution rejecting any
resolved address matched by the shared enumerated deny policy, address
pinning to prevent DNS rebinding TOCTOU, redirect denial, incremental
body streaming capped at 512 KiB before any parse
- IPv6 host extraction via typed `Url::host()` accessor (strips brackets
before SSRF check and provides the correct bare input form for reqwest
`resolve()` pinning; the bracketed form from `host_str()` fails
`IpAddr::parse` and does not match the URL authority key)
- Complete-operation deadline via `tokio::time::timeout` covering DNS
resolution through body streaming
- Bounded periodic refresh with configurable interval and hard snapshot
deadline
- Cancellation-safe RAII refresh permit: dropped on future cancellation
so the next caller can re-fetch
- Content-digest-gated generation counter: identical re-fetches preserve
generation; key rotations advance it
- Injectable clock (`now_fn: Arc<dyn Fn() -> DateTime<Utc> + Send +
Sync>`): production uses `Arc::new(Utc::now)`; all four deadline
creation and expiry checks use `(self.now_fn)()`, enabling
controlled-time testing without wall-clock sleep

`JwksSourceContract`: a closed value type that is the single source of
truth for the three deployment fields whose change alters which keys the
runtime trusts and how long it trusts them:

- `jwks_uri` — selects the authenticated key source; validated at
construction (HTTPS, no credentials/fragment, no bare private-IP host);
stored as the `Url`-normalized form so that equivalent spellings
(uppercase host, explicit default port `:443`, dot-segment paths like
`/.well-known/./jwks.json`) converge to the same `AssertionPolicyId`
- `refresh_interval_seconds` — defines bounded refresh behavior;
positive, ≤ 1 year, strictly < `key_snapshot_hard_deadline_seconds`
- `key_snapshot_hard_deadline_seconds` — defines the source's accepted
time rule; every `VerifiedAssertion.revalidation_dependencies` deadline
derives from this

`JwksSourceContract` is a required `IssuerPolicy` input and is included
in `derive_assertion_policy_id` after a domain separator.
`IssuerJwksConfig` embeds the contract instead of independently
restating these fields — startup validation rejects any contract
mismatch (`NipFiStartupError::JwksContractMismatch`).

### `crates/buzz-auth/src/nip_fi/verifier.rs`

- `FederatedAssertionVerifier<S>`: provider-neutral verifier over a
closed multi-issuer registry and sealed `IssuerKeySource`
- `Arc<S>: IssuerKeySource` forwarding impl (blanket seal for `Arc<S>`
in the sealed module) — one `Arc<ProductionJwksSource>` can be shared
across multiple verifiers; all observe JWKS refreshes through the shared
cache without rebuilding the verifier
- `AssertionKeySet`: crate-private constructor seals issuer binding — no
external crate can relabel issuer B's JWKS as issuer A
- Sealed `IssuerKeySource` trait closes the authority-construction seam
at both ends

### `crates/buzz-core/src/network.rs`

Renamed `is_private_ip` to `is_not_global_unicast` (compat alias
retained) and restored the complete IANA deny/exception table from this
branch's own history (`272dacadb`). The predicate is an enumerated
deny/explicit exception policy: addresses covered by a named deny rule
are rejected; addresses not covered by any explicit deny rule (e.g.
`fe00::1`) pass through. Deny rules are derived from the IANA
Special-Purpose Address Space registries (last updated 2025-10-09), with
globally-reachable exceptions carved out explicitly (e.g. PCP/TURN
anycast inside 2001::/23).

Blocked IPv4 classes: loopback (127/8), private RFC 1918 (10/8,
172.16/12, 192.168/16), link-local (169.254/16), unspecified (0/8),
broadcast, CGNAT/RFC 6598 (100.64/10), benchmarking/RFC 2544
(198.18/15), IETF Protocol Assignments (192.0.0.0/24, globally reachable
exceptions: 192.0.0.9 PCP anycast RFC 7723 and 192.0.0.10 TURN anycast
RFC 8155), documentation/RFC 5737 (192.0.2/24, 198.51.100/24,
203.0.113/24), deprecated 6to4 relay anycast (192.88.99.0/24, RFC 7526,
global=None → conservative deny), multicast/RFC 5771 (224/4), reserved
class-E (240/4).

Blocked IPv6 classes: loopback (::1), unspecified (::), ULA (fc00::/7),
link-local (fe80::/10), deprecated site-local (fec0::/10, RFC 3879),
multicast (ff00::/8), IETF Protocol Assignments envelope (2001::/23,
globally reachable exceptions: 2001:1::1–::3 PCP/TURN/DNS-SD anycast,
2001:3::/32 AMT RFC 7450, 2001:4:112::/48 AS112-v6 RFC 7535,
2001:20::/28 ORCHIDv2 RFC 7343, 2001:30::/28 DETs RFC 9374),
documentation (2001:db8::/32 RFC 3849, 3fff::/20 RFC 9637), 6to4
(2002::/16, RFC 3056), Discard-Only (100::/64, RFC 6666), Dummy IPv6
Prefix (100:0:0:1::/64, RFC 9780), SRv6 SIDs (5f00::/16, RFC 9252),
NAT64 local-use (64:ff9b:1::/48, RFC 8215). IPv4 embedded in mapped,
compatible, NAT64 well-known (64:ff9b::/96), and SIIT IPv4-translated
(::ffff:0:0:0/96) forms is checked recursively. All three callers (JWKS
boundary, webhook SSRF, link-preview SSRF) inherit the complete
predicate through the inline `is_private_ip` compatibility alias.

### Invariant coverage

**Canonical URI convergence.**
`jwks_contract_uri_canonicalization_convergence_and_divergence` asserts
that uppercase host, explicit `:443`, and dot-segment path
(`/.well-known/./jwks.json`) each produce the same `AssertionPolicyId`
as the canonical form; a genuinely different host or path diverges.
Mutation: storing raw input bytes instead of `parsed.to_string()` turns
the three convergence assertions red.

**Resolved-target and pin-input seam.**
`resolved_target_and_pin_key_seam_public_ipv6_and_fec0_rejection`
carries a public `2606:4700::1` URI through all three stages of
`fetch_jwks_inner`: `extract_url_host_and_port` yields the bare host (no
brackets), `resolve_and_check_ssrf` takes the IP-literal fast path and
returns the accepted `IpAddr`, and the extracted host string equals the
URL authority form (verifying the correct bare input to reqwest's
`resolve()` pin call). `fec0::1` traverses the same extraction and SSRF
stages and is rejected as `InvalidUri`. Network-free: both addresses are
IP literals with no DNS lookup. Mutation: restoring `host_str()`
brackets the address, `IpAddr::parse` fails, the SSRF fast path is
unreachable, and all three assertions flip red.

**Controlled original-deadline rotation.**
`shared_arc_source_verifier_rejects_expired_a1_accepts_a2` uses an
`AtomicI64`-backed injectable clock to advance past A1's original
absolute deadline without wall-clock sleep. A1's deadline is computed at
T0 and never mutated. The clock advances to T0 + HARD_DEADLINE_SECS + 1;
`get_snapshot` fires a re-fetch and installs A2. One unchanged
`FederatedAssertionVerifier` then rejects A1-signed tokens (deadline
enforced by the `key_set` read path) and accepts A2-signed tokens,
proves A2's generation is strictly greater, and confirms A2's deadline
is later than A1's original. Mutation oracle: replace the shared `Arc`
with an independently constructed source built from the same configs and
sharing the same controlled clock, warmed with a separate A1 fetch
before advancement. Post-advancement, `key_set()` on the verifier's
independent source filters the expired A1 snapshot (`filter(|c| now <
c.hard_deadline)`) and returns no keys — the verifier never re-fetches
and never observes A2. A1-reject stays green (the independent cache is
also expired, so no A1 keys are served), but A2-accept flips red,
because the verifier never observes A2. A2 acceptance is the reliable
shared-source oracle.

**Complete SSRF classifier boundary.** JWKS-boundary tests cover every
newly restored class through `validate_jwks_uri` (URI-validation path):
`192.0.0.1` (IETF Protocol Assignments interior), `192.0.0.9`/`.10`
(PCP/TURN anycast global exceptions), `192.88.99.1` (deprecated 6to4
anycast), `2001:2::1` (2001::/23 interior), `2001:1::1` (2001::/23
global exception), `100::1` (Discard-Only), `3fff::1` (documentation),
and `5f00::1` (SRv6 SIDs). URI validation and resolved-target
enforcement share the same `is_not_global_unicast` predicate, so these
URI-path tests exercise the complete classifier table. All pass
mutation: removing any deny branch makes the rejection assertion red;
removing any exception branch makes the acceptance assertion red. The
resolved-target enforcement path is covered separately by
`resolved_target_and_pin_key_seam_public_ipv6_and_fec0_rejection` for
`::1` (loopback), public `2606:4700::1`, and `fec0::1`.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
2026-09-01 13:00:40 -04:00
Tom Brow
c432a111ca feat(mobile): push notifications MVP (#6269)
This PR implements MVP, iOS-only,
[NIP-PL](8d2d0ff5ad/docs/nips/NIP-PL.md)-compliant
push notifications.

A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any
message that appears in the in-app Notifications tab.

## Enrollment flow
The first time the client first connects to a relay with
`BUZZ_PUSH_ENABLED`:
```mermaid
sequenceDiagram
    autonumber
    participant App as Buzz iOS app
    participant iOS
    participant Relay as Buzz relay
    participant Attest as Apple App Attest
    participant Gateway as Push gateway

    App->>Relay: Fetch NIP-11 push capability
    Relay-->>App: Push profile, current relay public key, and limits

    par
        App->>iOS: Request notification permission
        iOS-->>App: Permission result
    and
        App->>iOS: Register for remote notifications
        iOS-->>App: Device token
    end

    App->>Gateway: Request installation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Attest installation transcript
    Attest-->>App: Attestation proof
    App->>Gateway: Enroll device token and proof
    Gateway-->>App: Installation handle

    App->>Gateway: Request delegation challenge
    Gateway-->>App: Single-use challenge
    App->>Attest: Assert relay-key delegation
    Attest-->>App: Assertion
    App->>Gateway: Create delegation
    Gateway-->>App: Opaque endpoint grant

    App->>Relay: Publish encrypted push lease and filters
    Relay-->>App: Lease acknowledged
```

## Push-time flow

When a notification-eligible event is received by the relay:

```mermaid
%%{init: {
  "sequence": {
    "actorMargin": 20,
    "width": 110,
    "messageMargin": 18,
    "diagramMarginX": 8,
    "wrap": true
  }
}}%%
sequenceDiagram
    autonumber
    participant Relay as Buzz relay
    participant Gateway as Push gateway
    participant APNs as Apple Push<br/>Notification service
    participant iOS
    participant NSE as Notification service<br/>extension

    Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization

    Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload
    APNs-->>Gateway: 200 OK: request accepted
    Gateway-->>Relay: 200 OK: accepted status

    APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1
    iOS->>NSE: Invoke extension<br/>original notification content

    NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization
    Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003

    NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target
```

relay → push gateway → APNs -> NSE -> Notification Center

## Known limitations

The APNs wake payload is intentionally constant and opaque: it contains
no originating community or message identifier, in keeping with the
implemented NIP-PL privacy design.

The Notification Service Extension must therefore reconnect to the relay
and resolve eligible messages after each wake. Around overlapping wakes,
timing boundaries, or resolution windows, notification presentation may
occasionally omit an expected message or display a message more than
once.

This best-effort behavior is deliberately accepted for the current
implementation and will be measured during the internal rollout to
determine whether the user experience is acceptable before any broader
deployment; the implementation does not claim exactly-once presentation.

## Validation

Live end-to-end hardware validation used an internal remotely hosted
development relay and push gateway, the APNs sandbox, and a physical
iPhone 12 mini:

- A second real Buzz client published a uniquely marked message through
the hosted relay.
- The relay matched the message and sent the constant opaque wake
through the hosted gateway. The gateway made an actual APNs request; no
`simctl push` or simulated notification was used.
- The iPhone received the notification on its lock screen. The
Notification Service Extension reconnected to the relay, fetched the
event, verified its ID and signature, and replaced the placeholder
content with the real notification title and body.
- After the app populated its shared presentation cache, a final marked
notification visibly showed the sender display name, sender avatar, and
hashtag-prefixed channel name.
- Tapping a lock-screen notification opened Buzz and exercised the
notification-response path and navigated to the corresponding message.

Final validation with a dogfood-signed artifact and production App
Attest/APNs configuration remains a release step.

## Independent pre-reviews

- **First pass:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585)
found missing transient retries, executor-key rotation suppression,
duplicate installation renewal, and an unauthenticated challenge write
amplifier. These were resolved by [retry-safe
bootstrap](https://github.com/block/buzz/pull/6269/changes/12c66ea62)
and [authenticated renewal plus a cross-replica
quota](https://github.com/block/buzz/pull/6269/changes/8e5ece0bd).
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962)
found delegation generation burning and an edited applied migration,
resolved by [exact-generation
revocation](https://github.com/block/buzz/pull/6269/changes/c26d2159d)
and a [forward-only
migration](https://github.com/block/buzz/pull/6269/changes/956c1d099).
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1)
found no blockers.
- **Exact-head re-review:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68),
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849),
and
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb)
independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also
revalidated every remediation and the endpoint-specific App Attest
enrollment bound.

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Jordan Mecom <jm@squareup.com>
2026-08-28 15:15:09 -06:00
Will Pfleger
953a1798b2 feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776)
The closed, provider-neutral contract layer at the root of the NIP-FI
federated-identity dependency graph — Phase A, PR 1 of the plan. It has
no dependencies on any other PR and defines no database schema,
migration, runtime JWKS fetching, binding resolution, enrollment, or
request/proof binding; those belong to later PRs.

## What this adds

- **Multi-issuer assertion-policy config** (`IssuerRegistry`,
`IssuerPolicy`) keyed by exact `iss`. Identity is issuer-qualified
`(iss, sub)` throughout — equal `sub` under different `iss` are distinct
identities. The subject coordinate is fixed to the JWT `sub` claim
(`SUBJECT_CLAIM`), never configurable, so no deployment can seal a
mutable attribute like `email` as identity. Issuer URL and audience
remain deployment configuration.
- **The two deterministic semantic contract identities** —
`AssertionPolicyId = H(canonical assertion-policy contract)` and
`TransportContractId = H(canonical transport contract)` — derived by
length-prefixed, domain-separated SHA-256 so a semantic change moves
exactly its owning ID while benign JWKS rotation never changes policy
lineage. Set-valued policy inputs (audiences, algorithms, subject-class
values, scope capture) are canonicalized before derivation, so the ID is
invariant under permutation and duplication. Config fields that a
freshness class never reads are rejected at construction (an
`offline-jwt` policy cannot carry `maximum_status_age`), so the
canonical encoding stays total over valid configs and semantically
identical policies always derive one ID.
- **The single canonical verifier** (`FederatedAssertionVerifier`,
`FI-INV-16`) producing the origin-sealed, provider-neutral
`VerifiedAssertion` normalized result. Its constructor is crate-private,
so unverified claims cannot be promoted into authority. The issuer→JWKS
authority is entirely crate-owned: `AssertionKeySet` has no public
constructor and `IssuerKeySource` is sealed, so no downstream crate can
relabel one issuer's keys as another's. The authenticated key set is
bounded (`MAX_JWKS_KEYS`) and the bound is folded into
`AssertionPolicyId`, so an unbounded attacker-controlled `kid` scan
cannot be driven; every snapshot requires a finite positive hard
deadline.
- **Sealed revalidation dependencies** — `RevalidationDependencies`
carries the key-snapshot hard deadline and a `ConfidentialAssertion`
handle to the exact compact JWS (no `Debug`/`Display`/`serde` leak, sole
read path `compact_jws()`), so a changed snapshot can revalidate the
same evidence: a retained key revalidates, a removed key denies.
- **The privacy-preserving four-class denial contract** (`DenialClass`,
`FI-INV-13`) with the byte-exact Nostr text, HTTP status, body,
`Content-Type`, and `WWW-Authenticate` values fixed by the spec's
rejection table. An unreadable required current dependency maps to
`authorization_unavailable`/503, never to rejected evidence, and
rejected evidence never masquerades as a 503 at either end of the
pipeline: all bounded, dependency-independent checks (compact structure,
header, signature shape, policy, algorithm, token class) precede
key-source lookup, and all offline validation (token-class, key,
signature, audience, claims, time) completes before any status-witness
deferral. A wrong-`typ` or structurally malformed token, or
malformed/invalidly-signed input naming a current-status issuer,
therefore denies with `evidence_rejected`/403 rather than being reported
as a 503 availability signal.

## Corrections applied to the mined source

Mined from the `buzz-auth` verifier core in
[#1476](https://github.com/block/buzz/pull/1476) and corrected to the
settled spec (the merged `docs/nips/NIP-FI*.md`,
[#5946](https://github.com/block/buzz/pull/5946)), which settled after
#1476 was written:

- Token-class selection with exact `typ` enforcement. Two classes are
offered: `at+jwt` and `nip-fi+jwt`. There is deliberately no
generic/absent-`typ` "named compatibility" class: it cannot be proven
disjoint from an OIDC ID token by claim presence alone (an issuer can
mint an ID token carrying `client_id`), and the only authenticated
discriminator is `typ`, which such a mode declines to constrain.
- OIDC ID-token denial — denies even when `iss`, `aud`, and `sub` match,
via exact `typ` mismatch against every accepted class.
- The fixed `nostr_pubkey` claim accepted only as lowercase hex of
exactly one 32-byte key; bech32 and other aliases deny.
- Resource-owner / client-subject ambiguity denial and required
`client_id` for `at+jwt`.
- JWK admissibility: a key's `use` and, when present, `key_ops` must
authorize signature verification (a key restricted to other operations
such as `encrypt` is rejected), and the selected JOSE algorithm is bound
to the key's required family and curve (ES256↔EC/P-256, ES384↔EC/P-384,
EdDSA↔OKP/Ed25519, RS/PS↔RSA). The JWK `alg` is advisory; the actual key
material is what signs, so a JWK declaring a matching `alg` over
mismatched material (a different family or curve) is rejected before
signature verification.
- Spec-exact time arithmetic (`now < exp`, `iat <= now + skew`, `now <
iat + maximum_assertion_age`, equality at expiry is expired).
`exp`/`iat`/`nbf` accept finite integer or fractional RFC 7519
`NumericDate` values with checked, overflow-safe conversion; non-finite
and out-of-range values deny.

## Verification

In-crate tests in `crates/buzz-auth/src/nip_fi/verifier/tests.rs` sign
real ES256 assertions against a fixed test key and cover: the happy
path, exact-wire-text for all four denial classes, deterministic and
semantic contract IDs, canonicalization invariance, token-class
enforcement including ID-token denial, JWK `key_ops` rejection,
`nostr_pubkey` hex handling, time bounds, and multi-issuer selection
(same subject across distinct issuers yields distinct identities and
policy IDs). They also cover the round-3 contracts: the key-set bound
(oversized rejected, at-bound accepted, empty rejected), fixed-`sub`
identity (identity is `sub` not a configured `email`; a token without
`sub` denies), offline-before-deferral (invalid-signature,
wrong-audience, malformed-claim, and expired under a current-status
issuer each deny 403/`evidence_rejected`), and the revalidation
contracts (dependencies carry the deadline and exact JWS; a retained key
revalidates, a removed key denies). And the round-4 contracts:
algorithm↔key-material binding, covered exactly per accepted algorithm
(a table-driven matrix asserts every policy-acceptable algorithm —
ES256, ES384, EdDSA, RS256/384/512, PS256/384/512 — matches only its
required family/curve and rejects every other, so each mapping mutation
fails individually; and an ES256 token against P-384, RSA, and Ed25519
material each deny `InvalidKey`); malformed evidence classified before
key lookup (wrong-`typ`, two-segment, four-segment, empty-signature, and
non-base64url-signature tokens against an empty source deny 403 not
503); `maximum_status_age` inapplicable under offline-jwt rejected at
construction while current-status still requires it; and fractional
`NumericDate` (finite fractional `iat`/`exp`/`nbf` within bounds verify,
non-finite and out-of-range deny). Compile-fail doctests guard the
sealed authority seam (`AssertionKeySet::new`, `IssuerKeySource`) and
the absence of the named-compatibility token class.

## Notes

- `Co-authored-by` attribution is preserved for the mined author. The
#1476 `buzz-auth` commits are authored by Cea Stapleton Cordasco (the
salvage map records #1476 under Franco; the git history on that PR is
Cea's — surfacing for the attribution/closure record).
- Amends `docs/nips/NIP-FI.md` to remove the "named compatibility access
token" class and its `FI-TRACE-TOKEN-CLASS` oracle reference, so the
normative spec matches the two-class root implementation (a
generic/absent `typ` cannot be proven disjoint from an OIDC ID token).
Removal-only, no neighbor redesign; a `docs/nips/` grep confirms no
dangling cross-reference to the class remains.
- Adds `jsonwebtoken 10.4.0` (`aws_lc_rs`) as a workspace dependency.

Dependencies: none.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-28 10:29:25 -04:00
Luke Tornquist
113a33b7e4 Add database pressure observability (#6700)
## Why

Database pressure currently collapses several distinct delays into one
symptom. This adds the evidence layer needed to distinguish pool
acquisition wait, logical database operation time, advisory-lock wait,
and selected transaction duration before changing timeout or retry
policy.

This is the phase 2 Lane A observability bundle for
[#26](https://github.com/TheSentinel454/buzz/issues/26),
[#28](https://github.com/TheSentinel454/buzz/issues/28), and
[#33](https://github.com/TheSentinel454/buzz/issues/33). It is stacked
on #6668.

## What

- Record explicit reader/writer checkout wait and acquisition outcomes
with `buzz_db_pool_acquire_wait_seconds` and
`buzz_db_pool_acquisitions_total`.
- Extend the compile-time `#[datastore_span(name = "...")]` seam with
`buzz_db_operation_duration_seconds`, so operation labels remain static
source literals instead of request data.
- Route correctness-critical replacement, membership, push-gate,
deletion, and migration/schema-safety advisory locks through one
observer without changing their SQL, order, scope, or blocking behavior.
- Measure six internally owned transaction lifetimes with
`buzz_db_transaction_duration_seconds`, starting after `BEGIN` succeeds
and ending after explicit commit/rollback or scope exit.
- Emit root slow-operation warnings at 500 ms, logging the first slow
completion and then 1/100 per call site with only `operation`,
`outcome`, and `elapsed_ms`.
- Document names, units, fixed label vocabularies, measurement
boundaries, and blind spots in this PR description.

Fixed labels are deliberately small:

- `pool_role`: `writer`, `reader`
- `lock_type`: `replacement`, `membership`, `push_gate`, `deletion`,
`migration_schema_safety`
- `outcome`: `success`, `error`, `timeout` where SQLx/PostgreSQL can
distinguish it accurately
- `operation`: compile-time datastore names plus the six closed
transaction operation names documented in the runbook

No metric or slow warning contains community IDs, event IDs, event
kinds, coordinates, d-tags, SQL/query text, query IDs, returned errors,
or event content.

## Coverage boundaries

- Operation duration is the complete annotated logical function body,
not pure SQL execution; it may include implicit checkout, lock wait,
nested operations, and application work. Cancelled futures do not reach
its completion hook.
- Pool timing covers explicit helper checkouts, including proved-reader
routing and selected writer-owned transactions. Implicit SQLx checkout
through `&PgPool` remains folded into operation duration.
- Lock timing covers application-side blocking locks in the five named
families. Trigger/stored-procedure locks, channel-TTL locking, the usage
try-lock, and the audit service session lock remain outside this slice.
- Transaction timing covers only the six wholly owned boundaries
documented in the runbook. It excludes pool wait, `BEGIN`, asynchronous
rollback cleanup after an early return, and caller-owned
`Db::begin_transaction` lifetime.

## Relationship to #6229

#6229 is the incident-driven timeout precursor. This PR does not add or
change `statement_timeout`, `lock_timeout`,
`idle_in_transaction_session_timeout`, retries, audit durability, or
client-visible conflicts. It provides the missing distributions needed
to evaluate those policies later and intentionally leaves #6229's open
audit retry/durability finding untouched.

The branches overlap in `crates/buzz-db/src/lib.rs` and
`crates/buzz-db/src/migration.rs`, so a later rebase may need textual
conflict resolution, but the behavior is complementary rather than
duplicated.

## Risk assessment

Moderate-low. The primary risk is instrumentation overhead and added
static series. Cardinality is source-bounded, slow logs are
sampled/redacted root events, and the lock/transaction changes wrap
existing awaits without changing policy or ordering.

## Verification

Author workstation: `buzz-tornquist-db-pressure-observability`
(`2010927`), exact head `d7cf833e26c528adfcde3917ded80daf6f4ddac9`,
parent `6f50e6b2b2a996349149af61d35bdd6a355f77fd`.

- `cargo fmt --all --check` — passed
- `cargo clippy -p buzz-datastore-tracing -p buzz-db -p buzz-audit -p
buzz-search -p buzz-relay --all-targets -- -D warnings` — passed
- `cargo test -p buzz-datastore-tracing --quiet` — 4 passed
- `cargo test -p buzz-db --quiet` — 109 passed, 200 ignored
- `cargo test -p buzz-audit -p buzz-search --quiet` — 16 passed, 25
ignored
- `cargo test -p buzz-relay --lib --quiet -- --test-threads=1` — 906
passed, 48 ignored
- Native PostgreSQL focused tests for pool success/timeout/error, lock
success/contention/timeout/error, replacement, membership serialization,
push ordering, deletion fencing, migration/schema exclusion, and reader
fallback — 8 passed

The default-parallel relay run passed once; subsequent runs exposed the
existing load-sensitive
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` 504 at
the end of the suite. That test passes in isolation and the full relay
suite passes serially.

Independent exact-head review workstation:
`buzz-tornquist-db-pressure-observability-review` (`2013067`).
Formatting, the same all-target clippy command, datastore
instrumentation tests, DB unit tests, source privacy guards, and
diff/non-goal audits passed; no review findings.

Generated with Codex

---------

Signed-off-by: tornquist <tornquist@squareup.com>
2026-08-25 13:00:22 -04:00
Brad Seiler
d12dea4e67 Support community deletion in versioned media buckets (#6738) 2026-08-25 09:02:18 -07:00
renovate[bot]
f794216737 chore(deps): update rust crate async-trait to v0.1.92 (#6094)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [async-trait](https://redirect.github.com/dtolnay/async-trait) |
dependencies | patch | `0.1.91` → `0.1.92` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>dtolnay/async-trait (async-trait)</summary>

###
[`v0.1.92`](https://redirect.github.com/dtolnay/async-trait/releases/tag/0.1.92)

[Compare
Source](https://redirect.github.com/dtolnay/async-trait/compare/0.1.91...0.1.92)

- Resolve double\_must\_use clippy lint in generated code
([#&#8203;303](https://redirect.github.com/dtolnay/async-trait/issues/303))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yOS41IiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 18:29:04 -06:00
renovate[bot]
9390e11c9b chore(deps): update rust crate http-body-util to v0.1.4 (#5452)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [http-body-util](https://redirect.github.com/hyperium/http-body) |
dependencies | patch | `0.1.3` → `0.1.5` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>hyperium/http-body (http-body-util)</summary>

###
[`v0.1.5`](https://redirect.github.com/hyperium/http-body/compare/http-body-util-v0.1.4...http-body-util-v0.1.5)

[Compare
Source](https://redirect.github.com/hyperium/http-body/compare/http-body-util-v0.1.4...http-body-util-v0.1.5)

###
[`v0.1.4`](https://redirect.github.com/hyperium/http-body/releases/tag/http-body-util-v0.1.4)

[Compare
Source](https://redirect.github.com/hyperium/http-body/compare/http-body-util-v0.1.3...http-body-util-v0.1.4)

#### What's Changed

- Add `Fused` body combinator that always returns `None` once completed.
- Add `BodyExt::into_stream()` to convert a body into a `Stream`.
- Add `Full::into_inner()` to get the full `Buf`.
- Add `InspectFrame` and `InspectErr` combinators.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 18:28:38 -06:00
renovate[bot]
1a0a27d358 chore(deps): update rust crate http to v1.4.2 (#5451)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [http](https://redirect.github.com/hyperium/http) | dependencies |
patch | `1.4.0` → `1.4.2` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>hyperium/http (http)</summary>

###
[`v1.4.2`](https://redirect.github.com/hyperium/http/blob/HEAD/CHANGELOG.md#142-June-8-2026)

[Compare
Source](https://redirect.github.com/hyperium/http/compare/v1.4.1...v1.4.2)

- Fix `uri::Builder` to allow `"*"` as the path when scheme and
authority are also set, used in HTTP/2 requests.
- Fix `Uri` to properly reject `DEL` characters.

###
[`v1.4.1`](https://redirect.github.com/hyperium/http/blob/HEAD/CHANGELOG.md#141-May-25-2026)

[Compare
Source](https://redirect.github.com/hyperium/http/compare/v1.4.0...v1.4.1)

- Fix `PathAndQuery::from_static()` and `from_shared()` to reject inputs
that do not start with `/`.
- Fix `Extend` for `HeaderMap` to clamp max size hint and not overflow.
- Fix `header::IntoIter` that could use-after-free if the generic value
type could panic on drop.
- Fix `header::{IterMut, ValuesIterMut}` to not violate stacked borrows.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 18:28:22 -06:00
renovate[bot]
2cef92df67 chore(deps): update rust crate futures to v0.3.33 (#5445)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [futures](https://rust-lang.github.io/futures-rs)
([source](https://redirect.github.com/rust-lang/futures-rs)) |
dev-dependencies | patch | `0.3.32` → `0.3.34` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>rust-lang/futures-rs (futures)</summary>

###
[`v0.3.34`](https://redirect.github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0334---2026-08-11)

[Compare
Source](https://redirect.github.com/rust-lang/futures-rs/compare/0.3.33...0.3.34)

- Preserve cloned waker identity.
([#&#8203;3032](https://redirect.github.com/rust-lang/futures-rs/issues/3032))
- Updato `syn` to 3.
([#&#8203;3028](https://redirect.github.com/rust-lang/futures-rs/issues/3028))

###
[`v0.3.33`](https://redirect.github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0333---2026-07-18)

[Compare
Source](https://redirect.github.com/rust-lang/futures-rs/compare/0.3.32...0.3.33)

- Fix `ReadLine`'s soundness issue regarding to exception safety.
([#&#8203;3020](https://redirect.github.com/rust-lang/futures-rs/issues/3020))
- Fix unsound `Send` impl for `IterPinRef` and `Iter`.
([#&#8203;3003](https://redirect.github.com/rust-lang/futures-rs/issues/3003))
- Fix stacked borrows violation in `compat01as03` implementation.
([#&#8203;3012](https://redirect.github.com/rust-lang/futures-rs/issues/3012))
- Fix memory leak in `FuturesUnordered::IntoIter`.
([#&#8203;3005](https://redirect.github.com/rust-lang/futures-rs/issues/3005))
- Add `portable-atomic-alloc` feature and use it in `FuturesUnordered`.
([#&#8203;3007](https://redirect.github.com/rust-lang/futures-rs/issues/3007))
- Re-export `alloc::task::Wake`.
([#&#8203;3010](https://redirect.github.com/rust-lang/futures-rs/issues/3010))
- Update `spin` to 0.12.
([#&#8203;3014](https://redirect.github.com/rust-lang/futures-rs/issues/3014))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjkuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 18:27:52 -06:00
Logan Johnson
cc8a8b0dcb fix: bump h2 for RUSTSEC-2026-0258 (#6222)
🤖
## Summary
- Bump the locked `h2` version from `0.4.14` to `0.4.16` to clear
RUSTSEC-2026-0258, which affects h2 versions through `0.4.15`.
- Keep the change lockfile-only; no manifest or product-code changes are
included.

## Details
- `h2` is now `0.4.16` with checksum
`a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27`.
- The lockfile was regenerated by `cargo update -p h2 --precise 0.4.16`
using Cargo 1.95.0 / rustc 1.95.0, matching `rust-toolchain.toml` and
the CI pin.
- Cargo's canonical resolver output also rewrites 16 dependency
references to existing `windows-sys` package entries:
- `anstyle-query 1.1.5`, `anstyle-wincon 3.0.11`, `socket2 0.6.3`,
`termina 0.3.3`, and `uds_windows 1.2.1`: `0.61.2` → `0.60.2`.
  - `dirs-sys 0.5.0` and `nu-ansi-term 0.50.3`: `0.59.0` → `0.60.2`.
- `colored 3.1.1`, `errno 0.3.14`, `quinn-udp 0.5.14`, `rustix 0.38.44`,
`rustix 1.1.4`, `rustls-platform-verifier 0.7.0`, `seize 0.5.1`,
`tempfile 3.27.0`, and `winapi-util 0.1.11`: `0.59.0` → `0.52.0`.
- All four referenced `windows-sys` versions (`0.52.0`, `0.59.0`,
`0.60.2`, and `0.61.2`) were already present in the baseline lockfile.
There are zero new package entries or checksums, and the only package
record replacement is `h2 0.4.14` → `0.4.16`. There is no `wasmtime`
edge change.

## Validation
- `cargo-deny check` passed.
- `cargo build --workspace` passed.
- `just test-unit` passed: 455 passed, 0 failed, 1 ignored; all nine
package suites passed.

Signed-off-by: loganj <loganj@squareup.com>
2026-08-18 08:00:52 -07:00
Wes
e0940927ff fix(channels): return complete member rosters (#5765)
## Summary

- return complete channel rosters instead of truncating at 1,000 members
- chunk `event_mentions` inserts inside one transaction so large kind
`39002` snapshots remain discoverable by every `p` tag
- add a targeted `buzz-admin reconcile-channels --channel <uuid>`
force-republish path for stale discovery snapshots
- cover a 1,501-member roster, 11,000-tag mention index, and kind
`39002` tag construction past member 1,000

## Why

The relay builds NIP-29 discovery and several authorization decisions
from `get_members()`, but that helper silently returned only the first
1,000 active members. Desktop then counted the truncated kind `39002`
event, while late members could be rejected by roster-scanning member
actions.

Removing the roster cap exposes PostgreSQL's 65,535 bind-parameter
ceiling in mention indexing, so the insert is chunked transactionally to
preserve all-or-nothing indexing.

The existing reconcilers only fill missing discovery events. The
targeted admin option bypasses the separately known 1,000-channel
reconciliation-list ceiling and replaces an existing channel snapshot
using the configured production relay key.

## Attribution

This supersedes and builds on #3166 by @LordMelkor. Thank you for
identifying the roster boundary and contributing the original
complete-roster and mention-index patch. The production roster/query
changes and the two PostgreSQL regressions retain that work's shape;
this PR rebases it onto current `main`, adds relay coverage, and adds
the targeted repair operation requested for rollout.

## Validation

Exact pushed head: `24d02e4f3824150ed84913c9d230e675502e5b12`

- `cargo check -p buzz-db -p buzz-admin`
- `cargo test -p buzz-db
channel::tests::get_members_returns_full_roster_beyond_1000 -- --ignored
--exact --nocapture`
- `cargo test -p buzz-db
feed::tests::insert_mentions_indexes_rosters_past_bind_parameter_cap --
--ignored --exact --nocapture`
- `cargo test -p buzz-relay --lib
handlers::side_effects::tests::group_members_snapshot_keeps_members_past_one_thousand
-- --exact`
- `cargo run -q -p buzz-admin -- reconcile-channels --help`
- mandatory pre-push hook: branch-skew, desktop checks/typecheck/tests,
mobile tests, Rust tests, and desktop Tauri checks all passed on the
pushed head

## Rollout

1. Deploy the relay/backend build.
2. Run `buzz-admin reconcile-channels --channel <general-channel-uuid>`
with `BUZZ_RELAY_PRIVATE_KEY` configured.
3. Verify the replacement kind `39002` roster count matches the active
database membership count.

No schema migration or desktop release is required.

Fixes #3156
Supersedes #3166

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-13 14:48:04 -07:00
Kalvin C
8a2c9af2db feat(deletion): add durable whole-community deletion (#4425)
## Summary

Adds a durable, operator-controlled V1 for deleting an entire Buzz
community without deleting another tenant's data.

The workflow is exposed through `buzz-admin deletions`:

- `sweep` records independent fleet storage-taxonomy observations
- `submit`, `list`, `inspect`, and `approve` manage a deletion request
- `unblock` resumes a fail-closed request after an operator records
remediation identity and reason
- `run` and `drain` execute bounded work

Requests advance through a PostgreSQL-backed state machine and stop at
`retention_pending` after logical deletion has been independently
verified across PostgreSQL, object storage, and Redis.

This PR ships the engine and CLI, not a continuously running worker or
Kubernetes packaging. For V1, a cluster/VM administrator invokes
`/usr/local/bin/buzz-admin` from the existing relay image, for example
with `kubectl exec` or an equivalent container/VM exec path.

## What whole-community V1 removes

For the target community, V1 removes:

- rows from the allowlisted community-scoped PostgreSQL catalog,
including members, profiles, authored events and bodies, DMs, reactions,
mentions, memberships, tokens, workflows, moderation, audit, feedback,
and rate-limit state
- media sidecars and upload-attribution records under
`_meta/<community>/` and `_uploads/<community>/`
- Git repository pointers under `repos/<community>/`
- Redis keys under `buzz:<community>:*`

The community row survives as a permanent tombstone, and deletion
control-plane records remain as evidence of the request, approval,
execution, and result.

## Safety model

Deletion is not a broad `DELETE CASCADE` followed by optimistic cleanup.
The destructive boundaries are durable and fail closed.

### 1. Inventory and approval

- `submit` resolves the target and freezes the schema plus summary-only
storage inventory.
- Approval is bound to the exact request, community, and frozen
inventory digest.
- Unsupported manifest versions, malformed keys inside the target's
owned prefixes, live scoped-table/write-fence coverage drift,
frozen-inventory mismatch, and approval mismatch block execution rather
than guessing. Migration and catalog revision numbers are not
authorization gates; the executor validates the live safety shape
instead.
- Storage inventory is server-side prefix scoped to exactly:
  - `_meta/<community>/`
  - `_uploads/<community>/`
  - `repos/<community>/`
- The deletion path never lists the whole shared bucket and has no
arbitrary per-community object cap. Its listing work is proportional to
the target community's bindings, not total fleet storage.
- Fleet-wide taxonomy sweeps remain independent observability. They
report unknown writer shapes but do not gate deletion submission,
fencing, or destructive progress. Maintainers must add deletion taxonomy
coverage whenever a new community-owned object-key class is introduced;
writer-coverage tests bind the current media and Git writers to that
contract.

### 2. Quiesce, fence, and destructive freeze

- Writes continue through submission, inventory, and approval. They stop
when execution moves the target into `quiescing` and then establishes
the durable fence.
- Already-admitted external effects finish under heartbeated
serving-write leases; the exact admitted lease may renew while the
community is quiescing, but new lease acquisition is rejected. The
executor drains admitted leases before destructive work.
- Invite minting after quiescing begins fails as typed `AccessDenied`
(HTTP 503 at the relay boundary) before an invite can be persisted.
- Database triggers enforce the community write fence across the
complete catalog of community-scoped tables. Startup/readiness and
destructive execution validate that catalog so a newly added but
unfenced table cannot silently escape.
- **Named isolation assumption — fresh write snapshot.** Every writer
transaction that can reach a community-fenced relation must use
PostgreSQL `READ COMMITTED`; each guarded write therefore observes a
statement snapshot no older than acquisition of the community deletion
lock. `REPEATABLE READ` and `SERIALIZABLE` can retain a pre-fence
snapshot and are unsupported for writers. The writer pool refuses
non-`READ COMMITTED` sessions at connection setup, and both SQL fence
functions reject an explicit per-transaction isolation override with
SQLSTATE `25000`. Configuration-delivered bad isolation can surface
through SQLx as a pool-acquire timeout because every `after_connect`
attempt is rejected; the precise `community writes require READ
COMMITTED isolation` reason remains observable when the SQL guard is
reached. Read-only replica transactions are outside this assumption.
- Holding the shared advisory lock until the guarded write executes is a
separate liveness condition: under `READ COMMITTED`, releasing it early
does not permit resurrection because the trigger rechecks the fence, but
it can turn a fleet sweep into a statement-wide SQLSTATE `55000` abort.
- After the fence closes writers, storage is re-enumerated into chunked
side-table rows. Per-prefix counts and digests bind those concrete keys
to the destructive manifest.
- Manifest chunk insertion, update, and deletion are protected after
freeze. This closes the race where an unbound key could otherwise appear
after the manifest was committed.

### 3. Checkpointed destruction

- Target-owned object bindings are deleted from the frozen destructive
manifest in bounded batches with durable progress.
- The concrete key list lives in chunked side-table rows rather than one
request-row JSON value. It supports large communities, resumable
execution, and terminal cleanup.
- Missing objects are accepted as idempotent crash-window outcomes;
malformed ownership, changed evidence, and unexplained target-prefix
drift fail closed.
- PostgreSQL purging remains scoped by `community_id`, including the
guarded NIP-RS hard-delete path discovered with real Desktop kind
`30078` read-state data.
- Redis cleanup explicitly scans and `UNLINK`s only
`buzz:<community_id>:*`. Natural expiry is insufficient because some
keys, including tunnel generation counters used as fencing state, are
deliberately persistent.

### 4. Independent verification

- PostgreSQL logical absence is checked after purge.
- The three target-owned storage prefixes are freshly inventoried again
and must be empty.
- Redis requires two complete empty namespace scans.
- Only after all three stores pass does the request advance through
`logically_verified` to `retention_pending`.

## What V1 deliberately does not erase

### Shared content-addressed storage

Per-community deletion removes bindings, metadata, attribution records,
and Git pointers. It does **not** physically delete fleet-shared CAS
bytes that another community may still reference:

- media blobs and thumbnails
- Git manifests, packs, and indexes (`manifests/`, `packs/`, and `idx/`)

Safe reclamation requires a separate fleet-wide reachability and
retention GC. Unknown keys elsewhere in the shared bucket do not block
one community's deletion; malformed or unrecognized keys inside that
community's three owned prefixes still fail closed.

### External retained copies

The online logical-deletion proof does not erase object
versions/replicas, database backups/WAL, CDN copies, provider retention
copies, or observability exports. Those require their own retention and
purge controls.

### Member-only erasure

This PR erases a whole community. It does not implement the different
operation "erase one npub while preserving the community."

Removing membership or accepting NIP-09 is not member erasure. A
member-only workflow would need to find and selectively remove or redact
authored event content and pubkeys, profile data, DMs, reactions,
mentions, memberships/roles, tokens, workflows/subscriptions, upload
attribution, moderation/audit history, repository attribution, and
identity embedded in tags or JSON. It would also need explicit rules for
ownership transfer, surviving replies and thread metadata, audit-chain
integrity, immutable Git history, and shared-CAS reachability. That
requires a pubkey-level fence and selective graph rewrite; it is a
separate deletion product, not a safe extension of this whole-tenant
worker.

## In scope

- migration `0029_community_deletion.sql`: requests, approvals, leases,
manifest chunks, checkpoints, tombstones, and the universal write-fence
catalog
- durable executor leases, generations, heartbeats, retry/block state,
and resumable stage transitions
- operator-driven `sweep`, `submit`, `list`, `inspect`, `approve`,
`unblock`, `run`, and `drain` commands
- serving-path fences for database writes and external effects across
event ingest, media, Git, workflow, push, invites, mesh/tunnel, and
related paths
- target-prefix-only storage inventory, summary manifests, post-fence
destructive chunks, and bounded batch deletion
- exact community Redis namespace purge and two-pass absence
verification
- cross-community isolation, crash/resume, manifest-integrity,
writer-taxonomy, and schema/migration regressions
- desired-state `schema/schema.sql` support without requiring a SQLx
migration ledger

## Deferred / not covered

- dedicated Helm/chart worker Deployment, service account, secrets,
probes, resources, and network policy
- autonomous `buzz-admin deletions worker` poll loop and worker-only
health server
- least-privilege separation among migration, relay-serving, and
destructive execution roles
- fleet-wide shared-CAS physical GC
- backup/provider/CDN/observability retention completion
- member-only erasure
- provider-native conditional-delete improvements
- a general force-continue escape hatch; permanent safety failures
remain fail closed unless an operator remediates the cause and records
an audited `unblock`

The removed continuous-worker implementation remains deferred; no remote
follow-up branch is claimed by this PR.

## Validation

### Current PR head and repository state

Current pushed head: `359d8402ee15f049768f54156f67b953c7a7e2ed`, rebased
onto `cc9a2f783375e51a6e8d1f2f9d01d5f7e22813d1` (`origin/main` at push
time). The complete PR diff is now 47 files, 9,834 additions, and 517
deletions.

The bespoke source-scanner stack was removed to keep this PR scoped to
community deletion. Tyler/team requested the underlying fenced-write
safety behavior, not `ast-grep`,
`crates/buzz-db/tests/community_fenced_writes.rs`, its 27 fixtures, or
the new `scripts/lints/community_*.yml` rules. Those scanner-specific
files, dependencies, Hermit links, and runner wiring are absent from the
current tree. The production database write fence, startup/destructive
live-catalog validation, and deletion behavior remain.

Source validation on this exact SHA passed:

- `cargo fmt --all -- --check`
- `bash -n scripts/run-tests.sh`
- `cargo nextest run -p buzz-db --all-targets`: 102 passed, 173 skipped,
0 failed
- `cargo nextest run -p buzz-deletion --all-targets`: 10 passed, 9
skipped, 0 failed
- `cargo nextest run -p buzz-admin --all-targets`: 1 passed, 0 failed
- affected-package/all-target Clippy with warnings denied
- lockfile consistency
- Helm 3.16.4 lint and all 44 chart unit tests
- Helm region controls using that fixture: default
`BUZZ_S3_REGION=us-east-1`, explicit `eu-west-2` override, and
blank-region schema rejection

The prior Kubernetes battery below was run against
`928992237358a3294621ac0280830b77155abc04`. It remains useful evidence
for the patch-equivalent production deletion implementation, but it is
**not** claimed as exact-SHA evidence for current head
`359d8402ee15f049768f54156f67b953c7a7e2ed`; the current cleanup removes
only scanner/test/tooling infrastructure. CI restarted for the new head
after the rebase and is pending. Human review remains
`CHANGES_REQUESTED`.

### Prior-head live Kubernetes deletion and safety gates

The full program used one immutable image, real PostgreSQL, Redis,
MinIO, and a three-relay Kubernetes release:

- source: `928992237358a3294621ac0280830b77155abc04` (**prior head**)
- image: `buzz-e2e:sha-928992237358`
- immutable image digest:
`sha256:a1a204f4618ac22d9e210be5e5290645a15d79831ae30b0e44379357c8e4a895`
- evidence root:
`/tmp/buzz-e2e/20260807T033025Z-928992237358-full-gates/`
- evidence-manifest digest:
`82875c5bc9bea7370b796a7aef3457b3a1c8306c84c59e0f7388bbb5ad30e865`

Passed gates at that prior head:

- **Chart/operator region:** default `us-east-1`, explicit nondefault
propagation, blank-region schema rejection, live in-pod environment, and
an in-pod taxonomy sweep over 18 objects with zero unknown.
- **Fenced writers and lifecycle:** open-write/fence ordering;
100-attempt anti-starvation; invite, push matcher, and exhausted-reaper
bystander isolation; non-`READ-COMMITTED` rejection; manifest/tombstone
contracts; eight-failure stage block and audited `unblock`.
- **Destructive lifecycle:** submit → approve → run →
`retention_pending`; PostgreSQL tombstone and Redis/S3 verification
true; zero retries/errors; terminal reruns rejected with exit 5.
- **Fresh 10,001-object crash boundary:** exactly two chunks (10,000 +
1). The executor deleted chunk 0 from MinIO while its PostgreSQL stamp
was row-lock-blocked, was killed with `SIGKILL`, left one object and
both stamps absent, then resumed the same request under generation 2 to
zero objects and terminal state.
- **Independent dead-owner recovery:** a dedicated executor claimed
generation 1, blocked before effects, and was killed through containerd
with `SIGKILL` (no TERM cleanup). The request remained owned and
unreclaimable before lease expiry; a successor claimed generation 2
after 60 seconds and completed with two attempts and zero retries.
- **Three-pod socket isolation:** ordinary NIP-42 and joined
huddle-audio target witnesses on every replica received exact `1008 /
community deleted`; healthy-tenant witnesses on those pods remained
live; deleted-host reconnect returned HTTP 404.
- **Health/provenance:** all replicas independently returned ready and
retained the exact image digest before/after destructive runs and an
audio-enabled rolling restart; PostgreSQL, Redis, and MinIO were healthy
at close.

Instrument corrections were retained as evidence rather than counted as
product failures: a foreground PostgreSQL forward caused an initial
`PoolTimedOut`; Kubernetes pod deletion exercised graceful TERM rather
than dead-owner recovery; shell-background socket witnesses died with
their parent; and the first image build hit the corporate TLS proxy.
Detached forwarding/witnesses, containerd `SIGKILL`, and the configured
internal CA/Artifactory mirror produced the discriminating runs without
weakening product security.

### Prior-head cleanup

For the prior-head Kubernetes run, the Helm release was removed,
namespace absence was verified, run-owned Screen sessions were absent,
and that source worktree remained clean. The evidence manifest was
independently recomputed and every indexed artifact passed `shasum -a
256 -c`. The current `359d8402` source worktree is also clean after the
scanner-only cleanup and push.

---------

Signed-off-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz>
2026-08-12 09:25:58 -07:00
Will Pfleger
c966b862fe fix(deps): bump webbrowser to 1.2.4 for RUSTSEC-2026-0257 (#5659)
## What

Bumps `webbrowser` from `1.2.1` to `1.2.4` in both lockfiles
(`Cargo.lock` and `desktop/src-tauri/Cargo.lock`) to clear
[RUSTSEC-2026-0257](https://rustsec.org/advisories/RUSTSEC-2026-0257).

## Why

The advisory landed in the RustSec DB and flipped the `Security` job
(`cargo-deny check`) red on `main` — the same job passed on identical
lockfile state before the advisory was published. `webbrowser` 1.2.1
substitutes the URL into the Unix `BROWSER` env template *before*
tokenizing, allowing browser argument injection (e.g.
`--remote-debugging-port`). `crates/buzz-agent` calls
`webbrowser::open()` for the OAuth flow
(`crates/buzz-agent/src/auth.rs`) with an internally-constructed HTTPS
URL, so practical exploitability is low, but the gate is correctly
blocking. Fixed in `1.2.2`+.

## Scope

Lockfile-only. The `crates/buzz-agent/Cargo.toml` constraint is already
`webbrowser = "1"`, so no manifest change is needed. `webbrowser` 1.2.4
pulls in `objc2-app-kit` as a new transitive dependency; the
`windows-sys` edge churn re-unifies to versions already present in the
lockfile (no new `windows-sys` version is introduced).

## Verification

- `cargo-deny check` passes locally on the pinned toolchain (`advisories
ok, bans ok, licenses ok, sources ok`); RUSTSEC-2026-0257 no longer
reported in either lockfile.
- `cargo check -p buzz-agent` compiles clean against `webbrowser 1.2.4`.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-12 08:07:16 -07:00
Dave Grochowski
397796c5f3 feat(tracing): add PostgreSQL tracing spans (#3678)
## Why
Expose PostgreSQL datastore latency within existing request traces so
slow logical database operations can be identified without recording
tenant data or query arguments.

## What
- Add client spans around logical PostgreSQL operations across the
database facade, search, audit, replica fencing, and command persistence
- Use a dedicated `buzz_datastore` target and `db.system.name =
"postgresql"` for filtering and backend classification
- Exclude health-check database calls and scrub raw identifiers and
errors from newly traced paths

## Risk Assessment
Medium — this instruments frequently used datastore paths and increases
trace volume when enabled, but does not change SQL execution or
datastore behavior. Existing OpenTelemetry filtering controls export.

## References
- Pre-push clippy and fast unit-test hooks passed

Generated with Amp

---------

Signed-off-by: David Grochowski <dgrochowski@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-08-12 08:04:54 +10:00
Mic Neale
240cdd3ea1 chore: mesh upgrade, clean up legacy special case code, simplify model selection for mesh (#5289)
Shared compute now has exactly two model choices: MeshLLM's virtual
`mesh`
model, or a model you name. Buzz picks between them in one place, and
buzz-agent no longer knows meshes exist.

## What changed

- **MeshLLM v0.74.0 → v0.75.1.** v0.75.0 added
`degrade_to_single_model`, so a
  `model=mesh` request is answered by one served model when there is no
committee to form, instead of failing. v0.75.1 adds Mesh-LLM#1196, which
skips stale pre-0.75 runtime cache entries rather than aborting startup
on
them — without it, anyone who had run mesh on 0.73/0.74 could not start.
- **Deleted the client-side mesh catalog probe.** buzz-agent used to
poll
`/v1/models` (5s TTL, 30s cooldown, two-observation debounce) to decide
whether `mesh` was safe to send. MeshLLM now decides per request, so the
  polling, its hysteresis, and its 503 fallback are gone.
- **One mapping point.** `relay_mesh_wire_model()` turns the stored
value into
a wire name: `auto` becomes `mesh`, a named model passes through. The
spawn
env, the ACP harness, and the readiness probe all use it, so they cannot
disagree — previously `BUZZ_ACP_MODEL` and the probe both said `auto`, a
name
  the mesh does not advertise.
- **Removed the `nostr-relay-pool` advisory exception.** #5404 allowed
RUSTSEC-2026-0243 "after mesh-llm migrates to nostr-sdk >= 0.45".
v0.75.1
does, so the retired crate is gone from both lockfiles and the exception
  would only mask a future advisory for it.
- **Deleted `scripts/ensure-mesh-native-runtime.sh`** and its six
justfile call
sites. It built llama.cpp from source into the runtime cache; the app
already
  downloads the signed release runtime itself, and CI never called it.

## Why it is better

**−639 lines of Rust.** Availability is decided by the node that knows
the
answer, per request, instead of by a client cache that could be stale
for up to
30 seconds. A second worker joining now takes effect on the next request
rather
than after two confirming probes.

## Behaviour change

A 503 on an explicit `mesh` request takes the ordinary transport retry
under
the same model instead of failing over to a second one — there is no
second
model to fail over to now. MoA repairs partial committee results
internally
before it reaches that point.

## Validation

`crates/buzz-relay/examples/mesh_agent_e2e.rs` now sends `mesh` where it
previously sent `auto` or the physical model id, so no leg was covering
what
Buzz actually puts on the wire. 4/4 on gemma-4-E4B, gemma-4-26B-A4B, and
Qwen3-8B — including a real ACP tool call through `mesh` into
buzz-dev-mcp,
asserted by reading the written file back off disk.

Hand-tested in the desktop app on both gemma-4 sizes: picked Auto, agent
logged
`model_id=mesh`, replied in channel.

## Not covered

A committee that forms and then loses a worker returns 502, and that
needs two
workers to reproduce — not testable on one machine.

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-08-11 11:40:07 -04:00
Will Pfleger
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>
2026-08-10 10:47:37 -04:00
renovate[bot]
e1ff91ecc1 chore(deps): update rust crate anyhow to v1.0.104 (#4447)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) | dependencies |
patch | `1.0.103` → `1.0.104` |
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.103` → `1.0.104` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>dtolnay/anyhow (anyhow)</summary>

###
[`v1.0.104`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.104)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.103...1.0.104)

- Update `syn` dev-dependency to version 3

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4zLjIiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-09 09:09:50 -07:00
renovate[bot]
08de85c592 chore(deps): update rust crate arc-swap to v1.9.2 (#4448)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [arc-swap](https://redirect.github.com/vorner/arc-swap) | dependencies
| patch | `1.9.1` → `1.9.2` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>vorner/arc-swap (arc-swap)</summary>

###
[`v1.9.2`](https://redirect.github.com/vorner/arc-swap/blob/HEAD/CHANGELOG.md#192)

- Document RefCnt must not panic
([#&#8203;208](https://redirect.github.com/vorner/arc-swap/issues/208)).

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4zLjIiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-09 09:09:26 -07:00
renovate[bot]
12b1f56648 chore(deps): update rust crate async-trait to v0.1.91 (#4458)
This PR contains the following updates:

| Package | Type | Update | Change | Pending |
|---|---|---|---|---|
| [async-trait](https://redirect.github.com/dtolnay/async-trait) |
dependencies | patch | `0.1.89` → `0.1.91` | `0.1.92` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>dtolnay/async-trait (async-trait)</summary>

###
[`v0.1.91`](https://redirect.github.com/dtolnay/async-trait/compare/0.1.90...0.1.91)

[Compare
Source](https://redirect.github.com/dtolnay/async-trait/compare/0.1.90...0.1.91)

###
[`v0.1.90`](https://redirect.github.com/dtolnay/async-trait/releases/tag/0.1.90)

[Compare
Source](https://redirect.github.com/dtolnay/async-trait/compare/0.1.89...0.1.90)

- Update to syn 3

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4xMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-09 09:08:58 -07:00
renovate[bot]
d7cc724fa5 chore(deps): update rust crate diffy to v0.5.1 (#4466)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [diffy](https://redirect.github.com/bmwill/diffy) | dependencies |
patch | `0.5.0` → `0.5.1` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>bmwill/diffy (diffy)</summary>

###
[`v0.5.1`](https://redirect.github.com/bmwill/diffy/blob/HEAD/CHANGELOG.md#051---2026-07-18)

[Compare
Source](https://redirect.github.com/bmwill/diffy/compare/0.5.0...0.5.1)

##### Fixed

- [#&#8203;85](https://redirect.github.com/bmwill/diffy/pull/85)
  Merge conflict markers are now always placed on their own lines.
  Previously, a conflicting hunk at the end of a file without a trailing
  newline glued the next marker onto its last content line, producing
  unparseable output. This matches `git merge-file --diff3` behavior.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4zLjIiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-09 09:08:38 -07:00
renovate[bot]
7dd8791d07 chore(deps): update rust crate async-compression to v0.4.43 (#4456)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[async-compression](https://redirect.github.com/Nullus157/async-compression)
| dependencies | patch | `0.4.42` → `0.4.43` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>Nullus157/async-compression (async-compression)</summary>

###
[`v0.4.43`](https://redirect.github.com/Nullus157/async-compression/releases/tag/async-compression-v0.4.43)

[Compare
Source](https://redirect.github.com/Nullus157/async-compression/compare/async-compression-v0.4.42...async-compression-v0.4.43)

##### Other

- Fix hang when decoding a corrupt subsequent zstd frame
([#&#8203;470](https://redirect.github.com/Nullus157/async-compression/pull/470))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4xMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-09 09:08:08 -07:00
renovate[bot]
e668c6bb49 chore(deps): update rust crate clap to v4.6.6 (#4465)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) | dependencies |
patch | `4.6.1` → `4.6.6` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.6.6`](https://redirect.github.com/clap-rs/clap/compare/clap_complete-v4.6.5...clap_complete-v4.6.6)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.6.5...v4.6.6)

###
[`v4.6.5`](https://redirect.github.com/clap-rs/clap/compare/clap_complete-v4.6.4...clap_complete-v4.6.5)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.6.4...v4.6.5)

###
[`v4.6.4`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#464---2026-07-21)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.6.3...v4.6.4)

##### Internal

- Update to syn v3

###
[`v4.6.3`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#463---2026-07-20)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.6.2...v4.6.3)

##### Fixes

- *(derive)* Allow `"literal".function()` as attribute values

###
[`v4.6.2`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#462---2026-07-15)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.6.1...v4.6.2)

##### Fixes

- *(help)* Say `alias` when there is only one

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4xMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-09 09:07:46 -07:00
Will Pfleger
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`](78c87ae20e))
- feat(desktop): adding rich link previews to messages
([#3818](https://github.com/block/buzz/pull/3818))
([`1922d49cb2`](1922d49cb2))
- feat(relay): accept kind:30179 private managed-agent events at ingest
([#5133](https://github.com/block/buzz/pull/5133))
([`ad923353a2`](ad923353a2))
- fix(media): require authenticated reads
([#4610](https://github.com/block/buzz/pull/4610))
([`769ac70b74`](769ac70b74))
- feat(identity): recover desktop identity from a signed-in phone
([#4845](https://github.com/block/buzz/pull/4845))
([`6eb65919f1`](6eb65919f1))
- ci: prove the relay-driven mesh lifecycle — discover, join, infer,
deny — with real nodes
([#3862](https://github.com/block/buzz/pull/3862))
([`38bf642fcf`](38bf642fcf))
- relay: fuzz WebSocket 1012 restart-close timing on graceful drain
(BUZZ_DRAIN_JITTER_MS)
([#4542](https://github.com/block/buzz/pull/4542))
([`e14fff74d0`](e14fff74d0))
- fix(reactions): support max-length custom emoji
([#3833](https://github.com/block/buzz/pull/3833))
([`2ea9385015`](2ea9385015))
- fix(channels): restrict private-channel invitations
([#4612](https://github.com/block/buzz/pull/4612))
([`efe1893dd3`](efe1893dd3))
- fix(workflow): bind trigger author to the signed event
([#4607](https://github.com/block/buzz/pull/4607))
([`885bed35ee`](885bed35ee))
- fix(git): revoke access for banned relay members
([#4608](https://github.com/block/buzz/pull/4608))
([`997b8caaa4`](997b8caaa4))
- Define private managed agent wire protocol
([#4593](https://github.com/block/buzz/pull/4593))
([`067c085f37`](067c085f37))
- perf(relay): index channel-id lookups and skip trace-only reads
([#4647](https://github.com/block/buzz/pull/4647))
([`bc9e6528a7`](bc9e6528a7))
- Polish mobile inbox and media flows
([#4512](https://github.com/block/buzz/pull/4512))
([`feccf4eabc`](feccf4eabc))
- fix(git): allow deleting the default branch
([#4297](https://github.com/block/buzz/pull/4297))
([`fc598f5f8d`](fc598f5f8d))
- feat(projects): add buzz projects CLI commands (NIP-MP kind:30621)
([#4020](https://github.com/block/buzz/pull/4020))
([`b7bb15122e`](b7bb15122e))
- perf(relay): serve relay-membership checks from the read replica
([#4124](https://github.com/block/buzz/pull/4124))
([`ac4fa13b8e`](ac4fa13b8e))
- fix(relay): allow open relays to set their NIP-11 workspace icon
(kind:9033) ([#3998](https://github.com/block/buzz/pull/3998))
([`5765fc74b7`](5765fc74b7))
- feat(relay): accept kind:30621 multi-repo projects at ingest
([#3171](https://github.com/block/buzz/pull/3171))
([`cb9701cd30`](cb9701cd30))
- feat(relay): raise hosted community limit to five
([#3829](https://github.com/block/buzz/pull/3829))
([`10d5a26414`](10d5a26414))
- fix(relay): align NIP-11 max_limit with REQ ceiling
([#3635](https://github.com/block/buzz/pull/3635))
([`23f0c26b1c`](23f0c26b1c))
- feat(relay): gate kind 30178 team-catalog reads behind the shared tag
([#3358](https://github.com/block/buzz/pull/3358))
([`114d40d9d3`](114d40d9d3))
- fix(db): isolate usage metrics advisory-lock test on scratch DB
([#3670](https://github.com/block/buzz/pull/3670))
([`dba97eecd9`](dba97eecd9))
- perf(presence): reduce heartbeat frequency
([#3783](https://github.com/block/buzz/pull/3783))
([`bf139e8d0b`](bf139e8d0b))
- 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`](4933672eb4))
- feat(replica): portable heartbeat-token fence with snapshot-local
reader routing ([#3268](https://github.com/block/buzz/pull/3268))
([`63496cc1d4`](63496cc1d4))
- fix(git): channel binding tooling + author remediation for unbound
repos ([#3626](https://github.com/block/buzz/pull/3626))
([`788b3c002b`](788b3c002b))
- feat: configure S3 URL addressing style
([#3400](https://github.com/block/buzz/pull/3400))
([`7012d86d52`](7012d86d52))
- feat(tracing): correlate trace IDs in relay logs
([#3608](https://github.com/block/buzz/pull/3608))
([`005b5b819a`](005b5b819a))
- fix(relay): avoid subscription lock inversion
([#3413](https://github.com/block/buzz/pull/3413))
([`22be8bb351`](22be8bb351))
- feat(cli): add users set-status command for NIP-38 profile status
([#3253](https://github.com/block/buzz/pull/3253))
([`60158fce3e`](60158fce3e))
- feat(relay): make Postgres pool size configurable, default 50
([#3191](https://github.com/block/buzz/pull/3191))
([`2ce2d71cc3`](2ce2d71cc3))
- feat(tracing): add datastore tracing plumbing
([#2760](https://github.com/block/buzz/pull/2760))
([`e94b9aeda0`](e94b9aeda0))
- feat(invites): add use-limited invite links
([#3141](https://github.com/block/buzz/pull/3141))
([`d500c2d5cf`](d500c2d5cf))
- feat(admin): show reported message content in report detail
([#3149](https://github.com/block/buzz/pull/3149))
([`f069a85503`](f069a85503))
- resolve findings ([#3150](https://github.com/block/buzz/pull/3150))
([`9b0f744804`](9b0f744804))
- Revert "fix(cli,relay): resolve agents by verified owner"
([#3168](https://github.com/block/buzz/pull/3168))
([`a041e2d21e`](a041e2d21e))
- fix(cli,relay): resolve agents by verified owner
([#2615](https://github.com/block/buzz/pull/2615))
([`c3084b36d9`](c3084b36d9))
- fix(security): enforce durable community ban on NIP-43 relay-admin
kinds 9030-9033 ([#3128](https://github.com/block/buzz/pull/3128))
([`e2e0079101`](e2e0079101))
- fix(security): authorize kind:9000 role changes in both directions
([#3017](https://github.com/block/buzz/pull/3017))
([`00ecf2cac7`](00ecf2cac7))
- feat(desktop): handle project work from Inbox
([#3117](https://github.com/block/buzz/pull/3117))
([`c5c4f390b6`](c5c4f390b6))
- feat(relay): make per-owner community limit configurable via
BUZZ_MAX_COMMUNITIES_PER_OWNER
([#2599](https://github.com/block/buzz/pull/2599))
([`2a051a404d`](2a051a404d))
- feat(relay): add author-only-unless-shared read gate for kind 30175
([#2768](https://github.com/block/buzz/pull/2768))
([`ab3af82871`](ab3af82871))
- fix(core): block IPv6 transition SSRF targets
([#2801](https://github.com/block/buzz/pull/2801))
([`c26bf5945d`](c26bf5945d))
- fix(workflow): bypass system proxies for webhooks
([#2800](https://github.com/block/buzz/pull/2800))
([`60a171b19e`](60a171b19e))
- fix(audit): hash created_at at the precision Postgres stores
([#2638](https://github.com/block/buzz/pull/2638))
([`264a56a226`](264a56a226))
- feat(desktop): make pull request reviews actionable
([#2510](https://github.com/block/buzz/pull/2510))
([`9081ab0ec9`](9081ab0ec9))
- fix(relay): decompress gzip-encoded git smart-HTTP request bodies
([#2670](https://github.com/block/buzz/pull/2670))
([`5ca36e7b91`](5ca36e7b91))
- fix(sharing): preserve agent/team snapshot tEXt chunks through media
sanitization ([#2438](https://github.com/block/buzz/pull/2438))
([`b096b0a15a`](b096b0a15a))
- fix(relay): send 1012 restart close to all clients on graceful drain
([#2575](https://github.com/block/buzz/pull/2575))
([`1911c69aa2`](1911c69aa2))
- fix(media): sanitize animated image uploads
([#2524](https://github.com/block/buzz/pull/2524))
([`8f8f5fa5a4`](8f8f5fa5a4))
- fix(channels): strip leading hash prefixes from names
([#2250](https://github.com/block/buzz/pull/2250))
([`d0ab3fdb05`](d0ab3fdb05))
- feat(relay): make Redis pool size configurable, default 16
([#2521](https://github.com/block/buzz/pull/2521))
([`bcc3e13069`](bcc3e13069))
- 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`](61cc738ee8))
- feat(media): add S3-truth per-community storage sweep
([#2044](https://github.com/block/buzz/pull/2044))
([`bd37a4d584`](bd37a4d584))
- feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests
([#2206](https://github.com/block/buzz/pull/2206))
([`7e34bee62c`](7e34bee62c))
- Revert "feat(relay): inventory unreachable Git objects"
([#2275](https://github.com/block/buzz/pull/2275))
([`0fb820f9bf`](0fb820f9bf))
- feat(relay): inventory unreachable Git objects
([#2264](https://github.com/block/buzz/pull/2264))
([`3afc9dae15`](3afc9dae15))
- relay: add author_type label to buzz_events_stored_total
([#2243](https://github.com/block/buzz/pull/2243))
([`b9f54c43fe`](b9f54c43fe))
- fix(git): make project branch workflows reliable
([#2213](https://github.com/block/buzz/pull/2213))
([`166f27be4b`](166f27be4b))
- feat(cli): manage repository protection rules
([#2193](https://github.com/block/buzz/pull/2193))
([`f94324598d`](f94324598d))
- feat(cli): add agents archive/unarchive/archived subcommands
([#2173](https://github.com/block/buzz/pull/2173))
([`7d7992067b`](7d7992067b))
- fix(mobile): sanitize Android image uploads
([#2188](https://github.com/block/buzz/pull/2188))
([`ee21da90bd`](ee21da90bd))
- fix(cli): paginate channel directory queries
([#2181](https://github.com/block/buzz/pull/2181))
([`03fe19d603`](03fe19d603))
- fix(mobile): image upload fails due to unstripped metadata
([#2185](https://github.com/block/buzz/pull/2185))
([`37f15b2001`](37f15b2001))
- perf(relay): compact Git packs before manifest limits
([#2172](https://github.com/block/buzz/pull/2172))
([`80e0ab16b0`](80e0ab16b0))
- perf(relay): cache Git pack hydration
([#2169](https://github.com/block/buzz/pull/2169))
([`a4d82ec722`](a4d82ec722))
- fix(relay): bound and observe Git read operations
([#2167](https://github.com/block/buzz/pull/2167))
([`5f7c93d9c1`](5f7c93d9c1))
- relay: gate push enqueue on live leases; batch matcher pipeline
(T1b/T1a-repair/T2b) ([#2145](https://github.com/block/buzz/pull/2145))
([`e43b2d5aac`](e43b2d5aac))
- relay: add audit logging disable switch
([#2134](https://github.com/block/buzz/pull/2134))
([`bf5acabdde`](bf5acabdde))
- relay: skip TTL deadline bump for known-permanent channels (T1a
write-amp) ([#2125](https://github.com/block/buzz/pull/2125))
([`2e936d439c`](2e936d439c))
- fix(git): carry NIP-OA delegation in auth event
([#2120](https://github.com/block/buzz/pull/2120))
([`c12257d57a`](c12257d57a))
- Route lag-tolerant reads to an optional Postgres read replica
([#2084](https://github.com/block/buzz/pull/2084))
([`29c48883d3`](29c48883d3))
- fix: recover community access visibility
([#2074](https://github.com/block/buzz/pull/2074))
([`ca384d082d`](ca384d082d))
- feat: proxy feedback-scoped admin attachments
([#2059](https://github.com/block/buzz/pull/2059))
([`d7f918e3cb`](d7f918e3cb))
- feat: add read-only deployment moderation dashboard
([#1999](https://github.com/block/buzz/pull/1999))
([`68e670e001`](68e670e001))
- Bug-bash round 2: table scroll, Goose instructions, workflow mention
wake ([#2034](https://github.com/block/buzz/pull/2034))
([`64b8fea6dc`](64b8fea6dc))
- Strip media metadata on clients and reject it at the relay
([#2006](https://github.com/block/buzz/pull/2006))
([`5cfd69cb0c`](5cfd69cb0c))
- [codex] Hold Git concurrency permits through streaming (BUZZ-SEC-018)
([#1916](https://github.com/block/buzz/pull/1916))
([`7baea42abb`](7baea42abb))
- [codex] Enforce shared relay admission limits (BUZZ-SEC-019)
([#1917](https://github.com/block/buzz/pull/1917))
([`73fc0ec6cf`](73fc0ec6cf))
- [codex] Block banned actors from moderation commands (BUZZ-SEC-007)
([#1915](https://github.com/block/buzz/pull/1915))
([`caa195ca58`](caa195ca58))
- [codex] Fix relay WebSocket admission limits
([#1682](https://github.com/block/buzz/pull/1682))
([`d3ce971fc7`](d3ce971fc7))
- feat: add invite QR and mobile direct join
([#1957](https://github.com/block/buzz/pull/1957))
([`648cbf3610`](648cbf3610))
- fix(join-policy): require legal consent on hosted invites
([#1987](https://github.com/block/buzz/pull/1987))
([`2e1577f76f`](2e1577f76f))
- [codex] Prevent actor-tag UI impersonation
([#1931](https://github.com/block/buzz/pull/1931))
([`c540ec9678`](c540ec9678))
- Scope relay runtime state by community
([#1658](https://github.com/block/buzz/pull/1658))
([`d52dedb06f`](d52dedb06f))
- Apply optional relay join policy across join flows
([#1894](https://github.com/block/buzz/pull/1894))
([`6c2d667575`](6c2d667575))
- feat(media): require auth for relay media reads
([#1926](https://github.com/block/buzz/pull/1926))
([`f308762852`](f308762852))
- feat(relay): add community unarchive endpoint
([#1908](https://github.com/block/buzz/pull/1908))
([`6b9641db2b`](6b9641db2b))
- feat(relay): gate Git web GUI separately
([#1901](https://github.com/block/buzz/pull/1901))
([`34dc7dec75`](34dc7dec75))
- mesh: upgrade runtime, enforce membership, add shared compute provider
([#1656](https://github.com/block/buzz/pull/1656))
([`54638ff4bb`](54638ff4bb))
- Route Git scratch through configured volume
([#1884](https://github.com/block/buzz/pull/1884))
([`2318b3096c`](2318b3096c))
- feat(relay): gate usage metrics behind stable leader
([#1814](https://github.com/block/buzz/pull/1814))
([`59e9821503`](59e9821503))
- Relay mesh: cross-pod tunnel + huddle transport (buzz-relay-mesh)
([#1670](https://github.com/block/buzz/pull/1670))
([`ccb021d713`](ccb021d713))
- feat(push): deliver accepted relay events as wakes
([#1866](https://github.com/block/buzz/pull/1866))
([`bffbc5f22c`](bffbc5f22c))
- fix(db): resolve duplicate migration version
([#1863](https://github.com/block/buzz/pull/1863))
([`08ad38a07f`](08ad38a07f))
- Add private product feedback sidecar
([#1857](https://github.com/block/buzz/pull/1857))
([`af190c93e1`](af190c93e1))
- feat(relay): add durable community archival
([#1834](https://github.com/block/buzz/pull/1834))
([`2b15a72675`](2b15a72675))
- feat(push): add public APNs gateway
([#1770](https://github.com/block/buzz/pull/1770))
([`1c006822e4`](1c006822e4))
- feat(relay): add atomic community ownership transfer
([#1845](https://github.com/block/buzz/pull/1845))
([`52e42ccb9f`](52e42ccb9f))
- Bound NIP-RS retention and search indexing
([#1771](https://github.com/block/buzz/pull/1771))
([`1b4703021d`](1b4703021d))
- Add optional standalone pairing relay to Helm chart
([#1799](https://github.com/block/buzz/pull/1799))
([`9b47c8548f`](9b47c8548f))
- fix(relay): publish membership snapshot on provisioning
([#1761](https://github.com/block/buzz/pull/1761))
([`0950d392b7`](0950d392b7))
- feat(relay): per-community usage metrics
([#1723](https://github.com/block/buzz/pull/1723))
([`620822899a`](620822899a))
- refactor(desktop): remove vestigial MCP toolsets config
([#1776](https://github.com/block/buzz/pull/1776))
([`dfec75b3c0`](dfec75b3c0))

**To release:** merge this PR. The tag and build will happen
automatically.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 12:26:24 -04:00
Kalvin C
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>
2026-08-07 08:42:01 -07:00
Michael Neale
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>
2026-08-05 23:13:16 -04:00
Tyler
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>
2026-08-02 16:17:55 -04:00
Tyler
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 @ 28ae6cd21) as ONE PR: the
provider
binary, the desktop changes that make it work, the harness inactivity
reaper,
the Sprig image, and the conformance/live-test suites.

Channel: buzz-remote-agents (29414326-dba7-402d-b384-b1b34d63a2e6),
thread c42b70ef.

## What's here (by lane)
- **crates/buzz-backend-kubernetes** (Dawn): stdin/stdout JSON provider,
info +
  deploy; pure classify.rs (one match arm per spec state-machine row);
reconcile/GC with ownership-marker gate + same-clock orphan check;
per-attempt
immutable Secrets; three-tier env with clear-then-write authoritative
tier.
- **Desktop** (Mari): KD3 launch block from resolved descriptor, KD5
pre-secret
negotiation gate (resolve-once → stage-and-digest → info → protocol gate
→
deploy), KD1 Windows extension strip, bundling (externalBin + Justfile +
release/canary workflows + stub loops), tauri.windows.conf.json platform
  override (Decision B: no Windows artifact).
- **buzz-acp** (Max): KD4 BUZZ_ACP_EXIT_AFTER_INACTIVITY reaper
(pool-independent;
reset only at accepted dispatch; in-flight turn/heartbeat defers, never
resets);
BUZZ_ACP_EXIT_AFTER_INACTIVITY + BUZZ_ACP_NO_PRESENCE reserved. KD8 fix.
- **Image + tests** (Perci): Dockerfile.sprig (digest-pinned bases, exec
buzz-acp
PID 1, relay-scoped credential config), image contract script, provider
conformance suites (golden wire fixtures shared with desktop tests),
live-local
  runbook (namespace-scoped, shared-cluster safe).
- **Docs** (Sami, first commit): citation re-pin c1bca1b56 → 28ae6cd21
(44/49
were already byte-exact; 3 offsets fixed) + I3 presence-bound correction
(below).

## Named spec deviations (deliberate, each with rationale)
1. **No baked default image yet.** ghcr.io/block/buzz-sprig is
unpublished
(verified: anonymous pull 403 vs control 200). Omitted `image` returns
an
   in-band field-required error instead of a default.
2. **Image override STRICTER than spec §Image:** digest-only
(`name@sha256:<64hex>`); ALL tags rejected; `name:tag@digest`
normalized.
With no baked default the override is the only path, so tag-acceptance
would
make mutability the v1 norm. Strictness is reversible; a moved tag under
an
nsec is not. Baked digest default + tag re-acceptance = follow-up with
image
   publish.
3. **imagePullSecrets not in schema (v1).** Explicit user images may
rely on
namespace-preprovisioned pull credentials — the substrate boundary.
Field
added only if the publish decision proves it necessary. 9-field budget
intact.
4. **Decision A closed: writable empty workspace.** Nest projection =
named
   follow-up; no image-side scaffolding.
5. **Decision D overridden by Tyler (event b55398d8):** provider ships
bundled
with the desktop like buzz-acp/buzz-agent; spec §Distribution's separate
   release workflow deleted for v1.
6. **I3/vision presence bound corrected 90s → 180s.** PRESENCE_TTL_SECS
moved in
#3783 during this spec's base→merge window; the number was inherited,
not
chosen. Spec :206/:216/:928 + inline quote + VISION_REMOTE_AGENTS.md:59
corrected. ← Tyler: the vision is your document; this edit is flagged
for
   your explicit eyes.
7. **Spec citations are pinned to 28ae6cd21** (main at spec merge) and
resolve
   there, not at this PR's head — this PR's own lanes move
crates/buzz-acp/src/lib.rs by ~100 lines (19 citations across
KD4/KD6/KD7/
§Stop/§Launch data). Known Defects rows fixed BY this PR retire on
merge;
   the section documents main as of the pin.
8. **KD7 grace tension declared:** pod terminationGracePeriodSeconds=60
vs
KD7's measured ~87s shutdown tail at parallelism 10 (~197s at cap 32).
   KD7 is ruled out of scope, so L1-3's "enough grace for full graceful
shutdown" is NOT met at default config — deliberate, resolved by the KD7
   follow-up, not silently.

## Question for Tyler
Will ghcr.io/block/buzz-sprig publish PUBLIC? If private-by-policy,
§Image needs
an imagePullSecrets story before the baked-default follow-up can land.

## Out of scope (named follow-ups)
KD6 exit-code contract + KD7 shutdown budget (gate OnFailure), OnFailure
restart
policy, Windows provider binary, PVCs/nest projection, mesh
deployability,
sprig image publish workflow + baked multi-arch digest default.

## Reproduce locally (four traps that cost us real time)

**1. Git hooks inherit the invoking shell's PATH — pin the shell, not
just your
verification commands.** `rust-toolchain.toml` pins `1.95.0`, but the
rustup shim
that honors that pin lives in `~/.cargo/bin`. If Homebrew's cargo is
earlier on
PATH, `cargo` in this repo is 1.89.0, which cannot build the workspace
at all:

```
$ /opt/homebrew/bin/cargo check -p buzz-db
error: rustc 1.89.0 is not supported by the following packages:
  sqlx@0.9.0 requires rustc 1.94.0
  ...                                                    # exit 101
```

Verifying with `PATH="$HOME/.cargo/bin:$PATH" cargo test` does *not*
protect the
push: lefthook's `pre-push` → `just test-unit` re-resolves `cargo` from
the
shell's own PATH, so a green local run is followed by a hook failure on
a crate
you never touched. Export the PATH for the whole shell, not per-command.
This
bit twice.

**2. Line-scope your mutations, or the mutation edits its own
detector.** When
mutation-testing the respond-to guard, a whole-file `sed` on the mode
literal
touches 5 sites — the guard *and* the fixtures/assertions that test it.
The
mutation and its detector move together and the suite stays green, which
reads
as "this code is dead" when it actually means "you deleted the
experiment":

```
# WRONG — 5 sites, guard and tests mutate together
$ sed -i '' 's/"allowlist"/"allowlist-DISABLED"/g' src/env.rs
test result: ok. 145 passed; 0 failed          # false survivor

# RIGHT — 1 site, anchored to the guard's own definition line
$ sed -i '' '/^const RESPOND_TO_ALLOWLIST/s/"allowlist"/"allowlist-DISABLED"/' src/env.rs
failures:
    env::tests::allowlist_mode_with_an_empty_list_is_refused
    env::tests::an_allowlist_entry_that_is_not_64_hex_is_refused
test result: FAILED. 143 passed; 2 failed      # real kill
```

Restore by copying a pristine file back and confirming `git diff --stat`
is
empty, not by re-running an inverse `sed`.

**3. A completeness guard is not a correctness guard.** The shared wire
fixture
`tests/fixtures/provider-wire/deploy-full-launch.request.json` passed
every test
we had while containing four classes of invented data (wrong
`respond_to`
encoding, an env key no emitter writes, allowlist entries that fail the
harness's own 64-hex rule, a `launch.env` key from no descriptor layer).
The
provider's tests could not have caught this: its types are deliberately
indifferent to these values (`Option<String>`, `Vec<String>`, arbitrary
map), so
"the provider parses it" was never evidence that the desktop emits it.
The fix
was not a stronger provider assertion but a rule about provenance —
"recorded"
means executed-and-transcribed, and the desktop's whole-object equality
test is
the only enforcement that can exist. See the fixture README.

**4. Every drift this arc was a value that agreed with itself.** Five
invented
values were found, and not one was caught by an assertion failing — each
was
caught by someone asking where a value came from. A named constant
referenced
symbolically on both the fixture and assertion side. A `sed` that
mutated its
own detector. Six probe rows that all died at the same unrelated error.
A
descriptor struct literal compared against a fixture built from that
literal
(`launch.args: ["run","--session"]`, which the resolver actually returns
as
`["acp"]`). The general defense is not more assertions but provenance: a
stub is
a control that varies nothing, and the more faithful it looks the better
it
hides. Ask what executed, not what passed.

*Fixture-test determinism caveat (post-verification, Quinn + Dawn).* The
desktop's whole-object fixture test calls the real resolver, which
consults a
process-global harness registry whose own docs require
`registry_test_lock`
for any test touching it. The fixture test holds no lock and is
nonetheless
deterministic — but by containment, not by ordering. Measured, not
derived:
planting a definition with `id: "goose"` directly into the registry
(bypassing
the loader) changes the resolved descriptor from `args: ["acp"]` to
`args: ["--poisoned"]`, so `resolve_effective_harness_descriptor`
**does**
reach the registry for this id — it does not short-circuit on the
builtin
table first. Two controls discriminate: an empty registry and a registry
poisoned under a *different* id both return `["acp"]`. What actually
protects
the test is that the registry has exactly one writer
(`update_loaded_harness_registry`, reached only via
`warm_harness_registry_from_dir`) — but that writer concatenates **two**
sources of unequal strength (`custom_harnesses.rs:319-326`). Custom
files
pass through `load_custom_harnesses`, whose `check_id_collision` rejects
the
reserved builtin id `goose` case-insensitively at the loader — and that
leg
is tested (`load_applies_id_collision_check` writes a real `goose.json`
and
asserts the loader drops it). Preset definitions
(`preset_harness_definitions`, `presets.rs:177-193`) are a bare `.map`
over
`PRESET_HARNESSES` with **no collision check** — exhaustive call-site
enumeration at `60007fda4` finds four production `check_id_collision`
sites,
none on the preset path. That leg holds only because `goose` is not in
the
preset table today (intersection of TIER1 and preset ids is empty) —
executed, not just read: adding a preset with `id: "goose"`,
`args: ["--poisoned"]` and warming via the normal preset-only path
(`warm_harness_registry_from_dir(None)`, no custom dir, no direct
writer)
flips the fixture's emitted `launch.args` from `["acp"]` to
`["--poisoned"]`
at `60007fda4`, command/env/policy_env unchanged. So: no test in the
suite
can put a `goose` entry in the registry
via the custom path, and no preset currently carries one, so no
interleaving
can perturb this fixture — containment with one checked leg and one
coincidental one. A future fixture built on a **non-builtin** runtime id
has
no containment at all — it would be order-dependent against whatever
registry-writing test ran last and must take the lock.

*Late instance, found while reviewing the mode guard.* The guard
exact-matches
`respond_to` untrimmed and case-sensitively, which is only correct if
clap's
`ValueEnum` derive is case-sensitive. `config.rs` gives two answers: the
derive
at `:448-453` carries no `ignore_case`, while the crate's own tests call
`RespondTo::from_str(s, true)` — `ignore_case = true`. Reading the
source
supports either. Measured on the built binary instead: `owner-only`
starts,
`OWNER-ONLY` / `Owner-Only` / `ALLOWLIST` / `NOBODY` all exit rc=2
`invalid
value`. Case-sensitive at the CLI, so the guard is right — and right for
a
reason the source does not state. The `from_str(_, true)` tests exercise
a
different surface and are not evidence about the CLI.

*Corollary, and the sharper half.* When a test helper **reimplements**
production instead of calling it, the helper is a fork — and a fork can
be
right while production is wrong, or wrong in the same way, and the suite
reports green either way. Both `BUZZ_ACP_ALLOWED_*` gates are forked
like this:
production compares **strings** while the helpers compare **post-parse
enums**
(`config.rs:2623`) or re-derive the split
(`buzz-cli/.../channels.rs:1296`).
Production and the helper each carry their *own* copy of the empty-entry
filter
(`:1025` and `:1300`), so fixing one says nothing about the other.
Measured on
`buzz-cli`, restoring byte-exact between runs:

| tree | result |
|---|---|
| baseline | 274 passed |
| drop the empty-filter in **production** only (the real fix) | **274
passed** — no signal |
| drop it in the **test helper** only | **273 passed, 1 failed**
(`channels.rs:1338`) |

Two independent defects, stacked, and worse together than either alone:
production can be fixed with no test ever noticing, *and* the helper
cannot be
corrected without a false alarm demanding the bug back. The root cause
is one
bit of type information — `check_allowed_channel_add_policy(allowed_raw:
&str,
..)` cannot represent "unset", while production reads `env::var(..) ->
Result`,
where unset and `""` are different states. A helper whose parameter type
can't
represent all of production's input states isn't testing production's
states —
it's testing a subset it silently chose. Same family as the
struct-literal
descriptor and the fixture drift: the test and the thing it tests
agreeing with
each other, rather than the test measuring the thing. Neither defect is
in this
PR's diff (`git diff --name-only 28ae6cd21 <head> -- crates/buzz-cli` is
empty); both are now filed as NIP-34 issues on this repo: the fail-open
+
fork-helper defect at issue event `0524a4113f2d97fd…` and the respond-to
self-lock at `e32837498969b5e7…` (filed 2026-08-02 after Quinn measured
that
no prior filing existed — zero hits on GitHub `block/buzz` open *or*
closed
and zero on the relay's kind:1621 issues, against working positive
controls). The prescription was itself
mutation-tested before being written down: repairing the fork's
signature
(`Option<&str>` + assertion → `None`) still let the reintroduced
production
bug ship 274-green — an expressive fork is still a fork; it never
executes
production. So the `buzz-cli` fix has **three parts and one explicit
keep**:
drop the production filter; **delete** the helper and point its tests at
the
real `cmd_set_add_policy` (which self-discriminates by error variant —
`Usage` = refused, `Network(BadScheme)` = passed the gate — no relay
needed);
serialize the env-var tests behind one
**`tokio::sync::Mutex::const_new`**
lock taken with `.lock().await`, including the pre-existing `:1362`
integration test (the fork was silently buying test isolation — without
the
lock, parallel runs flake nondeterministically; a `std::sync::Mutex`
held
across `.await` trips `clippy::await_holding_lock` under `-D warnings`);
and
**keep** the then-dead `!allowed.is_empty()` clause with a comment
saying
why. It is unreachable-false (`split(',')` never yields an empty vec),
but it
is the only thing that keeps the reintroduced production bug detectable
—
mutation-tested: on a tree that deletes the clause, reintroducing the
empty-filter bug survives 275/0, because `""`/`","`/`" "` refuse either
way
and the filter goes semantically inert. Dead code can be load-bearing
for
tests: "provably unreachable" is an argument about behavior, never about
coverage. When a helper forks production, the fix has to delete the
fork:
any change that leaves two implementations standing can only ever be
verified against the one the tests call. *Final shape:* the keep and the
broad lock are both artifacts of the fork surviving in some form. The
extraction variant (Dawn, mutation-tested at `60007fda4`) removes the
tension: extract one `check_channel_add_policy_allowed(Option<&str>,
&str)`
that **production calls**, with the `Option` placed at the env boundary
where the `Result<String, VarError>` bit actually lives. 5/6 mutants
killed; the empty-filter survivor is proven **equivalent** (exhaustive
6174-pair check, 0 divergences, with a diverging negative control;
independently re-derived by a second generator — different tokens and
shape — 0 divergences on admitted policies, 500 on a non-admitted
control),
not a coverage hole — on a one-implementation tree there is no fork left
to
witness, so no dead clause needs keeping. One scope line on that
equivalence: it is **caller-conditional**, a property of the only
current
caller, not of the gate function — `cmd_set_add_policy`'s own match at
`:1027-1034` admits only three policies before the gate runs; a second
caller reaching the gate with arbitrary strings resurrects m1 as a real
hole. The lock does not disappear, it
narrows (Dawn's own correction, caught by Mari): lock exactly the tests
that mutate the process env — three-plus-one on a fork tree, two on the
extraction tree — behind one `tokio::sync::Mutex`, and the lock is part
of
the assertion, not hygiene: with it deleted, the gate test fails 8/8
runs
deterministically by receiving `Network(BadScheme)` where it expects
`Usage` — the unset test's `remove_var` clobbers the other's `set_var`,
and
**the gate test passes straight through the gate**, a false negative on
the
exact authz assertion the test exists to make. State it as an outcome:
these two tests must not observe each other's env writes. 276/0 stable
across 5 parallel runs, clippy `-D warnings` clean; independently
verified
(patch applied to a second worktree: result blob `d67e584be` matches the
patch index, full mutant matrix reproduces row for row). One new row no
earlier
prescription covered: collapsing unset into `Some("")` fails **closed**
—
an unconfigured deployment refuses every policy — killed by the unset
test.
Patch: `OUTBOX/BUZZ_CLI_ADD_POLICY_GATE_EXTRACT_FIX.patch`. The filed
issue
(`0524a411…`) carries the fork-shape prescription; whoever picks it up
should prefer the extraction shape, drop the dead-clause keep with it,
and
keep part 3 outcome-shaped: serialize whichever tests mutate the env.

## Verification (final HEAD `60007fda4`)
- Full touched-package suites at each integration merge (log in plan
file).
  At candidate parent `00e5b5fe9`: buzz-backend-kubernetes 154,
buzz-acp 673, desktop tauri 2100+3, pnpm 3908, workspace clippy/fmt/tsc
  all clean. The only delta to `60007fda4` is one character in
  `scripts/test-k8s-sprig-image-live.sh` (heredoc escape so the readlink
probe evaluates pod-side, not host-side at render); `crates/` tree hash
  is byte-identical at both SHAs, so the Rust receipts attach by tree
  identity. buzz-backend-kubernetes suite re-run in-shell at
  `HEAD == 60007fda4`: 154 passed.
- Adversarial one-HEAD gate (Sami): guard matrix 12/12, predicate
mutants
  7/7, doomed-invocation finding closed end-to-end; tree-hash carry to
  `60007fda4` confirmed (crates/buzz-backend-kubernetes blob unchanged).
- Live-local pass per TESTING.md + skill-buzz-testing (Perci, at
`60007fda4`): explicit `docker-desktop` context, digest-qualified image
  imported into node containerd `k8s.io` namespace, pull policy `Never`;
  pod printed `DIGEST_ABI_OK`, `resolved_spec` and `image_id` both the
  exact requested digest, script exit 0. Dedicated per-run namespace,
  ownership labels on every object, scoped cleanup verified empty after.
- Implementation review (Wren) at `60007fda4`: 9.6 minimalness /
  9.4 elegance / 9.3 correctness, no blocker.
- `origin/eva/k8s-backend` == `60007fda4` (ls-remote verified; SHA
  identity is byte identity).

---------

Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
2026-08-02 14:39:27 -04:00
Will Pfleger
9d6726e5b3 chore(deps): bump nostr-relay-pool for RUSTSEC-2026-0224 (#4139)
Bump `nostr-relay-pool` from 0.44.1 to 0.44.2 to clear
[RUSTSEC-2026-0224](https://rustsec.org/advisories/RUSTSEC-2026-0224),
which addresses verification-cache poisoning that could let forged Nostr
events bypass signature validation on redelivery.

The dependency is transitive through `nostr-sdk`; this PR updates only
the corresponding package version and checksum in `Cargo.lock`. The
advisory currently marks every open PR red until this fix merges.

- `cargo test -p buzz-sdk -p buzz-cli` passes: 271 + 241 tests
- `cargo deny check advisories` passes
- `just fmt-check` passes

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
2026-08-01 17:04:05 +00:00
John Matthew Tennant
c104eecfb3 feat(desktop): import local Pocket voices (#3259)
## Context

Pocket TTS currently offers bundled reference voices. People also need a
local, private way to add a voice without sending audio to a cloud
service.

## Summary

Add a Pocket voice import flow to Voice settings. Buzz opens the native
file picker, decodes common audio formats in the reusable `buzz-voice`
crate, canonicalizes the selected audio, stores it under a
content-derived identity in app data, selects it, and lets the user
delete it later.

## Changes

- Accept WAV, M4A, MP3, FLAC, OGG, and AIFF files between 2 and 30
seconds, including multichannel sources.
- Decode and downmix accepted audio to canonical mono 32 kHz PCM16 WAV
before hashing and storage.
- Store imported voices behind stable `pocket:imported:<sha256>`
identities and content-addressed files.
- Keep absolute file paths inside the native process and expose only
voice metadata to React.
- Include imported voices in Pocket preview and live huddle playback.
- Add Add voice and delete controls while preserving the bundled Pocket
voice catalog.
- Fall back to Mary when the selected imported voice is deleted.
- Keep durable import, selection, and deletion successful when a live
TTS worker acknowledgement is delayed.
- Preserve bundled voices when optional import metadata is unreadable
and keep failed deletion retryable.

## Related issue

None found.

## Testing

Production decoding was exercised with WAV, M4A with AAC, MP3, FLAC, OGG
Vorbis, and AIFF fixtures. Each format canonicalized to mono 32 kHz
PCM16 WAV. Manual validation in the combined daily-driver build covered
native-picker import, Preview, live-huddle playback, deletion, and Mary
fallback.

## Screenshots

The Voice settings card preserves the bundled Pocket catalog and adds
the local Add voice action.

![Pocket TTS voice
import](https://raw.githubusercontent.com/block/buzz/c03ba29060ca544c5ac3394c212f376651b386a3/pr-3259--pocket-voices.png)

## Reviewer-reproducible examples

Create common-format fixtures and run them through the production
importer:

```bash
. ./bin/activate-hermit
fixtures="$(mktemp -d)"
ffmpeg -hide_banner -loglevel error -f lavfi -i "sine=frequency=220:duration=3" -ac 2 -ar 44100 "$fixtures/voice.wav"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a aac "$fixtures/voice.m4a"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" "$fixtures/voice.mp3"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" "$fixtures/voice.flac"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a libvorbis "$fixtures/voice.ogg"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a pcm_s16be "$fixtures/voice.aiff"
BUZZ_VOICE_IMPORT_TEST_DIR="$fixtures" \
  cargo test -p buzz-voice imports_common_audio_format_fixtures -- --ignored --nocapture
```

Exercise import persistence, synthesis, deletion, and bundled-voice
fallback with an installed Pocket model:

```bash
BUZZ_POCKET_MODEL_DIR=/path/to/pocket-model-bundle \
  cargo test -p buzz-voice --test pocket_import_audio \
  objective_import_synthesis_delete_and_mary_fallback \
  -- --ignored --nocapture
```

Exercise the native-picker boundary, selection, preview dispatch,
deletion, cancellation, and invalid-file states:

```bash
cd desktop
pnpm build:e2e
pnpm exec playwright test tests/e2e/voice-settings.spec.ts --project=smoke
```

---------

Signed-off-by: John Tennant <jtennant@block.xyz>
Signed-off-by: John Tennant <johnmatthewtennant@gmail.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: John Tennant <jtennant@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
2026-07-31 11:15:09 -04:00
Tyler
61ba9dfaa0 refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910)
Relands **#2467** (extract `buzz-voice` crate) and **#3208** (Pocket
voice settings) onto main, after #3266 and #3180 merged.

## Why a fresh PR
The repo is squash-only with delete-branch-on-merge. Squashing #3266
deleted `jtennant/pocket-tts-2026-04`, which was #2467's base — GitHub
auto-closed #2467 and it cannot be reopened. Squash merges also sever
ancestry, so GitHub's natural merge-base reports phantom conflicts for
the whole remaining stack.

## Content provenance
- Byte-identical to the blessed `jt/buzz-voice-refactor` branch
(`93029c577`, tree `6729e0eff` — reviewed by Dawn (#2467) and Max
(#3208) at exact heads) **except** the three files where #3180 and #3208
genuinely interact.
- Three-file resolution (union of both sides):
- `huddle/mod.rs` — #3180's pipeline re-exports + #3208's
`agent_tts_routing` imports.
- `huddle/state.rs` — `reset_preserving_generation` preserves both
`huddle_generation` (#3180) and `tts_enabled` (#3208); test sets merged
into one `tests` module.
- `desktop/src/testing/e2eBridge.ts` — both switch arms kept; no
duplicate case labels.

## Verification at cf32dacd7
- `cargo test` (desktop/src-tauri, pinned 1.95.0): **2047 + 3 pass / 0
fail** (14 ignored: 8 keychain, 4 real_relay, 2 flag-gated)
- `cargo clippy --all-targets -- -D warnings`: clean; `cargo fmt
--check`: clean
- `cargo check --workspace` (root, includes new `buzz-voice` member):
clean; `cargo test -p buzz-voice`: 5/0
- `pnpm test`: **3885 / 0**; `tsc --noEmit`: clean; lint: clean

The 3180×3208 interaction resolution is getting an independent team
re-review before merge.

Buzz channel: buzz-desktop-voice `fd5fb402-b651-4238-89b1-bb3e2fa4dc96`,
thread `b4798ecc`.

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-07-31 09:41:09 -04:00
Tyler
4933672eb4 feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
## Summary

This is **part 1 of 2** split out from #3467 (per Tyler's request),
carrying only the mesh-scoped changes. The agent/ACP response-behavior
changes and the new `send_message` tool stay in #3467 as part 2. All
commits are @michaelneale's work, cherry-picked with authorship
preserved.

- Upgrade embedded Mesh to v0.74.0 (tag-pinned instead of commit rev)
and use canonical Gemma model IDs.
- Keep shared compute serving through member joins, roster changes, app
recovery, and community switching.
- Wait for actual model readiness and avoid resuming incomplete
downloads after quit.
- Leave `BUZZ_AGENT_THINKING_EFFORT` unset by default so each model's
chat template picks its own thinking default (`none` suppressed Gemma
tool-calling entirely; pinning `low` made Qwen3 burn ~4x output budget).
Explicit agent/persona/global values still win.

## Relationship to #3467

Contains the mesh commits from #3467 (`2cd640b23`, `0ad81c341`,
`ad13ed841`) rebased onto current main, with one deliberate exclusion:
the `crates/buzz-agent/src/llm.rs` reasoning→text parser change from
`2cd640b23` is **not** here. That change unconditionally affects every
OpenAI-compat/Responses provider, so it belongs with the reply-behavior
work in part 2, where it can be reviewed as what it is.

Not included (remaining in #3467 / part 2):
- typed `send_message` tool in dev-mcp + `BUZZ_ACP_SEND_MESSAGE_TOOL`
gating
- plain-reply delivery fallback in buzz-acp
(`BUZZ_ACP_DELIVER_PLAIN_REPLIES`)
- the mesh_agent_e2e P5/P6 rewrite (exists to prove the reply path)
- the two `env.insert` preset opt-ins in `relay_mesh.rs` for the flags
above
- the llm.rs parser change

This PR is independently mergeable; part 2's flags are all off by
default so it can land before or after.

## Testing

- `cargo test -p buzz-relay --locked` — 780 passed (one telemetry test
is order-sensitive under parallel default settings; passes in the
pre-push suite and standalone, unrelated to this diff — files untouched
here).
- `just desktop-tauri-test` (default features) — 1877 passed.
- `cargo test --locked --features mesh-llm` in `desktop/src-tauri` —
1961 passed, including the new relay-mesh preset and
coordinator/recovery tests.
- Both `Cargo.lock`s resolve with `--locked` against the v0.74.0 tag.
- Full pre-push hook suite green (rust-tests, desktop-check/test, tauri
checks).

Live validation of the mesh v0.74 upgrade itself is documented on #3467
(two-Mac cross-version test).

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-30 11:37:27 -04:00
Tyler
63496cc1d4 feat(replica): portable heartbeat-token fence with snapshot-local reader routing (#3268)
## Summary

Expands read-replica usage on the relay per the Rev 2 design (thread
39d1e174 in #buzz-read-only-replica-usage): replaces the
Aurora-incompatible WAL-LSN read-side fence observation with a portable
**heartbeat token**, makes the freshness proof **snapshot-local to the
serving reader session**, and adds a default-off bounded-staleness gate
for head fetches.

### Probe (writer side, unchanged ordering)
The ordered writer scan is kept verbatim — `S = clock_timestamp()` →
masked-visibility `pg_stat_activity` oldest-xact scan — and now ends by
committing a heartbeat token **last** on the same pinned connection
(single-row `UPDATE replica_heartbeat ... RETURNING token, epoch`,
migration `0026`). The single-row update serializes all pods' probes, so
tokens are globally commit-ordered; the three-bucket completeness
argument carries over unchanged. Ring retains `(token, committed_at,
fence_wall)`; epoch change resets the ring; a same-epoch token
regression (restore adversary) clears the ring **and rotates the epoch
on the writer** so pre-rewind readers fail the epoch check. Cadence 1s.

### Routing (snapshot-local proof)
Every routed read opens `BEGIN ISOLATION LEVEL REPEATABLE READ, READ
ONLY` on a reader session and observes the heartbeat as the
transaction's **first statement** — the proof's snapshot is exactly the
snapshot the page, participants batch, and bridge aux closure read from
(`ReadSession` carries the open transaction; drop = rollback).
Fail-closed everywhere: begin failure, missing heartbeat row, epoch
mismatch, token below the ring, over-budget entry → writer.

- **Predicate B (cursor pages, default-on):** same completeness math as
the existing fence — cursor timestamp must be ≤ the proved wall; thread
candidate-terminal and above-wall pages re-run on the writer.
- **Predicate A (head fetches, default-OFF):** gated by
`BUZZ_REPLICA_HEAD_MAX_AGE_SECS` (0 = off, clamped to fence staleness).
Bounded-stale head semantics are an explicit product decision — **do not
enable anywhere without Tyler's backdated-event-semantics acceptance.**

### Observability
`buzz_db_route_decision{path, decision, reason}` across all five paths,
`buzz_db_replica_heartbeat_age_seconds` gauge, and per-decision debug
logs carrying the proved token plus backend identity: `addr:port pid=N`,
prefixed with `aurora_db_instance_identifier()` when the endpoint
supports it (probed once per process on an autocommit checkout; SQLSTATE
42883 caches a definitive false; identity is evidence, never a routing
gate).

## Review & verification

- **Wren:** full review 9/9/9 at `5f81b10e5`; identity delta re-review
approved at exact head `fedb46368` (90/90 unit, clippy `-D warnings`,
fmt independently reproduced).
- **Max:** local E2E **PASS** at `5f81b10e5` — isolated PG17 writer +
two streaming standbys behind HAProxy; paused-reader legs split exactly
as designed (6 replica/fresh + 6 writer/stale), recovery clean, RR
snapshot hardening observed in runtime logs. Evidence:
`WORK_LOGS/2026-07-28_REPLICA_HEARTBEAT_REPLACEMENT_SHA_E2E.md`.
- **My gates at head (same shell):** buzz-db 90 unit + 143
Postgres-gated green (scratch DBs); buzz-relay 767/768 — lone red is the
pre-existing `mesh_demo` flake, red on base; clippy `-D warnings` + fmt
clean.
- Key regression tests:
`routed_request_holds_one_snapshot_across_page_and_aux`
(mutation-verified both directions), same-epoch rotation,
capability-probe negative, head-gate truth table, divergent-fixture
routing suite.

## Post-merge plan

Merge publishes the immutable `sha-*` main image → bb-public PR pins it
→ ArgoCD sync → Max runs the production cursor-routing canary on Aurora
(positive identity branch proven live). Head gate stays off. Client
`since`-widening ships separately.

---

## Update — full read routing (Rev 6, commit `9fa3c9c0b`)

Extends routing to the remaining read seams per
`PLANS/REPLICA_FULL_READ_ROUTING_DESIGN.md` Rev 6 (thread cf5deba4 in
#buzz-read-only-replica-usage).

### New routed seams — ALL deploy-default dark
The new seams (`query_events_routed`, `query_events_routed_bounded`,
`count_events_routed`, `get_events_by_ids_routed`,
`query_feed_{mentions,needs_action,activity}_routed`) are
**Bounded-only** and gated on `BUZZ_REPLICA_READ_MAX_AGE_MS`: unset ⇒
every new seam records `writer/disabled` and merging is a no-op. COUNT
and feed/by-ids never take the covered arm (deletion visibility: covered
bounds insert-completeness only). **Note:** the pre-existing cursor
paths (channel windows, thread pages) are *not* gated by this env var —
they route at B=0 today and that status quo is unchanged.

### Reader pool (D4/D5)
- Lazy pool (`connect_lazy`, `min_connections(0)`): reader-down at boot
can't crash the relay; a warn-only boot ping is the only boot-time
visibility, and it primes the Aurora identity capability cache. Priming
is an optimization only — the routed path itself spends a single acquire
budget regardless (see the single-checkout fix below), because a boot
ping that *fails* is correlated with exactly the reader-unavailable case
the budget bound exists for.
- `READER_ACQUIRE_TIMEOUT` = 150ms; a miss fails closed to the writer
with reason **`reader_acquire_timeout`** — named for the mechanism, not
a diagnosis. The budget includes cold connects and sqlx's `size` counts
in-flight dials, so this metric alone does not distinguish contention
from slow connection establishment (see `proved_reader` doc-comment for
the runbook guidance; the pool gauges are 10s samples and are for
capacity planning).
- `BUZZ_DB_READ_POOL_SIZE` sizes the reader independently (invalid/0
inherits writer sizing); `read_pool_stats().max` reports the reader's
own ceiling.

### Community isolation (formal-model question)
Isolation is structural — an explicit `community_id = $n` predicate
compiled into every query builder; no RLS, no session-GUC tenant state
(zero `CREATE POLICY` across migrations). The `_on` variants reuse the
exact same builders with only the executor swapped. Proven by a
seven-seam two-community divergent-fixture test
(`routed_reads_are_confined_to_the_requested_community`),
mutation-tested by Dawn: all four single-predicate stubs killed; the
mention-join feeds are defended in depth (three independent predicates)
so only complete removal leaks there.

### Accepted limitation
D6: client-side staleness on bounded reads (up to `_MS`) is accepted
product behavior when the gate is enabled; gate stays off at merge.

## Update — single acquire budget per routed read (commit `dd26caa9f`)

Max found (and Dawn independently reproduced, 302–330ms measured) that
the boot-unavailable cold path spent **two** stacked
`READER_ACQUIRE_TIMEOUT` budgets: the Aurora capability probe did its
own `pool.acquire()` before `begin_with` acquired again. Fixed by
acquiring **once** per routed read — the capability probe runs on the
held connection (`reader_aurora_capability_on`) and the read-only
`REPEATABLE READ` transaction begins on that same connection
(`Transaction::begin` accepts a `PoolConnection` at `'static` via sqlx's
`MaybePoolConnection`). Reason codes unchanged; capability still never
negatively cached. Measured routed fallback: ~150ms (one budget).

Ships with a PG-gated regression fixture
(`routed_fallback_spends_one_acquire_budget_when_aurora_cache_is_cold`,
authored by Dawn): size-1 reader saturated, capability cache asserted
cold, routes through `count_events_routed`, asserts writer answer +
one-budget elapsed + `writer/reader_acquire_timeout` label
(mutation-tested: `pool_busy` and `reader_validation_error` mutants both
killed). It fails at 330ms on the previous commit and passes on this
one.

### Verification at `dd26caa9f` (pinned toolchain rustc 1.95.0 via repo
`bin/`; ambient rustc 1.89 fails sqlx resolution — always prepend
`bin/`)
- buzz-db: 94 unit + **150/151** PG-gated serial (`--test-threads=1`;
suite is not parallel-safe against one Postgres). The 1 red is
`test_usage_metrics_lock_has_single_owner_and_releases_on_drop` — a
**pre-existing** same-key-same-database advisory-lock collision with any
live relay pointed at the shared `buzz` DB (the relay re-arms
`USAGE_METRICS_LOCK_KEY` on a 300s tick). It passes on a private scratch
DB with the relay still running — verified this run. Remedy is a scratch
`TEST_DATABASE_URL`, **not** terminating lock holders: inspect
`pg_locks`/`pg_stat_activity` and resolve the owning process first; an
idle advisory holder may be a live dev relay. Pre-existing, not a #3268
blocker — filed as #3619.
- buzz-relay: 773/773; clippy `-D warnings` + fmt clean. (`mesh_demo`
echo test is a known pre-existing Redis-dependent flake — Dawn confirmed
it fails identically on the base commit, untouched by this PR.)
- Dawn: independent re-verification at `dd26caa9f` — byte-identical diff
application confirmed, suite reproduced, and fixture proven to still
discriminate (fails at 320ms with only the production fix reverted).
Gate clear. Max: independent rig pass at `dd26caa9f` — 94/94 unit,
**15/15 routing/fallback matrix** (cursor behavior, default-dark seams,
seven-seam confinement, one-snapshot, dead-reader/no-URL fallback,
hard-delete fail-closed, reader max), 773/773 relay, private-DB lock
control. Gate clear; recommends merge + staged bb-block rollout
(no-reader-URL no-op → cursor-only → observe `buzz_db_route_decision` →
conservative nonzero `BUZZ_REPLICA_READ_MAX_AGE_MS`; rollback is
config-only).

## Known gap — CI does not run the PG-backed fixtures (#3622)

Dawn found post-sign-off (confirmed by Max and Eva at `dd26caa9f`):
**every Postgres-backed fixture in this PR is `#[ignore]`d and no CI job
selects it.** `Unit Tests` runs `cargo nextest run -p buzz-db --lib`
(`Justfile:279-285`, ignored tests excluded); the only `--run-ignored
ignored-only` invocations (`ci.yml:690`, `:702`) filter to
`relay_invite::tests`. So the one-budget regression, the seven-seam
isolation fixture, and the fence/floor-guard/fallback tests have zero
automated execution — the verification record above is exhaustive but
manual, at this exact SHA.

Not introduced by this PR (the `#[ignore]` + narrow-filter pattern
predates it; 34 ignored tests total) and not a merge blocker: all new
seams are dark until `BUZZ_REPLICA_READ_MAX_AGE_MS` is set. But it
changes the rollout gate — **do not set a nonzero
`BUZZ_REPLICA_READ_MAX_AGE_MS` in bb-block until CI enforces these
fixtures.** Filed as #3622 (explicit CI selection against the existing
backend-integration Postgres archive; ordering note: #3619 must land
first or be excluded, since widening the filter would select the
colliding usage-metrics lock test).
## Live redteam results (Max, real Helm + PG17 streaming replication at
`dd26caa9f`) — #3643, #3644

Max deployed this exact SHA via the repo chart against a physical
writer/standby pair and attacked it. **The PR's fenced routing passed
every fault**: healthy baseline routes proven on the real standby;
paused WAL replay moved head/cursor reads to `writer/stale`; B=0 moved
routed reads to `writer/disabled`; reader-URL removal rolled back
cleanly.

The redteam also found a **pre-existing** production risk this PR does
not create and does not fix: NIP-50 search builds its own eager,
unfenced pool straight from `READ_DATABASE_URL` (`main.rs:389-402`,
introduced by #2084, present in the deployed bb-block `sha-dd222a5` and
bb-public `sha-22be8bb` images, and both production ESOs already
template `READ_DATABASE_URL`). Measured: ~30s search stall on reader
loss, acknowledged-but-unsearchable writes under replica lag (unaffected
by `BUZZ_REPLICA_READ_MAX_AGE_MS`), fatal eager connect blocking pod
startup when the reader is down, and `replica=true` FTS even at B=0.
Filed as **#3643** (fix: pin FTS to the writer, or fence it like the
routed seams) with **#3644** for the writer-pointing-reader-URL
telemetry gap. Aurora's cluster-ro writer-fallback DNS softens the
outage modes in prod but not the lag mode.

**Revised ladder consequence:** step 2 ("add `READ_DATABASE_URL`, budget
unset") is NOT a no-op with today's binary — it hands FTS an unfenced
replica pool. Rollout order is now: merge (still a true no-op —
bb-block/bb-public already run the unfenced-FTS code and this PR only
adds dark seams) → fix #3643 → CI enforcement #3622 → then reader URL +
budget per the original ladder, re-running Max's three deployment tests
(reader-down latency, reader-down cold boot, paused-replay
read-your-own-write search).
## Activation gate (consolidated) — merge is clear; ALL of the below
precede any nonzero `BUZZ_REPLICA_READ_MAX_AGE_MS`

Six independent verification passes at `dd26caa9f` (Dawn ×2 incl.
byte-level + defang check, Max matrix + live Helm redteam, Wren full
CRUD regression + greenfield 0026 + 151/151 PG, Eva). Every induced
fault was either handled correctly by this PR's machinery or traced to
pre-existing main code. Remaining work gates **activation, not merge**:

1. **#3643** — unfenced NIP-50 FTS pool (pre-existing #2084; live in
prod today; also explains the blackhole search hang Wren observed — the
routed path was measured bounded at 151-152ms against a silent endpoint,
sqlx `inner.rs:252-255`).
2. **#3651** — reader `statement_timeout`: statements after acquire are
unbounded; mid-transaction blackhole reproduced hanging ≥15s (Dawn).
Small fix via `.after_connect` on the reader pool.
3. **#3622** — CI enforcement of the PG fixtures (with #3619 ordering).
4. **Recovery-conflict cancellation live test** — standby cancels a
routed read mid-snapshot → complete writer page, never partial (#3651's
fix also bounds the cancellation-never-arrives shape).
5. **DDL replication lag test** — migration on writer + paused replay +
budget on → fallback, not client-visible SQL errors.
6. **Reader-tx soak** — 30-60min mixed load; no idle-in-tx accumulation
on the standby.

Then Max's three deployment tests (reader-down latency, reader-down cold
boot, paused-replay read-your-own-write search) re-run on the fixed
binary before the first nonzero budget on bb-block.

---------

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-29 20:35:15 -04:00
Kalvin C
7012d86d52 feat: configure S3 URL addressing style (#3400)
## Summary

- add one strict `BUZZ_S3_ADDRESSING_STYLE=path|virtual` setting shared
by media and Git/CAS storage
- preserve path-style defaults for bundled Compose/Helm MinIO while
supporting Railway's virtual-hosted bucket contract
- fail startup on invalid or non-Unicode values before dependency
connection, and validate the Helm value with the same two choices
- document operator mappings and why endpoint and bucket remain separate
for routing and SigV4 signing

## Best-practice rationale

AWS documents both URL forms and favors virtual-hosted addressing for
S3, while compatibility endpoints such as the bundled MinIO deployment
can require path style. `rust-s3` defaults to virtual/subdomain
addressing and provides `with_path_style()` for the explicit
compatibility case.

Some providers buckets only support as virtual-hosted bucket styles.
This PR therefore uses one explicit, provider-neutral switch rather than
endpoint heuristics or fallback behavior, while retaining `path` as
Buzz's backward-compatible default.

Sources:
-
https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html
- https://docs.rs/rust-s3/0.37.0/s3/bucket/struct.Bucket.html
- https://docs.railway.com/storage-buckets#url-style
-
https://github.com/minio/minio/blob/master/docs/config/README.md#domain

## Validation

- `cargo fmt --all`
- `cargo check --workspace --all-targets`
- targeted `buzz-media` and `buzz-relay` parsing/client-construction
tests for defaults, strict errors, and both URL styles
- Helm unittest: 45/45 passed
- Compose config/render validation passed
- local MinIO path-mode relay startup passed the Git A3 conformance
probe and became ready
- unreachable object storage failed startup and readiness never opened
- push hooks completed the broader Rust and desktop suites successfully

---------

Signed-off-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
2026-07-29 17:00:41 -07:00
Kalvin C
d500c2d5cf feat(invites): add use-limited invite links (#3141)
## Summary

- add database-backed v2 invite links with optional maximum-use limits
and atomic final-slot redemption
- preserve v1 invite compatibility while adding
exhausted/expired/invalid client handling across desktop, web, and
mobile
- emit structured claim-outcome logs with community, invite ID, outcome,
maximum uses, and post-claim count

## Verification

- `cargo fmt --all -- --check`
- `cargo test -p buzz-db` (85 passed, 134 Postgres-dependent ignored)
- `cargo clippy -p buzz-db --all-targets -- -D warnings`
- desktop `npm run typecheck`
- push hook: desktop checks/tests, desktop Tauri tests, Rust tests, and
branch-skew passed
- Postgres integration tests were previously reviewed green at the
pre-rebase tree; local rerun on this session was unavailable because
Postgres/Docker were not running
- mobile push-hook check could not start because Flutter is unavailable
locally

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1c4alndp82zyt9veaklm5d965quss79vlhk9awv7qu5erwhmf42qqlvc25c <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-07-27 15:19:39 -07:00
Tyler
9b0f744804 resolve findings (#3150)
Fixes all six HIGH findings from the buzz security report, one commit
per finding. Independently reviewed to approval by Max at `0158ae542`,
plus a deep isolated live pass (clean-room compose stack, weird ports,
full product matrix) at the same head — see the buzz-security thread for
evidence. `fe65c07c3` merges current `origin/main` on top (new commit,
no rebase), inheriting the nostr 0.44.6 bump (#3135) and relay-admin ban
gate (#3128).

## Findings and fixes

| Finding | Commit | Fix |
|---|---|---|
| 003 — quinn-proto RUSTSEC-2026-0185 | `e5dcdec72` | Bump quinn-proto
0.11.14 → 0.11.16 (lockfile-only) |
| 002/004 — linkify-it quadratic-parse DoS (GHSA-22p9-wv53-3rq4,
GHSA-v245-v573-v5vm) | `923b3c20f` | pnpm override `linkify-it: ^5.0.2`;
`pnpm why` confirms a single 5.0.2 copy |
| 001 — media reads served unauthenticated by default | `0f277e3e2` |
Helm `requireMediaGetAuth` defaults to `true` + rendered-chart test
pinning the default |
| 006 — removed workflow owners retain webhook-exfiltration authority |
`4749bd56c` | Fail-closed per-fire authority gate (current owner/admin
membership) on **all four** trigger doors (on_event, scheduler
pre-claim, manual trigger, webhook — masked as generic 404), save-time
gate for `call_webhook` defs, durable disable-on-removal wired to kinds
9001 + 9022 |
| 005 — git Smart-HTTP reads ignore channel membership | `e648f2dba` +
`0158ae542` | `authorize_git_read`: caller's **current active
membership** in the repo's bound channel, checked before any
hydration/subprocess on all three read doors (`info_refs` for both
services + `upload_pack` POST). Uniform generic 404 denials (no
membership probing), no repo-owner bypass, first-`buzz-channel`-tag
binding semantics fail closed on ambiguous duplicates (mutation-verified
test). Resolution follows the live kind:30617 announcement, so
deleted/replaced announcements deny immediately. The committed
`e2e-git-perms.sh` guest scenario previously asserted the vulnerability
— now asserts denial. |

## Behavior changes to be aware of

1. **Unbound repos fail closed for git reads.** `buzz repos create`
emits no `buzz-channel` tag, so CLI-created repos without a binding are
unreadable via git HTTP. Correct per finding 005's fail-closed posture;
a follow-up could bind CLI-created repos at creation time.
2. **006 is conservative:** a workflow disabled on owner removal does
not auto-re-enable if the owner is re-added — explicit re-enable
required.
3. Merge conflict resolution in `fe65c07c3`: kept main's
`@radix-ui/react-dismissable-layer` 1.1.19 bump alongside the linkify-it
security override (`pnpm-workspace.yaml` + lockfile).

## Verification at the merge head `fe65c07c3` (same shell)

- buzz-relay `--lib`: 761 passed / 1 failed — the lone red is the known
pre-existing `mesh_demo::demo_join_forwarded_arm_round_trips_echo` 504
flake, present on main
- SEC-005 module incl. PG behavioral matrix: 8/8 (removed-member,
never-member, owner-no-bypass, deleted-30617, malformed/ambiguous
binding, owner-mismatch all denied)
- buzz-workflow 153/0, buzz-db 84/0; `clippy --all-targets -D warnings`
+ `fmt --check` clean
- Desktop JS 3637/3637, tsc clean, biome clean,
file-size/px-text/pubkey-truncation gates clean
- `helm lint` + `helm unittest` (40/40) on `deploy/charts/buzz`
- All five pre-push hooks green (desktop-check, desktop-test,
rust-tests, desktop-tauri-test, branch-skew)

Prior review evidence at `0158ae542` (pre-merge): Max's independent
exact-head approval + clean-room live regression pass
(`WORK_LOGS/2026-07-27_SECURITY_HIGH_LIVE_TEST.md` in his workspace).
Max will re-run the deep local pass at this post-merge head before
merge.

---------

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-27 14:18:24 -04:00
Tyler
31e2de1966 fix(deps): bump nostr to 0.44.6 for RUSTSEC-2026-0216 (NIP-44 remote DoS) (#3135)
## Summary

`cargo-deny` started failing on **every** PR and on `main` when
**RUSTSEC-2026-0216** was published mid-afternoon today. Nothing in the
tree changed — cargo-deny fetches the advisory DB at run time, so main's
own `Security` job passed at `00ecf2c` and then began failing on the
same commit.

```
error[vulnerability]: Remote Denial of Service via malformed NIP-44 v2 payload
  Cargo.lock:432  nostr 0.44.3  —  RUSTSEC-2026-0216
advisories FAILED, bans ok, licenses ok, sources ok
```

The `nostr` NIP-44 v2 decrypt path reads a 2-byte unpadded-length prefix
via `buffer[0..2]` **after** the HMAC check passes, without verifying
the decrypted buffer holds 2 bytes. A sender who holds the conversation
key — i.e. any DM sender — can craft a payload that decrypts to 0 or 1
bytes and panic the receiver. Remote DoS through any relay that delivers
the event. No key material, plaintext, or memory corruption.

Affects `0.26.0` through `0.44.4`. Fixed in `0.44.5`.

## The change

Lockfiles only, 6 insertions / 6 deletions. The manifest already
declares `nostr = "0.44"` — a caret range — so `0.44.6` needs no
`Cargo.toml` edit.

| Lockfile | Before | After |
|---|---|---|
| `Cargo.lock` | 0.44.3 | 0.44.6 |
| `desktop/src-tauri/Cargo.lock` | **0.44.4** | 0.44.6 |

**The desktop lockfile is the part worth reviewing.**
`desktop/src-tauri` is excluded from the root workspace
(`Cargo.toml:31`), and the `Security` job runs `cargo-deny check` at the
repo root — so it never sees that lockfile. It was pinning a vulnerable
*and* yanked `0.44.4` that no CI check would ever have flagged. Desktop
calls `nip44::decrypt` at `commands/identity.rs:495`. Credit to @Eva for
catching this; I'd have shipped the root-only fix and left it sitting
there.

**This isn't optional maintenance.** `0.44.0` through `0.44.4` are all
yanked on crates.io. `0.44.5` and `0.44.6` are the only live versions in
our range — staying put isn't an available option.

### On the two extra lines in the desktop lockfile

The desktop bump also repoints two existing dependency edges:

```
nostr-derive: syn 2.0.118 -> syn 1.0.109
tempfile:     getrandom 0.4.3 -> getrandom 0.3.4
```

I checked these rather than waving them through: **no packages are added
or removed** — both versions were already present in the graph, so only
which edge points where changed. The resolution is stable across
repeated re-resolves, and a plain re-resolve without the bump produces
zero diff, so this isn't pre-existing lockfile staleness leaking in.

## Verification

At this commit, in a clean worktree off `origin/main`:

- `cargo-deny check advisories` → **`advisories ok`**, exit 0. The same
tree before the bump reported `advisories FAILED` with this advisory, so
the check is doing real work, not passing vacuously.
- `./scripts/run-tests.sh unit` → all five packages pass.
- `cargo test -p buzz-core` 229/229, `-p buzz-cli` 250/250, `-p
buzz-relay --lib` 750 pass / 1 fail — the sole failure
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` is
pre-existing and reproduces identically at unmodified `00ecf2c`.
- `desktop-tauri-test` passed in the pre-push hook, which exercises the
crate whose lockfile changed.

## Why not a `deny.toml` ignore

Considered and rejected. This is a reachable panic triggerable by any DM
sender, and buzz-acp agents decrypt DMs from arbitrary senders.
Suppressing it would ship a live remote-DoS to every agent and client in
order to make a dashboard green.

## Note on `spin`

The yanked `spin 0.9.8` / `0.10.0` warnings in the same job are **not**
what fails CI — the log has exactly one hard error, this one. They're
`warning[yanked]`, and warnings don't fail the build. `spin` is also
three levels transitive (`mesh-llm-host-runtime → mdns-sd → flume →
spin`) under a dev-dependency, so it isn't ours to bump. Left alone
deliberately.

## Follow-up

Unblocks #3128 (relay-admin ban gate), which has a zero dependency-file
delta and will inherit this cleanly once main is merged in.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-27 12:04:23 -04:00