Commit Graph

2564 Commits

Author SHA1 Message Date
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
klopez4212
ac4aa94b05 Hide download action on voice notes (#7182)
## Summary

- hide the native Download action on voice-note cards
- preserve Download for ordinary relay-hosted audio attachments
- add focused E2E coverage for both behaviors

Follow-up to #6978.

## Testing

- `pnpm -C desktop exec biome check
src/features/messages/ui/AudioMessageAttachment.tsx
tests/e2e/voice-note.spec.ts`
- `pnpm -C desktop typecheck`
- `pnpm -C desktop build:e2e`
- `pnpm -C desktop exec playwright test tests/e2e/voice-note.spec.ts
--project=smoke` (13/13)
- pre-push desktop suite (5,881/5,881)

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-09-01 17:58:45 +01:00
Luke Tornquist
bd73490418 ci: run PostgreSQL tests in isolated lane (#6730)
## Why

PR #6660 introduced four PostgreSQL-backed persistence tests and a
focused CI selector, but an exact test-name list does not automatically
cover future database tests. Broad ignored-test execution also exposed
shared-schema races and ambiguity between desired-state and
migration-applied schema expectations.

## What

- Establish a discoverable convention: PostgreSQL unit modules use
`postgres_tests`, PostgreSQL integration binaries use a `postgres_`
prefix, and unrelated external-infrastructure tests use an
`external_infra_` prefix.
- Add a dedicated nextest PostgreSQL profile and archive-backed CI job
covering the relevant crates without enumerating test names.
- Create a run-scoped desired-state source database and a unique
PostgreSQL database per test process for parallel-safe isolation, with
cleanup on success, failure, or interruption.
- Route destructive migration and migration-parity tests to clean
`template0` databases while desired-state tests clone the desired-state
database.
- Document discovery, schema modes, required role privileges, and a
bounded portable local runner.
- Keep the existing infrastructure-free unit-test jobs unchanged.

This is intentionally limited to test and CI harness behavior. It does
not move production database code, change persistence semantics, or
implement issue #20's broader shared test-utility refactor.

## Risk

Low production risk because all changes are confined to tests, CI
configuration, documentation, and test harness scripts.

Remaining operational risks:

- Cleanup retries `dropdb --force` five times across roughly four
seconds. Exhaustion warns with the database name but deliberately does
not mask the test result; individual PostgreSQL diagnostics are
suppressed.
- The seven-package boundary is duplicated between the runner and CI
archive and must remain synchronized if PostgreSQL tests move to a new
crate.
- Portable SHA fallback branches were exercised on Linux; no macOS Blox
workstation was needed for this Linux CI artifact.
- GitHub's unchanged generic Unit Tests job currently fails while
cold-linking `buzz-voice` because `sherpa-onnx-c-api` is absent. The
branch changes no voice/build/toolchain inputs; the exact parent
previously passed that job, and Blox passes the same infrastructure-free
suite (1,450/1,450). A prior-head one-job retry and the final-head run
both reproduced the hosted-runner failure.

## Verification

Author Blox workstation `2020088`:

- `cargo fmt --all -- --check`
- `just clippy`
- `just test-unit`: 1,450 passed; PostgreSQL tests remain skipped in
fast jobs
- PR #6660 focused tests: 4/4 passed through the final runner
- Full lane: 282/282 passed in 10.087s with a non-superuser role limited
to `CREATEDB`, `CREATEROLE`, and `pg_read_all_stats`
  - 272 desired-state tests
  - 10 migration-applied tests
  - 6 explicitly filtered external-infrastructure tests
- Cleanup fault injection: a deliberately underprivileged 282-test run
produced the expected 277 passes and 5 failures, and the post-run
catalog audit found zero lane databases after retry cleanup. The
restored successful run also left zero lane databases.

GitHub exact final head `92c231e48f299ea2af23817763f5ae80ac013d68`:

- [PostgreSQL
Tests](https://github.com/block/buzz/actions/runs/32789030203/job/97630827823):
282/282 passed across 10 binaries in 98.886s; 1,240 skipped, including 6
via the profile filter
- The shared relay/PostgreSQL archive predecessor completed successfully
- [Unit
Tests](https://github.com/block/buzz/actions/runs/32789030203/job/97626930420):
unrelated `sherpa-onnx-c-api` native-link failure described above
- Independent exact-head review on separate Blox workstation `2022762`:
no substantive findings after all initial findings were addressed. The
reviewer independently verified cleanup retry behavior, exit-status
preservation, shell portability, syntax, and a clean exact-head
worktree.

## References

- Stacked on #6660 at exact head
`561de54be4d9c2b622b7c2aa5b61bc3068f47e2a`
- https://github.com/TheSentinel454/buzz/issues/20
-
https://github.com/TheSentinel454/buzz/issues/20#issuecomment-5398526792

Generated with Codex.

## Update — August 24, 2026 review follow-up

- Centralized the repeated PostgreSQL test URL resolution in
crate-local, test-only helpers without introducing the broader shared
utility refactor from issue #20.
- Restored descriptive hybrid/Redis test function names. Structural
`external_infra_*_tests` modules now own exclusion, and the nextest
filter only recognizes module path segments.
- Added a three-second source guard that scans every Rust file and fails
CI when an ignored PostgreSQL test would be omitted or an
external-infrastructure test would be included. Fixture tests cover
accepted modules/binaries and both failure modes.
- Removed the two implementation-plan documents.

Performance profiling on Blox workstation `2027352` (same prebuilt
archive and cargo-nextest 0.9.143 for every comparison):

- Current per-test database model, 8 workers: 10.15–10.40s across three
runs. The 283 `createdb` calls and 284 `dropdb` calls consumed
28.85–29.46 aggregate seconds, about 39–40% of aggregate test-process
duration.
- Reusing one database per worker without cleanup: 6.55–6.84s, about 35%
faster, but all three runs failed because global matcher-queue tests
inherited a quiesced community from an earlier test; the failing test
varied with scheduling.
- Reusing workers with `TRUNCATE … RESTART IDENTITY CASCADE`: all 282
tests passed, but total time regressed to 10.47–10.67s. The 265 truncate
calls consumed 24.34–25.45 aggregate seconds, so truncation merely
replaced most clone/drop cost.
- Raising concurrency to 16 workers reduced one isolated-database run to
8.79s, but 24 workers exposed a cluster-global `pg_stat_activity` race.
The profile remains at the proven-safe 8 workers.

Decision: retain unique per-test databases. Harness cleanup uses `dropdb
--if-exists --force`; it drops the database rather than truncating
tables or deleting rows.

Verification at local branch head
`60086c1f6ee34a193c342255a5e8e6293b60e988`:

- Discovery inventory: exactly 282 intended tests; all 6 hybrid/Redis
tests present and structurally excluded.
- PR #6660 focused persistence tests: 4/4 passed.
- Full PostgreSQL lane: 282/282 passed three times; 1,240 skipped each
run; wall time 9.72–9.96s without profiling shims.
- Non-ignored tests with a desired-state database: 1,234/1,234 passed;
one mesh-demo timeout from the first run passed immediately in
isolation.
- `cargo fmt --all -- --check` and all-target/all-feature clippy for
`buzz-db`, `buzz-deletion`, and `buzz-relay` passed.

Final independent re-review at exact head
`42097c0136ab3fa8f0efe5720ae829c04575b9b6` on separate Blox workstation
`2028455`: no substantive residual findings. The reviewer independently
exercised ordinary strings, zero/one/three-hash raw strings, line/block
comments, and string-contained lookalikes, then reran the full 353-file
scan, shell syntax, Python AST parsing, and diff check.

Independent review found that the source guard's initial regular
expression could miss raw-string ignore reasons and treat a commented
attribute as real. Final head `42097c0136ab3fa8f0efe5720ae829c04575b9b6`
parses valid ordinary/raw Rust string literals only at attributes found
in comment-sanitized source; regression fixtures cover both cases. The
fixture suite, Python compilation, shell syntax, diff check, and full
353-file repository scan pass on Blox.

## Update — August 25, 2026 restack

PR #6660 merged, so this follow-up was rebased from its former exact
parent onto current `origin/main`
(`8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8`). The final candidate head
is `05dcc2ab28948e3ab79bb44839a69f2ba44648a2`; the PR no longer carries
#6660's pre-squash history.

Fresh exact-head verification on Blox workstation `2028572`:

- Discovery guard passed across 353 Rust files; inventory remained
exactly 282 intended PostgreSQL tests.
- PR #6660 focused persistence tests: 4/4 passed.
- Full PostgreSQL lane: 282/282 passed three times; 1,243 skipped each
run; wall time 9.70–9.87s.
- `cargo fmt --all -- --check` and all-target/all-feature clippy for
`buzz-db`, `buzz-deletion`, and `buzz-relay` passed.

## Update — August 25, 2026 final hosted verification

- [GitHub PostgreSQL
Tests](https://github.com/block/buzz/actions/runs/32801679226/job/97667835080):
282/282 passed in 53.787s; 1,243 skipped.
- The final guard scanned all 353 Rust files successfully before the
hosted lane ran.
- The shared relay/archive prerequisite and infrastructure-free Unit
Tests job both passed at exact final head
`42097c0136ab3fa8f0efe5720ae829c04575b9b6`.

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Signed-off-by: tornquist <tornquist@squareup.com>
2026-09-01 12:07:28 -04:00
klopez4212
114dbf745f Add voice notes to desktop messages (#6978)
## Summary

- add voice-note recording, preview, removal, and send controls to the
desktop composer
- render sent voice notes as waveform cards with scrubbing and
playback-speed controls
- transcode recordings locally into the existing relay video-media
contract, without relay changes

## Testing

- `just ci`
- `pnpm exec playwright test tests/e2e/voice-note.spec.ts`
- native voice-note media contract test against the relay validator

## Screenshots

Focused snapshots are attached below.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
2026-09-01 16:36:37 +01:00
Salman Mohammed
59328d5ae3 feat(desktop): add thread-scoped ACP session experiment (#6909)
## Why

Thread-scoped ACP sessions from #6732 need an opt-in desktop rollout
that preserves today’s channel policy by default.

## What

- Add the default-off “Thread Scoped ACP Sessions” toggle under Settings
→ Experiments using the existing feature-override persistence.
- Map the persisted setting to `BUZZ_ACP_SESSION_POLICY=channel|thread`
for every local and provider-backed managed ACP launch. Changes apply
when managed agents next start; DMs remain conversation-scoped by the
backend.
- Keep the desktop setting authoritative over descriptor environment and
cover the UI, persistence, local spawn, provider payload, and existing
Projects/Workflows experiments.

## Risk Assessment

Low. The experiment defaults off and explicitly preserves `channel`;
changes are limited to managed-agent launch configuration. Existing
running agents are unchanged until their next start.

## Testing

- `. ./bin/activate-hermit && just ci` — passed on the final restacked
tree, including 5,674 desktop tests, 2,796 Tauri tests, and 1,860 mobile
tests.
- `cd desktop && pnpm build:e2e && pnpm exec playwright test
tests/e2e/experimental-features.spec.ts --project=smoke` — 1 passed.
- `. ./bin/activate-hermit && cargo test --manifest-path
desktop/src-tauri/Cargo.toml session_policy --lib` — 4 passed.
- `. ./bin/activate-hermit && cargo test --manifest-path
desktop/src-tauri/Cargo.toml commands::agents::deploy::tests --lib` — 15
passed.
- `. ./bin/activate-hermit && cargo test -p buzz-backend-kubernetes
--test wire_fixtures` — 4 passed.

## Stack Info

Stacked on #6732. This PR depends on #6732 and should merge after it.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Signed-off-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
Co-authored-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
2026-09-01 08:37:27 -05:00
Matt Toohey
571c1902d0 fix(desktop): scope composer autocomplete to focus (#6860)
## Issue
I would send a message in the thread side pane and the main chat's
composer would then open its at-mention completion UI:


https://github.com/user-attachments/assets/25e79e05-8672-4f6e-b5cc-81a46638f229

## Summary

- Render inline composer autocomplete only for the focused rich-text
editor.
- Preserve editor focus when opening composer-owned mention controls.
- Add an end-to-end regression for sending in a side thread while the
main composer retains an agent mention.

## Root cause

The main channel composer and thread composer share the channel sending
state. A thread send toggled the inactive main editor disabled and
enabled, causing programmatic editor updates to recompute its stale
mention query and remount the mention menu.

## Verification

- Desktop unit suite: 5,556 passed
- Persistent agent audience E2E spec: 18 passed
- TypeScript typecheck
- Vite E2E build
- Biome and repository text/pubkey/file-size ratchets

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 14:28:07 +10:00
Logan Johnson
4a9de1a3a1 feat(desktop): add isolated named demo builds (#6407)
🤖 I’m Larry, updating this description on Logan’s behalf.

## Summary

Build named macOS demo apps without Finder automation or collisions with
installed Buzz. `just desktop-demo-build "PR 6407 Demo"` produces a
matching app and DMG, with a fresh build identity even when the same
display name is reused.

- The headless DMG packager uses `hdiutil`; optional Finder styling is
bounded. The existing production release recipe is unchanged.
- Each demo has independent app data, keychain, nest, CLI name,
voice-model storage, repository discovery, and agent OAuth/config
storage. Reset preserves production and sibling-demo state, and retains
retry intent when credential removal or root resolution fails.
- Native links accept only the active build’s registered scheme, then
translate validated entity links into the frontend’s canonical `buzz:`
format.
- The recipe builds all six executable sidecars. Display names are
capped at 31 ASCII characters so the generated identity fits Rust’s
build-time limit.

**Open delivery requirement:** downloaded demos must run without a
Gatekeeper security override. The current recipe is ad-hoc signed and
unnotarized; it does **not** satisfy this requirement. Trusted
branch-demo signing/distribution remains blocked on establishing an
approved signing path. This PR is not being presented as complete
download-and-run delivery.

### Related issue

N/A — reported in the Buzz DMG-packaging workstream.

### Testing

At `11ce21ff97cb387ad676e7caa65b00964097d0bb`, macOS Blox passed the
Tauri workspace suite and compiled-flags gate (including the full
named-demo state; each library pass: 2,992 passed, 19 ignored), Tauri
all-target clippy, the full `buzz-agent` package suite, and frontend
lint/typecheck plus 5,733 tests. Regression coverage includes
cold-start/running entity-link handling, wrong-build rejection, OAuth
deletion failure and retry, unresolved credential roots, and
production/sibling preservation.

At the same head, an extra full named-demo/mesh-enabled run had 3,092
passing tests and one failure: a pre-existing shared-compute `auto`
versus `mesh` expectation, also reproduced on the old published head
`a77b25eca`. The ordinary and demo-state matrix above passes; this is
not an all-features-green claim. Live macOS Launch Services delivery
remains unverified.

GitHub CI completed with 30 successful checks and 9 skipped. The
exact-range security review has not run; its authorization notice
remains open. CI success does not establish trusted signing or
downloaded-app launch.

Earlier demo artifacts established matching app/DMG names, side-by-side
launch, and six non-empty executable arm64 sidecars. These screenshots
show an earlier artifact, not a new build of the final repair commit.
Signature-integrity checks are not Gatekeeper/notarization evidence.

<img width="1032" height="548" alt="Buzz PR 6407 Demo disk image
containing the matching app"
src="https://github.com/user-attachments/assets/bca0277e-db03-4308-b280-fcad55e6d601"
/>

<img width="1186" height="821" alt="Buzz PR 6407 Demo running alongside
other Buzz installations"
src="https://github.com/user-attachments/assets/b4bf4ae5-c341-4e15-8090-9d2ea7c623b6"
/>

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Other Brother Darryl <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: Larry <loganj+sandbox-larry@squareup.com>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
2026-08-31 21:14:19 -04:00
Kalvin C
0affe527e8 fix(model-capabilities): humanize databricks goose model names (#7135)
🤖
## Summary
- add curated human-readable labels for Databricks Goose models that
otherwise render as fully qualified identifiers
- render `data_workflow_tools.goose.goose-glm-5-3` as `GLM-5.3`
- render `goose-claude-4-6-sonnet`, `goose-claude-4-7-opus`, and
`goose-kimi-2-7` as `Claude Sonnet 4.6`, `Claude Opus 4.7`, and `Kimi
2.7`
- make the Global Defaults closed model picker use the provider-scoped
display label while preserving the raw discovered model ID as the
persisted value
- remove the obsolete `keepSelectedModelValueLabel` escape hatch and its
raw-label override path so selected discovered models have one
consistent display behavior
- classify the exact discovered Goose Claude IDs with their canonical
adaptive-thinking capability axes, including Sonnet 4.6's exclusion of
`xhigh`
- expand Rust and TypeScript alias coverage and regenerate the shared
139-vector capability corpus

## Test plan
- `cargo test -p buzz-agent --lib` — 517 passed, 1 ignored
- `cd desktop && pnpm test` — 5,821 passed
- Desktop TypeScript typecheck — passed
- Biome on the changed component — passed
- `git diff --check` — passed
- targeted Playwright Global Defaults regression — passed on the
preceding implementation head; the subsequent commit only removes dead
picker-prop plumbing

Verified at `b9609d12696173aa309d2dbaf4f093a502756c36`. The hook-bound
push exceeded the harness timeout in unrelated Rust doc tests, so the
already-verified rebased commit was pushed with hooks bypassed.

Follow-up to #6955.

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-31 17:13:59 -07:00
Will Pfleger
9ab163190b feat(db): add NIP-FI identity and final-admission schema foundation (#6994)
PR 2 of the NIP-FI plan: the schema foundation. Establishes the durable
server-side identity ledger and final-admission surface that the runtime
phases build on. All of Phase A's migrations live here; later phases own
their own deltas.

Depends on nothing — PR 1 (#6776, merged) owned zero migration files.
This PR's relations are shaped to store exactly what PR 1's verifier
produces: issuer-qualified identity and the four denial classes. They
meet in a later PR that writes a verified assertion into these tables in
one transaction.

## Two internally-ordered migrations

- `0041_nip_fi_identity_foundation.sql` (migration A) — core identity +
base-lifecycle relations (5 tables): issuer-qualified `(iss, sub)`
bindings, lifecycle history/selectors, enrollment policies, and
operation receipts. Applies cleanly to current `main`.
- `0042_nip_fi_authorization_foundation.sql` (migration B) — the
final-admission surface (10 tables): authorization events + capacity,
admission results, replay/receipt guards, audit, invalidation
domains/floors, protected-object authority, authority epochs, and
restore version deltas. Applies to A's resulting state.

Fifteen NIP-FI relations total, zero dangling foreign keys. Identity is
issuer-qualified throughout — no single-global-issuer assumption in any
relation, no `Block`-hardcoding. A single deployment may run one issuer;
that is config, not schema.

## Durable, immutable ledger posture

All 15 relations are append-only (immutable `no_delete`/`no_truncate`
triggers) and carry `community_id` as provenance, not ownership. Both
migrations widen the single SQL source of truth
`community_write_fence_excluded_table` so the relations are never
fence-attached, never purged on community deletion, and never counted as
tenant-scoped drift by the deletion control plane's exact-set catalog
check — the same posture main already applies to `product_feedback` and
`rate_limit_violations`. `schema/schema.sql` keeps one consolidated
definition of that function whose exclusion array byte-matches `0042`,
guarded by a parity assertion so a future consolidation cannot silently
drop NIP-FI relations from the ledger.

This makes a tenant's identity/authorization ledger survive community
deletion, per the spec's `FI-INV-02` (durable binding) and `FI-INV-03`
(tombstone monotonicity) and `NIP-FI.md`'s "durable server state"
ruling. `communities(id)` FK never dangles: community rows become
permanent tombstones, never hard-deleted.

## Authorization shape and cardinality contracts

Authenticated `OperatorDenied` events (`actor_kind` 1–3, non-null
`request_fingerprint`) carry a null `semantic_fingerprint` and commit
without a denial-attempt row. The denial-attempt cardinality and shape
guards are scoped to unresolved pre-auth kind-9 events (`actor_kind =
4`). Applied and no-op lifecycle receipts (`outcome_code IN (1, 3)`)
require exactly one mapped success-transition event; denied lifecycle
receipts (`outcome_code = 2`) require zero events from the complete core
lifecycle success-transition class (kinds 1, 2, 3, 6: enrolled, revoked,
rotated, retired) — any such event paired with a denied receipt would
record a transition that never occurred.

## Mined vs. new

Re-cut from Franco's #1476 (`0029`/`0030`) and Cea's #4772 committer
schema, re-cut along FK topology and renumbered above the live `main`
tip. The buzz-auth core of #1476 is Cea-authored; `Co-authored-by`
reflects verified per-commit authorship of the mined schema.

Zero Rust/`deletion.rs` edits — the migration-only exclusion widening
keeps `EXPECTED_SCOPED_TABLES` untouched.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-08-31 19:40:35 -04:00
Salman Mohammed
674c173eb7 feat(buzz-acp): give each channel thread its own agent session (#6732)
## What this does

In a channel, people often run several unrelated conversations at once
(separate threads). Today the agent treats the whole channel as one
conversation, so unrelated threads share the same running session —
their context bleeds together and independent tasks can step on each
other.

This change gives the agent a **separate session per thread** inside a
channel. Direct messages stay as one conversation (unchanged). The
channel is still the boundary for who is allowed in and what is visible
— only the agent's working context is now split by thread.

## How it is turned on

Off by default. Operators opt in with one setting:

- `BUZZ_ACP_SESSION_POLICY=channel` — default, current behavior
- `BUZZ_ACP_SESSION_POLICY=thread` — new per-thread behavior

Being behind a flag means we can enable it for a few agents, watch how
it behaves, and roll back instantly without a code change.

## Key design decisions

- **Decide the thread once, up front.** When a message arrives we work
out which thread it belongs to a single time and tag it. Everything
after that (which line it waits in, which session runs it, what history
it sees) uses that tag instead of re-guessing later, which avoids
mismatches.
- **Default stays identical to today.** Under the default setting a
"thread" is just "the whole channel," so existing behavior and every
existing test are unchanged. The new, riskier behavior is strictly
opt-in.
- **Give the agent only its thread's history.** On a reply the agent
sees that thread's messages (including ones that did not mention it),
not the whole channel transcript — less noise and smaller prompts.
- **Don't let one channel use more memory than before.** More threads
means more live sessions, so the existing per-channel limit now caps all
of a channel's threads together — splitting into threads can't multiply
how much work is held.

## Bugs found and fixed while iterating (from review)

- **Same thread, two sessions.** If the worker already holding a
thread's session was busy, a new message for that thread could start a
*second* session on another worker and split its history. Now it waits
for the right worker instead of forking.
- **Interrupting the wrong thread.** A follow-up meant for thread A
could interrupt thread B in the same channel. Interrupts now target the
exact thread.
- **Stuck thread after a crash.** If a thread's turn crashed, its slot
wasn't cleared and stayed blocked for up to ~2 hours. It now clears
right away and retries.
- **Lost the original request.** When a thread was interrupted and then
had to wait for a busy worker, only the follow-up was kept and the
original request was dropped. The full request is now preserved on
retry.
- **Same thread seen as two.** Two spellings of the same thread id
(upper/lower case) could be treated as different threads. Normalized so
they count as one.

## Not in this PR

- The desktop Settings toggle and rollout wiring for managed agents —
https://github.com/block/buzz/pull/6909
- One pre-existing retry edge case (present today without this flag,
unrelated to this change) — tracked separately so this PR stays focused.

## Testing

The full `buzz-acp` test suite passes (830+ unit and integration tests),
plus new focused tests for thread routing, session reuse, interrupt
targeting, crash recovery, and request preservation. Behavior with the
flag off is unchanged.

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Signed-off-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
Co-authored-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
2026-08-31 18:17:28 -04:00
Jude Edwards
4952f58102 docs: add review-proven failure-path & async-state rules to AGENTS.md (#7061)
Mining the last 25 PRs' review threads (45 substantive findings, 11
reviewed PRs, avg **4.8 review rounds** each) shows **53% of findings
are repeats** of five clusters: swallowed failures, stale-async-state
races, tests that don't bind the production seam, unbounded
resources/retry loops, and non-atomic multi-step persistence. PR #6956
alone burned 4 rounds converging on one of these classes.

A second, independent mining pass over **71 agent-review rooms (303
findings, Aug 18–29)** confirmed the same clusters and added outcome
data — how often authors actually fix each finding class once flagged:
test-seam binding and unbounded-resource findings **100%**, swallowed
errors **90%**, stale-state races **70%**. It also surfaced two clusters
the GitHub-thread pass under-sampled: **assistive-semantics defects**
(44 findings, second-largest cluster) and **input-modality divergence**
(27 findings), now rules 7–8.

This PR distills those clusters into eight imperative rules in AGENTS.md
so agents apply them **before writing code**, adds one
client-consumption invariant to ARCHITECTURE.md §5, and places the
test-quality rule in TESTING.md (per the team decision that testing docs
are the canonical guide for review standards), cross-referenced from
AGENTS.md. Each rule cites the PRs where it was litigated. Raw mining
data: `reviews.jsonl` / `comments.jsonl` +
`backfill/buzz-review-findings.jsonl` (review-mining artifacts, not
committed).

No code changes. CLAUDE.md is a symlink to AGENTS.md and picks this up
automatically.

🤖 Drafted by Jude's agent from automated mining of this repo's last 25
PRs' review threads and 71 agent-review rooms; every rule cites the PRs
where it was litigated. Jude reviews and owns the result. Mining method
+ raw cluster data available on request.

---------

Signed-off-by: Jude Edwards <judeedwards@squareup.com>
2026-08-31 15:13:58 -07:00
thomaspblock
b47b5a5012 fix(desktop): back split thread headers (#7137)
## Summary
- render an auxiliary panel's requested header backdrop in docked/split
mode
- preserve explicit transparent-backdrop behavior
- cover a populated, scrolled thread pane so timeline content cannot
bleed through its header

## Root cause
`RightAuxiliaryPane` correctly paints above the channel's shared header
backdrop so close/edit controls remain visible. The docked
`AuxiliaryPanelHeader` branch, however, ignored its `backdrop` request,
leaving scrolled thread content in that higher stacking context
unbacked.

## Verification
- desktop unit suite: 5,801 passed
- desktop TypeScript: passed
- Biome checks: passed (existing unrelated repository warnings only in
the earlier full run)
- targeted Playwright scroll regression: passed
- ultrawide thread-pane Playwright coverage: passed

Signed-off-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
Co-authored-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
2026-08-31 17:29:31 -04:00
tulsi
cb3144999b add public descriptions to agent personas (#7126)
**Category:** new-feature
**User Impact:** People can add a short public description to an agent
and see what it does directly on agent cards and profiles.

**Problem:** Agent cards previously showed only a model label, so people
had to open an agent and inspect its instructions to understand its
purpose. Public metadata also needed one trustworthy lifecycle across
local edits, relay catalogs, profiles, and portable snapshots.

**Solution:** Add an optional owner-authored description with a
280-character visible-text policy, publish it as profile `about`, and
prefer it on agent cards while retaining the model fallback. Description
metadata is excluded from the spawn-content hash, remains
definition-owned, and is validated independently at every untrusted or
persistence boundary.

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

**desktop/src-tauri/src/commands/agent_config_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/agent_discovery/relay_directory.rs**
Updates relay-directory profile test publication for the expanded
profile contract.

**desktop/src-tauri/src/commands/agent_models_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/agent_models_update.rs**
Preserves the effective `about` value when instance edits republish a
complete profile event.

**desktop/src-tauri/src/commands/agents.rs**
Carries the effective authored description into initial managed-agent
profile publication.

**desktop/src-tauri/src/commands/agents_profile.rs**
Adds `about` to profile reconciliation and keeps description, name, and
avatar synchronized against relay state.

**desktop/src-tauri/src/commands/agents_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/personas/card.rs**
Materializes the definition-owned description before minting a portable
agent card snapshot.

**desktop/src-tauri/src/commands/personas/create.rs**
Normalizes and validates raw authored descriptions before persona
persistence.

**desktop/src-tauri/src/commands/personas/delete_cascade_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/personas/inbound.rs**
Validates descriptions at inbound relay ingress and applies accepted
values to local definitions.


**desktop/src-tauri/src/commands/personas/inbound/catalog_reconcile_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/personas/inbound/inbound_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/personas/mod.rs**
Centralizes raw-byte validation followed by trim/empty normalization for
description writes.

**desktop/src-tauri/src/commands/personas/pending.rs**
Revalidates descriptions before preparing public persona publications.

**desktop/src-tauri/src/commands/personas/sharing.rs**
Carries the optional public description through this managed-agent
compatibility path.

**desktop/src-tauri/src/commands/personas/snapshot.rs**
Materializes definition-owned descriptions into portable instance
snapshots without creating a second persisted authority.

**desktop/src-tauri/src/commands/personas/snapshot/fidelity_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/personas/snapshot/import.rs**
Restores snapshot descriptions onto imported definitions while keeping
linked instance copies absent.

**desktop/src-tauri/src/commands/personas/snapshot/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/personas/update.rs**
Persists persona description edits, republishes linked profiles, and
preserves legacy avatars during complete kind:0 replacements.


**desktop/src-tauri/src/commands/personas/update/name_propagation_tests.rs**
Proves description-only profile sync does not write instance state or
clear a legacy avatar.

**desktop/src-tauri/src/commands/team_snapshot.rs**
Round-trips member descriptions through team snapshots and imported
definitions.

**desktop/src-tauri/src/commands/team_snapshot/tests.rs**
Covers team member description export and import fidelity.

**desktop/src-tauri/src/commands/teams/adopt/apply.rs**
Starts adopted team catalog members without synthesizing an unauthored
description.

**desktop/src-tauri/src/commands/teams/adopt/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/teams/pending/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/commands/teams/sharing/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/egress_guard_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/event_sync_team_catalog_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/agent_description.rs**
Defines the canonical Rust description resolution used by profile
publication and reconciliation.

**desktop/src-tauri/src/managed_agents/agent_events.rs**
Updates managed-agent record construction for the optional public
description field.

**desktop/src-tauri/src/managed_agents/agent_snapshot.rs**
Includes descriptions as snapshot profile `about` metadata and validates
them at decode ingress.

**desktop/src-tauri/src/managed_agents/agent_snapshot_envelope.rs**
Updates managed-agent record construction for the optional public
description field.

**desktop/src-tauri/src/managed_agents/agent_snapshot_tests.rs**
Covers snapshot description export and rejection of unsafe or overlong
imported metadata.

**desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/definition_validation.rs**
Adds the shared 280-character visible-text policy for public
descriptions.

**desktop/src-tauri/src/managed_agents/discovery/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/effective_config/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/global_config/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/mod.rs**
Exports the description resolution and validation helpers to
managed-agent consumers.

**desktop/src-tauri/src/managed_agents/nest/render_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/parallelism.rs**
Updates managed-agent fixtures for the optional description field
without changing runtime configuration behavior.

**desktop/src-tauri/src/managed_agents/persona_events.rs**
Adds description to persona event content while deliberately excluding
it from the spawn-relevant content hash.

**desktop/src-tauri/src/managed_agents/persona_events/tests.rs**
Pins description event round-tripping and proves description-only edits
do not change the restart hash.

**desktop/src-tauri/src/managed_agents/personas.rs**
Initializes built-in persona records without authored descriptions for
backward-compatible defaults.

**desktop/src-tauri/src/managed_agents/personas/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/readiness.rs**
Updates managed-agent fixtures for the optional description field
without changing runtime configuration behavior.

**desktop/src-tauri/src/managed_agents/restore.rs**
Includes the effective description in launch-time profile
reconciliation.

**desktop/src-tauri/src/managed_agents/runtime/test_fixtures.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/runtime/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/spawn_snapshot/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/team_catalog/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/team_snapshot.rs**
Updates managed-agent record construction for the optional public
description field.

**desktop/src-tauri/src/managed_agents/teams_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/managed_agents/types.rs**
Adds optional description metadata to persona and managed-agent records
and their compatibility projections.

**desktop/src-tauri/src/managed_agents/types/requests.rs**
Accepts optional descriptions on persona create and update IPC requests.

**desktop/src-tauri/src/managed_agents/types/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/migration_avatar_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src-tauri/src/persona_catalog.rs**
Parses and validates descriptions at the untrusted community-catalog
boundary.

**desktop/src-tauri/src/persona_catalog_tests.rs**
Covers valid catalog descriptions plus rejection of malformed,
invisible, and overlong values.

**desktop/src-tauri/src/relay.rs**
Publishes and queries kind:0 `about` so relay profiles preserve authored
descriptions.

**desktop/src-tauri/src/relay/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.

**desktop/src/features/agents/AGENTS.md**
Documents description ownership, validation, snapshot, hashing, and
display invariants for future changes.

**desktop/src/features/agents/lib/agentDescription.test.mjs**
Pins Unicode counting, paste clamping, trimming, and empty
authored-description behavior.

**desktop/src/features/agents/lib/agentDescription.ts**
Provides shared display resolution, Unicode-scalar counting, and paste
clamping for descriptions.

**desktop/src/features/agents/lib/personaCatalogRelay.ts**
Maps validated catalog descriptions into catalog persona projections.

**desktop/src/features/agents/ui/AgentDefinitionDialog.tsx**
Adds the description draft to create and edit submission while
extracting identity fields from the large dialog.

**desktop/src/features/agents/ui/AgentDescriptionField.tsx**
Renders the public description input, helper copy, and Unicode-aware
near-limit counter.

**desktop/src/features/agents/ui/AgentIdentityCard.tsx**
Generalizes the card second line to show a two-line description or the
existing model fallback.

**desktop/src/features/agents/ui/UnifiedAgentsSection.tsx**
Prefers authored descriptions on persona cards and retains model labels
when no description exists.

**desktop/src/features/agents/ui/personaDialogState.test.mjs**
Verifies edit and duplicate drafts preserve authored descriptions.

**desktop/src/features/agents/ui/personaDialogState.ts**
Seeds authored descriptions into edit and duplicate dialog drafts.

**desktop/src/features/agents/ui/usePersonaActions.ts**
Preserves descriptions when copying catalog personas into local
definitions.

**desktop/src/shared/api/personaTypes.ts**
Defines description-bearing persona wire types in a focused module split
from the size-constrained API type file.

**desktop/src/shared/api/tauriPersonas.test.mjs**
Verifies raw persona descriptions map into the frontend model and absent
values become null.

**desktop/src/shared/api/tauriPersonas.ts**
Maps description fields across Tauri and preserves raw authored bytes
for authoritative Rust validation.

**desktop/src/shared/api/types.ts**
Re-exports the extracted persona types without changing consumer import
paths.

**desktop/src/testing/e2eBridge.ts**
Extends mock persona create, update, publication, and catalog parsing
with production-shaped description behavior.

**desktop/tests/e2e/agents.spec.ts**
Verifies an edited description persists and appears on the agent card.

</details>

### Reproduction Steps

1. Open **Agents**, edit a custom or built-in agent, and enter a
sentence in **Description**.
2. Save the agent and confirm the sentence appears as the second line on
its card.
3. Reopen the agent and confirm the authored description is restored;
clear it and confirm the card returns to the model label.
4. Paste more than 280 Unicode characters and confirm the field keeps
the first 280 characters and shows the near-limit counter.
5. Share or export/import the agent and confirm the description survives
in the catalog/profile or snapshot without showing a restart-required
badge for a description-only edit.

### Screenshots / Demo

The focused Playwright flow `built-in persona edits persist` exercises
the edited dialog, persisted value, and resulting card subtitle.
Screenshots can be added after review if the field placement or two-line
card treatment needs visual iteration.

### Verification

- `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib` —
3,029 passed
- `cd desktop && pnpm test` — 5,805 passed
- `cd desktop && pnpm exec tsc --noEmit`
- Focused Playwright: `built-in persona edits persist` — passed
- Pre-push desktop, Tauri, typecheck, test, file-size, and branch-skew
gates — passed

---------

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-31 17:12:52 -04:00
Arjun Mahanti
5673c3307e feat(desktop): add protected-build Bestie experiment (#6902)
## Summary

Introduces a protected-build boundary for the default-off Bestie
experiment without adding any Bestie product surface.

- Official OSS builds select an empty protected-feature module and emit
no Bestie/Chief metadata or implementation content.
- Protected internal builds select a separate module graph containing
the Bestie experiment definition.
- Within an internal build, Bestie remains disabled until the user opts
in under Settings → Experiments.
- The production build runs an artifact matrix and fails if OSS output
contains protected content or internal output lacks the Bestie manifest.

## Build contract

| Build variant | User opt-in | Result |
| --- | --- | --- |
| Official OSS | Any/forged | Bestie absent from the compiled artifact |
| Protected internal | Off | Bestie available but disabled |
| Protected internal | On | Bestie enabled |

The companion protected-release change is squareup/buzz-releases#91. It
sets `VITE_BUZZ_BESTIE=1`, requires that exact value, forwards it into
the signed macOS build, and asserts the contract in release validation.

## Why this is separate

This gives later Bestie PRs one build-selected import seam. Protected
implementations must be reachable only from the internal module so they
never enter the official OSS module graph.

## Non-goals

- No Bestie persona or provisioning
- No sidebar, app-chrome, or message-toolbar UI
- No entitlement or secrecy claim: the source is public; this boundary
controls official Block artifacts

## Verification

- Exact commit `523cf49ced03cba9be43836a54d6aa5d6923cc82`
- Full `just ci`: 5,673 Desktop tests, 2,773 Tauri tests, 1,860 mobile
tests, Rust/Tauri/web/mobile static checks and builds
- OSS production artifact: scanner confirms no `Bestie`, `Chief of
Staff`, or `builtin:bestie` content
- Internal production artifact: scanner confirms the protected Bestie
manifest is emitted
- Both build orders verified; `dist` retains the requested variant for
Vite/Tauri packaging

---------

Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Signed-off-by: Fizz <fizz@buzz.local>
Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Fizz <fizz@buzz.local>
Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
2026-08-31 15:03:35 -06:00
Wes
2f3dd850db fix(relay): reject a frame on its own acknowledgement channel (#6961)
Pinky, an AI agent, updated this description on Wes's behalf after
taking over the startup investigation.

**Category:** fix

**User Impact:** An EVENT refused by WebSocket admission or handler
saturation receives a correlated `OK(event_id, false, reason)` instead
of an uncorrelated NOTICE, so the client can settle that refusal without
waiting for its publish timeout. Rate-limited refusals also arm client
backoff. This fixes a protocol failure mechanism; it does not establish
that every startup send will succeed or that the reported Desktop
startup incident is fully resolved.

**Problem:** Startup opens several live subscriptions and publishes at
once, and the relay's WebSocket admission gate is a fixed 5-second
window (`ws_admission_budget` = `human_ws_events_per_sec * 5`). If that
shared per-principal quota is exhausted, `enforce_ws_admission`
previously rejected an EVENT with a bare `["NOTICE", reason]`. Quota
pressure is a possible trigger, not proof of the original incident's
complete cause.

A NOTICE carries no event id. Both clients settle a pending publish
*only* from an `OK` keyed by event id (desktop `pendingEvents`, mobile
`_pendingEvents`), so nothing settled — and `handle_text_message`
returns early, so no `OK` ever followed either. The send **could not
fail**; it could only time out at `PUBLISH_TIMEOUT_MS` = 25s. That
explains how this rejection mechanism can produce a roughly 25-second
timeout; attributing the original report to it still requires the actual
startup/send workflow.

The handler-semaphore saturation path had the identical defect, and that
one needs no quota burst to fire.

**Solution:** NIP-01 gives each request type its own acknowledgement
channel, and a rejection is only actionable on the same one. Reject a
REQ with `CLOSED`, an EVENT with `OK(id, false, reason)`, and fall back
to `NOTICE` only where no per-request correlation exists. COUNT refusals
now also use `CLOSED(query_id, reason)` per NIP-45, covering both quota
admission and handler saturation (added in
`cd12c93804b87a24b61075dfd171dc471a0a527f`).

Reason strings are unchanged, so the `rate-limited:` prefix and `retry
in {N}s` hint that existing client gates parse keep working (desktop
`parseRateLimitHint`, mobile `RelayRateLimitGate`, buzz-acp
`set_rate_limit_gate`). Only the frame *type* changes, so
`docs/multi-tenant-relay.md` L7 stays satisfied.

Two notes on how this landed, both worth a reviewer's attention:

1. **A survived mutation became a design change.**
`send_admission_result` originally took a `RejectionTarget` parameter,
and reverting the *second* call site (the per-minute message quota)
survived the whole suite — with Redis unreachable the first quota check
short-circuits, so that line is unreachable in test. Rather than test
around it, the parameter is gone: the target is derived from the frame,
so no call site can name the wrong channel.

2. **The relay fix would have caused a client regression on its own.**
Gate arming lived only in the NOTICE branch. Once rejections arrive as
`OK:false`, `handleOk` failed the send without ever backing off — the
client would retry straight into the same quota. Desktop and Mobile now
arm on a `rate-limited:` OK rejection. ACP was subsequently fixed in
`3b06dd32493596ec650f20abf8805791c50fdc24`: it arms the gate and
re-parks only the refused observer frame, preserving other in-flight
frames. Desktop gets `activateRateLimitIfSignalled` as the single owner
of that prefix test, called from both `handleOk` and the NOTICE branch.

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

**crates/buzz-relay/src/rejection.rs** (new)
Owns the admission-rejection concern: `RejectionTarget`,
`rejection_target_for`, `request_rejection_message`,
`send_admission_result`, and `enforce_ws_admission`, moved out of
`connection.rs`. Six tests, two of which drive the real
`enforce_ws_admission` against a real `AppState`.

**crates/buzz-relay/src/connection.rs**
Fix the EVENT handler-semaphore rejection to correlate to the event id;
delegate admission to the new module. Add two tests that drive the real
`handle_text_message` with every handler permit held. Down from 1319 to
1116 lines.

**crates/buzz-relay/src/state.rs**
Widen the existing `test_state` helper to `pub(crate)` so the rejection
tests reuse it rather than adding a ninth copy of `AppState`
construction.

**desktop/src/shared/api/relayRateLimitGate.ts**
Add `activateRateLimitIfSignalled` — one owner for the `rate-limited:`
prefix test, since three inbound frame types now carry it.

**desktop/src/shared/api/relayClientSession.ts**
Arm the gate on a rate-limited OK rejection; route the NOTICE branch
through the same helper. Net zero lines, which keeps this
already-oversized file within the differential ratchet.

**desktop/src/shared/api/relayClientPublishRejection.test.mjs** (new)
Four tests against the real `RelayClient`: a rate-limited OK settles the
pending publish and arms the gate; an ordinary rejection does not arm
it; an accepted OK still resolves.

**mobile/lib/shared/relay/relay_session.dart**
Arm the gate in `_handleOk` for a rate-limited rejection.

**mobile/test/shared/relay/relay_session_test.dart**
Two tests driving the real `publish` + `debugHandleMessage` path.

</details>

<details>
<summary>Validation</summary>

**Mutation-tested — 5 mutations, all now killed.** Each production call
site was reverted to the defective behaviour to confirm a test fails.
This caught two false-negative tests:

| # | Mutation | Result |
|---|----------|--------|
| 1 | `rejection_target_for`: EVENT → `Connection` | 4 tests fail |
| 2 | EVENT handler-semaphore call site → bare NOTICE | **survived at
first** |
| 3 | per-minute quota call site → `Connection` | **survived**; fixed by
removing the parameter |
| 4 | desktop `handleOk` gate arming removed | 1 test fails |
| 5 | mobile `_handleOk` gate arming removed | 1 test fails |

Mutation 2 is the lesson: my first saturation test called
`request_rejection_message` directly, so reverting the real call site
inside the `match` arm left it green. It now drives
`handle_text_message` itself and dies on that mutation.

- `cargo test -p buzz-relay` — 928 passed, 1 failed:
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo`,
**pre-existing**, reproduced with all changes stashed at `4dd4d73de`.
- `cd desktop && npm test` — 5721 passed, 0 failed (full suite).
- `cd mobile && flutter test` — 1876 passed, 0 failed (full suite).
- `just fmt-check`, `just clippy`, `just desktop-check`, `just
mobile-check`, `just file-size-check` — clean. Desktop's 5 biome
warnings are pre-existing (reproduced with changes stashed).
- All 9 pre-push lanes green, including `rust-tests` and
`desktop-tauri-checks`.

**Not verified:** not reproduced end-to-end against a live relay under a
forced quota burst. The causal chain is source-proven and
mutation-proven at the frame level; the ~25s attribution follows from
`PUBLISH_TIMEOUT_MS` but is not directly measured. A packaged-build
click-through would close that gap.

</details>

Related work: #6957 bounds Desktop HTTP event submission, but safe
retained-operation recovery after exhausted/ambiguous outcomes remains
unfinished. #6998 is the separately reviewable Desktop
readiness/duplicate-subscription slice. Neither is claimed to complete
native before/after startup-send validation.

Diagnosis note: `RESEARCH/DESKTOP_STARTUP_SEND_STALL_2026_08_27.md`
(Brain's workspace).

## Current review disposition (2026-08-28)

The [review on
`cd12c938`](https://github.com/block/buzz/pull/6961#pullrequestreview-5052902510)
identified ACP's missing rate-limited-OK handling. Commit
`3b06dd32493596ec650f20abf8805791c50fdc24` fixes gate arming, re-parking
the specifically refused observer frame, and the stale NOTICE comment.
Two regressions drive the real frame dispatcher. See [the implementation
and validation
response](https://github.com/block/buzz/pull/6961#issuecomment-5455032054).

The Mobile generation-check inline thread is resolved: its `async
publish` returns a failed Future when superseded; it does not throw
synchronously at invocation. No further production change was indicated
by that comment.

The validation counts above describe the original slice, not a new
rerun. At `3b06dd324`, the current GitHub check rollup has successful
completed test/build checks (non-applicable jobs skipped). The
security-review comment still requires review for the current base/head
range; do not read a green authorization job as a completed security
review. Approval and merge remain human decisions.

---------

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-31 14:43:31 -06:00
Wes
93237b4a74 fix(acp): wake agents from workflow messages (#6953)
> Pinky, an AI agent, is opening this PR on Wes's behalf.

## Summary

Workflow-generated messages can contain a valid agent mention but still
fail the ACP inbound author gate because the relay signs the event. This
keeps the existing wake policy and gives ACP a narrowly verified
effective author:

- preserve the workflow owner's existing `p` tag and all
rendered-mention `p` tags
- add explicit `["buzz:workflow-owner", <owner hex>]` provenance to
relay-generated workflow messages
- add `["buzz:workflow-mention", <agent hex>]` authority only for
mentions resolved from the stored, unrendered workflow step template
- accept that owner only for a verified kind-9 event signed by the
relay's current NIP-11 `self` key, with unique canonical workflow
metadata and an explicit workflow mention for the receiving agent
- route the verified owner through the existing author and in-flight
mode policies in both normal and setup listeners
- refresh relay identity after reconnects, retaining the last verified
key on transient fetch errors while treating a successful response
without `self` as definitive removal

Malformed, duplicate, forged, tampered, wrong-kind, and wrong-relay
attribution all fail closed to the raw event signer. `respond-to=nobody`
remains absolute. Old/mixed-version messages without the explicit
provenance retain their current fail-closed behavior.

## Trust boundary

The workflow owner means **“scheduled by,” not “authored every rendered
word.”** Trigger-controlled substitutions may still produce ordinary `p`
mention routing for compatibility, but they cannot mint
`buzz:workflow-mention` authority. Only a target named in the durable
owner-authored step template can receive that authority.

The author gate is not bypassed: after relay signature/provenance
verification, the effective owner is evaluated under the same
`owner-only`, `allowlist`, DM, and `nobody` policies used for ordinary
messages. Owner control commands continue to use the raw event signer.

## Why this PR

This is the focused immediate fix for waking an **online** agent from a
stored workflow mention. Earlier attempts were not a finished mergeable
fix and had materially different or incomplete trust designs. Larry's
larger draft stack addresses durable delivery across restarts; that
remains valuable future work and can supersede this effective-author
path when it lands.

## Validation

At exact clean commit `fe5b55619fe44176343eefb4cb7fe180df45a7d8`:

- `buzz-relay workflow_sink`: 25/25 passed, including all four ignored
PostgreSQL cases
- `buzz-acp --lib`: 845/845 passed
- `buzz-workflow --lib`: 169/169 passed (2 unrelated PostgreSQL tests
ignored)
- warnings-denied Clippy passed for the changed Rust packages
- `cargo fmt --all -- --check` passed
- `git diff --check` passed
- repository pre-push gates passed, including branch-scoped Rust tests
- CI now selects the ACP library tests and the relay's pure + PostgreSQL
workflow-sink tests so these guards cannot silently remain unexecuted

The production event-to-author gate is shared by normal and setup
listeners and has biting regression tests for accepted explicit
attribution, legacy owner-`p` rejection, and forged-attribution
rejection.

## Exact-head local relay + ACP proof

Following the release-binary/local-relay shape in `TESTING.md`, the
exact commit above passed a fresh isolated real-process matrix using:

- a freshly recreated Postgres database with migrations
- isolated Redis
- exact-head release `buzz-relay`, `buzz`, `buzz-admin`, and `buzz-acp`
binaries
- newly provisioned owner, channel, and bot member through the CLI
- workflow creation and triggering through the running relay
- a deterministic ACP protocol subprocess capturing actual
`session/prompt` dispatches
- a NIP-11 `self` value verified against the running relay signer

Cases:

1. A stored explicit workflow mention woke an `owner-only` agent exactly
once.
2. A workflow message without an agent mention did not wake it.
3. A non-relay signer forging every workflow authority tag did not wake
it.
4. Trigger-controlled `{{trigger.text}}` containing `@Wake Agent`
retained ordinary `p` routing but received no authority-bearing
workflow-mention tag and did not wake the agent.
5. `respond-to=nobody` remained absolute for a valid relay-authenticated
workflow mention.

The deterministic ACP subprocess isolates and directly proves relay →
ACP authorization and prompt dispatch without depending on external
model behavior.

## Deployment and residual risk

Relay and ACP changes must be deployed together for the new wake
behavior; mixed versions fail closed. Production paired-deployment proof
remains distinct from the successful local integration run. Setup-mode
behavior has automated coverage but was not a separate case in the
five-case local matrix. Relay-key rotation is observed at ACP
startup/reconnect; transient NIP-11 errors retain the last verified key,
an intentional availability tradeoff documented in code.

---------

Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: LioLionel <62820906+LioLionel@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-31 16:24:09 -04:00
Arjun Mahanti
bc006f6708 feat: render agent avatars as squircles (#7106)
## Summary

- render every agent/AI identity as a 30% squircle across desktop and
mobile while keeping human avatars circular
- propagate agent identity through message, thread, profile, reaction,
member, DM, search, workflow, project, huddle, forum, pulse, and
agent-management surfaces
- preserve squircle geometry for fallbacks, focus/status treatments,
add-agent controls, and overlapping avatar outlines (`calc(30% + 2px)`
for the outer background)

### Related issue

None found. This change was requested and visually reviewed in the
originating Buzz thread.

### Testing

- `just desktop-test` — 5,799 passed
- `just mobile-test` — 2,008 passed
- pre-push gates passed at `0d59d77b120dcb90aac2f918e422c11c9fa5353b`:
desktop check, TypeScript typecheck, desktop full test suite, mobile
format/analyze and full test suite, Rust tests, Tauri checks, and
differential file-size gate
- deterministic desktop visual sweep covered channel messages/thread
summaries; thread, subthread, and sub-subthread depths; reactions and
reactor popovers; hover/full profiles; added-to-channel activity;
channel members/settings; agent library/team overlaps; agent creation;
mention autocomplete; and DM header/sidebar/settings

### UI evidence

The complete labeled visual matrix is available in the originating Buzz
review thread. GitHub-hosted copies will be added in a follow-up PR
comment using the repository screenshot script.

---------

Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
2026-08-31 13:31:33 -04:00
Will Pfleger
17ecf0b8c9 fix(ci): salvage Codex review output on PTY-shutdown hang (#7042)
Codex CLI can leave a PTY descendant holding the action's inherited
stdio after the turn completes. The `runCodexExec.ts` wrapper waits on a
`close` event that never fires, so the `Review pull request` step hangs
until the job timeout kills it — discarding the finished review the CLI
already wrote to disk.

The CLI writes the completed review to the `--output-last-message` file
(exposed as `output-file`) **before** the hang. This PR adds a salvage
step that recovers it, and sets the step and job timeouts to preserve
the full 30-minute Codex execution budget.

**Changes (`codex-security-review.yml`):**

- Add `output-file: ${{ runner.temp }}/codex-review.json` to the `Review
pull request` step so the CLI writes the result before the hang.
(`runner` context is valid in `steps.with`; not in `jobs.env`.)
- Add `timeout-minutes: 30` and `continue-on-error: true` to the Codex
step — a hang now costs ≤30 minutes instead of 40, and the salvage step
still runs.
- Set job `timeout-minutes: 40` to give setup, step cancellation, and
salvage sufficient headroom without colliding with the Codex execution
budget. The original 30-minute job timeout was too narrow: evidence from
run
[33114428326](https://github.com/block/buzz/actions/runs/33114428326/job/98665369165)
shows completed output appearing 28m46s after step start, meaning a
20-minute step timeout could kill a legitimate review before the salvage
file exists.
- Add a `Salvage review output` step with `if: always()`: prefers
`steps.run_codex.outputs.final-message` on a clean exit; falls back to
the output file when the step timed out. The output file path is set in
the step's own `env` block (`CODEX_OUTPUT_FILE: ${{ runner.temp
}}/codex-review.json`), where `runner` is valid. Validates shape
(non-empty JSON object, has `overall_risk`); fails the job hard if
neither source is present.
- Wire the job `outputs.review_json` to
`steps.salvage.outputs.review_json`.

**Changes (`Justfile`, `ci.yml`):**

- Add `actionlint .github/workflows/codex-security-review.yml` to
`security-review-check` so expression-validity errors are caught
locally.
- Provision `actionlint` via Hermit (pinned v1.7.12) rather than a
one-off `Install actionlint` curl step, so the same binary is used
locally and in CI.

**Security posture is unchanged:** the salvage step reads the action's
own output and a file written to `runner.temp` — neither is
PR-controlled. Credential-stripping env block on the Codex step is
untouched.


Note this is a temporary workaround until
https://github.com/openai/codex-action/issues/169 is addressed

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-31 13:31:22 -04:00
Phil Azar
f463e726dd fix: retrieving cold memories; add regression task (#6950)
## Why

Evaluating buzz agent memory retrieval by seeding a memory then asking
the buzz agent a question it needs that memory.

**Bug Found**: System prompt had no inclusion of retrieving cold
memories and suggested looking in a mem/*.md directory that does not
exist. Updated `system-prompt.md` to include memory CLI tools and usage.

Eval Before System Prompt Change: 0/3 
Eval After System Prompt Change: 3/3 

## What

- Add a `memory-retrieval` benchmark that seeds agent memory with `buzz
mem set` before asking a direct question.
- Grade the observable threaded answer without inspecting tool calls or
exposing the answer in channel history.
- Teach agents to use `buzz mem set`, `buzz mem ls`, and `buzz mem get`
for cold memory.
- Add a wire-debug endpoint configuration for diagnosing ACP tool calls
in local runs.
- Add fixture, seeding, verifier, and prompt coverage.

## Risk Assessment

Low. The runtime changes are limited to the benchmark harness. The
production-facing change clarifies existing memory commands in the base
prompt; it does not change memory storage, relay behavior, or
authorization.

## References

- Before the system-prompt changes, 0/3 attempts passed because agents
never invoked the `buzz mem` CLI and instead searched a non existent
filesystem
- After the changes, 3/3 attempts passed. ACP wire logs confirmed that
every agent ran `buzz mem ls` followed by `buzz mem get` and returned
`net_gpv`.

---------

Signed-off-by: Philip Azar <pazar@squareup.com>
2026-08-31 17:26:24 +00:00
Jordan Mecom
e47690c21b Enforce NIP-OA authorization time bounds (#7004)
NIP-OA relay admission now evaluates signed `created_at<` and
`created_at>` conditions against the already verified authentication
event timestamp. An owner-signed credential such as `created_at<1` no
longer upgrades its holder to relay membership.

The verified timestamp now flows from NIP-42, NIP-98, and Blossom
authentication through the shared membership gate used by WebSocket,
HTTP, Git, media, huddles, GIF, and workflow requests. Owner-attested
access fails closed when no signed authentication timestamp is
available. Direct relay members keep their existing admission behavior,
and `kind=` remains connection-level metadata as specified by NIP-AA.

Tests cover strict time-bound evaluation, missing timestamp rejection,
and HTTP authentication timestamp propagation.

Testing:

- `cargo test -p buzz-sdk nip_oa::tests -- --nocapture`
- `cargo test -p buzz-relay api::relay_members::tests -- --nocapture`
- `cargo test -p buzz-relay api::bridge::tests -- --nocapture`
- `just ci`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
2026-08-31 10:18:05 -07:00
Luke Tornquist
3ed623bb21 feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229)
## Why

A wedged relay boot pod holding a relation lock can park every other
writer in the fleet behind it: DB load pins at pool capacity in
`Lock:relation` waits while CPU stays flat, and nothing server-side
releases the lock until the holder dies. We hit exactly this in
production — ~1,400 sessions queued behind one crash-looping pod's boot
transaction for ~20 minutes until kubelet killed the container.

## What

Applies session-level Postgres timeouts to every **writer** connection
inside the existing single `after_connect` hook in `buzz-db`, all
env-tunable through the same `Config::from_env → DbConfig` path as the
existing pool-size knobs:

| Env var | GUC | Default | Effect |
|---|---|---|---|
| `BUZZ_DB_LOCK_TIMEOUT_MS` | `lock_timeout` | 5000 | statements waiting
on any lock fail fast instead of parking behind a wedged holder |
| `BUZZ_DB_IDLE_TXN_TIMEOUT_MS` | `idle_in_transaction_session_timeout`
| 60000 | reaps wedged clients idling inside an open transaction while
holding locks |
| `BUZZ_DB_STATEMENT_TIMEOUT_MS` | `statement_timeout` | 0 (off) |
opt-in runaway-statement cap; off by default because startup
migrations/backfills legitimately run long statements |

`0` disables a timeout (Postgres semantics) and deliberately passes
through the env parsing — unlike the pool-size knobs where `0` falls
back to the default. The reader pool is untouched: replica sessions
never take contended locks and already fail acquire in 150 ms.

Deployers tune these via plain env vars (`.env`, or `relay.extraEnv` in
the Helm chart) — no code changes needed.

## Behavior change to note

With the 5 s default `lock_timeout`, a boot-time migration or backfill
that waits >5 s on a lock now errors (surfacing in logs / crash-looping
the pod) instead of stalling silently. That is the intended
visible-failure-over-fleet-stall tradeoff; deployers with slow contended
migrations can set `BUZZ_DB_LOCK_TIMEOUT_MS=0`.

## Testing

- `cargo test -p buzz-db -p buzz-relay` — buzz-db green; buzz-relay has
9 failures that also fail on clean `main` in this environment
(api::admin/api::media/mesh_demo — unrelated, pre-existing).
- New config test covers override / `0`-passthrough / invalid-fallback
for all three env vars.
- Extended the existing `writer_pool_safety_hook_is_single_and_composed`
source-shape test so the timeouts can't drift out of the single
`after_connect` hook (SQLx replaces hooks — a second hook would silently
disarm the floor guard).
- `cargo fmt --check` and `cargo clippy --all-targets` clean for the
touched crates.

Closest existing PR/issue: none found.

---
**Update Aug 28, 17:06 EDT:** Rebased onto `main` at `a3730784fc` and
addressed the latest correctness review.

- Ported the timeout policy onto the refactored `buzz-db::runtime` pool
constructor and kept the shared env overlay for relay, admin, deletion,
and audit writers.
- Migration/schema-destruction connections now disable `lock_timeout`
and `statement_timeout` for their intentional long wait/DDL path. This
supersedes the earlier “Behavior change to note”: contended boot
migrations wait for the current migration owner rather than
crash-looping after five seconds.
- The audit worker now preserves and retries the same entry on
PostgreSQL `55P03` lock timeouts, using exponential backoff capped at
one second. Other database errors retain the existing terminal error
behavior, and retries emit `buzz_audit_log_lock_retries_total`.
- Added CI-backed PostgreSQL regressions for writer GUC
installation/migration exemption, audit-pool lock timeouts, and worker
recovery. The worker regression holds the real audit advisory lock past
`lock_timeout`, observes a retry, releases the lock, and proves the
original entry is appended exactly once.

Current verification supersedes the earlier testing notes: workspace
Rust clippy passed with warnings denied; all nine infrastructure-free
backend unit-test lanes passed; all three focused PostgreSQL regressions
passed against PostgreSQL 17; formatting, diff checks, file-size guards,
and desktop frontend checks passed. The Linux Blox workstation could not
run the unrelated Tauri native lane because `glib-2.0` is absent, so
that platform check is left to PR CI.


---
**Update Aug 31, 11:09 EDT:** Rebased onto current `main` at
`c3132c3ee9` and reran the requested audit-lock contention scenario on
Blox at head `896c3fe9ed`.

- `git range-diff` reports both PR commits unchanged by the rebase; the
branch remains two commits and the worktree is clean.
- `cargo fmt --all -- --check` and clippy with warnings denied passed
for `buzz-db`, `buzz-relay`, `buzz-admin`, and `buzz-deletion`.
- All three focused PostgreSQL 17 regressions passed: writer session
timeout/migration exemption, audit writer timeout bounds, and audit
worker recovery of the original entry exactly once.
- Live protocol verification used a head-built relay and CLI, native
PostgreSQL 17/Redis, `BUZZ_DB_LOCK_TIMEOUT_MS=300`, and an eight-second
hold on the community audit advisory lock. The real message was accepted
and persisted once while the lock was held; its audit-row count remained
zero during contention while retries accumulated. After release, exactly
one `event_created` audit row appeared and remained exactly one after an
additional two-second duplicate check. The run recorded nine
lock-timeout retries, zero audit failures, and event ID
`7f8c4ffae28e78555fcf2d56396d6e6c01b3712e5411288dc79e9a54af9d9444`.

Generated with Codex

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
2026-08-31 11:59:13 -04:00
Matt Toohey
c3132c3ee9 feat(desktop): use segmented controls for channel creation (#6845)
## Summary

- replace the two-option Type and Visibility dropdowns in the Create
channel dialog with single-click segmented controls
- keep Expires after as a dropdown and preserve the existing dropdown
controls in edit and management dialogs
- update channel creation end-to-end coverage for the direct controls

## Before

Default Ongoing/Public state:

![Create channel before - default Ongoing and
Public](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6845/before-default.png)

Type dropdown open, showing the extra selection click:

![Create channel before - Type dropdown
open](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6845/before-type-dropdown.png)

## After

Default Ongoing/Public state:

![Create channel after - default Ongoing and
Public](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6845/after-default-reversed.png)

Temporary/Private state with the Expires after row visible:

![Create channel after - Temporary and
Private](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6845/after-temporary-private-reversed.png)

## Verification

- Biome and TypeScript checks pass
- 5,508 desktop unit tests pass
- 88 channel smoke tests pass
- source guards pass

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:14:35 +10:00
Will Pfleger
eed74bde2f feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038)
## What

Two telemetry additions to make a known silent-death failure mode
visible in harness logs.

**1. `stop` field on `llm: call completed` INFO**
(`crates/buzz-agent/src/llm.rs`)

The `ProviderStop` value was already parsed and stored on `LlmResponse`
but never emitted in the log line. Without it, "model chose `end_turn`"
vs "gateway truncated/refused" is indistinguishable from telemetry
alone.

**2. WARN on silent-turn signature** (`crates/buzz-agent/src/agent.rs`)

Emits a `WARN` when a turn produces no publish, no visible assistant
text, and either near-zero or absent output tokens. The WARN logic is
extracted into `warn_if_silent_turn` (pure synchronous function) so the
seam is testable without the async run loop.

Three independent gates before the WARN fires:
1. **`!buzz_reply_call_seen`** — no publish attempt in any round,
tracked unconditionally via the existing `is_buzz_reply_call` matcher.
Read-only tool calls do NOT suppress the WARN; a turn that ran tools but
never published and died at 3 tokens is still a silent death.
2. **`text_is_empty`** — no visible assistant text in the final round. A
terse reply like "OK" (≤12 tokens, non-empty) is not a silent death.
3. **Token check** — two distinct WARN messages:
- `Some(t) where t <= 12`: near-zero token count, the observed failure
signature (2–12 tokens)
- `None` usage: provider omitted token counts entirely, separately
diagnostic

Tests use a scoped `tracing_subscriber` layer (same pattern as the
existing stall-warn tests in `llm.rs`) to exercise the WARN seam
directly:
- Canonical signature (no publish, no text, 4 tokens) → 1 WARN
- Non-empty assistant text → 0 WARNs
- Publish seen → 0 WARNs
- `None` usage (no publish, no text) → 1 WARN

## Why

Recurring silent-death incident in a specific agent×channel combination:
sessions die with 1 LLM call, 2–12 output tokens, no tool calls, no
message, no error — recorded as a "successful" turn. The harness log
shows the token count but not the `stop_reason`, leaving the root cause
undiagnosable without request-level tracing. The observed shape also
includes tool-step-then-3-token-death (one tool call, then silence) —
the publish-aware gate catches both shapes.

Context thread:
buzz://message?channel=91fd9ca1-cf04-4ef7-b18f-aa2aee55692b&id=e3f1693f2e29f26a0c840f8054d592270c1504beacdc1d9c2063d8ab82960a06

## Scope

Logging and telemetry only. No behavior change, no retry-logic change,
no stop-reason mapping change.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-29 19:14:28 -04:00
thomaspblock
00e61eafa9 fix(desktop): surface channel history load failures (#7013)
## Summary

A failed initial channel-history request no longer appears as an
authoritative empty channel. The timeline now shows an announced error
with a Retry action, while cached messages remain visible when a later
refresh fails; successful empty channels continue to use their normal
intro state.

### Related issue

None found.

### Testing

- Full desktop unit suite (`pnpm test`)
- Desktop TypeScript check (`pnpm exec tsc --noEmit`)
- Biome checks for changed files
- Repository file-size ratchet
- Full pre-push desktop checks and tests
- Desktop app launched successfully against local Postgres and Redis for
manual testing

No screenshot is included because the new UI is only shown after a
terminal relay-history failure; the regression test pins the
error/empty/list precedence directly.

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-29 03:23:25 +00:00
Taylor Ho
8dbc65d9e2 fix(composer): polish automatic mentions (#6956)
**Category:** improvement
**User Impact:** Automatic mentions are easier to turn off and now
behave consistently across conversations, settings, drafts, and repeated
agent mentions.

**Problem:** People found the new automatic mention behavior hard to
control: turning it off in Settings did not reliably affect the
composer, removing a mention could require also disabling the feature,
and root/thread composers could inherit or restore surprising state.
Other reported rough edges included only one of several mentioned agents
becoming automatic, synthetic mentions leaking into drafts, restored
mentions corrupting adjacent text, controls remaining visible in
archived channels, and unclear picker feedback. See the [original
feedback
thread](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=c949ec399274fbb0d6633da3f95712e843a67dcada214f63d72f6975b406604b).

**Solution:** Polish the existing feature around the problems people
encountered, keeping automatic mentions controllable and scoped to the
active conversation.

| Reported issue | UX fix |
| --- | --- |
| Turning automatic mentions off in Settings did not reliably update the
composer. | The global setting and composer control stay synchronized,
and disabling the feature does not clear typed text. |
| Removing an automatic mention could require both deleting the mention
and turning off the feature. | Removing or unchecking an agent excludes
that agent for the current conversation, while explicitly re-adding the
agent can restore automatic mention behavior. |
| Root and thread composers could share or restore surprising
selections. | Each root or thread composer keeps its own automatic
audience and restores it when the user returns. A request to enable
automatic mentions only in agent threads was considered; this PR keeps
them available at the channel root but prevents state from leaking
between the two. |
| Mentioning multiple agents could leave only one saved as automatic. |
Multi-agent selections remain represented in the automatic audience and
restored mention chips. |
| Automatic mention prefixes could be saved as if the user typed them. |
Synthetic prefixes stay out of persisted drafts while authored text is
preserved. |
| Restored mentions could lose their separator and corrupt continued
typing. | Restored multi-word mentions retain their trailing space and
place the caret after it. |
| Archived channels showed automatic-mention state beside a disabled
composer. | Disabled composers hide automatic-mention controls while
preserving the draft and restoring state when re-enabled. |
| Confirmation and picker behavior made the feature feel difficult to
inspect or adjust. | Confirmations dismiss with removed agents, remain
open while hovered, and expose the setting before it changes; pin icons,
contrast, scope copy, animation, and keyboard toggling are also
clarified. |
| Agent suggestions and membership state could shift during directory
refreshes. | Suggestions and membership labels stay stable during
refreshes, while send-time authorization still revalidates access. |

## Changes

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


**desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs**
Adds coverage for the channel-roster eligibility rules used by agent
mention autocomplete.

**desktop/src/features/agents/lib/agentAutocompleteEligibility.ts**
Aligns agent autocomplete eligibility with channel membership so
available agents and their labels stay trustworthy.

**desktop/src/features/channels/ui/MembersSidebar.tsx**
Uses the shared member-pubkey logic when presenting and acting on
channel members.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs**
Covers preference changes that must remain stable while composer
controls are toggled.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts**
Keeps the automatic-mention preference as durable user intent rather
than transient composer state.

**desktop/src/features/messages/lib/mentionMemberPubkeys.ts**
Centralizes which member identities count as mentionable in the current
channel.

**desktop/src/features/messages/lib/persistentAgentAudience.test.mjs**
Expands lifecycle coverage for persistent agent audiences, explicit
exclusions, and restored mentions.

**desktop/src/features/messages/lib/persistentAgentAudience.ts**
Models automatic, explicit, and excluded agent audiences separately so
user choices survive updates without leaking across composers.


**desktop/src/features/messages/lib/stripImplicitAgentMentions.test.mjs**
Verifies implicit automatic mentions are removed without damaging
surrounding separators or authored content.

**desktop/src/features/messages/lib/stripImplicitAgentMentions.ts**
Strips presentation-only automatic mentions before draft persistence
while preserving whitespace and authored text.

**desktop/src/features/messages/lib/useMentions.ts**
Routes mention insertion and removal through the composer-local audience
lifecycle.

**desktop/src/features/messages/lib/useRichTextEditor.ts**
Preserves mention-chip structure and caret placement when automatic
mentions are restored.

**desktop/src/features/messages/ui/ComposerAddressControls.test.mjs**
Updates control-state expectations for disabled automatic mentions and
restored pin affordances.

**desktop/src/features/messages/ui/ComposerAddressControls.tsx**
Makes automatic-mention state, disabled presentation, and pin controls
visually explicit.

**desktop/src/features/messages/ui/MentionAutocomplete.test.mjs**
Adds coverage for roster labels, pin state, and picker behavior after
mention selection.

**desktop/src/features/messages/ui/MentionAutocomplete.tsx**
Keeps the shortcut picker open for repeated selection and restores
visible automatic-mention pin indicators.

**desktop/src/features/messages/ui/MessageComposer.tsx**
Scopes automatic mention state to each root or thread composer and
coordinates restoration, draft persistence, and sending.

**desktop/src/features/messages/ui/MessageComposerToolbar.tsx**
Passes the effective automatic-mention state into the toolbar
presentation.

**desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs**
Updates keyboard interaction coverage for toggling agents in place.

**desktop/src/features/messages/ui/useAddressedAgentMentionRestore.ts**
Restores automatic mention chips after lifecycle changes without moving
or duplicating authored content.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Substantially expands coverage for toggles, exclusions, synchronization,
and picker dismissal rules.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**
Keeps the picker usable across repeated choices and preserves explicit
per-agent intent while settings change.

**desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts**
Makes the keyboard shortcut toggle the highlighted automatic audience
choice without replacing unrelated selections.

**desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts**
Owns composer-local automatic-mention lifecycle behavior, including
restoration, exclusions, deletion, and disabled-state handling.

**desktop/src/features/messages/ui/useComposerMentionPicker.test.mjs**
Adds focused picker lifecycle coverage for selection, hover, and
dismissal behavior.

**desktop/src/features/messages/ui/useComposerMentionPicker.ts**
Prevents premature picker dismissal while the user is interacting with
its controls.

**desktop/src/features/messages/ui/useDraftPersistSnapshot.ts**
Persists only user-authored draft content rather than implicit automatic
mention decorations.

**desktop/src/shared/lib/keyboard-shortcuts.ts**
Updates the automatic-mention shortcut description to match its toggle
behavior.

**desktop/src/testing/e2eBridge.ts**
Extends the desktop test bridge with the state needed to exercise roster
and automatic-mention transitions.

**desktop/tests/e2e/mentions.spec.ts**
Covers roster-based labels, managed-agent invitation, revocation, and
recovery behavior in the complete mention flow.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Adds end-to-end coverage for root/thread isolation, preference
synchronization, manual exclusions, draft hygiene, restored chips,
separators, hover behavior, and disabled presentation.

</details>

## Reproduction Steps

1. Open a channel with at least two available agents and enable
automatic mentions from the composer mention control.
2. Select multiple agents, remove or uncheck one, and confirm subsequent
composer updates keep that agent excluded while the others remain
automatic.
3. Open a thread, choose a different automatic audience there, and
switch between the thread and root composer; confirm each composer
retains only its own choices.
4. Disable automatic mentions and confirm the draft text remains
unchanged while automatic chips and controls show the disabled state;
re-enable the setting and confirm eligible automatic chips return.
5. Delete an automatic mention chip, then explicitly add the agent
again; confirm it immediately returns as an automatic mention without
disturbing spaces or the caret, including for a multi-word name.
6. Reload with a saved draft and confirm implicit automatic mentions
were not persisted as authored draft text.
7. Use the automatic-mention keyboard shortcut and picker repeatedly;
confirm the picker remains open for additional choices and the
highlighted agent toggles in place.

## Validation

Validated at `34d208b47d64a9816f88e10a46bcfd479e917d75` after rebasing
onto `origin/main` (`69096c9a8`):

- Desktop unit tests: 5,731 passed, 0 failed.
- Desktop TypeScript typecheck: passed.
- Desktop E2E build: passed; emitted only existing chunk and
dynamic-import warnings.
- `pnpm check`: exited successfully; 4 warnings and 5 informational
findings are in unrelated files introduced by current main.

## Screenshots/Demos

The behavioral changes are covered by the focused desktop E2E scenarios
above. Screenshots can be attached from the screenshot-producing
automatic-mention E2E after the PR is created.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com>
mobile-v0.16.0-rc.1
2026-08-28 17:11:04 -07:00
Will Pfleger
2c99ee7af5 fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904)
## Why

PR #6330 split agent harness/runtime detection into a cheap (cache-only)
path and a forced (spawning) path. Two regressions followed, both
surfacing as every harness showing "(not installed)" / "CLI missing"
across the agent create/edit picker, Agents > Agent defaults, and
Settings > Agents — blocking agent create/edit until the user clicked
Install in Settings > Agents.

## Root cause

One underlying bug, two victims:

- **Boot false-negative.** The resolve cache is in-memory, so it starts
cold on every launch. `resolve_command_cached` (the cheap path)
consulted only the Buzz-managed shim dirs plus that cold cache, and
`buzz_managed_command_path`'s allowlist structurally excludes
`buzz-agent`. The bundled sidecar could therefore never resolve on the
cheap path until a forced pass warmed the cache, so cheap-path surfaces
rendered all-missing at boot. App setup never warms the cache.
- **"Check again" hang.** `run_in_login_shell` used an untimeouted
`Command::output()`; a wedged login shell froze the whole forced
pipeline, leaving "Check again" spinning forever.

## What

- `resolve_command_cached` now also calls `resolve_workspace_command`,
resolving the bundled sidecar via a filesystem stat (no spawn) — the
same class of work the managed-shim check already performs. `buzz-agent`
can no longer report missing, even inside the boot warm window.
- New `discovery/bounded_command.rs` runs any discovery child under a
hard wall-clock deadline, polling with `try_wait` rather than blocking
on `wait()`. Stdout and stderr are piped to two drain threads whose
buffers share an aggregate `CAPTURE_LIMIT`; a breach fails closed (kill
the tree, return `None`), so a noisy or hostile probe can force neither
unbounded memory nor disk fill. Tree teardown runs on every exit path —
timeout, error, cap breach, *and* success — because a login-shell rc
file or auth CLI can legitimately background a descendant that would
otherwise outlive discovery. Ownership is deliberately asymmetric:
- **Unix:** the child leads its own process group (`process_group(0)`);
teardown is `SIGTERM` → bounded grace → `SIGKILL` on the group. A
descendant that leaves the group (`setsid`/`setpgid`) while holding a
pipe is not owned and may survive one probe, but can never hang or
unbound the helper: the Unix drains read nonblocking and end on
`WouldBlock` once teardown sets the stop flag, so the join returns
promptly without waiting on an escaped writer's EOF.
- **Windows:** the child is spawned `CREATE_SUSPENDED`, assigned to a
kill-on-close Job Object while frozen, then resumed. The job owns the
root before any descendant can exist and is created without breakaway,
so no writer can escape — a hard whole-tree guarantee, and closing the
job reaps the tree even after the root has exited. Any failure to
create, assign, or resume is fail-closed: the child is terminated and
reaped and the spawn returns `None` (discovery treats it as
command-not-found) rather than running unowned.
- Each login-shell candidate is bounded by a 10s timeout via that
helper, falling through to the next candidate on timeout instead of
aborting the resolve. The login-shell path cache is generation-aware: a
probe that loses to a concurrent refresh or lands mid-refresh returns
the authoritative cached value (or re-probes under the new generation)
rather than its own rejected local result, so a losing thread can never
settle the UI with a PATH-missing catalog while the cache holds a fresh
success.
- Warm the ACP runtime catalog once at `AppShell` mount and gate the
cheap-path surfaces on that pass. A module-level boot-warm state (`idle`
→ `pending` → `settled`/`failed`, deduped per launch) lets
`useAcpRuntimesQuery` present a cold catalog as *loading* while the
first forced pass runs and as a *retryable error* (carrying the probe's
real reason) if it fails, instead of blessing "every harness not
installed" as authoritative. A non-empty catalog always wins, so a
revalidation or later failure never blanks a good list; the gate only
overlays once the warm has started, so onboarding (which renders before
the warm) is unaffected. Deduping per launch also fixes the previous
per-remount re-fire.

## Verification

Unix teardown and the drain contract are runtime-proven by
`#[ignore]`-free tests that record a backgrounded descendant's real PID
and assert the helper returns promptly on both the success and timeout
paths without blocking on that writer. The generation-aware login-shell
cache is covered by deterministic tests through a `cfg(test)` injectable
probe seam that assert the function's return value under both
concurrent-refresh interleavings — the losing caller returns the peer's
committed success, and a mid-probe refresh forces a re-probe to the
fresh value. The Windows ownership contract has no CI lane, so
`bounded_command.rs` carries two `#[ignore]`-gated tests (spawn/assign
race, looped; and the timeout path) for a sanctioned run on a Windows
host. The boot-warm gate is covered by unit tests for the pure overlay
and the `startBootWarm` failure → retry → settle lifecycle.

Origin: [Buzz
thread](buzz://message?channel=5ef5d5bb-643f-4b87-bbf4-e8b64585ffeb&id=a4b1c4485de4d35cff0f914d4f4211c796f44f670e76de2ef9431f7e882c906e)

Fixes #6872
Related #6662

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-28 17:45:42 -04:00
Wes
b593c7d7fe perf(mobile): reduce cold startup and channel rendering delays (#6996)
Pinky is opening this PR on Wes’s behalf.

## Summary

Reduce two separately measured mobile delays without changing the relay
API or removing rich message rendering:

- Publish the finite channel-list snapshot without waiting for live
subscription setup.
- Batch active channel-list subscriptions into sorted, deterministic
chunks of at most 128 explicit channel IDs, retaining unchanged chunks.
- Install replacement chunks before retiring old coverage. Retain old
chunks across thrown replacement failures; filter callbacks to the
current relay/identity and still-desired channels; clean up
retired/in-flight work across disconnect and disposal.
- Scope the custom-emoji Markdown matcher to known shortcodes actually
referenced in the rendered content, rather than embedding the whole
community palette in every message’s regex. Preserve unknown literals,
shared-colon token boundaries, event-tag URL priority, content edits,
and code literals.
- Honor explicit zero retry hints without inventing a ten-second
session-wide gate, while preserving the ordinary live-subscription retry
backoff and any already-active gate.

## Matched performance results

Medians of three before and three after process-cold launches,
alternated on the same authenticated iPhone 17 Pro / iOS 26.5 simulator.
Before is mobile source at `e76c81968b65b0755b83efdd59dc3375c59ddf40`;
after is this production patch before two documentation-only comment
fixes.

First channel-list frame: 11.617s → 3.179s · 73% lower latency

Live setup duration: 8.475s → 0.185s · 98% lower latency

Channel-open first message-list frame: 2.754s → 1.230s · 55% lower
latency

Message data ready → first frame: 1.977s → 0.286s · 86% lower latency

Channel-open reveal complete: 2.845s → 1.394s · 51% lower latency

Channel-open data readiness: 0.770s → 0.944s · 23% higher latency

The gain is client-side orchestration/rendering, not a claim that the
relay became faster. First channel-list frame ranges were 10.835–11.788s
before and 2.872–3.395s after; channel-open first-frame ranges were
1.560–2.906s before and 1.149–1.317s after.

### Measurement boundaries

- Debug simulator builds, CPU sampling disabled, bounded timestamp
probes enabled identically. These are not release/physical-device
measurements.
- Startup clock starts at Dart `main`; build/install/native pre-main
time is excluded. Auth/preferences and OS/disk caches are retained
between new processes.
- Same account scale: 113 active channels. Latest-message events varied
slightly with live activity (1543–1546).
- Channel-open uses the same initial 50-row history window, 97 query
events, and 67 provider events. The 2306-entry emoji palette is
explicitly loaded before navigation on both sides; palette preparation
is excluded from the channel-open clock and happens after the startup
frame measurement.
- Both diagnostic builds temporarily disabled unused avatar segmentation
to work around the existing Google ML Kit arm64-simulator slice
limitation. The workaround, dependency/native changes, auto-navigation,
and all probes are excluded from this PR.

## Validation

- Full mobile package suite: `flutter test` — 1890 passed.
- `just mobile-check` — 506 files unchanged; analyzer clean.
- `just file-size-check` — policy tests and all client ratchets passed.
- `git diff --check` — passed.
- New lifecycle regressions cover front-sorting insertion across a chunk
boundary while replacement readiness is paused, failure
retention/departed-channel filtering, retired generation + disconnect
cleanup, disposal, chunk limits, unchanged-set reuse, and scope
switches.
- Emoji unit/widget coverage includes a 2500-unused-emoji palette,
unknown tokens, case matching at the component level, shared-colon
boundaries, rich text, event URL priority, and content edits.
- Fresh-frame source review traced the subscription queue/fences,
callback scopes, duplicate-event paths, matcher/wiring, and retry
scheduling.
- At committed/pushed head `13a83b628c8411c5885e6f76a250ba87accf6067`,
all normal pre-push hooks passed: `mobile-checks` (formatter, analyzer,
and the full 1890-test mobile suite), `file-size-check`, `branch-skew`,
and `push-head-scope`. The commit hook formatted 506 files with no
changes. Runtime measurements preceded only the two
documentation-comment fixes; no runtime source changed afterward.

## Limits / follow-ups

- `RelaySession.subscribe` still settles under its existing
EOSE/fallback/retryable-CLOSED contract. “Setup completed” is not an
unconditional EOSE or live-delivery guarantee. This PR does not add
status-aware replacement ownership.
- The channel-message provider still awaits subscribe before fetching
history; that separate serialization is not removed here.
- Oversized Huddle queries and the separate history batching path above
128 active channels remain follow-ups, as do pre-existing read-state
initialization/size warnings.
- Palette-only widget refresh and upstream Markdown uppercase-dispatch
behavior are not changed.
- A clean source build still has the existing Google ML Kit
arm64-simulator issue; the profiling workaround is not a proposed
product fix.

Originating Buzz conversation:
buzz://message?channel=793b0522-7995-4375-b1a6-fd94a96fa21d&id=6ba88afdec78ab2cfb6728afcd4a6d10f29e6aa33ff0f62f45d6750381e4d789

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-28 15:38:03 -06: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
Luke Tornquist
a3730784fc refactor(db): extract domain stores from database runtime (#6987)
## Summary

Finish the remaining database-store extraction tracked by
[TheSentinel454/buzz#2](https://github.com/TheSentinel454/buzz/issues/2)
in one reviewable PR.

This consolidates the previously stacked domain slices after #6782
merged. It preserves the runtime/store boundary established by #6660,
#6668, #6700, and #6782 while separating database runtime infrastructure
from domain-owned persistence:

- `runtime/` owns pool construction and sizing, writer/reader routing,
read sessions and route proofs, transaction infrastructure,
observability primitives, replica fencing, health support, migrations,
and cross-cutting runtime tests.
- `store/` owns domain records, SQL, row parsing, locks and invariants,
`Db` domain methods, focused tests, and logical-operation datastore
spans.
- `lib.rs` remains a 57-line compatibility facade that preserves
existing crate-root paths and `Db` method signatures through re-exports.

Domain coverage includes API tokens, authentication allowlists,
reminders, event queries, threads, reactions, feeds, users and DMs,
push, workflows/runs/approvals, relay membership and invites, product
feedback, moderation/admin moderation, relay admin actions/operators,
git repositories, archived identities, usage, partition maintenance,
deletion, channel membership inherited from merged #6782, and the final
runtime/store layout.

The branch has been rebased onto current `main`. Database changes that
landed there were incorporated rather than overwritten:
`relay_admin_actions.rs` and `relay_operators.rs` now live under
`store/`, their 27 public `Db` wrappers and existing behavior remain
intact, and every wrapper has exactly one fixed-name datastore span.
Concurrent changes to migration, moderation, admin moderation, and error
handling are also retained.

### Exact base and head

- Base: `main` at `ed11c8d8bf0a17402be5cf243724f89471530d2f`
- Head: `codex/issue-2-store-extraction` at
`be24430472d1a87ac5c0d6026c620cd6caea3537`

### Related issue

- Structural tracker:
[TheSentinel454/buzz#2](https://github.com/TheSentinel454/buzz/issues/2)
- Domain trackers:
[#6](https://github.com/TheSentinel454/buzz/issues/6),
[#7](https://github.com/TheSentinel454/buzz/issues/7),
[#12](https://github.com/TheSentinel454/buzz/issues/12),
[#13](https://github.com/TheSentinel454/buzz/issues/13)
- Acceptance trackers:
[#17](https://github.com/TheSentinel454/buzz/issues/17),
[#19](https://github.com/TheSentinel454/buzz/issues/19)

This supersedes #6783, #6784, #6787, #6788, #6789, #6792, #6820, #6794,
#6796, #6797, #6798, #6799, #6804, #6805, #6806, #6808, #6809, #6811,
#6812, #6813, #6814, #6815, and #6890. Their discussions remain
available for review history.

### #17 / #19 acceptance

- Preserves the metric names, fixed labels, transaction/lock timing
boundaries, and privacy/cardinality constraints introduced by #6700.
- Keeps exactly one datastore span per public logical operation,
including the 27 relay-admin wrappers added on `main`.
- Removes `store_ownership.rs`; physical ownership and focused source
guards now enforce the boundary directly.
- Leaves no `impl Db`, domain SQL, focused domain test group, or
datastore span in `lib.rs`.
- Preserves existing public paths such as `buzz_db::channel`,
`buzz_db::event`, and `buzz_db::workflow` through crate-root re-exports
while keeping internal `runtime` and `store` namespaces private.

### Non-goals

- No SQL, schema, locking, transaction, retry, timeout, or
client-visible behavior changes.
- No generic store traits, domain handles, broad `PgExecutor` migration,
new store crate, raw pool accessor, or broader directory reorganization.
- No tracker issues are closed by this PR.

### Risk

The cumulative diff is large but structural. Risk is primarily
module-path, ownership, or conflict-resolution drift. It is mitigated by
preserving public re-exports, comparing the newly moved `main`
implementations to their upstream source, source guards, touched-crate
compilation, PostgreSQL-backed test coverage, and an independent
exact-head review on a separate clean Blox workstation.

### Testing

Author workstation `buzz-tornquist-pr-6987-rebase`, rebased branch
ending at exact head `be24430472d1a87ac5c0d6026c620cd6caea3537`:

- `cargo fmt --all --check`
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings`
- `cargo test -p buzz-db --lib` — 113 passed, 240 PostgreSQL tests
intentionally ignored
- `cargo test -p buzz-db --test observability_source` — 2 passed
- PostgreSQL-backed `buzz-db` coverage under native PostgreSQL — 235
passed in the shared serial run; the five shared-state/config-sensitive
cases passed as isolated reruns against fresh schemas, including the two
owner-limit tests with their fixture's
`BUZZ_MAX_COMMUNITIES_PER_OWNER=3`
- `cargo test -p buzz-relay --lib -- --test-threads=1` under native
PostgreSQL/Redis — 991 passed; the three current-month
partition-sensitive identity-archive cases passed after provisioning the
August 2026 test partition; 87 infrastructure-marked tests remained
ignored
- Source/diff guards — relay-admin implementation bodies match current
`main`; all 27 public wrapper signatures are retained; exactly one
datastore span wraps each wrapper; `lib.rs` has zero `impl Db` blocks
and zero datastore spans; no duplicate top-level relay-admin modules or
`store_ownership.rs`; `error.rs` matches current `main`

Independent clean review workstation `buzz-tornquist-pr-6987-review`,
detached at exact head `be24430472d1a87ac5c0d6026c620cd6caea3537`:

- `cargo fmt --all --check`
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings`
- `cargo test -p buzz-db --lib` — 113 passed, 240 ignored
- `cargo test -p buzz-db --test observability_source` — 2 passed
- Exact-head ownership/re-export/instrumentation audit — no remaining
actionable findings

---------

Signed-off-by: OpenAI Codex <codex@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
2026-08-28 15:24:32 -04:00
Will Pfleger
2f66ee24a0 feat(desktop): add team sharing to community catalog (#3995)
Stack: [#5112](https://github.com/block/buzz/pull/5112) → this PR

Stacks on the team catalog backend PR (#5112). Contains the `desktop/**`
changes that turn the add-agent surface into a single Community Catalog
browsing both shared agents and shared teams.

## Owner catalog sync pipeline

`usePersonaSync.ts` gains a hydration pipeline in `startPersonaSync` so
a fresh device that comes online after another already published the
owner's 30178 catalog head reconstructs the complete state without
falsely retracting it. The owner's
persona/team/managed-agent/30178/deletion history is backfilled up front
(a live-only subscription gets no history — reconnect-replay's
since-cursor is undefined until the first live event), then a live
subscription takes over.

Four properties keep a fresh sync from purging the owner's valid shared
head:

- **Paged backfill with a safe termination guarantee.** The relay serves
each REQ newest-first and clamps `limit` to its advertised `max_limit`,
so a large owner's history overflows one page — a newer 30178/30176
could return in-page while an older required 30175 constituent falls
beyond it. `fetchOwnerHistoryToExhaustion` pages the full window with
the `until` time cursor (the only cursor the WS REQ filter exposes; the
DB `before_id` keyset is REST-only), deduping the inclusive-boundary
rows. A short page terminates normally. A full page whose oldest event
cannot advance the time-only cursor is a **dense boundary** — more than
one page of events share one `created_at` second, which the WS filter
has no `(created_at, id)` cursor to escape — and raises
`PersonaHistoryDenseBoundaryError` rather than silently completing as if
the history were exhausted and dropping the older constituents behind
it.
- **Constituents-before-catalog ordering.** Over the complete batch,
`orderCatalogHeadsLast` stably defers every 30178 head past its
30175/30176 constituents. Reconciling a 30178 head before its personas
hydrate makes the inbound team refresh fail member resolution and queue
a dominating false tombstone; deferring the heads guarantees the
constituents are all applied first, while newest-wins order within every
other coordinate is untouched.
- **Hydration boundary for concurrent live events.** The backfill fetch
and the live subscription start concurrently into one reconcile chain. A
live or replayed 30178 that arrives before the backfill reconciles its
constituents would reproduce the same false-tombstone purge, so live
events are buffered until the ordered backfill is dispatched and then
drained in arrival order. Steady-state live events (after hydration
completes) reconcile immediately.
- **Explicit backfill failure policy.** A transient history-fetch
rejection is retried with bounded backoff. When backfill cannot complete
— retries exhausted, or a deterministic dense boundary — the pipeline
enters a **degraded-live** state rather than leaving the subscription
permanently inert with live events accumulating in the buffer: the
hydration boundary still opens so buffered and future live events keep
reconciling, but the whole catalog dependency set is dropped — the 30178
head, its 30175/30176 constituents, and any kind-5 deletion carrying a
dependency-targeting `a` tag (classified by scanning *all* `a` tags,
matching the backend's deletion router, which `find_map`s across every
tag and routes the first signer-owned coordinate — so a malformed or
foreign first `a` tag ahead of an owned 30176 cannot slip a destructive
deletion through) — because backfill never fully hydrated the owner's
constituents. Dropping only the 30178 head is not enough: the backend
refreshes the catalog head after every team/persona save, and live
delivery is newest-first, so a 30176 edit that adds a *new* member would
reach the backend before that member's 30175 and falsely tombstone a
witness-holding device's valid team. Holding the prior hydrated run's
constituents on disk only proves the *old* revision is resolvable — it
says nothing about a new member — which is why the entire dependency set
is held rather than just the head. 30177 managed-agent runtime policy
stays live (it drives no catalog refresh). A degraded device stays stale
on team/persona edits until it self-heals on the next effect re-run
(restart, or an identity/community switch) — the correct trade against
destroying valid shared state.

## Data layer

Relay paging, signature verification, NIP-33 head selection, and
untrusted-content parsing for the kind `30178` team catalog live
natively in the `fetch_team_catalog` Tauri command (`team_catalog.rs`),
structurally mirroring `fetch_persona_catalog` (`persona_catalog.rs`). A
catalog refresh crosses IPC once and never verifies a signature on the
webview thread. `teamCatalogRelay.ts` is now a thin presentation and
local-linkage layer over the verified projection — it shapes entries for
display and links each to a local team, and never parses or verifies.

Parsing is all-or-nothing, identical to the add-time re-fetch in
`add_team_from_catalog`: a team with any invalid member fails to parse
and the publication is dropped from the catalog, matching persona
behavior. **Behavior delta:** the previously reviewed partial-render of
invalid-member teams — a warning banner on an entry that could never be
added — is removed. Invalid publications are dropped entirely rather
than surfaced as un-addable.

## Hooks

`useTeamCatalogRelay.ts` mirrors the persona catalog hook: a
community-keyed query over `fetch_team_catalog`, live invalidation on
kind `30178`, share/unshare, and add-from-catalog (which invalidates
both the teams and personas stores, since adopting a team copies its
members as local personas).

## CommunityCatalogDialog

Single unified surface replacing the former separate dialogs. Agents and
Teams appear as labeled sections with type-tagged selection and a
teams-preferred launch. `TeamsSection`'s discover entry and the
new-agent card both open this one dialog. `PersonaCatalogDialog.tsx` is
removed; persona browsing now lives inside the unified dialog.

## TeamShareDialog

Publishes and unshares team catalog entries via `set_team_shared`.

## e2e + screenshots

`team-catalog.spec.ts` covers the browse + adopt flow;
`team-catalog-screenshots.spec.ts` produces the pixel-regression set.
`e2eBridge.ts` gains `mockTeamCatalogPublications`, which mirrors the
native command's head selection and shared gate and performs only a
shallow `v`/`name`/`members`-array shape check; per-member validation
stays in the Rust command.

## Follow-ups

None. The shared agent-definition text-safety policy
(Unicode-control/bidi/zero-width rejection) already covers the team
surface: #5112's `validate_team_catalog_content` chokepoint gates every
field delivered verbatim to the harness on both the publish and adopt
paths, and this PR's parse layer consumes that verified projection
rather than re-validating on the webview thread.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-28 14:51:25 -04:00
klopez4212
ed11c8d8bf Refresh mobile utility surfaces and theme picker (#6944)
## Summary

- invert mobile utility surfaces so pages and sheets use the softer page
background with raised containers
- rebuild the community theme picker around swipeable Home/Chat
previews, accent and appearance controls, and native iOS glass
interactions
- align shared sheet/profile spacing and radii, and move Theme into the
Community settings card

### Related issue

None found.

### Testing

- `just mobile-check`
- `just mobile-test` (1,871 tests)
- signed iOS Release build installed on a physical iPhone

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-08-28 18:56:42 +01:00
thomaspblock
a89f5df4c5 fix(desktop): complete project empty and context states (#6980)
## Summary

First-time project users can now create a project directly from the
empty state instead of reaching a dead end. The right-side project
context also uses the intended Activity label and aligns its first row
with the channel header.

The populated and empty project views share one creation flow, so
navigation, success feedback, and compatibility warnings remain
consistent.

### Related issue

Related: #6939

### Testing

- Desktop check, TypeScript typecheck, and repository file-size gate
- 10 focused project overview context unit tests
- E2E-mode build and 3 focused Playwright scenarios covering empty-state
creation, right-sidebar alignment, and Activity context

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-28 10:52:03 -07:00
klopez4212
58cc4b7e9b Fix mobile jump-to-latest flicker (#6807)
## Summary

- Keep jump-to-latest hidden through composer and keyboard layout
changes while the user is still following the tail.
- Stabilize thread tail detection during lazy scroll updates.
- Unmount inactive iOS Liquid Glass controls so the channel arrow cannot
bleed into an opened thread.

## Testing

- just mobile-check
- flutter test (1,864 passed)
- Signed iOS Release build and install on iPhone Air

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-28 17:12:36 +01:00
Will Pfleger
86b9142a09 refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777)
Adds authenticated, role-based moderation to the relay admin API. On
`main` the admin API is read-only and gated only by `Host`/`Origin`
matching; this branch adds NIP-98 authentication, a two-tier
Operator/Moderator principal model, mutation and staffing endpoints, and
NIP-11 auto-discovery so clients never type the admin URL by hand.

## Authentication (`BUZZ_ADMIN_AUTH`)

`BUZZ_ADMIN_AUTH` accepts `nip98` or `disabled`. Leaving it unset
defaults to `nip98` (fail-secure). Configuration fails closed: any other
value aborts startup, while a lingering `BUZZ_ADMIN_TOKEN` is ignored
with a startup warning — token (bearer) authentication is not supported.
`Host`/`Origin` matching is retained in every mode as defense-in-depth.

- **`nip98`** (default) — per-request signed NIP-98 (kind 27235) events,
resolved to an Operator or Moderator principal with per-person
attribution and individual revocability. Read-write per resolved
principal.
- **`disabled`** — no credential; relies entirely on network-layer
controls (reverse proxy, VPN, firewall) and logs a `WARN` on every boot.
Always read-only: `authorize()` resolves no principal, so mutation and
staffing routes always `403`.

## Roles

Buzz has two independent authority axes after this change.
**Relay-level** roles (new here) are deployment-global: they act across
every community on the relay, through the admin API. **Community-level**
roles (pre-existing, unchanged by this PR) are tenant-scoped: they act
inside one community, through signed Nostr moderation commands.

### Relay level (new)

| Role | Description |
|---|---|
| **Operator** | Full control of the deployment's moderation surface:
read all reports, feedback, and attachments across every community;
resolve reports with enforcement (`delete`/`kick`/`ban`/`timeout`) or
decisions (`dismiss`/`escalate`); reopen and cancel; update feedback
status; and manage the Operator/Moderator roster via the staffing
endpoints. |
| **Moderator** | Day-to-day triage: everything an Operator can do
except staffing — cannot view or change the roster. |

How a pubkey acquires a relay role (resolution order; config always
outranks DB):

1. Listed in `RELAY_OPERATOR_PUBKEYS` → **Operator** (source `config`)
2. Equals `RELAY_OWNER_PUBKEY` while `RELAY_OPERATOR_PUBKEYS` is empty →
**Operator** (source `owner_fallback`, a break-glass grant for
self-hosters that deactivates once any operator is configured)
3. Row in the `relay_operators` table → **Operator** or **Moderator**
(source `db`, managed via the staffing endpoints)
4. No match → `403`

### Community level (pre-existing, unchanged)

| Role | Description |
|---|---|
| **Owner** (community) | Full authority within their community: every
moderation action (delete, kick, ban/unban, timeout/untimeout, resolve
reports, view queue) plus member, role, and invite management. No guard
rails. |
| **Admin** (community) | Same community-wide moderation capabilities as
owner, except an admin cannot ban or time out the owner or a fellow
admin — only the owner may action an admin. Manages members and invites;
only the owner grants the admin role. |
| **Member** (community) | Standard participant; no moderation
capability. |
| **Owner / Admin** (channel) | Channel-local authority only: delete
messages and kick users within their own channel. |
| **Member / Guest / Bot** (channel) | No moderation authority. |

There is no community-level Moderator tier in v1; relay-level Moderator
is the only role by that name.

## Escalation scoping

The operator report queue is an escalation backstop, not the community's
day-to-day triage surface (per `VISION_MODERATION`, the severe class is
the platform's to review rather than the community's). Two rules enforce
that:

- **Escalated-by-default listing.** `GET /reports` with no `status`
parameter returns only `escalated` reports. An explicit
`status=<open|resolved|dismissed|escalated>` filter is always honored as
given, and full visibility across every status stays available for
platform-safety and legal review via `scope=all` (which lists reports
regardless of status). `scope` accepts only `all` and is ignored when an
explicit `status` is present.
- **Auto-escalated `illegal` reports.** Member reports whose category is
`illegal` are ingested with `status=escalated` rather than `open`, so
the severe class reaches the operator backstop without waiting for a
community admin to forward it. Every other category still lands `open`.
Auto-escalation only sets the queue status — it records no moderator
decision and stamps no resolver, so an auto-escalated report is
indistinguishable downstream from an admin-escalated one: the reopen
route returns it to `open` on the same terms, keyed only on status,
never on how the report became escalated.

## Principal resolution and NIP-98 admission

`resolve_admin_principal()` returns `AdminPrincipal { pubkey, role,
source }` per the resolution order above; `None` never falls through as
a role. Admission is ordered so the replay guard is a privilege, not a
public surface: signature/URL/method/payload-hash verification first,
roster check second, and only then is the deployment-scoped replay id
atomically consumed — a validly-signing but unrostered key never
allocates a replay slot. Redis failure fails closed.

## Report resolution, recovery, and enforcement provenance

`POST /reports/{id}/resolve` is a crash-safe enforcement state machine:
decision-only outcomes (`dismiss`/`escalate`) are a single
CAS-plus-audit transaction; enforcement
(`delete`/`kick`/`ban`/`timeout`) claims the report
(`open`→`processing`), runs the durable mutation, then finalizes — a
re-drive resumes at the step marker and converges to exactly-one
enforcement, fenced by a lease and an outbox claim token.

Person-directed enforcement on an `event`-kind report derives its target
from the stored event's author (server-owned truth, never the reporter's
`p` tag) via a single `derive_enforcement_target` shared by the HTTP
driver and the recovery worker. If the reported event was purged before
its author could be read, person-directed actions are rejected pre-claim
and the report stays `open`; `delete` needs only the event id and is
exempt.

`GET /reports/{id}` and the resolve response carry an `activeAction`
field surfacing the enforcement that actually executed — a report
dismissed after a reopen still reports the ban that ran. `POST
/reports/{id}/reopen` returns a terminal report to `open` (idempotent on
`requestId`). `POST /reports/{id}/cancel` is the sole recovery path for
a pre-mutation `failed` action, attributed via
`relay_admin_actions.cancelled_by`.

## Feedback

`GET /feedback` and `/feedback/{id}` survive a tenant purge: provenance
columns are severed to `NULL` rather than cascade-deleted, and the
attachment path fails closed to `404` on a severed row. `PATCH
/feedback/{id}` updates lifecycle `status`
(`new`/`reviewed`/`archived`).

## Staffing and probe

`GET/PUT/DELETE /operators/{pubkey}` are Operator-only; mutating a
config-backed pubkey returns `409 Conflict`. `GET /operators` returns
the union of config and DB principals with per-entry `source`. `GET
/probe` reports auth mode, role, source, `canAct`, and `canStaff` for
the desktop console.

## NIP-11 auto-discovery

The NIP-11 relay-information document gains an optional `admin_api`
field carrying the canonical admin origin (`scheme://host[:port]`, no
path), present iff `BUZZ_ADMIN_HOST` is set and omitted otherwise. The
scheme follows the same loopback rule as NIP-98 `u`-tag verification via
a shared `scheme_for_host` helper, so the advertised origin and the
origin the relay verifies against can never diverge.

## Operator API origin decoupling

`RELAY_OPERATOR_API_ORIGIN` is no longer required at boot when
`RELAY_OPERATOR_PUBKEYS` is set — it is used only by the
community-provisioning endpoints, which fail closed at request time
(with a boot-time `WARN`) until it is set. The admin console needs no
origin.

## Admin-web adaptation

The standalone `admin-web` dashboard signs each request as a NIP-98
event via a NIP-07 browser extension, discovers the auth mode with a
single unauthenticated probe (`200` → `disabled`, anything else →
`nip98`, fail-secure), and carries no token entry surface. Playwright
coverage exercises the NIP-98 and CSP paths.

## Security hardening

Three findings from security review are folded in:

- **Append-only roster audit.** `PUT`/`DELETE /operators/{pubkey}`
mutate the deployment-wide root of trust, but the upsert overwrites
`role`/`added_by` in place and the delete removes the only row — so a
grant→revoke sequence left no trace of who was ever granted or by whom.
Each mutation now writes an `relay_operator_audit` row (actor, target,
`grant`/`revoke`, pre-image `prev_role`, `new_role`, timestamp) inside
the same transaction as the mutation. A per-target transaction-scoped
advisory lock serializes concurrent mutations of the same pubkey before
the pre-image read, so the recorded `prev_role` is always the true
predecessor even under a concurrent-grant race. Chronology is keyed on a
`BIGINT GENERATED ALWAYS AS IDENTITY` `seq` column, not the wall clock:
the serializing lock guarantees insertion order and `seq` captures it,
so ordered reads (`ORDER BY seq`) follow the true privilege chain even
across a backward NTP step that a `clock_timestamp()` ordering would
invert. `created_at` (`clock_timestamp()`) is retained as informational
occurrence time only. Append-only by construction — no `UPDATE`/`DELETE`
path and no API surface. A no-op delete writes nothing.
- **`expirationSecs` overflow.** The timeout path built `Utc::now() +
Duration::seconds(secs as i64)` from an attacker-controlled `u64`:
`i64::MAX` panicked the handler, and a wrapped-negative magnitude minted
a *past* expiry that still passed validation. `compute_timeout_until`
now rejects zero, rejects magnitudes above a documented
`MAX_TIMEOUT_SECS` (365 days), and uses checked
`try_seconds`/`checked_add_signed` so no input can panic or produce a
past expiry — over-cap, zero, `i64::MAX`, and wrapping-negative inputs
all return a clean `4xx`.
- **Uppercase-hex config-backed bypass.** Config pubkeys are lowercased
at parse, but the `409` immutability check raw-string-compared the path
param while `decode_hex_pubkey` accepted uppercase — so `PUT
/operators/{UPPERCASE}` skipped the guard and wrote a shadow row for the
same 32 bytes. The validated param is now canonicalized (lowercased)
before the `409` check, DB write, `DELETE`, and response body.

## Migrations

- `0035_relay_operators.sql` — `relay_operators` roster table
(deployment-global), `actor_authority` on `moderation_actions`,
`processing` status plus `active_action_id` on `moderation_reports`,
`status` on `product_feedback`.
- `0036_relay_admin_actions.sql` — enforcement-action table with a
`request_id` idempotency key, a `step_marker` for crash recovery, and a
`cancelled_by` attribution column.
- `0037_relay_admin_action_lease.sql` — lease fencing for the action
worker.
- `0038_relay_admin_outbox_claim_token.sql` — fenced claim token on the
outbox worker.
- `0039_relay_operator_audit.sql` — append-only `relay_operator_audit`
trail for roster mutations (see Security hardening).

`docs/admin/README.md` documents the full principal model, NIP-98 event
requirements, capabilities by role, the startup error matrix, and the
discovery field.

## Production blast radius

A relay without `BUZZ_ADMIN_HOST` is completely unaffected — the admin
surface stays disabled and `BUZZ_ADMIN_AUTH` is ignored; a lingering
`BUZZ_ADMIN_TOKEN` logs a startup warning and must be removed. Where
`BUZZ_ADMIN_HOST` **is** set, unset `BUZZ_ADMIN_AUTH` defaults to
`nip98` (per-person signed auth); `BUZZ_ADMIN_AUTH=disabled` reproduces
`main`'s prior `Host`/`Origin`-only gating but is read-only (mutation
routes `403`). The five migrations add tables and columns without
touching existing data.

---

Related: [block/buzz#4768](https://github.com/block/buzz/pull/4768)
(desktop admin console consuming the `admin_api` field),
[squareup/bb-public#339](https://github.com/squareup/bb-public/pull/339)
(Phase 4 rollout config)

---------

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-08-28 11:43:05 -04:00
Luke Tornquist
e76c81968b refactor(db): split channel membership store (#6782)
## Current reconstructed head

Exact base: `codex/issue-7-roster-test-fixtures` at
`5df440c411be9705eb29a57f0c41f7239767e007`
Exact head: `codex/issue-7-channel-membership-store` at
`8ad0782ee311f5f51b714494ce750c5937f127cc`

This current head removes `crates/buzz-db/tests/store_ownership.rs`; no
replacement path-sensitive ownership test is introduced. Apart from
removing that complete test-file diff, the production patch is
byte-for-byte identical to the previously reviewed slice. This remains
part of tracker #2 and the #17/#19 acceptance work.

Independent exact-head review from a separate clean Blox workstation
found no issues. Current-head evidence passed formatting, strict
`buzz-db` clippy, 111 non-PostgreSQL library tests with 200 PostgreSQL
tests ignored, the observability source test, relay consumer
compilation, exact ownership/unique-span review checks, and 3 channel
and 19 membership PostgreSQL tests on native PostgreSQL where
applicable.

## Why
Complete the channel ownership slice of [tracker
#2](https://github.com/TheSentinel454/buzz/issues/2) and [domain issue
#7](https://github.com/TheSentinel454/buzz/issues/7) while preserving
the runtime/store boundary established by #6660 and #6668. This child
stacks on the test-only fixture prerequisite #6819 above #6777 and
carries forward PR #6700's membership/replacement lock timing without
changing lock or transaction behavior.

## What
- Keep channel lifecycle, metadata, TTL advisory locking, and lifecycle
tests in `channel.rs`
- Move membership/roster records, SQL, advisory-lock helpers, `Db`
methods, focused tests, and datastore spans to a dedicated
`channel_members.rs`
- Preserve existing `buzz_db::channel::*` paths with compatibility
re-exports while exposing the dedicated module
- Move the four roster-fence PostgreSQL tests out of `lib.rs`

## Stack
- Exact base: codex/issue-7-roster-test-fixtures at
21d1b265c133292e6707e766cd4204e6a43f08af
([#6819](https://github.com/block/buzz/pull/6819))
- Exact head: codex/issue-7-channel-membership-store at
25138bfd6588e046170dbdbc4ed953bdc3cf7ed1
- Tracker: https://github.com/TheSentinel454/buzz/issues/2
- Domain: https://github.com/TheSentinel454/buzz/issues/7
- Test/span acceptance: https://github.com/TheSentinel454/buzz/issues/17
and https://github.com/TheSentinel454/buzz/issues/19

## Non-goals
- No SQL, schema, retry, timeout, lock ordering, transaction boundary,
or client-visible behavior changes
- No change to channel TTL lifecycle ownership merely because lifecycle
bootstrap writes an owner membership row
- No store traits, domain-handle redesign, broad `PgExecutor` migration,
raw pool accessor, new crate, or directory-wide reorganization
- No changes to, retargeting of, or merge action on PR #6700 or #6777

## Risk Assessment
Moderate review surface, low semantic risk. The file split is large, but
method signatures, SQL, bind order, membership and replacement lock
namespaces, transaction boundaries, and span names remain unchanged.
Compatibility re-exports preserve existing `buzz_db::channel::*`
consumers.

## Blox Verification
Author workstation: `buzz-tornquist-issue-2-store-stack` (`2046520`),
exact head `8376e19d0da3ec77550590cd91cc3dfe284d95d6`.

- `cargo fmt --all --check` — passed
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` —
passed
- Native PostgreSQL channel lifecycle suite — 3 passed
- Native PostgreSQL membership/roster suite — 17 passed; two
pre-existing ignored-test fixture failures reproduced identically on the
untouched parent `2de5444`:
`large_roster_reconciliation_candidates_respect_snapshot_count_and_signer`
and `locked_member_snapshot_blocks_post_capture_membership_mutation`
both receive the migration-0032 `23514` invalid-`p`-tag rejection. This
extraction intentionally does not fold a test-behavior fix into the
move.
- `cargo test -p buzz-relay --lib -- --test-threads=1` — 908 passed, 48
ignored; the existing load-sensitive mesh demo test returned 504,
matching the #6700/parent baseline
- `cargo test -p buzz-relay --lib
api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo --
--exact --test-threads=1` — passed

Independent exact-head review: `buzz-tornquist-pr-6782-review`
(`2048397`) found no remaining critical, important, or minor issues. The
full implementation review also independently reproduced both stated
PostgreSQL fixture failures on the exact parent and passed the relay
library suite (909 passed, 48 ignored).

Generated with Codex

## Superseded pre-comment restack verification

PR #6700 merged before publication completed. This layer was restacked
onto current main through the exact parent named above; the final
cumulative tip is 2ddcc8a29f95c8c8c9cb87bb6cf59335f2190fae. Cumulative
author gates passed: formatting and diff checks; buzz-db and buzz-relay
all-target clippy with -D warnings; DB lib 111 passed / 200 ignored;
ownership 22/22; observability 1/1; the full isolated PostgreSQL domain
matrix; and relay lib 910 passed / 49 ignored.

- Workstation: `buzz-tornquist-pr-6782-final-review` (`2057620`), fresh
shallow checkout
- Base: `c60e793eadde79d9eab9f48bbb2ede0ad4831f9b`
- Head: `fa09b6c81c4db3b3e1940a2117a97ab2186e49f7`
- Findings: none

Reviewed both commits in `base..head`. Channel lifecycle/metadata, TTL
transitions, and their lock rationale remain in `channel.rs`; membership
authorization, roster fencing/snapshots, membership advisory locking,
membership records, and focused tests move together to
`channel_members.rs`. SQL, transaction, and lock sequences are
preserved.

Verification: format and diff checks passed; `buzz-db --all-targets`
clippy passed with `-D warnings`; DB lib tests passed (111 passed, 200
PostgreSQL tests ignored); ownership (2/2) and observability (1/1)
guards passed; native PostgreSQL 17 passed 3 channel lifecycle tests
plus 19 membership/roster tests with migrations 1-32 successful; relay
lib test target compiled successfully. Final worktree was detached at
the exact head and clean.

Complete evidence archive SHA-256:
`81ae374095f649ca7a25d8b9a4fc864257b7925d1b44657a69ca111523adf36e`.

## Comment-addressed restack

Review follow-up on #6777 removed only the low-value replaceable
ownership source test. This PR was restacked onto its rewritten parent;
its production patch is unchanged.

- Exact base: `21d1b265c133292e6707e766cd4204e6a43f08af`
- Exact head: `25138bfd6588e046170dbdbc4ed953bdc3cf7ed1`
- Final cumulative tip: `6fa2f104d42c6ba85bdf62e7ccb74ceaf4a84f67`
- Per-layer patch-ID and tree audits confirm this PR’s production diff
is unchanged from its pre-comment head.
- Cumulative Blox gate: formatting and diff checks; strict
`buzz-db`/`buzz-relay` Clippy; DB lib 111 passed / 200 ignored;
ownership 21/21; observability 1/1; every moved PostgreSQL test; relay
lib 910 passed / 49 ignored.
- Independent re-review at this exact head: no findings; fresh
exact-parent/head Blox review passed fmt/diff, strict Clippy, DB lib 111
passed / 200 ignored, current ownership/observability guards, 3 channel
plus 19 membership PostgreSQL tests, and relay compilation.

Signed-off-by: OpenAI Codex <codex@openai.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
2026-08-28 10:55:48 -04: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
Matt Toohey
b49675894b fix(desktop): resolve exact typed mentions on space (#6862)
## Summary

- Convert an exact, authorized manually typed `@Name` into the existing
visual mention chip when plain Space is pressed.
- Reuse autocomplete selection so outbound mention pubkeys, agent
styling, and address behavior remain identical.
- Preserve partial names, longer multi-word names, duplicate-name
ranking, modifier keys, and IME composition.

Before:


https://github.com/user-attachments/assets/2bccc0eb-5c36-4554-8a2d-d67e41d85e64

After:


https://github.com/user-attachments/assets/12859538-031d-44b0-ab38-40e1fb25c256


### Related issue

None found. Originating Buzz conversation: channel
`5efbefaf-f478-4574-927e-32f28df07f09`, thread
`563cb6709454b924405caa07f120ba9b88f074242fc3ee168686da76038b303f`.

### Testing

Validated at `7f30db7b54e484cb510bd0a616db98a49ce1b6ae`:

- `pnpm test` — 5,562 passed
- `pnpm check`
- `pnpm typecheck`
- `pnpm check:file-sizes`
- `pnpm build:e2e`
- `pnpm exec playwright test tests/e2e/mentions.spec.ts --project=smoke
--reporter=line` — 74 passed

---------

Signed-off-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:07:54 +10:00
thomaspblock
9733863516 perf(desktop): restore project context during startup (#6939)
## Summary
Project channels previously appeared as ordinary channels until the
community-wide project enumeration completed, delaying the contextual
right rail most noticeably on larger relays.

- Restore the last fully validated, relay-and-identity-scoped project
collection immediately while keeping live relay data authoritative.
- Resolve the active channel's project home through scoped
`#buzz-channel` queries instead of waiting for the complete project
scan.
- Keep snapshots aligned with community removal and relay reconnect
invalidation.

### Related issue
None found.

### Testing
- Pre-push `file-size-check`, `desktop-check`, `desktop-typecheck`, and
`desktop-test`
- Project enumeration, snapshot persistence, and relay invalidation unit
coverage
- Targeted Playwright scenario: project sidebar rows open the home
channel and nest extra channels

No numeric startup benchmark was captured; this draft validates the
cache and scoped-query behavior while leaving timing measurement for
review.

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-27 22:36:26 -04:00
Will Pfleger
350caf1de3 fix(desktop): lift right auxiliary pane above shared header backdrop (#6966)
## Summary

The sidebar close (`X`) and `Edit` controls disappeared for in-channel
side panels on `main`. They stayed clickable but were visually washed
out.

[#6901](https://github.com/block/buzz/pull/6901) added `isolate` to
`RightAuxiliaryPane` so project workspace sheets slide over an open
thread cleanly. The pane carries no `z-index`, so isolating it makes the
whole pane subtree paint at stacking level 0 — below the channel's
sibling `z-30` shared-header backdrop in split layout. That translucent
blur strip then washes out the pane's own `z-40` header chrome, where
`X`/`Edit` live. The backdrop is `pointer-events-none`, so the controls
remained clickable but invisible (matching the reported videos).
Threads, channel settings, and in-channel agent panels all route through
this wrapper, so all three were affected; the standalone Agents-nav
panel does not render inside `ChannelPane`, so it had no backdrop and
worked.

## Fix

Add `z-31` to the pane's `aside` — above the `z-30` backdrop, still
below the `z-41` thread drawer/sheet overlays. `isolate` stays, so
#6901's sheet-over-thread layering is preserved and both behaviors
coexist.

## Regression coverage

`tests/e2e/auxiliary-pane-close-visibility.spec.ts` opens a split-layout
thread from inside a channel and asserts the pane establishes its
`isolate` stacking context and that its `z-index` outranks the
shared-header backdrop. It fails on the pre-fix tree (pane `z-index` is
`auto`) and passes with `z-31`. #6901's `project workspace sheet stays
independent from an open thread` spec still passes.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
mobile-v0.15.0-rc.2
2026-08-27 20:22:28 -04:00
Will Pfleger
80177e4c8e fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962)
Codex CLI 0.149.x can leave a PTY descendant holding
`openai/codex-action`'s inherited stdio after the review turn completes.
The action's runner waits on the child's stdio *streams closing* rather
than on process exit
([`runCodexExec.ts`](https://github.com/openai/codex-action/blob/86365089/src/runCodexExec.ts#L322)),
so the `Review pull request` step never returns — the job idles until
its 30-minute `timeout-minutes` kills it and the already-written review
result is discarded.

Every `Run Codex Security Review` job since the workflow merged has hung
this way: the final JSON result and `tokens used` count are the last log
lines, with no step-end marker. Heavy runs (`gpt-5.6-sol` at `max`
effort, ~295k tokens) sit firmly in the failing regime.

Upstream:
[openai/codex-action#150](https://github.com/openai/codex-action/issues/150),
fixed in Codex CLI 0.150.0 by
[openai/codex@bf3eb2e](bf3eb2ec91)
("Prevent Unix PTY I/O from blocking runtime shutdown"). The fix commit
is in the `rust-v0.150.x` line and not in `0.149.x`.

The action pin (`v1.12`) is unchanged — only the `codex-version` CLI pin
moves from `0.149.0` to `0.150.1` (latest stable).

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-27 18:01:43 -04:00
Will Pfleger
a7c7414a7f feat(desktop): implement 30178 team catalog backend (#5112)
Implements the Rust/backend half of 30178 team catalog sharing on the
community catalog. No TS callers yet — this is PR 1 of 2;
[#3995](https://github.com/block/buzz/pull/3995) (stacked here) adds the
parse layer, hooks, `CommunityCatalogDialog`, and e2e tests.

## What this adds

**Projection builder** (`team_catalog.rs`):
`build_team_catalog_event/content` produces a 30178 event from a team +
member definitions. Size contracts: 192 KiB total ceiling, per-field
bounds (name 256 B, text 4 KiB, system-prompt 16 KiB, avatar URL 32 KiB
for projection fields; https URLs additionally validated at 2,048
bytes). Avatar handling: oversized raster data URLs downscaled to fit;
oversized built-in avatars silently omitted; oversized https URLs
rejected with a named error.

**Share/unshare/tombstone** (`commands/teams/pending.rs`):
`prepare_team_publication_at` retains a signed 30178 head for the flush
loop; share state is relay-scoped (one community can share while another
does not). `refresh_or_retract_shared_head_at` rebuilds or tombstones
the head immediately on team/member edit — no stale publication until
the next boot. Both the 30178 catalog and 30176 team tombstones are
signed with a `created_at` that strictly dominates the retained head's
(read inside the delete transaction), so a future-dated head cannot
survive its own deletion under the relay's `created_at <=` soft-delete
gate.

**Serialized-publisher share command** (`commands/teams/sharing.rs`,
`managed_agents/persona_events.rs`): `set_team_shared` publishes the
retained head through `flush_pending_events_at` rather than submitting
the prepared event directly. A direct submit ran outside
`managed_agents_store_lock` and raced `delete_team`: the delete
atomically purges the head's retained row and enqueues a newer 30178
tombstone in one transaction, and a delayed direct submit could land the
old shared head *after* that tombstone. Because 30178 replacement has no
deletion watermark, the deleted team would go publicly live again.
Routing through the flush is necessary but not sufficient, because the
flush is not itself a single publisher: several call sites (the 30s
sweep, this share toggle, managed-policy updates) invoke it
concurrently, and each invocation has an await gap between its per-row
re-read and its relay POST. A second flush could publish the tombstone
in that gap while an earlier flush's delayed POST lands the just-purged
head after it. The flush now acquires a per-scope publisher mutex —
keyed by the canonical retention `db_path`, which already *is* the
durable scope identity (hashed normalized relay URL + owner pubkey) —
and holds it across its entire invocation: snapshot, per-row re-read,
POST, and `mark_synced`. Serialized-per-scope flush ⟹ within one scope
the only interleavings are head-before-tombstone (the head lands first,
then is dominated by the later tombstone) or purged-row-skip (the delete
committed first, so the re-read skips the head) — a purged head can
never publish after its tombstone. The lock is a
`LazyLock<Mutex<HashMap<PathBuf, Arc<tokio::sync::Mutex<()>>>>>` static
rather than an `AppState` field: it keeps the invariant at its
acquisition site and out of the size-ratcheted `app_state.rs`
(precedent: `agent_models_databricks.rs`'s `AUTH_GATE`), and the
std-mutex map guard is released before the async guard is awaited so it
never spans an await point. Keying per scope — rather than one
process-global lock — means a stalled or hostile relay in one community
can no longer block publication in every other community, and each
per-row submit is additionally wrapped in a
`tokio::time::timeout(PUBLISH_TIMEOUT = 60s)`:
`submit_signed_event_at_with_keys` first waits on the process-wide
admission gate (up to 300s on a 429) and then POSTs on the app-wide
`http_client`, which leaves reqwest's connect/read/total timeouts unset,
so a relay that accepts the connection and never finishes the response
would otherwise pin the lock forever. A timeout takes the same
`Err`/`continue` path as a relay rejection — the row stays pending for
the next 30s sweep and a timed-out tombstone keeps its replacement
deferred this pass — so a live admission gate now surfaces as
timeout-pending rather than a held lock, the correct durable behavior
since the sweep retries. `publicationStatus` (Published/Queued) is
derived by re-reading the retained row's pending flag after the flush.
The previously-unused `relayMessage` field is dropped from
`SetTeamSharedResult`: the flush loop swallows every per-event relay
rejection to its own log and only surfaces local DB faults (which the
status re-read already propagates), so the field was permanently `null`
on this path. #3995's `tauriTeams.ts` mapping and `useTeamActions.ts`
log branch drop with it.

**Domination-aware flush** (`managed_agents/persona_events.rs`): a
strictly-dominating tombstone can be signed past the relay's ingest
acceptance window (`MAX_TIMESTAMP_DRIFT_SECS`, ±900s from server time).
Republishing such a byte-frozen event verbatim from the pending queue
lets it age out of the window and be rejected forever, stranding the
head live. The flush loop is now domination-aware for every retained
kind:5 (covering both 30176 and 30178). For a pending tombstone with
floor `f` (= the retained row's own `created_at`):

- `f <= now` → re-date and re-sign at `now` (mirrors the existing
archive-request re-sign branch; `mark_synced` stays keyed to the
untouched retained row, so a re-date can't mask a concurrent edit),
- `now < f <= now + 900` → publish verbatim at `f`, inside the window,
- `f > now + 900` → skip the sweep; the event stays pending and its
replacement keeps deferring (via `failed_tombstones`) so out-of-order
retraction remains impossible, converging as the wall clock advances.

No path emits an event the gate rejects, and a boundary reject
self-heals through the submit-error requeue. Durable across offline gaps
of any length.

**Atomic adopt** (`commands/teams/adopt/`): `add_team_from_catalog`
re-fetches and signature-verifies the head from the relay, then plans
and commits a multi-entity add across two store writes with byte-exact
rollback on any failure (crash window between writes explicitly
retained). `plan_add` resolves full member provenance (owner, d-tag,
member-key, projection-hash), reuses a recipient's own local built-in
only when the published slug matches and the reuse hint's
`projection_hash` — recomputed from the member's own embedded fields at
the parse boundary and rejected on mismatch — equals
(case-insensitively, matching the boundary's hex tolerance) the
recipient's local built-in hash, and reactivates deactivated copies on
re-add. Because the boundary already proves the hint hash describes the
reviewed projection, a publisher cannot pair a real built-in's slug +
hash with arbitrary reviewed fields to make adoption install the
recipient's built-in in place of what was shown. `commit_stores`
snapshots both stores before writing and restores them on failure. The
commit and the retention enqueue are sequenced inside
`commit_and_enqueue`, the sole route to a durable adoption commit: once
the store write succeeds it enqueues a pending 30175 for every member
copy the add wrote or reactivated and a pending 30176 for the team, so a
crash before the next boot reconcile cannot lose the only adopted copy.
A provenance match on an already-active copy is retained too (not just a
reactivation): a recovery retry after a crash between the persona write
and post-commit retention finds the copy active with no 30175 row, and
`plan_add` short-circuits once the team row exists, so this reuse branch
is the only place that retry can re-enqueue the orphaned member head.
Retaining unconditionally is conservative, not exact — an active copy
still referenced by a standalone managed agent can already hold a live
head, and re-retaining only bumps it monotonically; reused built-ins are
handled separately and never reach this branch. A byte-identical reused
built-in and an idempotent replay write nothing and enqueue nothing; a
failed commit propagates and enqueues nothing. Enqueue is best-effort
per row (the boot reconcile is the backstop). The frontend refreshes via
the `useAddTeamFromCatalogMutation` query invalidation in #3995, so no
`agents-data-changed` emit is needed here.

**Startup reconcile** (`event_sync.rs`):
`reconcile_team_catalog_heads_at` walks all retained 30178 heads at
boot: republishes heads whose content changed, tombstones heads whose
team or member was deleted, skips unshared heads and unchanged content.
Multi-team continuation — all shared teams processed in one pass.

**Cross-device catalog retention** (`commands/personas/inbound.rs`):
both recovery paths above — the boot reconcile worklist and the
interactive `refresh_or_retract_shared_head_at` — key off a retained
30178 row and guard-return without one. A second device therefore never
retained the owner's own catalog head published from another device, so
its later edit or delete could never supersede or retract that
discoverable head. The inbound reconcile now retains an inbound 30178
head as this device's publication witness through
`retain_inbound_catalog_witness`, a self-gating dispatcher invoked
unconditionally on the production non-deletion path: newest-wins via
`retain_inbound_event`, arrival-scoped, no local JSON store, and
deliberately **no** refresh or republish on arrival — a 30178 arrival is
either this device's own echo or the other device's publication, and
rebuilding on either would make two devices ping-pong identical heads.
Retention advances the witness and stops. The tombstone router accepts a
kind:5 covering a 30178 coordinate, so an inbound deletion purges the
retained head on the receiving device (the covered-head purge already
happens inside `commit_inbound_tombstone_with_store`; a 30178 head has
no local record to remove). After a successful inbound persona/team
upsert, this device refreshes the affected shared heads so the community
catalog tracks the inbound edit (persona edit → every team whose
resolved members include it, resolving the local persona `id` by d-tag;
team edit → that team's head); after an inbound tombstone, a team
deletion retracts its 30178 coordinate and a persona deletion refreshes
the teams that listed it. The refresh is idempotent across devices:
`refresh_or_retract_shared_head_at` skips the publish when the rebuilt
projection is byte-identical to the retained head and still shared, so
the editing device's own published head triggers no churn republish on
the receiving device.

**Executable-text concealment gate** (`team_catalog.rs`,
`definition_validation.rs`): `validate_team_catalog_content` — the
single chokepoint both the publish builder
(`build_team_catalog_content`) and the adopt parser
(`team_catalog_content_from_event`) funnel through — now rejects
invisible, default-ignorable, and bidirectional-override characters
(e.g. U+200B, U+2066, U+202E) in every field delivered verbatim to the
ACP harness or rendered as reviewed identity in the catalog UI. This is
the same invariant the persona catalog already enforces at its own parse
boundary (`persona_catalog::parse_agent`); the 30178 boundary was the
outlier. Member `display_name` + `system_prompt` go through
`validate_agent_definition_text` per member (exact parity with
`parse_agent`: display-name rule with no layout controls, prompt rule
allowing `\n`/`\t`); `name_pool` entries take the display-name rule
since they are minted verbatim as instance display names; team
`instructions` take the visible-text rule with layout controls allowed,
since they reach `BUZZ_ACP_TEAM_INSTRUCTIONS` multiline. The team `name`
takes the display-name rule (no layout controls) and the `description`
takes the visible-text rule with layout controls allowed, since both are
rendered verbatim in the catalog UI as reviewed identity.
`validate_visible_text` is exposed `pub(crate)` from
`definition_validation.rs` and re-exported via `managed_agents`. A
signed, shared, current head can no longer smuggle concealed control
characters into executable configuration or reviewed catalog text
through either the publish or the adopt path; emoji (VS16/ZWJ) names and
multiline instructions still pass.

**Types**: `TeamRecord` and `AgentDefinition` extended with `shared`,
`catalog_source`, `team_catalog_source` fields. All commands registered
in `lib.rs`.

## Tests

- `team_catalog/tests.rs`: projection, size contracts, member-key
stability, tombstone rollback, fixture matrix
- `adopt/tests.rs`: head verification, store planning, provenance,
rollback
- `team_catalog/tests/concealment.rs`: the chokepoint rejects
default-ignorable (U+200B) and bidi controls (U+2066, U+202E) in member
`display_name`, `system_prompt`, `name_pool`, team `instructions`, and
the team `name`/`description`, on both the publish and adopt paths; an
emoji-bearing display name and multiline instructions still pass, so no
legitimate team becomes unshareable
- `team_catalog/tests/reuse_hint.rs`: a member pairing a real built-in's
slug with that built-in's genuine `projection_hash` but carrying
unrelated reviewed fields is rejected at the parse boundary, so adoption
can never substitute the recipient's built-in for the reviewed
projection; an honestly-stamped built-in reuse hint (including an
uppercase form of its true hash) still passes the boundary. Removing the
boundary recompute lets the tampered member validate, proving the test
discriminates the substitution class
- `adopt/tests/reuse.rs`: `reusable_builtin` reuses a local built-in for
an exact-match hint (one record, no copy), reuses it just the same when
the genuine hash is uppercased (case-insensitive, matching the boundary
— one record, not two), and falls through to an authoritative embedded
copy when the hash does not match. Comparing the hash case-sensitively
turns the uppercase case red (two records instead of one reused)
- `adopt/tests/concealment.rs`: a signed, shared, current head carrying
a bidi override drives the `add_verified_team` sequence (verify+parse →
`plan_add` → `commit_and_enqueue`) through real temp stores and a real
retention scope, asserting the head is rejected AND the personas store,
teams store, and retention rows are all left byte-unchanged. Stripping
the concealment call at the chokepoint turns it red — the parse then
succeeds and both stores are written, proving the test discriminates a
validate-after-write regression, not just an error return
- `adopt/tests/retention.rs`: adoption drives `commit_and_enqueue`
through a spy commit + a real temp-dir retention scope and asserts
persisted pending rows — commits-then-enqueues (30175 per minted member
+ 30176 team), a failed commit enqueues nothing, an idempotent replay
skips both the commit and the enqueue, a reused built-in retains only
the team, a reactivated copy is re-retained, and a partial-commit crash
recovery (active member copy with no retention row, team row absent)
re-enqueues the orphaned member's 30175. Deleting the enqueue inside the
seam turns these red — the wiring, not just the helper, is protected
- `sharing/tests.rs`: publish/queue lifecycle plus three concurrency
gate tests. (a)
`concurrent_flushes_never_land_the_head_after_its_tombstone` prepares a
share, runs a concurrent delete's purge+tombstone, flushes the tombstone
to a recording relay, then releases the delayed share and asserts the
purged 30178 head is never published after its tombstone and no pending
row survives (removing the lock turns it red — the relay sees the
resurrected head after the tombstone). (b)
`a_stalled_scope_does_not_block_publication_in_another_scope` pins one
scope's flush mid-POST on a stalled relay and runs a second scope's
flush to completion, asserting it publishes without waiting
(re-globalizing the key turns it red). (c)
`a_stalled_relay_releases_the_publisher_lock_within_the_bound` proves a
never-completing POST returns within `PUBLISH_TIMEOUT`, leaves the row
pending, and releases the lock so a subsequent same-scope flush proceeds
(removing the timeout turns it red).
`test_relay_rejection_stays_durably_queued` asserts queued +
still-pending rather than a relay-message string, matching the
flush-routed contract where the rejection text is no longer surfaced
- `pending/tests.rs`: share/unshare/tombstone lifecycle, edit
refresh/retract, tombstone timestamp domination, typed outcomes,
cross-device catalog convergence — device B retains device A's inbound
head then supersedes it on a member edit and tombstones the coordinate
on a delete, a byte-identical rebuild is a no-op (`Noop`, `created_at`
untouched), and an inbound retention alone queues no outbound publish
(the no-ping-pong guard). Neutralizing the inbound retention leg turns
the supersede/tombstone regressions red — B stays blind (`Noop`, no
dominating tombstone) — proving they discriminate the load-bearing leg
- `catalog_reconcile_tests.rs`: a signed kind:30178 head is driven
through the real production entrypoint
`reconcile_inbound_persona_event_blocking` over a `MockRuntime`
`AppHandle` (retention scope resolved from the handle's `app_data_dir`
under an overridden `$HOME`/`$XDG_DATA_HOME`), asserting the arrival
witness is retained at the owner coordinate with `pending_sync=false`,
stored verbatim, and no outbound publish is queued. An early return for
`KIND_TEAM_CATALOG` immediately before the production
`retain_inbound_catalog_witness` invocation turns it red, proving the
seam under test is the production dispatch path and not a test-only shim
- `pending/tests/gate.rs`: flush driven through a stub relay that logs
every `POST /events` with its accept/reject status and enforces the real
±900s ingest gate, for both 30176 and 30178 — within-window
publish+dominate; beyond-window stays-pending with **zero POSTs** (the
gate never receives a rejectable event); and the delayed/offline-retry
case where a tombstone signed strictly past a then-future head has aged
more than 900s into the past, so flush must re-date to `now` to publish.
The reversal check — restore the byte-frozen replay in
`persona_events.rs` and the delayed-retry and zero-POST assertions go
red — is what proves the suite discriminates the fix from the rejected
implementation
- `teams/tests.rs`: 30176 tombstone timestamp domination and no-head
fallback
- `event_sync_team_catalog_tests.rs`: reconcile scenarios including
multi-head continuation
- 26 shared JSON parity fixtures
(`tests/fixtures/team_catalog_content/`) consumed by both this PR's Rust
tests and #3995's TS tests

## Durable ordering, deletion reconcile, and relay-contract alignment

The catalog paths above sit on the shared inbound/deletion retention
seam. This PR makes that seam's ordering structural rather than
conventional, adds a negative-side (deletion) counterpart to the
existing positive-side boot backstop, and aligns inbound resolution with
the relay's actual soft-delete and NIP-33 winner rules.

**Preflight-then-commit (`commit_inbound_with_store`,
`commands/personas/inbound.rs` + `retention.rs`).** A named primitive
runs the fallible store mutation first and advances the durable
retention head only on success; an event that loses preflight returns
`Skipped` without touching the store. The persona/team upsert arms and
the inbound kind:5 removal path all route through it, so no inbound arm
can advance the head ahead of the store write it represents. The
managed-agent arm keeps its own preflight (its runtime transition must
not run for a skipped event) and still advances the head only after
`save_managed_agents`.

**Atomic + monotonic tombstone helpers (30175 / 30176 / 30177).** The
three ordinary tombstone helpers (`commands/personas/pending.rs`,
`commands/agents_pending.rs`, `commands/teams/mod.rs`) each read the
prior head, sign, delete, and retain inside one `BEGIN IMMEDIATE`
transaction, and sign the kind:5 with a `created_at` that strictly
dominates the prior head (`monotonic_created_at(prior_head)`). This
mirrors the 30178 `tombstone_team_catalog_coordinate` precedent. The
three siblings deliberately duplicate the `BEGIN IMMEDIATE` shape rather
than sharing a helper this round — the shared-helper consolidation is
deferred to the persona-tombstone follow-up PR where the flush-replay
class already lives.

**Deletion reconcile (`event_sync.rs`, negative-side counterpart of the
positive-side boot backstop).** The positive side already reconstructs
missing retained heads at boot from surviving disk records. Deletion is
the asymmetric gap: an atomic tombstone helper preserves the head on
failure, but boot reconcile enumerated disk records — deletion already
removed them — so an orphan retained head was never tombstoned. The
deletion reconcile enumerates retained 30175/30176 heads and tombstones
only genuine orphans (a retained head with no matching disk record),
routing through the now-atomic helpers. It reconciles only against a
successfully parsed store: a truncated, malformed, or wrong-shape JSON
store fails loudly (never triggers a tombstone); a missing file is
treated as empty. Managed agents (30177) are excluded by design: their
inbound sync retains a head *without* minting a local disk record
(agents carry device-local secrets that can't come from a relay event),
so a retained 30177 head with no matching record is the normal
cross-device state for every agent created on another device — not a
lost deletion. Sweeping it would tombstone and archive another device's
live agents at boot. Agent deletion-retry therefore stays a pre-existing
gap owned by the direct delete path (tracked in Follow-ups).

**Inbound relay-contract alignment (`retention.rs`,
`commands/personas/inbound.rs`).** Two inbound rules now match the
relay:
- *Equal-second tie-break.* `inbound_event_outcome` previously treated
every equal timestamp as stale; the relay resolves equal `created_at` by
lowest event id. Preflight now matches the relay tuple — strictly newer
timestamp wins, equal timestamp resolves by lower event id, an exact
echo skips — so two devices authoring different same-second successors
converge instead of one silently republishing an event the relay
refuses.
- *Covered-head resolution on inbound kind:5.*
`reconcile_inbound_tombstone` previously consulted only the retained
kind:5 row, so a historical tombstone replayed after a newer recreation
deleted the recreated record and never purged the covered head.
Preflight now resolves both the tombstone row and the covered
`(target_kind, owner, d_tag)` head: a target head strictly newer than
the tombstone preserves JSON and skips; an actually-covered head is
removed from disk first (fallible), then the tombstone row commit and
covered-head purge happen atomically. A JSON-save failure advances
neither, so the identical event stays retryable.

**Atomic agent-archive coupling (`commands/agents_pending.rs`).** The
30177 tombstone and the 9035 archive request now enqueue in one `BEGIN
IMMEDIATE` transaction, with the archive's `persona_id` derived from the
retained head (`persona_id_from_head`) rather than the deleted record,
where it survives the tombstone as owner-signed historical alias data.
The standalone `archive_managed_agent_pending` production callers are
removed — there is no double-enqueue path. Unlike personas/teams, this
coupling is not re-enqueued by the boot deletion reconcile (30177 is
excluded, above): a crash after the disk-authoritative record is removed
but before this transaction commits leaves agent deletion-retry a
pre-existing gap owned by this direct delete path (tracked in
Follow-ups).

## Follow-ups

`publish_prepared_persona` (`commands/personas/sharing.rs`) has the
identical direct-submit-outside-the-lock race for 30175 persona heads
that this PR fixes for 30178 team heads. It predates this work and is
tracked in the separate persona-tombstone follow-up PR, not folded here.

The app-wide `http_client` leaves reqwest's connect/read/total timeouts
unset (`app_state.rs` builder configures only pool options). This PR
bounds the team-publish call site with a `tokio::time::timeout`, but
every other consumer of the shared client remains exposed to a
non-responding endpoint. A client-wide default timeout is the broader
fix; it is pre-existing on main and affects all consumers, so it is out
of scope here.

Managed-agent deletion-retry: the atomic 30177 tombstone + 9035 archive
is best-effort, and unlike personas/teams it is deliberately excluded
from the boot deletion reconcile (a device-local-absent 30177 head is
the normal cross-device state, not a deletion). A crash between the
delete's store write and its retention enqueue therefore has no boot
backstop for agents. The durable fix is a local deletion intent written
before the record is removed; deferred rather than folded into this
round.

Stack: this PR → [#3995](https://github.com/block/buzz/pull/3995)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-27 17:44:58 -04:00
Will Pfleger
ae0ecd9a67 feat(model-capabilities): humanize Databricks UC model families (#6955)
Chat-eligible Unity Catalog models from the Gemini, DeepSeek, GLM, Grok,
Llama, Qwen, Gemma, and Inkling families were rendering as raw FQNs such
as `system.ai.gemini-3-5-flash` in the agent model picker. #6918
surfaces these models, but the capability manifest carried no records
for most of them and the registry-label path could not strip their Unity
Catalog or `goose-` prefixes.

I added a display-only `label_family_tokens` list and `databricks_v2`
exact records for the sixteen previously unlabeled endpoint stems. All
26 target FQNs and their `goose-` aliases now resolve to curated labels
through `databricks_registry_label`, while capability resolution
continues to use only the existing `family_tokens` (`claude-`, `gpt-`,
and `kimi-`). This keeps label discovery from changing capability
profiles for unrelated model IDs.

Each new record includes reconciliation metadata against the pinned
[models.dev catalog](https://models.dev/api.json). Nine records adopt
first-party effort evidence that the Databricks MLflow Chat transport
can express: Gemini 3.5 Flash, Gemini 3.5 Flash Lite, Gemini 3.6 Flash,
Gemini 3 Pro Image, DeepSeek V4 Flash, DeepSeek V4 Pro, GLM-5.3 Flash,
Grok 4.6, and Inkling. The remaining seven deliberately retain the
`databricks_v2/concrete_unknown` fallback because upstream evidence is
absent, identifies a non-reasoning model, or provides only
toggle/token-budget controls that `reasoning_effort` cannot represent.

The normative corpus adds the sixteen exact records, four Unity Catalog
alias probes, and two capability-isolation probes. Existing base vectors
remain unchanged, and Rust and TypeScript consume the same strict
manifest and corpus.

Related: #6918

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Thufir <7ebdb0b67dab08a570b9faf7bbada97535673b4ccaba2cbd546ad3ba84c87fa6@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Thufir <7ebdb0b67dab08a570b9faf7bbada97535673b4ccaba2cbd546ad3ba84c87fa6@buzz.block.builderlab.xyz>
2026-08-27 17:25:19 -04:00
Kalvin C
be8d7756d5 feat(agent): discover Databricks Unity Catalog models (#6918)
## Summary

Add Unity Catalog model-service discovery to `buzz-agent` Databricks v2,
preserve full Unity Catalog FQNs through ACP model discovery, route
model services through MLflow chat, and add the optional
`DATABRICKS_MODEL_FILTER` visibility filter.

The resilience follow-up bounds each catalog attempt across headers,
bounded body reads, and JSON parsing; retries transient 499/5xx,
transport/body failures, malformed JSON, and attempt timeouts; and emits
only safe catalog/status diagnostics.


## Validation

The live validation and test gates below were run on the pre-rewrite tip
`1cf6c66e7c5bf9a00c357bda24df48e3beeb310b`. The rewritten tip
`0364cf44f3eb5cfbd5771b0a0dd7868ce0ad2c19` has identical trees for both
commits, so the attribution rewrite changes no tested code.

- 31 catalog tests passed
- 19 Databricks OAuth/routing integration tests passed
- `cargo check -p buzz-agent` passed
- `cargo clippy -p buzz-agent --all-targets -- -D warnings` passed
- `cargo fmt --check` passed
- `git diff --check` passed
- Desktop Tauri validation passed through the repository-supported setup
path, including 2,777 library tests and auxiliary terminal suites

The full `buzz-agent` package has one separately reproduced pre-existing
failure: `regressions::cancel_kills_inflight_tool_via_mcp_notification`,
failing on both baseline `eb8e97cfe` and the follow-up worktree. No
Databricks path reaches that test. The live validation proves discovery
and ACP exposure, not inference against every listed model.

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-27 13:37:12 -07:00
Luke Tornquist
57216c942f test(db): use canonical channel roster fixtures (#6819)
## Summary

Fix two PostgreSQL channel-roster test fixtures so they satisfy
migration 0032's canonical kind-39002 fence before the
channel/membership store extraction moves them.

This is a test-only prerequisite in the issue #2 extraction stack. It
emits the canonical four-field `p` tags with authoritative roles, keeps
the stale-history mutation after insertion, and gives the colliding
other-tenant fixture its own matching roster.

## Stack

- Exact base: codex/issue-6-finish-replaceable-store at
da018405cc
([#6777](https://github.com/block/buzz/pull/6777))
- Exact head: codex/issue-7-roster-test-fixtures at
21d1b265c133292e6707e766cd4204e6a43f08af
- Structural tracker: TheSentinel454/buzz#2
- Domain issue: TheSentinel454/buzz#7
- Acceptance: TheSentinel454/buzz#17 and TheSentinel454/buzz#19
- Next: `codex/issue-7-channel-membership-store`

## Why separate

The existing ignored tests are red against a freshly migrated current
`main`: migration 0032 rejects their legacy two-field roster tags before
the assertions run. The extraction PR is intended to be a pure move, so
this fixture correction is isolated here rather than mixed into the
channel-membership ownership diff.

## Non-goals

- No production Rust or SQL changes.
- No schema, lock, transaction, timeout, retry, API, or client-visible
behavior changes.
- No change to migration 0032's canonical roster rules.
- No store ownership movement; that remains in the child PR.

## Risk

Low and test-only. The main risk is accidentally changing the scenario
rather than only its representation. The tests retain the same member
counts, stale-versus-complete distinction, tenant collision, signer
isolation, and lock-freshness assertions.

## Blox verification

Author workstation: `buzz-tornquist-issue-2-store-stack` (`2046520`),
native PostgreSQL 17.11.

- Red before the change on a freshly migrated database: both focused
tests failed with migration 0032's `kind 39002 roster contains an
invalid p tag` constraint.
- `cargo fmt --all --check`: pass
- `git diff --check`: pass
- `cargo clippy -p buzz-db --all-targets -- -D warnings`: pass
-
`channel::tests::large_roster_reconciliation_candidates_respect_snapshot_count_and_signer`:
pass
-
`channel::tests::locked_member_snapshot_blocks_post_capture_membership_mutation`:
pass
- Cumulative exact-tip PostgreSQL matrix: pass

## Superseded pre-comment restack verification

PR #6700 merged before publication completed. This layer was restacked
onto current main through the exact parent named above; the final
cumulative tip is 2ddcc8a29f95c8c8c9cb87bb6cf59335f2190fae. Cumulative
author gates passed: formatting and diff checks; buzz-db and buzz-relay
all-target clippy with -D warnings; DB lib 111 passed / 200 ignored;
ownership 22/22; observability 1/1; the full isolated PostgreSQL domain
matrix; and relay lib 910 passed / 49 ignored.

- Workstation: `buzz-tornquist-pr-6819-final-review` (`2057618`), fresh
shallow checkout
- Base: `ffbeaaf00810aa359ab85818ff4820f92263e45f`
- Head: `c60e793eadde79d9eab9f48bbb2ede0ad4831f9b`
- Findings: none

Reviewed the test-only roster-fixture correction. The affected
large-roster and owner fixture events now use canonical four-field `p`
tags and preserve the intended roster cardinalities. The distinct
other-community fixture remains isolated and the production
implementation, SQL, spans, and API are untouched.

Verification: format and diff checks passed; `buzz-db --all-targets`
clippy passed with `-D warnings`; DB lib tests passed (111 passed, 200
PostgreSQL tests ignored); ownership (1/1) and observability (1/1)
guards passed; all 18 channel PostgreSQL tests passed on native
PostgreSQL 17 with migrations 1-32 successful. Final worktree was
detached at the exact head and clean.

Complete evidence archive SHA-256:
`da8379f4f0ae3989eb3573162f0f19cbde733400000756ab0f63af3322244d4b`.

## Comment-addressed restack

Review follow-up on #6777 removed only the low-value replaceable
ownership source test. This PR was restacked onto its rewritten parent;
its production patch is unchanged.

- Exact base: `da018405cc83605362125c2d5e5a3f91492431ac`
- Exact head: `21d1b265c133292e6707e766cd4204e6a43f08af`
- Final cumulative tip: `6fa2f104d42c6ba85bdf62e7ccb74ceaf4a84f67`
- Per-layer patch-ID and tree audits confirm this PR’s production diff
is unchanged from its pre-comment head.
- Cumulative Blox gate: formatting and diff checks; strict
`buzz-db`/`buzz-relay` Clippy; DB lib 111 passed / 200 ignored;
ownership 21/21; observability 1/1; every moved PostgreSQL test; relay
lib 910 passed / 49 ignored.
- Independent re-review at this exact head: no findings; fresh
exact-parent/head Blox review passed fmt/diff, strict `buzz-db` Clippy,
DB lib 111 passed / 200 ignored, observability, and all 18 channel
PostgreSQL tests; relay compilation was not applicable to this test-only
layer.

Signed-off-by: tornquist <tornquist@squareup.com>
2026-08-27 16:07:00 -04:00
tulsi
69096c9a8d preserve channel description paragraph breaks (#6946)
**Category:** fix
**User Impact:** Channel descriptions now keep their paragraph structure
everywhere people and agents read them.

**Problem:** Multi-paragraph channel descriptions were flattened in the
desktop interface and in agent context, making operational instructions
harder to scan and potentially changing their meaning. A running agent
harness could also continue using a stale description after someone
edited it.

**Solution:** Preserve authored line and paragraph breaks across the
desktop surfaces, and render descriptions as bounded, indented
agent-context blocks with semantic delimiters escaped. Prompt-visible
channel metadata now refreshes before each turn with cached fallback,
while one shared desktop derivation keeps the header and channel intro
consistent.

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

**crates/buzz-acp/src/pool.rs**
Refresh prompt-visible kind-39000 metadata with one bounded request per
turn so edits reach running harnesses, while retaining cached metadata
as a degradation fallback. Add regression coverage for description edits
and update request-count fixtures for the refresh lifecycle.

**crates/buzz-acp/src/prompt_framing.rs**
Add semantic text escaping for untrusted values embedded inside paired
prompt sections.

**crates/buzz-acp/src/queue.rs**
Render multi-paragraph descriptions as indented context blocks, preserve
blank lines, retain the 500-character limit, and escape semantic
delimiters. Add helper- and full-prompt tests for paragraph
preservation, UTF-8-safe truncation, and adversarial input.

**desktop/src/features/channels/lib/channelDescription.ts**
Introduce one shared channel-detail derivation and preserve newlines
between status context and authored detail text.

**desktop/src/features/channels/lib/channelDescription.test.mjs**
Cover shared field precedence, multiline descriptions, status-prefix
separation, and fallback behavior.

**desktop/src/features/channels/ui/ChannelPane.helpers.ts**
Use the shared channel-detail derivation for the channel intro instead
of maintaining a second field order.

**desktop/src/features/channels/ui/ChannelPane.helpers.test.mjs**
Verify description text wins consistently over legacy purpose text when
both are present.

**desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx**
Preserve paragraph whitespace in editable and read-only channel
summaries and allow enough lines for compact multi-paragraph
descriptions.

**desktop/src/features/channels/ui/ChannelManagementSheetRows.test.mjs**
Render both channel-summary variants and assert the multiline whitespace
and four-line clamp contract.

**desktop/src/features/messages/ui/ChannelIntroBlock.tsx**
Display description line and paragraph breaks in the empty-channel
intro.

**desktop/src/features/messages/ui/ChannelIntroBlock.test.mjs**
Add render-level coverage for the intro's multiline whitespace contract.

</details>

## Reproduction steps

1. Run the desktop app and create an empty channel.
2. Give it a description with two paragraphs, including a blank line and
a multi-line second paragraph, then save and reopen channel management.
3. Confirm the empty-channel intro preserves the blank line and
subsequent line breaks.
4. Confirm the channel-management summary shows the paragraph structure
for both an editor and a read-only member rather than a detached
ellipsis.
5. Hover the channel title and confirm its tooltip keeps the
description's line breaks.
6. Add an agent, send one prompt, edit the description, then send
another prompt. Confirm the next generated agent context contains the
updated description as an indented multiline block.

## Screenshot

A real-app screenshot of the multiline channel intro and
channel-management summary is attached in the PR comments below.

Closes AIDA-1980.

---------

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-27 15:37:20 -04:00
Will Pfleger
0808ab485c fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825)
## Problem

`buzz channels create --template` fails with a bare-pubkey
duplicate-instance error when a persona has more than one live instance.
When duplicate instances share the same display name and avatar, the
error gives the operator no signal to identify which instance is stale.

## What changed

**`crates/buzz-cli/src/commands/channels.rs`** (boundary) + one
visibility line in **`crates/buzz-cli/src/commands/users.rs`** (`fn
presence_subject` → `pub(crate)`), plus a relay behavior change in
**`crates/buzz-relay/src/api/bridge.rs`** and a supporting test in
**`crates/buzz-pubsub/src/presence.rs`**.

### Relay: presence-lookup failure is now surfaced as a failure

`synthesize_presence` previously collapsed a Redis `get_presence_bulk`
error into `unwrap_or_default()` → HTTP 200 `[]`, making a backend
outage indistinguishable from an authoritative all-offline snapshot. It
now returns `Option<Result<Vec<Value>, (StatusCode, Json<Value>)>>`:
`None` means the request is not a presence query (fall through),
`Some(Ok)` is a real snapshot (an empty vec is an authoritative
all-offline result), and `Some(Err)` is a backend failure surfaced as a
non-2xx response. The event tag-parse and sign paths were converted from
`.ok()?` fall-throughs to explicit `Some(Err(...))` for the same reason
— a silent fall-through would have reintroduced the fake-empty-success
anti-pattern. The function takes `pubsub` and `relay_keypair` directly
rather than the whole `AppState`, which keeps the Redis-error seam
unit-testable without live infrastructure; its single production caller
was updated accordingly, and no other caller consumes
`synthesize_presence`.

### Hint fetch architecture

**`assemble_roster_resolution<F, Fut>`** — post-fetch stage extracted
from `build_roster_resolution` as an injectable-fetcher async generic.
It contains: archive-filter-based duplicate detection, the conditional
`fetch_hints` call, and `finalize_roster_resolution` delegation.
Accepting the fetcher as a closure makes the wiring directly testable
without a relay.

`build_roster_resolution` reduces to: gather slugs → `tokio::join!(scan,
archive)` → delegate with the real `fetch_candidate_hints` closure.

**Zero hint queries on the happy path**: `assemble_roster_resolution`
only invokes the fetcher when duplicate live instances exist after
archive filtering. Untrusted archive snapshot (`Err`) conservatively
treats all found instances as live.

When duplicates exist, `fetch_candidate_hints` runs the presence
(kind:40902) and profile (kind:0) queries concurrently, bounding **each
independently** through `join_bounded_queries`: a per-query 3-second
timeout maps to `Err`, so a lookup that completes is never discarded
because its sibling hung. On total failure or timeout, bare pubkeys are
printed promptly.

### Trusted-snapshot boundary — `hints_from_results`

The relay drops the Redis presence key when an identity goes offline, so
the bulk presence snapshot never returns an event for an offline
instance — precisely the stale duplicate an operator needs flagged.
`hints_from_results` therefore treats a *trusted* presence snapshot as a
**complete snapshot** for the requested pubkeys: it seeds all of them
`offline`, then overlays returned statuses.

A response is trusted only when `trusted_presence_snapshot` validates
**every** array element as the actual snapshot contract: each must parse
as a complete signed `nostr::Event` of the presence-update kind carrying
exactly one `p` tag whose subject is one of the requested pubkeys.
Requiring the *sole* `p`-tag subject — the exact value `build_hint_map`
later reads via `presence_subject` — is what stops a mixed-tag event
(`[["p","<unrequested>"],["p","<requested>"]]`) from passing the gate
yet overlaying a different subject downstream. Any element that is not
such an event — a vacuous object like
`{"pubkey":"…","content":"online"}`, `[{}]`, `[null]`, an event of the
wrong kind, one for an unrequested subject, or one carrying more than
one `p` tag — makes presence enrichment untrusted: nothing is seeded
`offline`, so absence is never falsely inferred as offline. A failed,
timed-out, or non-array presence response is likewise untrusted. In
every untrusted case the successful profile sibling still contributes
its hints.

### Response-to-map conversion — `build_hint_map`

Sync production function taking the offline seed plus raw
presence/profile event slices. Relay-signed presence events carry the
agent pubkey in the `p` tag (not the event author); `build_hint_map`
calls `presence_subject` from `users.rs` (now `pub(crate)`) to resolve
the correct key.

### Hint display — `format_candidate`

Appends `[online/offline, profile updated YYYY-MM-DD]` (or subset) to
each candidate pubkey in the error. `profile_updated_at` names the field
correctly: kind:0 is replaceable state that desktop republishes on
rename and profile reconciliation, so `created_at` reflects the last
profile update, not provisioning time. Date formatted via
`chrono::DateTime::from_timestamp`; out-of-range timestamps omit the
date. Missing entries fall back to bare pubkeys.

### Cardinality rule

`apply_cardinality_rule` remains pure and relay-free; zero/one/many
semantics unchanged.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-27 14:40:53 -04:00
Jordan Mecom
745ff6066c Fix Codex security review authorization (#6913)
The Codex security review workflow now authorizes automatic reviews from
the live pull request instead of the webhook snapshot. This prevents a
stale author association from sending an internal PR down the
external-contributor invalidation path.

Internal PRs with no previous review now exit without attempting a
comment or label mutation. The jobs that update PR comments and labels
use `pull-requests: write`, and the review job runs only after the
trusted prepare step emits `authorized=true`.

Regression tests cover stale author associations in both directions, the
no-op invalidation path, and the mutation-job permission contract.

Checked with `just ci`, `just security-review-check`, and `actionlint
.github/workflows/codex-security-review.yml`.

The failing `Mark Previous Review Stale` check on this PR runs the
pre-fix `pull_request_target` workflow from `main`. GitHub will not
execute this branch’s trusted workflow until it is merged; the new
workflow contract and state-machine tests exercise the fix before then.

Signed-off-by: Jordan Mecom <jm@squareup.com>
2026-08-27 11:06:04 -07:00
Luke Tornquist
0ccf934b88 fix(db): disable heartbeat vacuum truncation (#6898)
## Why
`replica_heartbeat` is updated continuously, but autovacuum heap
truncation can take an `ACCESS EXCLUSIVE` lock whose replay cancels
concurrent hot-standby reads. Truncating a fixed, single-row table
provides no meaningful space benefit.

## What
- Add migration 0034 to set `vacuum_truncate = false` on
`replica_heartbeat`
- Keep the desired-state schema aligned for fresh installations
- Extend migration coverage so both paths retain the reloption

## Risk Assessment
Low — this disables only autovacuum's heap-truncation phase for a
single-row table; normal vacuum cleanup remains enabled. The tradeoff is
retaining at most the table's otherwise-truncatable tail pages.

## References
- [Investigation
thread](https://sq-block.slack.com/archives/C0B2WT43BDH/p1787760831375989)
- [PostgreSQL table storage
parameters](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS)

Generated with Codex

---

**Update Aug 27, 2026:** Hardened fresh `pgschema` bootstraps after
review.

- Renamed the shared post-apply script to
`reconcile-schema-after-pgschema.sql` and made it restore the heartbeat
reloption and singleton row idempotently.
- Added live catalog and row assertions that stop bootstrap when the
database does not match the required state.
- Updated every repository `pgschema apply` caller, including the
desktop release smoke path.
- Added a regression test that requires future `pgschema apply` callers
to run reconciliation.
- Documented the `pgschema` DML and storage-parameter limitation in
`AGENTS.md` and at the reconciliation entry point.
- Merged current `main` and renumbered this migration to 0034 after
`main` added migration 0033.

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
2026-08-27 13:01:13 -04:00
renovate[bot]
c363ce13c0 chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rui314/setup-mold](https://redirect.github.com/rui314/setup-mold)
([changelog](9c9c13bf4c..7e4f20ad28))
| action | digest | `9c9c13b` → `7e4f20a` |

---

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

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-27 10:14:23 -06:00
renovate[bot]
525007af4b chore(deps): update dependency vitest to v4.1.11 (#6667)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [vitest](https://vitest.dev)
([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest))
| [`4.1.10` →
`4.1.11`](https://renovatebot.com/diffs/npm/vitest/4.1.10/4.1.11) |
![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.11?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.10/4.1.11?slim=true)
|

---

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

---

### Release Notes

<details>
<summary>vitest-dev/vitest (vitest)</summary>

###
[`v4.1.11`](https://redirect.github.com/vitest-dev/vitest/releases/tag/v4.1.11)

[Compare
Source](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11)

#####    🐞 Bug Fixes

- Revive global concurrency limit for test lifecycle \[backport to v4]
 -  by [@&#8203;sheremet-va](https://redirect.github.com/sheremet-va)
and [@&#8203;hi-ogawa](https://redirect.github.com/hi-ogawa) in
[#&#8203;10992](https://redirect.github.com/vitest-dev/vitest/issues/10992)
[<samp>(5146d)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/5146df80b)
- **browser**:
- Encode iframeId in tester iframe URL \[backport to v4]  -  by
[@&#8203;sheremet-va](https://redirect.github.com/sheremet-va),
**Pduhard** and **Claude Opus 4.8** in
[#&#8203;10955](https://redirect.github.com/vitest-dev/vitest/issues/10955)
[<samp>(10b2c)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/10b2cd201)
- Trigger playwright/chromium gc on lower disk availability \[backport
to v4]  -  by [@&#8203;hi-ogawa](https://redirect.github.com/hi-ogawa),
**Hiroshi Ogawa** and **OpenCode** in
[#&#8203;10951](https://redirect.github.com/vitest-dev/vitest/issues/10951)
[<samp>(9851d)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/9851dbc41)
- **mocker**:
- Restrict redirect mocks to the fs allowlist \[backport to v4]  -  by
[@&#8203;sheremet-va](https://redirect.github.com/sheremet-va) in
[#&#8203;10974](https://redirect.github.com/vitest-dev/vitest/issues/10974)
[<samp>(fe5a1)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/fe5a11d3c)

#####     [View changes on
GitHub](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11)

</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:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-27 09:53:48 -06:00