Commit Graph

2593 Commits

Author SHA1 Message Date
Matt Toohey
91c3f8da8f test(desktop): seed the override test with a live preview feature
The merge with main crossed two changes: main added the thread-scoped
ACP session experiment and seeded its store test with `projects: false`,
while this branch promoted Projects out of preview-features. `getOverrides`
filters IDs that are no longer in the manifest, so `setOverride` rewrote
storage without the stale `projects` key and the assertion failed.

Seed with `pulse` instead — still a preview feature, so the test keeps
binding what it was written to bind: setting one override preserves the
others rather than clobbering them.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 16:17:49 +10:00
Matt Toohey
d220d5d3d8 refactor(desktop): split the Project Canvas package test module
tests.rs had grown to 1249 lines, over the 1000-line repository ceiling
the differential file-size gate enforces. Split it along the seams that
were already there as section comments, keeping the shared fixtures in
the parent module:

- packaging: activation, the bundled template, validation, byte limits
- containment: what the host serves and refuses — declared-script
  bootstrap, capability gating, path containment, CSP, navigation, ipc
- avatars: the `__buzz/avatar/<pubkey>` route

All 42 tests move unchanged; only the import paths differ.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 16:13:15 +10:00
Matt Toohey
37677a8810 Merge remote-tracking branch 'origin/main' into jitter/channel-project-features-on-thomas
Signed-off-by: Matt Toohey <contact@matttoohey.com>

# Conflicts:
#	preview-features.json
2026-09-02 16:06:29 +10:00
Matt Toohey
6ed11ffe5e fix(desktop): restore the Projects sidebar row and overview route
The channel-first migration removed the Projects row from the sidebar's
primary menu and turned /projects into a redirect to home, leaving the
projects overview (ProjectsScreen) unreachable: the sidebar Projects
section's "Browse all" action and every other goProjects() caller
silently landed on the Inbox.

Bring back the entry point without reverting the migration:

- /projects renders ProjectsScreen again (lazy, in Suspense) instead of
  redirecting to /. The usePreviewFeatureWarning("projects") call from
  the pre-promotion route stays gone.
- The primary menu shows Projects under Inbox again, wired through
  onSelectProjects and projectsOverviewActive exactly as before, but
  with no FeatureGate: Projects was promoted out of preview features on
  this branch and the "projects" feature id no longer exists, so the
  old gate would only re-flood the console with unknown-feature
  warnings.
- channel-project-features.spec.ts pinned the row's absence; it now
  asserts the row is visible alongside the sidebar Projects section.
  badge.spec.ts already required the row (primary rows share the same
  inactive emphasis) and passes again without changes.

Verified: desktop unit suite 5943 passing; tsc, biome clean; the
channel-project-features and badge smoke specs pass (20 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 15:53:14 +10:00
Matt Toohey
d77611dd08 feat(desktop): let users resize Project Canvas widgets
Widgets could be dragged and nudged but their footprint stayed fixed by
the package, so a dense reviews list or cramped tasks card could not be
adjusted without editing data/*.json. Resize is the same kind of direct
manipulation as drag, so it follows the same host-persisted,
revision-independent layout contract.

- `canvas.layout` gains an optional `sizes` map alongside `widgets`,
  validated like positions: bounded dimensions (16 to 100 000), the
  widget-id charset as keys, at most 256 entries, inside the existing
  64 KiB port cap. Optional keeps packages that predate size persistence
  valid — malformed layout messages count toward frame teardown.
- Size overrides persist and replay independently of positions:
  resizing a widget must not pin its position and vice versa, so a later
  package revision can still move a resized widget (and resize a moved
  one). `projectCanvasLayout.ts` sanitizes stored sizes like points,
  pruning malformed entries and folding them into the record caps.
- The template gets a per-widget resize handle: a focusable button
  (aria-label "Resize {title} widget", the label's single owner) that
  drags with the pointer and resizes with arrow keys (24px steps, 48px
  with Shift), snapping to the 24px grid and clamping at 192x144 so a
  widget cannot shrink out of reach. Saves go through the SDK's
  debounced `layout.save`, which now clamps dimensions into the accepted
  range so a save can never emit an invalid port message.
- Reset restores package sizes and clears stored overrides. The e2e mock
  package grew a resize control and had to slim its shell comment: the
  descriptor `url` data-URL cap is 8192 characters and the first version
  exceeded it by 12, failing every canvas frame activation.
- Also updates the template-harness avatar assertion broken by
  3ed98a804: with both a pubkey and a data URL the SDK now prefers the
  `./__buzz/avatar/` route, per projectCanvasSdkAvatar.test.mjs.

The base-prompt/nest-skill documentation for `layout.save({..., sizes})`
landed with 3ed98a804's doc edits; this change binds those strings in
the buzz-acp doc test.

Verified: canvas unit suites 75 passing (size sanitization, corrupt
records, `__proto__` safety, override independence, keyboard resize and
clamp, reset — bound to the real template + SDK in jsdom), buzz-acp
doc-binding test, tsc --noEmit, biome, and all 9 project-canvas
Playwright e2e tests including the extended resize/reload/reset flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 15:08:28 +10:00
Matt Toohey
3ed98a8048 feat(desktop): serve Project Canvas avatars over a host route
Canvas frames run with `connect-src 'none'`, so a widget's only avatar
source was a `data:` URL inlined into the RPC reply. Every picture was
therefore charged against the 64 KiB port-message ceiling, and overrunning
it failed the whole lookup — display names included — so the host could
inline only a handful and the rest fell back to initials.

Add `buzz-canvas://…/__buzz/avatar/<pubkey>`: the host publishes fetched
avatar bytes to the backend, and the frame loads them as ordinary images,
lazily and cached, outside any message. Widgets pass `pubkey` to
`sdk.ui.avatar` instead of `avatarUrl`; the existing `avatarUrl` path
still renders, so packages written before the route keep working.

The store is keyed by project rather than by load, because publishing and
frame creation race in both directions and a 404 is never retried. It is
bounded by an LRU over 4 projects, 64 avatars and 512 KiB per project, and
32 KiB per avatar. Bytes are magic-byte-matched against their declared
type, the content type served is an allowlisted `&'static str` rather than
the caller's string, the route requires `project.people.read`, and the
project comes from the bound load, never from the request.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 15:02:24 +10:00
Matt Toohey
5bad6f3b1a feat(desktop): persist Project Canvas widget positions across reloads
Widget placement was session-only: the template seeded `state.positions`
from the package defaults on every `host.init` and nothing wrote back, so
an arrangement was lost on navigating away, restarting, reloading the
canvas, an agent presentation update, answering the consent banner, and
community switching. Pan reset the same way.

Persist the arrangement host-side, keyed by (community, project,
dashboard, widget) and deliberately *not* by revision — an agent editing
the package must not throw away where the user put things, while consent
stays per-revision. Only overrides are stored, so an untouched widget
keeps following the package and a later revision can still move it.

- `canvas.layout` is a new child-to-host port message, not a
  `canvas.command`: it is direct user manipulation of host-rendered
  chrome, so it needs no capability and no consent, but it is strictly
  validated — bounded pan and per-widget coordinates (±100 000), the
  existing widget-id charset as keys, at most 256 widgets, and the
  existing 64 KiB port cap. `projectCanvasRpc` explicitly excludes it, so
  layout can never reach the broker.
- `projectCanvasLayout.ts` mirrors `projectCanvasConsent.ts`: one
  localStorage record per community+project holding a per-dashboard map.
  A write replaces its dashboard wholesale (stale widget ids self-prune),
  an empty layout deletes the entry and the last one clears the record,
  and stored dashboards (32) and record bytes (128 KiB) are capped by
  dropping the least recently written. Corrupt or oversized records read
  as no layouts; write failures degrade to package defaults next load.
  This is refreshable preference state, not a durable record.
- The host reads layouts at frame mount into `host.init` and persists
  `canvas.layout` beside the `canvas.rendered` branch. Both callbacks
  capture the binding they were built for, so a message landing while the
  host switches community or project can never read or write under the
  next binding's key.
- `sdk.layout.save({dashboard, pan, widgets})` sanitizes and trailing-
  debounces (300ms, last write wins) through the queued-until-init send
  path. The debounce is load-bearing: a held arrow key auto-repeats far
  past the host's 60-per-10s port limit, which kills the frame rather
  than failing one message.
- The template seeds positions and pan from `layouts[dashboard.id]`,
  saves after a drag end, keyboard nudge, and pan end, and its reset
  button becomes a true layout reset — package pan *and* positions, then
  a save that clears the record. Widgets move in place rather than
  re-rendering, so live subscriptions survive it. Combined with
  coordinate clamping, a stranded widget is always recoverable.
- Extract `ProjectCanvasFrame` and the Tauri command wrappers out of
  `ProjectCanvasHost.tsx`, which the new wiring pushed to 1035 lines,
  past the 1000-line file-size gate.

Tests: protocol coverage for the widget cap, non-finite and out-of-range
coordinates, malformed widget-id keys, and unknown fields; new storage
tests for round-trip, wholesale replace, deletion, corrupt/oversized
records, both caps, and a widget named `__proto__`; three template
harness tests driving the real template and real SDK — a persisted
override reaching `data-world-x/y` and the transform, a nudge burst
producing exactly one debounced message carrying only the moved widget's
delta, and reset clearing to empty overrides and default pan; and a
Playwright spec binding the host seam end to end (arrange, reload into a
new load id and revision, reset, reload). Each was checked falsifiable:
dropping the init merge, the reset's position restore, or the host's
persisted `layouts` fails the matching test.

Verified: desktop unit suite 5923 passing, Tauri lib suite 3077 passing,
buzz-acp prompt tests passing, canvas e2e specs 9 passing, and clippy -D
warnings, rustfmt, tsc, biome, px-text, and the file-size gate clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 12:32:18 +10:00
Matt Toohey
31c2735c5c fix(desktop): downscale Project Canvas avatars so real ones reach widgets
Widgets rendered initials for every person who actually had an avatar.
Canvas frames run with `connect-src 'none'` and an `img-src` that
excludes remote origins, so the host inlines avatars as `data:` URLs in
the RPC payload — but it forwarded them at full resolution and then
rejected anything over 48 KiB. A 512px gravatar, the size the app itself
requests, is ~230 KB and ~307,000 characters once base64-encoded: 6.3x
over the cap. `inlineAvatarDataUrl` fetched it, encoded it, discarded it,
and cached the miss for the rest of the session, so `people.lookup`
always answered `avatarDataUrl: null`.

Re-encode instead of rejecting. `avatarDownscale` center-crops the
decoded image to a square, redraws it at no more than 96px (canvas
avatars render at 20-42px, so this stays crisp at 2x), and encodes WebP
with a JPEG fallback for webviews that silently return a PNG from
`toDataURL("image/webp")`. The same gravatar lands at ~4 KB. Decode and
encode sit behind an injectable seam because neither exists under Node.

Also bound the combined size, a second and independent failure: avatars
were capped individually at 48 KiB, but a whole response shares the
64 KiB port-message ceiling, so two large avatars in one `people.lookup`
trip `postSized` and the frame receives a `too-large` error instead of
the result — losing every display name too, not just the pictures.
`selectAvatarsWithinBudget` keeps avatars in order until 40 KiB is spent
and degrades the rest to initials; the lookup path and the
channel-snapshot path in `ProjectChannelHome` both go through it.

`fetchAvatarBlob` splits out of `fetchAvatarDataUrl` so the canvas path
can re-encode raw bytes while self-profile storage keeps caching the
verbatim data URL it puts in localStorage. Both still share one fetch
policy and the same 256 KB ceiling.

Tests pin the downscaler on target size (drawing at full size fails it),
center-crop geometry, the no-upscale rule, the WebP-to-JPEG fallback,
degradation to null rather than a broken image, and bitmap release on
both the success and failure paths; the budget is pinned on ordering,
index alignment, and staying under PROJECT_CANVAS_MAX_PORT_MESSAGE_BYTES
for a full 32-person lookup.

Verified: desktop unit suite passes 5914, and biome, tsc, and the
pubkey-truncation guard are clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 12:21:15 +10:00
Taylor Ho
1c8321cd08 fix(desktop): retain automatic mentions only in threads (#7144)
**Category:** fix
**User Impact:** Top-level channel messages now notify only agents
explicitly selected for that message, while thread replies visibly
retain their addressed agents.

**Problem:** Channel-root and thread composers presented the same
automatic-mention model even though retained recipients are only
predictable within an ongoing thread. That could make a new top-level
message notify an agent the sender did not deliberately choose for that
message.

**Solution:** Make retained audiences a thread-only capability. Root
messages remain explicit and one-shot; threads retain visible, removable
agent recipients, with the automatic-mention setting exposed directly in
the mention picker.

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

**desktop/src/features/channels/ui/ChannelPane.tsx**
Removes persistent audience state from the channel-root composer.

**desktop/src/features/messages/ui/ComposerAddressControls.tsx**
Uses the broader **Manage mentions** label because the picker includes
people as well as automatic agent controls.

**desktop/src/features/messages/ui/ComposerAddressControls.test.mjs**
Locks the updated accessible label and active treatment.

**desktop/src/features/messages/ui/MentionAutocomplete.tsx**
Shows the right-aligned automatic-mention setting directly, uses
thread-specific copy, preserves keyboard/focus behavior, and keeps the
current mention when retention is unchecked.

**desktop/src/features/messages/ui/MentionAutocomplete.test.mjs**
Covers the always-visible setting, compact layout, copy, and
thread-scoped agent actions.

**desktop/src/features/messages/ui/MessageComposer.tsx**
Separates unpinning an agent for future replies from removing its
current draft mention.

**desktop/src/features/messages/ui/MessageComposer.types.ts**
Narrows retained audience contexts to threads.


**desktop/src/features/messages/ui/persistentAgentAudienceHosts.test.mjs**
Prevents channel-root and new-message hosts from opting back into
retained audiences.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**
Splits unpin and current-mention removal semantics.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Verifies unpinning retains the current draft mention.

**desktop/src/features/settings/ui/AgentsSettingsPanel.tsx**
Describes the preference as addressing selected agents in thread
replies.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Moves retained-audience lifecycle coverage to thread composers and adds
root, settings, layout, focus, keyboard, unpin, and draft regressions.

**desktop/src/features/messages/ui/MessageComposerAutocompletes.tsx**
Preserves composer focus ownership while routing the thread-only
controls.

**desktop/src/features/messages/ui/useComposerFocusOwnership.ts**
Keeps focus within the composer while interacting with its mention
overlay controls.

</details>

### Reproduction steps

1. Enable **Automatically mention agents** under agent settings.
2. In a channel root, select an agent and send a message. Confirm the
agent is addressed once, no retained-recipient control appears, and the
next root message has no agent recipient.
3. Open a thread and select an agent. Confirm the visible recipient
persists into later replies.
4. Open **Manage mentions** in the thread composer. Confirm the
automatic-mention setting is immediately visible, right-aligned, and
labeled **Address selected agents in thread replies**.
5. Uncheck a selected agent. Confirm its current draft mention remains,
while later replies no longer retain it automatically.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-09-01 16:07:35 -07:00
Kalvin C
560fea7b4f feat: add databricks fable 5.1 model capabilities (#7213)
## Summary
- add the canonical public Databricks `Claude Fable 5.1` model record
- match the existing Fable family contract: adaptive thinking,
`low|medium|high|xhigh|max`, default `high`, Anthropic Messages, and no
normalization
- add generated Rust/TypeScript corpus coverage and canonical Global
Defaults label/persistence coverage
- remove deployment-specific catalog references from the repository

## Scope
This is metadata and regression coverage only. It does not change
model-capability resolution or production frontend logic.

## Validation
- `cargo fmt --check`
- `node --test
desktop/src/features/agents/ui/modelCapabilitiesCorpus.test.mjs`
- `cargo test -p buzz-agent`
- `pnpm build:e2e`
- `pnpm exec playwright test --project=smoke --grep 'defaults render the
Fable 5.1 label without changing the persisted id'` — 1 passed
- repository search for the removed catalog prefix — no matches

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-09-01 16:06:17 -07:00
Will Pfleger
d4420eb47f docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
Rewrites `docs/nips/NIP-FI.md` as a stateless spec and deletes the five
companion profiles (NIP-FI-CONF, NIP-FI-DELEG, NIP-FI-EDGE,
NIP-FI-LIFECYCLE, NIP-FI-MODEL) that presuppose relay-side state.

## What changes

### `docs/nips/NIP-FI.md` — full rewrite

The relay-side authority engine (bindings, receipts, lifecycle,
invalidation, leases, delegation, enrollment modes, TOFU) is removed.
The spec settles exactly five things:

1. **Assertion contract**: JWT claims (`iss`, `sub`, `nostr_pubkey`,
`aud` (required), `iat`/`exp`, finite TTL bounds), npub binding
semantics, required NIP-42 proof-of-key pairing. Evidence rules
(assertion-key-mismatch → `authorization_denied`, freshness) preserved
from v1.

2. **Verification**: offline against configured per-issuer JWKS
snapshots; multi-issuer; fail-closed on missing/expired/unverifiable.
Cites `crates/buzz-auth/src/nip_fi/` (PR 3 / `70895b355`) as
implementing the assertion-verification procedure. The
`require_attested_key` flag in `IssuerPolicy` is the enforcement
primitive for the unconditional `nostr_pubkey` requirement; conformance
requires forcing it true for every issuer — follow-on code outside this
PR.

3. **Session policy**: required finite `max_connection_lifetime_seconds`
knob with no permissive default; re-auth on every reconnect; no in-band
renewal.

4. **Admin disconnect API**: session-only normative text with a
dedicated `VerifyCommandJwt` procedure — distinct `typ:
nip-fi-command+jwt`, signed `method`/`path` binding, normative
`maximum_command_age` bound (0 < value ≤ 60 s), atomic `(iss, jti)`
reservation as the final admission step after all pure checks.
Non-normative note documents cumulative residual access: `max(0,
min(exp, iat + maximum_assertion_age) - now)` assuming issuance stops;
states that previously issued assertions remain valid for reconnection
until their authority expires; states that continued issuance extends
access with no protocol-level bound. Non-normative note documents the
session-only vs deny-until-TTL trade-off. This verifier and endpoint are
follow-on code.

5. **Deletions**: bindings, enrollment, lifecycle, receipts, leases,
invalidation, delegation gone. "Out of scope" section names every issuer
and deployment concern explicitly.

### Companion profiles deleted

NIP-FI-CONF, NIP-FI-DELEG, NIP-FI-EDGE, NIP-FI-LIFECYCLE, NIP-FI-MODEL —
all presuppose relay-side binding/lifecycle state.

## What does NOT change

The entire `crates/buzz-auth/src/nip_fi/` Rust crate (PR 3 /
`70895b355`) implements the assertion-verification procedure described
in this spec. No Rust code changes in this PR. The
`require_attested_key` enforcement integration and the
`VerifyCommandJwt` / disconnect endpoint implementation are explicit
follow-on code work.

## Follow-on work (separate PRs after spec merges)

- `require_attested_key` forced-true startup enforcement
- `VerifyCommandJwt` implementation and disconnect endpoint
- Down-migration dropping migrations 0041/0042 (0042 before 0041 per FK
order)
- `schema.sql` NIP-FI sections removed
- `buzz-db` migration test blocks cleaned up

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-01 19:05:39 -04:00
ravarora2
beb76406c1 feat(relay): add detailed readiness metrics (#7149)
## Summary

This PR makes relay readiness failures diagnosable without weakening the
existing fail-closed readiness contract. It distinguishes Postgres pool
acquisition from query execution, Redis pool acquisition,
deletion-catalog validation, and the overall two-second deadline;
exports a bounded Prometheus contract for rollout dashboards; and fixes
the concurrency, shutdown, and listener-boundary semantics needed for
those signals to be trustworthy.

## Why

The previous `/_readiness` implementation exposed only an aggregate
ready/not-ready result. During a rollout, operators could not tell
whether a pod was blocked on:

- acquiring a Postgres connection;
- executing the Postgres readiness query;
- acquiring a Redis connection;
- validating the deletion catalog; or
- the shared readiness deadline.

Adding metrics to the existing handler also exposed three correctness
hazards that this PR resolves:

1. the same handler is mounted on both the public application listener
and the private Kubernetes health listener, so public requests could
otherwise distort rollout telemetry;
2. concurrent probes can finish out of order, allowing an older result
to overwrite newer current-state gauges; and
3. a probe started before SIGTERM can finish afterward, incorrectly
return `200 ready`, and resurrect ready gauges while the process is
draining.

## Behavior

### Readiness evaluation

- Postgres, Redis, and deletion-catalog checks still run under one
shared two-second deadline.
- Postgres distinguishes pool acquisition timeout/error from query
timeout/error.
- Redis distinguishes pool acquisition timeout/error. This does not
claim a Redis command round trip.
- The deletion catalog distinguishes operation timeout/error.
- Multiple failures are reported as `multiple_dependencies_failed`;
exhaustion of the shared deadline is reported as `overall_timeout` when
no more specific completed outcome wins.
- Readiness remains fail-closed: every dependency must succeed for `200
{"status":"ready"}`.

### Ordered publication and shutdown

`ReadinessCoordinator` is process-owned and uses one mutex as the
linearization point for probe generations, current-state publication,
and terminal shutdown.

- Every completed dependency attempt may contribute its truthful counter
and duration observation.
- Only the newest admissible probe generation may publish current-state
gauges.
- An older, slower probe cannot overwrite a newer probe's gauges.
- `begin_shutdown()` and probe commit serialize through the same
coordinator.
- Once shutdown commits, an in-flight probe cannot return ready or
publish ready/current dependency gauges, even if its dependency work
later succeeds.

Shutdown without dependency evaluation records only:

- `buzz_readiness_checks_total{reason="shutting_down"}`; and
- `buzz_readiness_state{check="overall"} = 0`.

It does **not** fabricate dependency failures, dependency state changes,
or zero-duration latency samples. If shutdown wins after an in-flight
evaluation actually ran, those completed dependency attempts may remain
as attempt telemetry, but they cannot overwrite shutdown-dominant
current state.

### Listener and response contract

- The private health listener's `/_readiness` route is the sole
authority for rollout readiness telemetry.
- The public application listener retains `/_readiness` for
compatibility, evaluates the same dependencies, and preserves the
existing response shape, but it emits no `buzz_readiness_*` metrics.
- Ready responses remain `200 {"status":"ready"}`.
- Shutdown responses remain `503 {"status":"shutting_down"}`.
- Failed private-health responses include the bounded `reason` plus
`postgres`, `redis`, and `deletion_catalog` booleans.
- Failed public compatibility responses retain the dependency booleans
but omit the new detailed reason.
- No header, query parameter, path value, or other request-controlled
value becomes a metric label.

## Prometheus contract

The final schema is intentionally capped at **99 raw Prometheus series
per pod**.

| Metric | Type | Labels | Raw series/pod |
|---|---|---|---:|
| `buzz_readiness_checks_total` | counter | `reason` | 12 |
| `buzz_readiness_dependency_checks_total` | counter | `dependency`,
typed `outcome` | 11 |
| `buzz_readiness_check_duration_seconds` | histogram | `check` | 72 |
| `buzz_readiness_state` | gauge | `check` | 4 |
| **Total** |  |  | **99** |

### Closed label sets

`reason`:

```text
ready
shutting_down
postgres_pool_timeout
postgres_pool_error
postgres_query_timeout
postgres_query_error
redis_pool_timeout
redis_pool_error
deletion_catalog_timeout
deletion_catalog_error
overall_timeout
multiple_dependencies_failed
```

Valid `dependency` / `outcome` pairs are enforced by typed enums:

- `postgres`: `success | pool_timeout | pool_error | operation_timeout |
operation_error`
- `redis`: `success | pool_timeout | pool_error`
- `deletion_catalog`: `success | operation_timeout | operation_error`

`check` is `overall | postgres | redis | deletion_catalog`.

The readiness histogram has 15 configured buckets concentrated around
the two-second deadline:

```text
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5,
0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, +Inf
```

The contract deliberately removes the redundant overall `result` label
and the histogram `outcome` label. Pod, ReplicaSet, version, rollout,
raw error, SQL, URL, tenant, user, community, pubkey, and
request-controlled values are prohibited application labels;
infrastructure enrichment can supply deployment identity outside the
application metric.

## Postgres production seam and CI

The real `Db::readiness_check` path now supports deterministic
production-seam testing while preserving the same acquisition/query
implementation used by the relay. The isolated PostgreSQL lane
automatically discovers and executes three ignored integration tests
covering:

- a held sole connection causing pool timeout, followed by recovery
after release;
- closed-pool acquisition error;
- acquisition success followed by query timeout;
- classified query error;
- cancellation while waiting for a connection;
- cancellation during an in-flight query; and
- eventual pool recovery with waiter/in-flight state balanced.

This prevents the central SQLx/Postgres behavior from being merely
compiled but never executed in CI.

## Tests and verification

- `cargo fmt --all -- --check`
- `./scripts/test-postgres-test-discovery.sh`
- complete `cargo test -p buzz-db`
- focused `cargo test -p buzz-relay readiness`
- real-router `cargo test -p buzz-relay real_health_route_`
- controlled PostgreSQL execution of all three ignored readiness tests
- production health router -> real `GET /_readiness` -> Prometheus
render assertions
- public-router requests proving zero rollout telemetry
- deterministic out-of-order A/B probe tests
- SIGTERM-during-probe tests proving shutdown dominance
- exported metric name/type/exact-label/bucket assertions
- exported raw-series allowlist and 99-series ceiling assertion

The route-to-scrape regression test uses the production Prometheus
builder and verifies the `2`, `2.5`, and `+Inf` readiness buckets. It
fails if the health recording call, health route, public/health
boundary, generation fence, shutdown fence, or bucket override is
removed.

## Risk assessment

**Medium.** This changes the live readiness publication path and adds
synchronization around probe commit/shutdown. The risk is bounded by:

- preserving the existing dependencies and shared two-second deadline;
- keeping readiness fail-closed;
- using a short, process-local mutex only at begin/commit linearization
points, not across dependency awaits;
- retaining the public compatibility endpoint while isolating its
telemetry;
- enforcing typed, low-cardinality labels and an exported series
ceiling; and
- covering the production router, Prometheus exposition, real PostgreSQL
seam, concurrency ordering, and shutdown races.

## Operational notes

- Dashboards should treat `buzz_readiness_state` as the latest sampled
current state, not as an event stream.
- `shutting_down` should be excluded from dependency-failure alerts
because no dependency failure is implied.
- Do not use `default_zero()` for missing current-state data;
missing/stale is unknown, not healthy.
- Use histogram buckets, heatmaps, max, or average until the Datadog
distribution metadata confirms percentiles are enabled; do not title a
widget p95 before that live readback.

## Non-goals

This PR does not add the broader process-startup lifecycle,
worker/listener supervision, shutdown coordinator, WebSocket/huddle
handoff, client recovery telemetry, dashboard mutations, Datadog
configuration changes, or deployment changes. Those remain separate
follow-up work.

## References

- [Staging dev relay image
runbook](https://github.com/block/buzz/blob/main/docs/staging-dev-relay-images.md)
- Readiness telemetry contract: `deploy/charts/buzz/README.md`

---------

Signed-off-by: Ravneet Arora <rarora@squareup.com>
2026-09-01 18:31:30 -04:00
Salman Mohammed
42aeb1571c feat(desktop): add Pi agent preset (#7208)
## Summary

Add Pi as a selectable desktop ACP runtime via `pi-acp`, including
discovery guidance and its official logo.

### Related issue

Follow-up to closed #6546.

### Testing

- Focused Tauri preset and desktop logo tests
- TypeScript typecheck

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-01 17:41:09 -04:00
Will Pfleger
5aed49b505 feat(buzz-agent): add DatabricksAuthCoordinator single-flight OAuth (#5545)
## What

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

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

## How

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

## Tests

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

## Scope / follow-ups

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

## Stack

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-01 15:59:21 -04:00
Wes
e5a7e26a10 fix(desktop): preserve keyring identity during recovery (#7203)
Pinky is opening this PR on Wes's behalf.

## Summary

Carry forward the fix from #7103 by **Taksh / @Chessing234**, with the
original author and DCO sign-off preserved in commit `99dd19191c`. A
separate Pinky-authored commit fixes and strengthens the regression
tests.

`recover_from_keyring` previously deleted an unreadable keyring identity
before checking for a valid leftover `identity.key`. A migrated
installation normally has a marker but no file, so that deletion could
destroy the only remaining key material.

- Attempt valid-file recovery before any explicit keyring deletion.
- If a migration marker exists but no valid file can be recovered, enter
`Lost` without deleting the unreadable keyring value.
- Retain the existing no-marker clear/generate policy.
- Correct the stale deletion expectation in
`corrupt_keyring_recovers_valid_file_without_rotating`, retaining its
identity and persistence checks.
- Add an explicit no-delete assertion to
`corrupt_keyring_with_valid_file_recovers_before_delete`.

Production recovery behavior matches the contributor's fix; follow-up
production-file edits only clarify comments and restore a doc comment.
The existing migration durability order remains **store → uncached
verification → marker → file removal**. Lost-state signing/startup gates
and explicit user re-import remain intact.

### Related issue

Fixes #6218. Linked continuation/replacement of #7103, not a competing
implementation. The original PR remains open for maintainer disposition.
Searched existing keyring PRs and identity/keyring issues; #7103 is the
direct duplicate and source contribution.

### Testing

Validated head: `b6a5512ba85f9e76a039f69714ed04a98eabc710`, based on
`4365883151698cd30e31cf4091629543b61c2478`.

- **Focused baseline:** `cd desktop/src-tauri && cargo test --lib
app_state::` — 52 passed, 0 failed.
- **Mutation check:** in an isolated copy, restore the original
delete-before-file-probe ordering without changing the test file.
`corrupt_keyring_with_valid_file_recovers_before_delete` fails at the
new `store.deleted.borrow().is_empty()` assertion (0 passed, 1 failed;
expected exit 101). The candidate working tree was never mutated.
- **Actual pre-push hooks passed, without bypass:** `push-head-scope`,
`branch-skew`, `file-size-check`, and `desktop-tauri-checks`.
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --workspace
--all-targets -- -D warnings`
- `cd desktop/src-tauri && cargo test --workspace` — 3,156 passed, 0
failed, 20 ignored across test binaries.
- Pre-commit Tauri formatting and DCO hooks passed.
- Independent read-only review by Brain found no further defects in the
final range and verified the baseline/mutation artifacts and contributor
attribution. This is not a GitHub approval.

Local validation used the repository's sidecar stubs for compilation.
Full-repository `just ci`, a native-app recovery workflow, and
live-keyring fault injection were not run. GitHub CI and an actual
current-range Codex security review are still pending; local hooks do
not substitute for those results. No UI markup changed, so screenshots
are not applicable.

### Limits

This prevents destructive cleanup on the marker-backed recovery path; it
does not identify what originally made the keyring value unreadable or
guarantee recovery of already-corrupt material. Markerless last-copy
preservation remains a pre-existing limitation outside this change. Only
the two app-state Rust files change; no new dependency, workflow, or
secret-export surface is introduced.

---------

Signed-off-by: Taksh <takshkothari09@gmail.com>
Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: Taksh <takshkothari09@gmail.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
2026-09-01 13:43:01 -06:00
Tom Brow
42b42447b0 feat(mobile): prepare buzz-push-gateway for deployment (#7158)
A few things to prepare for deploying the Buzz push gateway as
implemented in #6269:
- Cut `buzz-push-gateway` chart `0.2.0` from the current config
(certificate auth, dogfood client)
- Datadog-compatible metrics
- Align the desired schema, metrics docs, and production ingress
guidance with the current dogfood deployment contract

## Links

- [BUZZ-17: Publish the MVP-compatible gateway
chart](https://linear.app/squareup/issue/BUZZ-17/publish-the-mvp-compatible-gateway-chart)
- [BUZZ-26: Add Datadog-compatible push-gateway metrics and correct
source
drift](https://linear.app/squareup/issue/BUZZ-26/add-datadog-compatible-push-gateway-metrics-and-correct-source-drift)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
push-chart-v0.2.0
2026-09-01 12:19:48 -07:00
Wes
4794a5cecf ci: relax file-size ceilings by surface (#6485)
## Summary

- raise Desktop Rust's differential file-size ceiling from 1,000 to
1,500 lines
- raise Desktop frontend and Mobile ceilings from 1,000 to 1,200 lines
- keep Web at 1,000 lines and preserve the existing no-growth ratchet
above each ceiling

## Why

The flat 1,000-line limit is forcing mechanical trimming in Desktop and
Mobile even for small cohesive changes. Surface-specific ceilings
relieve that pressure without granting Web or every authored component a
blanket 2,000-line budget.

## Testing

- `just file-size-check`
- `cd desktop && pnpm exec biome check scripts/check-file-sizes.mjs`
- `node --check mobile/scripts/check-file-sizes.mjs`
- `git diff HEAD^ --check`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
mobile-v0.16.0-rc.2
2026-09-01 19:14:15 +00:00
Tom Brow
b270437a62 fix(mobile): isolate extension linker flags; complete iOS build in CI (#7187)
### What changed?

Clear inherited linker flags on the NotificationService target in Debug,
Release, and Profile. The extension continues to link BuzzPushKit
through its target framework phase, while Runner retains its CocoaPods
plugin flags.

Extend the macOS mobile CI lane to install Flutter dependencies and
build the complete unsigned iOS Release app. This exercises the
production Flutter, CocoaPods, Runner, BuzzPushKit, and
NotificationService build graph on every mobile change.

### Why?

The iOS 0.16 RC archive exposed that NotificationService inherits
Runner-only CocoaPods linker flags through the shared Flutter xcconfig.
That made the extension link Flutter plugins without the Flutter engine
and fail on unresolved Flutter symbols.

The previous CI coverage built only the standalone BuzzPushKit package,
so it could not detect an app or extension linker regression.

### How is it tested?

- A complete unsigned Release build of the Runner scheme succeeded with
Xcode and linked, embedded, and validated `NotificationService.appex`.
- The new GitHub Actions lane builds the full unsigned iOS Release app
with `flutter build ios --release --no-codesign --no-pub`.
- Pre-commit and pre-push hooks passed.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
2026-09-01 12:13:39 -07:00
Will Pfleger
4365883151 chore(ci): lower Codex security review effort (#7179)
Changes `CODEX_REASONING_EFFORT` from `max` to `high` in the
`security-review` job.

`max` increases both normal review latency and exposure to the upstream
PTY-shutdown hang
([openai/codex-action#169](https://github.com/openai/codex-action/issues/169))
where Codex finishes writing its output file but holds stdio open. The
composite `uses:` action ignores the 30-minute step timeout and
continues running until the 40-minute job timeout fires; at that point
GitHub cancels the entire job and the `always()` salvage step never gets
to run. `high` retains a high reasoning setting while trading some depth
for speed, reducing typical completion time and shrinking the window
during which a hung process blocks the salvage path.

No other behavior changes. Action pin, CLI version, model, output
schema, prompt, and timeout values are unchanged.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-01 14:20:36 -04:00
Will Pfleger
cae158ce7f fix(dev-mcp): extend shell timeout cap to 20 minutes and align outer budgets (#7185)
Raise the `buzz-dev-mcp` shell timeout ceiling to 1,200,000 ms (20
minutes) while keeping the omitted-value default at 120,000 ms (2
minutes), and align the two outer timeout layers that bound every MCP
tool call.

## What changed

**`crates/buzz-dev-mcp`** — shell timeout cap raised to 1,200,000 ms;
tool and parameter descriptions updated to match.

**`crates/buzz-agent`** — `BUZZ_AGENT_TOOL_TIMEOUT_SECS` default raised
from 660 s to 1,260 s (20-min cap + 60 s cleanup headroom). The old 660
s default would have killed the MCP server at 11 min, before a 20-min
shell call could finish. README updated.

**`crates/buzz-acp`** — `DEFAULT_IDLE_TIMEOUT_SECS` raised from 900 s to
1,500 s and its doc comment updated (1,200 s max shell + 300 s breathing
room). The old value was documented as "600 s max shell + 300 s" — stale
arithmetic.

## Tests

- Existing boundary tests in `buzz-dev-mcp` (default, exact-cap,
over-cap, `u64::MAX`) all pass.
- `default_idle_timeout_is_1500_seconds` — pinned constant replaces old
900 s assertion.
- `default_tool_timeout_is_1260_seconds` — new pinned constant in
`buzz-agent`.
-
`budget_ordering_invariant_shell_cap_plus_headroom_fits_within_idle_timeout`
— new const assertion in `buzz-acp` that encodes the full three-layer
ordering: shell cap (1,200 s) ≤ agent tool timeout (1,260 s) < ACP idle
timeout (1,500 s) < max turn duration. A future edit that inverts any
tier fails to compile.

---------

Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-09-01 18:20:25 +00:00
Wes
e17a0d44c3 fix(dev): keep the canonical profile when launching from desktop/ (#7143)
## Summary

Carl, an automated agent, implementing on Wes's request.

Fix ordinary Desktop dev checkouts being misclassified as linked
worktrees when launched through `just production`, `staging`, or other
recipes that source `instance-env.sh` from `desktop/`.

From the repository root, Git reports both directories as `.git`. From
`desktop/`, it can report an absolute `--git-dir` and `../.git` for
`--git-common-dir`. The textual comparison treats them as different and
chooses a branch-suffixed app profile (for example `.dev.main`) instead
of the canonical `.dev` profile populated by the agent-adoption script.

- Request `--path-format=absolute` for both paths. Actual linked
worktrees still have distinct Git/common directories and retain their
existing isolated app identities.
- Add eight isolated fixture cases executing the real environment script
with real Git: ordinary root/subdirectory on main and feature branches,
linked root/subdirectory, detached linked subdirectory, and symlinked
ordinary subdirectory. Paths contain spaces. Only native icon generation
is stubbed; no user profile, keychain, app, or relay is accessed.
- Run the regression in the existing always-run CI contract job.

No agent migration, global-settings copy, keychain changes, profile
merging, branch-label redesign, or packaged-release behavior changes.
Existing Git `--path-format` support is required (Git 2.31+).

### Related issue

Searched open PRs for `instance-env` and worktree titles, and open
issues for dev-profile/adoption reports. No matching path-normalization
fix found.

- #7142 touches `instance-env.sh` for descriptive labels. This fix is
independent; it uses a distinct test filename to avoid colliding with
that PR's new `test-instance-env.sh`.
- #6915 proposes a separate release-profile launch recipe; this fix
preserves the current dev-profile contract instead.

### Testing

At exact head `0a09a2f3716b0378546b66be2302b750ed4330b9`:

- `scripts/test-desktop-instance-detection.sh`: **8/8 passed**.
- Before the production change, the same regression failed on the
ordinary checkout's `desktop/` case: actual
`xyz.block.buzz.app.dev.main`, expected `xyz.block.buzz.app.dev`.
- `bash -n scripts/instance-env.sh
scripts/test-desktop-instance-detection.sh`: passed.
- `git diff --check`: passed before commit; pre-commit and commit-msg
hooks completed.
- Applicable pre-push gates passed: branch-skew, push-head-scope, and
file-size policy (6 tests plus all three entrypoints).
Rust/Desktop/Mobile package lanes were correctly inapplicable to this
shell/CI-only diff.

No native app launch or first-boot key import was performed. The
validated boundary is the generated launch environment/profile identity,
not successful agent startup. Hosted CI has not been checked or
represented as green.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-01 11:55:42 -06:00
Will Pfleger
70895b355f feat(buzz-auth): add production NIP-FI federated assertion runtime (#7109)
Depends on #6994 (merged).

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

## What

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

## Changes

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

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

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

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

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

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

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

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

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

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

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

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

### Invariant coverage

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

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

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

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
2026-09-01 13:00:40 -04:00
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
380cc506e1 feat(desktop): let Project Canvas widgets send direct messages
Add a dm.send command to the canvas widget SDK so a widget can deliver a
message to a user's DM as the viewing user, behind a new consent-gated
app.dm.send capability.

- Protocol: app.dm.send joins the capability schema and the consequential
  (consent-required) set in projectCanvasProtocol.ts, mirrored in the Rust
  manifest validator's ALLOWED_CAPABILITIES. The consent banner names it
  "send direct messages as you" and now joins three-plus capability
  phrases as a serial list instead of chaining " and ".
- Broker: dm.send takes strict {pubkey, message} params — 64-hex
  recipient, message capped at 2,000 chars and rejected when blank after
  trimming — and dispatches to a new sendDirectMessage delegate with the
  recipient lowercased. Like the existing `user` open target, the
  recipient is any valid pubkey rather than a host-source row, because
  legitimate recipients come from people.lookup/people.search results,
  which are not host sources; containment is the per-revision consent
  gate, the 10-per-minute command budget, and a host toast on every send.
- Delegate: useProjectCanvasBroker captures tenant scope (normalized
  relay URL + signer pubkey) before the first await so the backend fails
  closed if the community or identity changes mid-send, opens the DM via
  useOpenDmMutation (which also upserts the conversation into the
  sidebar), and sends over the HTTP path because a freshly opened DM has
  no live subscription yet. ProjectChannelHome feeds the community relay
  URL through a new relayUrl parameter.
- Host: success toasts "Canvas sent a direct message"; failures title as
  "Canvas direct message failed" instead of borrowing the task-update
  title.
- Template: the support dashboard's bug reporter now delivers the report
  as a DM to the project owner (resolved via project.metadata) when
  app.dm.send and project.metadata.read are granted, showing "Report
  sent"; without the capability it stages the report exactly as before.
  The bundled manifest requests app.dm.send.
- Docs: base_prompt.md and nest_skill.md document dm.send, its params,
  and the third consent capability; the buzz-acp doc-binding test now
  asserts both `dm.send` and `app.dm.send` so the prompt cannot drift.

Tests: new broker coverage for the capability gate, param bounds, blank
and oversized messages, unknown keys, and recipient normalization; the
external-template harness drives the real bug reporter through the SDK
(metadata query -> dm.send command -> success copy) and pins the staged
fallback when the capability is missing; protocol tests cover the
granted/consent/effective sets with the new capability; the Rust
manifest test accepts the full eight-capability set.

Verified: desktop unit suite 5898 passing, Tauri lib suite 3077 passing,
buzz-acp prompt tests passing, canvas e2e spec 7 passing, and clippy -D
warnings, rustfmt, biome, tsc, and the file-size gate all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 16:30:27 +10:00
Matt Toohey
602e11b605 fix(desktop): embed the Project Canvas seed template in the binary
Seeding a project's first canvas read `resources/project-canvas-template`
from disk at activation time, resolved from the running executable's
directory in dev builds. That directory is the cargo target directory —
shared across worktrees, rewritten by sibling builds, and deleted wholesale
by toolchain cleanups. When it disappeared under a running app the
first-ever activation of `buzz-bugs` failed with `inspect project canvas
path: No such file or directory (os error 2)` and could not self-heal.

Make the seed bytes part of the binary and drop the on-disk copy, so no
runtime lookup exists to go stale. This follows `resources/pocket-voices`,
which already embeds 7.6 MB of `.wav` through `include_bytes!` and is
deliberately absent from `bundle.resources`. The template is consumed only
as seed bytes: agents author canvases from `CANVASES/index.json` →
`sourcePath`, never from the template, so the disk copy had no remaining
consumer.

- New `project_canvas_package/template.rs` embeds the tree with
  `include_dir!` and walks it into a path-keyed map, running every key
  through `validate_relative_path` and skipping `.DS_Store` exactly as the
  on-disk scan does. The validated package is cached in a `OnceLock` so the
  SHA-256 over 2.7 MB runs once per process, and validation happens on the
  blocking task rather than the command's async caller.
- `build.rs` gains `cargo:rerun-if-changed=resources/project-canvas-template`.
  `include_dir!` expands to one `include_bytes!` per file, so rustc notices
  edits but not a file added or removed; without this line the binary can
  ship a stale file list.
- `scan_package` splits into `ensure_package_dir` + `validate_package_files`.
  The latter is now the single validation gate for both disk-read and
  embedded packages — there is no second, weaker path for the template.
  `prepare_snapshot`, `seed_if_missing`, `get_or_activate`, and `activate`
  take `Option<&ValidatedPackage>`; `accept_agent_update` still passes
  `None` so an agent update can never seed a canvas that does not exist.
- Delete `template_path`/`template_path_from`, the `AppHandle` parameter
  they forced onto two commands, and `bundle.resources` in
  `tauri.conf.json`. With no on-disk copy there is no second source of truth
  to go stale, and the `.app` bundle shrinks by 2.7 MB.
- `ensure_package_dir` replaces `ensure_no_symlink` + `Path::is_dir()` with
  one non-following `symlink_metadata`, so an absent directory is named
  instead of dying unnamed inside the metadata call. Strictly stronger: the
  symlink check still runs on non-following metadata, and the directory test
  now reads off that metadata rather than a link-following `is_dir()`.

Tests: `bundled_template_seeds_a_valid_snapshot` and
`first_activation_seeds_the_validated_template` repoint at the embedded
package and an in-memory map. New `bundled_template_contains_the_expected_tree`
compares the embedded key set against a test-time walk of the source tree,
`bundled_template_paths_are_package_safe` asserts every key is relative and
traversal-free, `missing_package_directory_names_the_path` pins the new
error ordering, and `package_root_symlink_is_rejected` covers the package
root itself, which no existing test did. Each was checked falsifiable:
dropping one embedded file fails the first two, restoring the old ordering
fails the third, and removing the symlink arm fails the fourth.

Verified: Tauri suite passes 3077 tests, clippy -D warnings, rustfmt, and
the file-size gate are clean. `cargo build --bin buzz-desktop` emits no
`resources/` directory next to the executable, so the built binary has no
on-disk template to read. A live canvas open was not exercised — that needs
an interactive `just staging` session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 16:10:14 +10:00
Matt Toohey
61226c68be feat(desktop): add a live data SDK to Project Canvas widgets
Implement the Project Canvas plan: live queries, task actions, navigation,
and standard components for sandboxed widget packages.

- RPC plumbing: child-to-host canvas.query/subscribe/unsubscribe/command/
  open messages with strict schemas, tiered budgets answered as error
  results instead of frame kills (16 concurrent subscriptions, 10 commands
  per minute, 3 opens per 10s, 64 KiB port messages), and a disposal-fenced
  dispatcher (projectCanvasRpc) so late completions never post to a closed
  port.
- Data broker (projectCanvasBroker): project.metadata,
  project.channels.list, project.reviews.list, project.tasks.list/get,
  people.lookup (max 32 pubkeys, avatars inlined as data URLs up to 48 KiB
  through a 32-entry LRU), and people.search, all scoped by page-fed
  sources; live subscriptions re-push on source changes with serialized
  dedupe, and commands/opens resolve against those sources before any
  delegate runs so widgets can never widen scope.
- New capabilities project.tasks.read, project.people.read,
  project.tasks.write, and app.open in the Rust manifest validator and TS
  protocol, with per-(binding, revision) consent for the consequential
  pair: an approve/deny banner gates them, denial degrades the canvas to
  read-only, and widget-initiated writes surface toasts.
- Task actions: NIP-34 status events (kinds 1630-1633) via the previously
  unused buildGitStatusTags, tasks.setStatus/assign/unassign mutations, and
  useLiveProjectWorkItems keeping issues and reviews fresh over a relay
  subscription with backoff, reconnect overlap, and debounced invalidation.
- Navigation: canvas.open targets channel/user/task/review behind app.open;
  ProjectChannelHome routes them and remounts the workspace sheet with the
  selected item.
- SDK runtime served at /__buzz/sdk.js and /__buzz/sdk.css before every
  package script: data.query/liveQuery/command, app.open, capabilities(),
  and ui.avatar/reviewRow/channelRow themed by --buzz-* variables. The
  bundled template's activeChannels and reviews widgets now render from
  liveQuery instead of fixture snapshots, a tasks widget adds Mark done/
  Reopen/Assign to me, and the SDK surface is documented in base_prompt.md
  and nest_skill.md.

Tested: desktop unit suite 5895 passing (new broker, rpc, protocol,
template, and issue-template tests), Tauri lib 3076 passing, buzz-acp
prompt tests, canvas e2e spec 7 passing with new capability-badge and
consent assertions, and clippy, fmt, biome, tsc, px-text, and file-size
gates all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 14:45:32 +10: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
Matt Toohey
78d5f6c292 fix(desktop): resolve the canvas template next to the exe in dev builds
Opening a project canvas from a dev build failed with "Canvas could not
load — resolve project canvas template: unknown path". template_path uses
Tauri's resource_dir(), whose dev-mode detection requires the executable
to live under a directory literally named target/; with CARGO_TARGET_DIR
redirected elsewhere (as on this machine, ~/.cache/cargo-target/<toolchain>),
that heuristic fails and the macOS fallback (exe_dir/../Resources) does not
exist, so resource_dir() returns UnknownPath and the canvas never activates.
tauri-build still copies bundle.resources next to the binary, so the
template was present the whole time at exe_dir/resources/.

In debug builds, fall back to the executable's directory when resource_dir()
fails — exactly where the dev branch of Tauri's own resolution would have
pointed. Release builds pass no fallback and keep failing loudly; bundled
apps resolve Contents/Resources as before. The resolution is extracted into
template_path_from so tests bind the production seam: the fallback, the
resource-dir preference, and the no-fallback error path are each covered,
and a new bundled_template_seeds_a_valid_snapshot test seeds a snapshot from
the real shipped template through the production loader.

Also remove the two stale "projects" feature gates left behind when
Projects was promoted out of preview-features (the sidebar FeatureGate
wrapper and the project-detail route's usePreviewFeatureWarning). Both
fail open, but they flooded the dev console with hundreds of
"[FeatureFlags] Unknown feature id: \"projects\"" warnings.

Verified: the fallback directory on this machine contains the copied
template, and the module's 31 tests pass, including the four new ones.
Full Tauri lib suite passes 3074 tests; desktop JS suite passes 5874;
clippy -D warnings, rustfmt, tsc, and biome are clean. A live re-test of
the canvas view was not possible because the user's staging session still
holds this worktree's Vite port; restarting `just staging` picks up the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 12:01:58 +10:00
Matt Toohey
81dc9ec699 Merge remote-tracking branch 'origin/main' into jitter/channel-project-features-on-thomas
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 11:25:58 +10:00
Matt Toohey
1b171e833b fix(desktop): allow the configured dev URL in allow_webview_navigation
The Canvas navigation policy only allowed `http://localhost:1420` in debug
builds, but every `just` desktop recipe derives a per-worktree Vite port from
a hash of the worktree root (`scripts/instance-env.sh`) and injects it as
`build.devUrl`. `1420` is only what a bare `pnpm tauri dev` would use.

Because wry installs the navigation delegate before it issues the first
`loadRequest`, the cancelled navigation was the *initial* page load, not a
later link: WKWebView returned `WKNavigationActionPolicy::Cancel`, the bundle
never fetched, React never mounted, and the reveal timer gave up with
"initial render did not commit before reveal timeout" and showed an empty
window. `just dev`, `just staging`, and `just production` all opened blank on
macOS; the plugin is macOS-only, so Linux and Windows were unaffected.

Take the dev URL as a parameter and compare origins instead of hardcoding a
port. `lib.rs` passes `webview.config().build.dev_url`, which is the same
config the app was built with — `--config` merges into it via `TAURI_CONFIG`
at build time, so it is the origin the webview is actually pointed at.
Comparing full origins keeps the containment the old predicate had: a
different port, a different host spelling, and a non-http scheme are all
still refused, and release builds have no `devUrl`, so plain http stays
blocked there.

Two regression tests bind the guard to that seam — the configured origin is
allowed, and `localhost:1420`, `127.0.0.1` on the same port, and a missing
dev URL are not. Reverting to the hardcoded port fails the first one.

Verified with `just desktop-standalone`: the app boots on this worktree's
port 30164, the webview's Vite HMR client connects and forwards frontend
console output, and the reveal-timeout line is gone. Tauri fmt, clippy, and
the file-size gate pass; the Tauri suite passes 3059 tests. One unrelated
test (`forced_discovery::cheap_discovery_reports_absent_before_any_forced_probe`)
fails only under the full parallel run and passes in isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 11:20:52 +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
Matt Toohey
92c2e2ffc0 fix(desktop): start the Canvas update socket inside the async runtime
The app aborted on every macOS and Linux launch. `ipc::start` runs
synchronously from Tauri's `setup` hook, on the main thread with no Tokio
runtime in context, and called `tokio::net::UnixListener::from_std` there.
That call registers the socket with the reactor, so it panicked with "there
is no reactor running" — and because `setup` runs inside tao's
`did_finish_launching`, a non-unwinding ObjC callback, the panic escalated to
`panic_cannot_unwind` and killed the process before a window ever appeared.

Bind the socket synchronously as before (so stale-socket and permission
failures still surface as a returned error), then hand the std listener to
`spawn_serving`, which converts it inside the spawned task where the reactor
is live. Socket cleanup on a failed conversion matches the existing accept
error path.

Covered by a unit test that calls `spawn_serving` from a plain sync context,
exactly as `setup` does, and asserts the socket still accepts a connection;
it reproduces the original panic when the conversion moves back to the call
site.

Verified with `just staging`: the app now boots past `did_finish_launching`,
creates `CANVASES/.runtime/agent-updates.sock` with 0600 permissions, and
reaches the frontend. Full Tauri test suite (3031), clippy, fmt, and the
file-size gate pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 10:19:37 +10:00
Matt Toohey
3eb9a239bb feat(desktop): add channel project features and project canvas
Move projects onto the channel-first stack: a channel can opt into
project features and surface them as tabs alongside chat, instead of
projects living as a separate top-level surface.

Desktop:
- Channel project features are opt-in per channel, with a feature bar,
  settings pane, and tabs for the canvas, resources, repos, reviews, and
  meetings; project home redirects to its channel
- Sidebar nests project channels under a project disclosure group, adds
  move destinations, and keeps section storage in sync with moves
- Extract the join-channel banner and the channel pane main column, and
  pull sidebar scroll lock and unread DM previews into their own modules
  to stay under the file-size ratchet
- Survive unmounted editor views and listener teardown races via
  mountedEditorView and safeUnlisten

Project Canvas:
- Sandboxed local canvas packages served over a Tauri custom protocol,
  with manifest validation, path security, and snapshot storage that
  supports pending/committed widget updates and revision pruning
- React canvas host and surface plus a shared protocol module
- Bundle a project-canvas-template resource with demo dashboards,
  widgets, and styles

CLI and agent surface:
- Add `buzz canvas notify` to tell the local desktop that a widget's
  presentation or data changed; it runs locally with no relay connection
- Update the ACP base prompt and nest skill docs for canvas authoring

Also promotes Projects out of preview-features.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 10:19:37 +10: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