Commit Graph

2625 Commits

Author SHA1 Message Date
Arjun Mahanti
80b61e05ba fix(design-system): remove secondary page descriptions
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
2026-09-09 12:35:07 -07:00
Arjun Mahanti
cb51ef90a3 fix(design-system): simplify focused component pages
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
2026-09-09 12:16:41 -07:00
Arjun Mahanti
b3b7de52bc fix(design-system): simplify catalog links and nest component navigation
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
2026-09-09 11:46:28 -07:00
Arjun Mahanti
8e786de355 feat(design-system): add focused pages for every component
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
2026-09-09 11:37:23 -07:00
Arjun Mahanti
8e18061562 feat(design-system): add persistent Normal and Compact density
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
2026-09-09 11:22:22 -07:00
Arjun Mahanti
22605ed388 feat(design-system): add AI composer and compact card headers
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
2026-09-09 10:55:23 -07:00
Arjun Mahanti
4c89a709e8 fix(design-system): avoid hidden preview tab stops
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
2026-09-09 10:36:52 -07:00
Arjun Mahanti
76525c9bdb fix(design-system): unify layout details and glass tab behavior
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
2026-09-09 10:32:35 -07:00
Arjun Mahanti
7c7139e3ae fix(design-system): tighten spacing and animate transient state
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-09 10:16:26 -07:00
Arjun Mahanti
5df4b5e283 fix(design-system): preserve navigation link semantics
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-09 09:50:04 -07:00
Arjun Mahanti
6a672bd0fa fix(design-system): include notices only during production builds
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-09 08:53:07 -07:00
Arjun Mahanti
0cbb4d6f34 feat(design-system): ship the interactive catalog and generated UI foundation
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-09 08:50:47 -07:00
Arjun Mahanti
31b53e1afd feat(design-system): add portable open-source component library
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-09 08:24:52 -07:00
Arjun Mahanti
20927e4355 feat(design-system): establish portable open-source foundations
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-09 08:03:48 -07:00
morgmart
f8f07a3b33 fix(desktop-next): make coloured text legible on tint hovers, and widen the audit
A second pass over the colour system, checking the surfaces the first audit did
not cover.

The gap was tint hovers. DESIGN.md says a tint carries meaning and takes coloured
text, but the audit only measured text against the four neutral surfaces — so
`--bg-*-tint-hover`, the hardest surface in each coloured family, was never
checked. Four pairings were below target: light danger (Lc 56.6) and warning
(59.2), and dark accent and info (58.4 each). All four passed WCAG AA, which is
why nothing else caught them.

Size step 11 against the tint hover rather than the neutral panel, the same rule
already applied to the neutral ramp: light danger-11 #b91c1c -> #a80008, light
warning-11 #a16207 -> #9c5e00, dark accent-11 and info-11 #8fbaff -> #95c0ff.
Minimum moves in OKLCH holding hue and chroma, so the hues do not shift.

The audit now covers every tint and tint hover in all five families. Verified by
falsification: reverting any one of the four fails the check.

Two findings that are deliberately not changes:

Hairline dividers stay at 1.2-1.8:1. WCAG's 3:1 non-text rule covers boundaries
needed to identify a control or its state, not grouping lines; Radix ships 1.29
and Apple 1.26 for the same job. Raising them would draw the box the fill already
implies. Recorded in DESIGN.md, along with the note that a control's own outline
is a different question when the input layer lands.

`--ring-focus` is an accent against accent: legible on every neutral surface at
3.0-3.5:1, but 1.1:1 on `bg-accent`, where it vanishes. Not yet reachable — the
token is unregistered and unused — so the constraint is documented at the token
instead of being solved speculatively: the component layer that introduces focus
owns a surface-coloured offset, and there is no second ring token.

Glass surfaces were checked by compositing each translucent value over the app
gradient's own stops; all clear the target in both modes, which the relit
neutral ramp is what earns.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-04 11:07:31 -07:00
morgmart
b7dbe90001 fix(desktop-next): relight the dark text ramp and enforce contrast in CI
Three fixes, and a guard so they cannot regress silently.

`text-on-warning` pointed at `var(--neutral-12)`, which is near-black in light
mode and near-white in dark. The warning fill is the same amber in both, so dark
mode put #f5f5f5 on #eab308: APCA Lc 34, WCAG 1.76:1. It is a literal now — a
paired-text token follows its fill, never the mode.

The dark neutral text steps were too dim. `text-secondary` (#8f8f8f) measured
Lc 38–41 and `text-tertiary` (#6e6e6e) Lc 23–26 against an Lc 60 body target,
while passing WCAG AA at 4.4–5.6:1 — legal, and visibly murky. Raise dark
neutral-9 to #a6a6a6 and neutral-10 to #c1c1c1, sized against bg-float
(#2a2a2a), the lightest dark surface and so the binding case. Light neutral-9
moves #a3a3a3 -> #909090 for the same reason at the meta target. Both ramps stay
monotonic, and bg-inverse-hover (the other consumer of neutral-10) still carries
its paired text at Lc 70.

This is APCA's polarity asymmetry doing real work: light-on-dark needs more
separation than the same WCAG ratio implies, so the dark ramp's text steps sit
above where mirroring the light ramp would put them.

`text-disabled` stays below target deliberately — low contrast is the signal
that a control is unavailable, and WCAG exempts inactive controls for the same
reason. Raising it would make disabled read as enabled. Recorded as the one
exception, with that reason.

The guard is the durable part. `scripts/check-contrast.mjs` measures every text
role against every surface it can sit on, in both modes, and fails the build
below target. It parses `tokens.css` and resolves each `var()` chain rather than
holding a copied list of values, so it cannot drift from the system it audits.
`scripts/apca.mjs` is the algorithm, asserted against the published reference
values on every run so a bad edit to the maths fails loudly rather than quietly
shifting every verdict. Verified by falsification: reverting either fix fails
the check, and perturbing one APCA exponent trips the reference assertion.

Also corrects an earlier misreading: the two `.dark` blocks are not duplicate
declarations. The first holds the dark ramp, the second overrides surface roles
so the panel lifts off pure black. That is the intended layering.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 21:41:51 -07:00
morgmart
972edc6fa6 fix(desktop): deepen the default accent so its white label text is legible
The default accent was #3b82f6, and white label text on it measures WCAG 2
3.68:1 — below the 4.5:1 AA minimum for normal text. That applied to every
primary button and active sidebar row in a fresh install, not to a setting
someone had opted into.

Move it to #2563eb, one step deeper: WCAG 5.17:1 and APCA Lc 80, so it now
passes both standards. Radix, Bootstrap, and Tailwind's blue-600 all sit in this
range, which is where a blue has to be to carry white text.

Darkening the fill is the fix rather than switching the label to black. Black on
the old hue scores WCAG 5.71 and would "pass", but measures APCA Lc 40 —
perceptually unreadable, and visibly worse. This is the documented WCAG 2 flaw
on saturated blue: the formula underweights the blue channel and ignores
polarity. Apple ships white on #0088ff–#3daefc in Messages at WCAG 2.4–3.5 for
the same reason. DESIGN.md now records the position (judge with APCA, report
WCAG, constrain the fill rather than degrade the text) so it is a decision with
evidence rather than a preference.

Retiring a hex is not only cosmetic: it leaves the old value in people's
storage, where it is no longer in ACCENT_COLORS and therefore fails the
community-theme allowlist — parseCommunityThemePreference would return null and
discard the user's syntax theme and follow-system flag along with the accent.
Both storage paths now migrate the retired value instead: the community
preference maps it on parse, and the local accent upgrades on read (all three
read sites go through one helper). A regression test covers the migration and
asserts an arbitrary hex is still rejected, so the migration cannot become a
general escape hatch; removing the mapping fails that test.

The communityThemeSync fixture moves to a current accent — it covers coordinate
logic, and a retired hex there would be rewritten mid-assertion for reasons
unrelated to what it tests.

Verified: 6112 desktop tests pass, tsc clean, biome clean.
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 20:16:58 -07:00
morgmart
4c2e417b37 style(desktop-next): wrap the token table into three equal columns
The value column had been holding a single unbroken gradient literal, which under
the default `auto` layout claimed most of the table's width and pushed the other
two columns into a scroll. Switch to `table-fixed` with an explicit colgroup so
the thirds hold regardless of content, and let cells wrap instead of scrolling —
the horizontal scroll container is gone.

The value uses `break-all` rather than `break-words`: a gradient is one token with
no spaces to break at, so word-boundary wrapping still overflows. The swatch stays
pinned to the first line while the value wraps beside it, so the kind label moves
under the value rather than competing with it for the same line.

Verified equal columns and zero overflowing cells at 380, 480, 700, 1024, and
1280px.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 18:43:00 -07:00
morgmart
69149c7a1b feat(desktop-next): add a token table, and declare four missing paired-text roles
The colour page explains the system but never lists the tokens themselves, so
there is nowhere to answer "what does bg-inset actually paint". Add
/design/color/table: every token in one table — the name you type, the base
token it resolves through, and the value it paints.

The values are read from the live CSS rather than typed into the registry. A hex
column written by hand is a second source of truth for the same fact: edit a ramp
and the documentation is silently wrong. This needs two different reads, because
neither answers both questions — getComputedStyle returns the resolved value
(#ffffff) and has already lost the indirection, while walking document.styleSheets
finds the declared value (var(--neutral-1)), which is the only place the chain
still exists. So CSSOM for what a token points at, computed style for what colour
that is. It re-resolves on scheme change, which is visible in dark mode where
bg-panel points at neutral 3 rather than neutral 1.

The table immediately found a real defect. The registry has documented
text-on-danger, text-on-success, text-on-warning, and text-on-info since the
status groups were generated, but tokens.css never declared them — all four
resolved to nothing, so text on a danger button fell back to inherited near-black
on red. Declare them and register them with Tailwind. text-on-warning is
neutral-12, not white: the warning fill is amber (#eab308) and white on it fails
contrast.

Also spell it "Color" in the nav, per the product's US English. The route is now
/design/color, with the table nested under it. The file is color_.table.tsx —
the trailing underscore keeps the URL while opting out of nesting, because
otherwise /design/color becomes a layout route whose component must render an
Outlet, and ColourPage does not, so the parent page rendered in place of the
table. Component and type names still say Colour; renaming those is a separate
change from what the interface says.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 18:35:39 -07:00
morgmart
bd627f675d Merge remote-tracking branch 'origin/main' into morganm/design-system
* origin/main:
  chore(release): release Buzz Desktop version 0.5.21 (#7301)
  fix(scripts): copy global-agent-config.json in buzz-adopt-prod-agents (#7303)
  feat(relay): add early startup lifecycle logs (#7258)
  docs(nip-fi): add Blossom kind-24242 media possession-proof exception (#7278)
  fix(desktop): wrap message tables within the available pane (#7279)
  🤖 fix(desktop): harden smoke E2E tests against Bestie overlay and toast timing (#7270)
  Show status and huddle indicators beside names (#7112)
  Add mobile voice notes (#7121)
  perf(desktop): publish mention sends before waking agents (#7154)

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 15:39:53 -07:00
Will Pfleger
75f101d8b4 chore(release): release Buzz Desktop version 0.5.21 (#7301)
## Buzz Desktop release v0.5.21

- **Frozen main:** `88687876f7808a2fd742b7eb2e4b9f87d999ad8d`
- **Reviewed candidate:** `47e4e0347fb4142df55b03e0e3dc537fe33a5d62`
- **Previous desktop release:** `desktop-v0.5.20`
- **Proposed immutable tag:** `desktop-v0.5.21`

This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.

The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-09-03 17:41:22 -04:00
Will Pfleger
8e7b275cb0 fix(scripts): copy global-agent-config.json in buzz-adopt-prod-agents (#7303)
## What

Extend `scripts/buzz-adopt-prod-agents.sh` to copy
`agents/global-agent-config.json` from the prod app-data store to dev as
part of the existing atomic bundle commit, and document what the script
cannot copy (baked build-time defaults).

## Why

`global-agent-config.json` holds the owner's global agent defaults
(`env_vars`, `provider`, `model`, `preferred_runtime`). Previously the
script carried the dev copy across verbatim — it was absent from the
bundle drop list and not in `RECORD_FILES`. After a `just reset`, the
dev build inherited stale global defaults (or none at all) instead of
mirroring prod, inconsistent with how all other bundle files are
handled.

The "WHAT THIS DOES NOT DO" header lacked any mention of baked
build-time defaults, which are a separate (and common) source of
prod/dev parity confusion.

## Changes

- **`RECORD_FILES`** — add `global-agent-config.json`. The file has no
volatile fields (`GlobalAgentConfig` struct =
`env_vars`/`provider`/`model`/`preferred_runtime`), so it joins the
verbatim-copy path already used by `teams.json` — no new transform.
- **Stage-seed block** — add `global-agent-config.json` to the `rm -rf`
drop list so the prod overlay replaces it cleanly; remove its stale
mention in the "carried verbatim" comment.
- **Docs/comments** — update "4-file bundle" → "5-file bundle" in the
DMG concurrent-run warning and the step 2 comment; list the file
explicitly in the step 2 header comment.
- **"WHAT THIS DOES NOT DO" header** — add a bullet explaining that
baked build-time agent defaults (`BUZZ_BUILD_BUZZ_AGENT_PROVIDER` /
`BUZZ_BUILD_AGENT_ENV` via `option_env!` in `build.rs`) are compiled
into the binary, not stored in app-data. Includes the correct `just
production` invocation pattern and a `.env` warning.
- **Preflight symlink checks and dry-run output** iterate `RECORD_FILES`
and pick up the new entry automatically — no other changes needed.

Verified: `bash -n` clean, `shellcheck` clean, `--dry-run --force` shows
`[dry-run] stage global-agent-config.json (0600)` in the expected
position.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-03 17:32:10 -04:00
morgmart
61e3792118 refactor(desktop-next): take colour and type specimens out of cards
A swatch on a grey fill is being judged against a surface the product will never
reproduce, and the dividers between swatches were redundant — each colour already
separates itself. Same for type specimens: a box around a size sample changes the
contrast you are evaluating it in.

Roles and specimens now sit directly on the page, separated by space alone. The
distinction is between two list shapes rather than a preference: `Rows` keeps
dividers for uniform rows where the eye needs a line to track along (the size and
tracking ramps, the audit table); `Specimens` has neither container nor divider
for entries that carry their own visible difference.

The swatch keeps its hairline. `bg-panel` is white on a white page, so without it
the most-used role in the system renders as nothing — that is a genuine boundary,
not decoration, and it is what makes the no-container treatment safe in both modes.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 13:07:51 -07:00
morgmart
30847f42ff feat(desktop-next): add the type system, fix unreachable border roles
The design system site rendered black borders and dividers while the tokens
said `#d4d4d4`. The border roles were declared in layer 2 and never registered
with Tailwind, so `border-primary` silently resolved to `--color-primary` — the
*text* colour — and every hairline drew at text contrast. Tailwind's `--color-*`
is a single namespace and text/borders share the emphasis names while holding
different values, so borders now register as `--border-color-*`.

Add typography as the second foundation, same three layers as colour: a nine-step
size ramp plus leading, tracking, and weight ramps in layer 1; ten public roles
in layer 2, each carrying its whole setting via Tailwind v4's
`--text-<name>--<property>` convention, so `text-body` alone produces correctly
set text and two supposedly identical labels cannot drift apart. Inter Variable
and JetBrains Mono, both already shipped in every current Buzz client. Every
size derives from one virtual rem, so keyboard zoom and the font-size preference
work by construction.

Soften the documentation surface to match where the product is going: regions
separated by soft fills rather than outlines, hairlines only for genuine
boundaries and never above `border-secondary`, no page-wide gradient behind
reading columns, and no all-caps labels anywhere.

The `/design/typography` page renders from the registry, so it cannot drift from
the system. Add `scripts/check-type.mjs` enforcing the four rules a token cannot
express — no arbitrary sizes (px or rem), no `uppercase`, no hand-applied
tracking, no size role paired with a weight or leading utility.

Also collapse the nav to a wrapped row below `lg`; the fixed 256px column
overflowed the viewport under 420px.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 12:50:39 -07:00
ravarora2
88687876f7 feat(relay): add early startup lifecycle logs (#7258)
## Why

Early relay failures can currently appear as a container restart without
a trustworthy in-process account of whether crypto, structured logging,
configuration, relay identity, or the metrics listener failed. Most of
those steps happen before the Prometheus exporter exists, so their
chronology belongs in logs rather than metrics.

Implements the logs-only early-startup slice of #7238. Post-bind
Prometheus exporter supervision is tracked separately in #7284.

## What changed

- create a process lifecycle recorder before the Tokio runtime and emit
a fixed, versioned JSON schema directly to stderr;
- record started and exactly one terminal event for `crypto_init`,
`tracing_init`, `config_load`, `key_load`, `metrics_bind`, and the
aggregate `process_telemetry` phase;
- keep every status and reason bounded and suppress raw errors that
could contain credentials, keys, URLs, or other secrets;
- return typed metrics-install errors so `metrics_bind` can be
classified without logging raw values, while preserving the existing
public `metrics::install` API;
- document the logs-only evidence contract and add real child-process
regressions for success and failure paths.

This PR adds **no startup metric families** and no dashboard contract.
Existing application metrics remain unchanged.

## Verification

Exact head: `8faf7526822a119efa035e58b2b3c59aa67fc81d`

- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-relay --all-targets -- -D warnings`
- relay binary target: 13 passed, 1 PostgreSQL-only test ignored
- real relay child-process lifecycle target: 9 passed
- full relay package library target: 1,023 passed, 89 ignored; the same
six media tests failed at `crates/buzz-relay/src/api/media.rs:1145` with
`Sqlx(PoolTimedOut)` because local PostgreSQL is unavailable
- three independent exact-head reviews found no correctness, security,
compatibility, lifecycle-accounting, logs-only-scope, or test-adequacy
finding

All exact-head GitHub CI gates are green, including lint, unit tests,
PostgreSQL, relay/backend/desktop integration, both Linux server
cross-compiles, Windows/macOS builds, and security checks.

## Staging verification

- exact multi-architecture image:
`dev-sha-8faf7526822a119efa035e58b2b3c59aa67fc81d-run-33708188952-1`
- immutable manifest:
`sha256:26cad28266a6bb0b0e7081eb6091d374e5489f8bb78c475a4a65737dee86cc67`
- image workflow: https://github.com/block/buzz/actions/runs/33708188952
- focused staging deployment:
https://github.com/squareup/builderbot-platform-core-infrastructure/pull/314
- replacement ReplicaSet `buzz-d68764bc7` has two Ready pods with zero
restarts
- Datadog received one complete, contiguous sequence 1-12 from each pod;
both end with `process_telemetry/terminal/succeeded` at 3 ms
- queries scoped to the replacement ReplicaSet return no data for the
removed `buzz_startup_phase_terminal` or
`buzz_startup_phase_duration_seconds` families

The experimental Row 7 was removed from the Buzz Startup & Rollout
Safety dashboard. This logs-only PR deliberately adds no replacement
dashboard row.

---

**Update Sep 3, 12:26 ET:** Clarified the review boundary: this PR does
not close the broader #7238. Later exporter-task termination is
pre-existing runtime behavior and is now explicitly tracked in #7284; no
production code or staged image changed in this update.

Generated with Codex

Signed-off-by: Ravneet Arora <rarora@squareup.com>
2026-09-03 13:37:51 -04:00
Will Pfleger
752cbfc037 docs(nip-fi): add Blossom kind-24242 media possession-proof exception (#7278)
Adds the media/Blossom possession-proof exception section to
\`docs/nips/NIP-FI.md\`.

## What this changes

Encodes the condition set confirmed by Thufir's security review
(2026-09-03) as normative MUSTs in NIP-FI. Kind-24242 Blossom auth
events are admitted as the NIP-FI pairing possession proof for **media
routes only** — a named, bounded exception with an explicit precedent
fence against expansion.

### Scope fence

Kind-24242 proofs are valid only on:

| Proof type (`t` tag) | Valid route | Method |
|---|---|---|
| `upload` | `PUT /upload` (and temporary alias `PUT /media/upload`
until removed) | PUT |
| `get` | `GET /media/{hash…}`, `HEAD /media/{hash…}` | GET, HEAD |

All other protected routes MUST reject kind-24242 proofs.

### Upload proofs

Exactly one `x` tag over consumed body bytes; temporal check precedes
body consumption.

### Read proofs (the relaxation Will approved)

Host-wide MAY: no `x` required. Exactly one `server` tag matching the
resolved tenant host is a MUST. Optional `x` must match parent hash if
present.

Named residual (verbatim in spec): within at most 60 seconds from
minting (plus 5s future-skew), a captured full header set allows reading
any media blob on exactly one tenant host — read-only,
membership-checked, revocable, not state-changing, not cross-tenant.

### Freshness (Thufir option 2)

- `created_at <= now + 5s` (bounded future skew)
- `now - created_at <= 60s`
- Exactly one `expiration`, valid at admission, satisfying `expiration
<= created_at + 60s`

### Transport/cardinality

Exactly one each of `Authorization` (Nostr scheme), `t`, `expiration`,
`server`; `x` at most once; reject any duplicate, malformed, or
conflicting instance.

### Per-request pairing

Full assertion verification, exact key equality between assertion
`nostr_pubkey` and kind-24242 signer, deny-map enforcement on every
request. Stub gap named.

### Compliance note

PR #7264 implementation is explicitly non-compliant until the bounded
hardening task lands (named gaps: multi-tag acceptance, 3600s window,
optional `server`).

## Behavioral oracle

`FI-TRACE-HTTP-INGRESS` extended to cover kind-24242 admission and
denial cases.

## Scope

Docs-only. No code changes. The code hardening is a separate follow-on
task.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-03 12:48:23 -04:00
Logan Johnson
6cf514ed90 fix(desktop): wrap message tables within the available pane (#7279)
## Summary
- Let Markdown tables use the available message width instead of their
maximum-content width, inheriting the renderer's existing
`wrap-anywhere` handling for long tokens and links.
- Top-align header cells as well as body cells. Give cells a `min-w-24`
readability floor so short labels do not collapse into one-letter
columns; retain the existing table-local scrollbar when many columns
genuinely cannot fit.
- Preserve semantic table markup, links, inline code, and surrounding
message layout. Add three browser regressions through the real message
renderer (channel, narrow thread, and many-column overflow).

### Related issue
Fixes #5313. No matching open table-readability PR found.

Owner-authorized task channel: `819b36d5-7371-4ed0-bf9b-d461d723779e`

Source:
buzz://message?channel=819b36d5-7371-4ed0-bf9b-d461d723779e&id=3bf2d1bd5df28fcb481e71159dc1be2b3888918b05f67e270354fbfea65a8c90

### Testing
Original production candidate:
`4427f2e6f9ef85a01a267fe9eabd1cf13cacfb78`; base:
`7a9a5233d9d755e715be0c585cf7850e935d28cf`.

- `pnpm -C desktop check` — passed.
- `pnpm -C desktop test` — 6,110 passed, none skipped.
- `pnpm -C desktop typecheck` — passed.
- `BUZZ_PROTECTED_BUILD_OUTPUT=<isolated-production-directory> pnpm -C
desktop build` — passed.
- `pnpm -C desktop build:e2e --outDir <isolated-candidate-directory>` —
passed.
- `just file-size-check` and `git diff --check` — passed.
- Chromium / mock Tauri bridge: all 3 new Playwright tests passed
against the fixed candidate build. Both wrapping tests fail on the
unchanged base build, while the overflow-fallback test passes. Each
browser run used a unique output directory and a non-reused local server
pinned to its build directory.
- At 883px channel message width: table scroll width **1,395 → 883px**.
At 292px thread message width: **1,395 → 292px**. All cells top-aligned;
long tokens/URLs wrap, link destinations and code text remain intact,
short labels stay on one line, and document width remains 1,280px.
- Focused fresh-frame review checked actual before/after screenshots and
the complete diff. It caught one-letter label wrapping in an early
width-only candidate; the final cell-width floor and regression
assertion address that finding.

Limitations: native Tauri/WebKit and relay-backed integration were not
exercised for this CSS-only delta. Repository-wide `just ci` was
attempted but exceeded the local command budget during unrelated Rust
compilation; it is **not** reported green. The full affected TypeScript
package gates above passed on the exact candidate. Production/E2E builds
retain existing chunk-size and mixed static/dynamic import warnings.

Before/after screenshots are posted below using the repository's
screenshot publication script. No merge, production installation, or
runtime restart is requested.


### CI-driven test-only follow-up
Current head: `925d964b6cf31ba704d74baf73769110774b2789`.

Smoke shard 3 exposed an older test in `messaging.spec.ts:457` that
still required three columns of ordinary prose to overflow horizontally
([failure
log](https://github.com/block/buzz/actions/runs/33771325404/job/100702242706)).
This expectation contradicts the intended wrapping change. Updated its
name and assertion to require containment; the separate many-column
local-scroll test is unchanged. No production code or harness settings
changed in this follow-up.

- Affected browser validation: **4/4 passed** (updated existing
prose/narrow table case plus the three new channel/thread/overflow
cases).
- Reused the immutable `4427f2e6` E2E build on a fresh non-reused
server; production source is unchanged at the current head. The
package/build checks above remain evidence for that unchanged production
tree, not a claim of rerunning the full suite at the new SHA.
- Targeted Biome check, file-size gate, and `git diff --check` passed.
- Current-head CI:
https://github.com/block/buzz/actions/runs/33773490446
- Prior unrelated Bestie baseline failure and reproduction:
https://github.com/block/buzz/pull/7279#issuecomment-5528077181
- Security authorization and review must target the **current** head,
not the old screenshot/build SHA. A Block organization member must
comment exactly `@buzz-security-review
925d964b6cf31ba704d74baf73769110774b2789`.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-09-03 16:07:14 +00:00
Will Pfleger
7a9a5233d9 🤖 fix(desktop): harden smoke E2E tests against Bestie overlay and toast timing (#7270)
## What

Two remaining E2E hardening fixes from the Desktop Smoke flake pattern
introduced by `ac5a18697` (Bestie — added `VITE_BUZZ_BESTIE=1` to
`.env.e2e` and mounted `BestieGlobalOverlay` globally).

The toast/DM-retry fix landed independently in main via #7127
(`input.hover()` + bounded `toHaveCount` wait); that hunk is dropped
from this PR.

## Fixes

### `agent-control-regressions.spec.ts:240` — Stop does not accept an
unconfirmed or foreign-channel result

**Cause:** Playwright 1.60.0's `page.clock.install()` fakes all timers
including `requestAnimationFrame`. The test called it before opening the
settings menu. With RAF frozen, the `DropdownMenuContent`'s `zoom-in-95
duration-150` CSS enter-animation never advances — Playwright's
stability check observes a continuously-changing bounding box until the
30s test timeout.

**Fix:** Re-sequence so the menu is opened on real time first. After
`openAgentActivity`, open the trigger, assert visibility/enabled, call
`waitForAnimations(page)` to settle the enter-animation (real
`setTimeout`, no fake clock installed yet), then install the clock. The
`fastForward(8_001)` correlation timeout still works because it's
scheduled after the clock is active. Pointer actionability preserved —
normal `stop.click()` (no `force`) fails with pointer-interception under
a covering surface.

### `message-feedback-snapshots.spec.ts:97` — profile hover uses the
channel hover surface

**Cause:** `channel.hover()` triggers a CSS `transition-colors`
animation. With the Bestie `LayoutGroup` mounted, `evaluate()` captures
a mid-transition background value that never matches the profile card's
settled token.

**Fix:** `waitForAnimations(page)` after `channel.hover()` and before
reading `channelHoverColor`.

## Evidence

- Target specs pass: stop-turn 8/8, profile-hover passes.
- Full `agent-control-regressions.spec.ts` (7 tests) green.
- `just desktop-typecheck` clean at pushed head `0fbfe2a9f`.
- No production code changed.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-09-03 08:59:49 -04:00
klopez4212
c6ca9d94d9 Show status and huddle indicators beside names (#7112)
## Summary

- show status and huddle emoji beside names in DMs and message rows
- provide accessible tooltips, fallback status emoji, and profile-menu
icon replacement
- add the desktop status editor with preset durations, a ShadCN
calendar, and a capped half-hour time menu

## Validation

- desktop checks, typecheck, and file-size guard
- 5,802 desktop tests
- focused Playwright coverage (3 passed)
- E2E build and native Builderlab staging verification

Updated visual snapshots are attached in the PR comments.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-09-03 09:39:37 +00:00
klopez4212
6f5ec4a731 Add mobile voice notes (#7121)
## Summary
- add iOS and Android voice-note recording and preview directly in the
mobile composer
- add waveform playback, scrubbing, speed controls, haptics, and
one-shot playback in chat
- package recordings in a canonical H.264/AAC MP4 envelope on both
platforms so existing relays accept them
- preserve the shared composer interaction and attachment-card treatment
across mobile platforms

Mobile counterpart to #6978.

## Testing
- `just ci`
- `just mobile-check`
- `just mobile-test` (2,026 tests)
- Android debug build compiled, installed, launched, and Voice note
verified in the attachment menu on Pixel 10
- signed iOS device build installed on iPhone

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-09-03 08:07:04 +00:00
Matt Toohey
40220d5618 perf(desktop): publish mention sends before waking agents (#7154)
## Summary

Cuts perceived agent-mention send latency by publishing the message
first and waking the agent afterwards, instead of blocking the send on a
synchronous agent start/deploy round-trip. A send that mentions a
stopped or undeployed managed agent now shows the message immediately;
the wake runs fire-and-forget after the relay accepts the publish. The
already-running-agent send also gets faster via revalidation dedupe and
NIP-11 caching.

## Changes

### Publish-first agent wake

- Wakes for mentioned managed agents are collected during send
preparation and flushed fire-and-forget only after `await send(...)`
resolves. No start can fire — and no "your message was sent" toast can
appear — for a message the relay never accepted; every abort path
(cancel, readiness error, publish rejection, dismissed non-member
prompt) simply drops the queue. Persona-create wakes ride the pending
draft behind the non-member prompt for the same reason.
- Each wake is bound to the tenant scope captured at send time: the new
`useDetachedAgentStart` hook passes `expectedRelayUrl` +
`expectedSignerPubkey` with every start, so a wake that outlives a
community switch fails closed at the backend instead of spawning against
the new tenant. A wake whose scope has not resolved yet (identity query
still loading, blank stored relay URL) is refused with a recoverable
toast rather than fired unscoped.
- In-flight wakes are deduped through a module-level map keyed by
`(relay URL, pubkey)` — the same tenant pair the backend keys on — so
two quick sends or two composers cannot double-spawn a cold agent during
the seconds-long start window. Entries are deliberately retained across
community switches (the key *is* the tenant scope, so a retained entry
can never affect another community, and clearing it let an A→B→A round
trip deploy a provider agent twice) and self-clean when the start
settles.
- Wake-failure toasts are fenced to the community they fired in via a
module-level scope mirror: a start that settles after a community switch
logs instead of rendering community A's failure over community B's UI,
and an A→B→A return re-delivers the warning where it is actionable.
- Membership attach and access-policy writes stay synchronous, so the
harness's first kind-39002 read still sees the channel.

### Replay floor

- The send timestamp travels with the wake as `BUZZ_ACP_REPLAY_FLOOR`,
threaded through both local spawns (`spawn_agent_child`) and provider
deploys (`deploy_to_provider` injects it into `launch.policy_env`), so
the harness's startup watermark replays back past the just-published
triggering message no matter how long the spawn takes. `buzz-acp` clamps
the floor to `[now − 15min, now]`.
- The floor is captured at enqueue time, not flush time — the flush runs
post-publish, so a flush-time stamp could exceed the message's
`created_at` and skip the very message the floor exists to cover.
- On local spawns the caller's floor is asserted *after* the user env
layering (and the ambient parent-process value is stripped
unconditionally), so a saved persona/global/agent env entry cannot
shadow this send's floor — mirroring the shadow-strip the provider path
applies to `launch.env`. Both halves share one `REPLAY_FLOOR_ENV_VAR`
const.

### Send-path latency reductions (already-running agents)

- Mention revalidation is deduped: the publish-boundary pass reuses the
pre-side-effect authorization pass unless an awaited round-trip actually
separated the two (background upload, link-preview settlement, DM
expansion, a real access-policy/membership write, or active-huddle
enrollment). This preserves the #5681 authorization boundary while
making the common send single-pass.
- NIP-11 `self` lookups are cached per relay URL for 5 minutes. Only
verified values are cached — non-2xx and malformed responses stay
retryable — and URL keying keeps community switches from serving another
relay's identity.
- `applyReusableAgentAccessPolicy` now reports its relay write
explicitly (`{ agent, wrote }`) instead of signalling through object
identity, so the revalidation trigger above is load-bearing by
construction.

### File splits

Four files crossed the repository file-size ratchet during this work;
one cohesive unit was extracted from each rather than raising a ceiling
— `runtime/setup_payload.rs`, `commands/agents_create_fields.rs`,
`app_state_accessors.rs`, and `useEnsureAgentMentionsReady.ts`. The
ratchet is green at the tip.

### Review follow-ups

The three concrete findings from the first review round are fixed at the
tip: the pre-publish wake and its false "your message was sent" toast
(fixed by queueing wakes behind the publish), the stale cross-community
failure toast (fixed by the scope-mirror fence), and the A→B→A duplicate
provider deploy (fixed by retaining the tenant-keyed in-flight entries
across switches). The fast-path admission-staleness point is answered in
the review thread: deferred paths already re-validate at the publish
boundary, and the remaining fast-path window is milliseconds against an
irreducible network-transit race.

Mid-branch send-perf instrumentation was added to attribute the residual
spinner latency and reverted once that analysis concluded — it is
net-zero in this diff.

### Deferred follow-ups

Durable mention catch-up via `event_mentions` (option 2 step 3) and
backend deploy-epoch coalescing for the wake paths that do not funnel
through `useDetachedAgentStart` (Agents-panel Start, restore,
inbound-persona deploys) are intentionally left for separate changes.

## Testing

- `cargo test --lib` on desktop/src-tauri: 3054 passed; clippy `-D
warnings` + fmt clean
- Desktop unit tests: 5856 passed (the 5 failures are the pre-existing
`inboxReopenNavigation` / `useRetainedProjectGitViews` baseline, present
on origin/main); `tsc --noEmit` and biome clean
- Full mentions (87), channels (89), and community-rail (25) Playwright
smoke suites against `pnpm build:e2e` bundles, with 3× stress reruns of
each new spec
- The load-bearing regression specs were confirmed red on the pre-fix
code: publish-failure → zero starts and no false toast, the dedupe hold
(1 call vs 2), the fail-closed scope refusal, the rail-switch toast
fence, and the A→B→A retention spec (1 deploy vs 2)
- New unit coverage pins the queue contract (enqueue-time floors,
attach-seam queueing), the scope capture and verbatim relay-URL handoff,
the dedupe map's keying and settle-then-repermit behavior, the unscoped
refusal, the toast-scope mirror, the `{ agent, wrote }` contract, and
the replay-floor env layering on both spawn paths

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

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 14:46:11 +10:00
morgmart
7cde504d7b feat(desktop-next): scaffold new client with the colour system and a design system route
Starts a second desktop client rather than reworking the existing one, so the
design system can be built from scratch without unpicking 476 raw colour usages
and 72 hover values first. The existing desktop/ client is untouched.

What lands:

- The colour system in three layers: private ramps hold values, public roles
  hold meanings, components only ever use roles. Tailwind's default palette is
  deleted with --color-*: initial, so a raw palette class is a build error.
- A machine-readable token registry, so the design system documentation renders
  from the system itself and cannot drift from it.
- /design — foundations, vocabulary, and the growth procedure, with a live
  light/dark toggle. Typography, spacing, radius, and motion are stubs that
  state what is still to decide rather than pretending to a system.
- DESIGN.md for the judgement tokens cannot express, and AGENTS.md for the rules
  an agent needs before writing any UI.

Accent is a slot rather than a colour: nothing above the ramp knows the hue, so
it can change, become a preference, or vary per theme without touching a
component. Every dark value is authored rather than observed, because the design
exploration it derives from is light-only.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-02 20:15:02 -07:00
Logan Johnson
5073e075d7 fix(desktop): unify owned-agent cloud provenance markers (#7129)
Unify presentation-only cloud provenance across agent identity surfaces. Keep successful local-inventory and verified-ownership gates; preserve main availability and mention spacing behavior.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-09-03 02:35:37 +00:00
Logan Johnson
d5a73b9f35 fix(desktop): derive agent availability from relay presence (#7127)
🤖

## Requested rebase published — ef40744b

Rebased onto fetched main **47d068e2109d077414cbf2f4f1c927f6d051037a**,
published **ef40744b3aeb4baaf8c81416e1a644fb5b315f91** with the exact
expected-old `df7fad6a` force-with-lease. No merge.

Manual conflicts were additive: preserve main's exact-key identity
documentation alongside the availability contract, and retain both
Bestie props and the shared availability reader in
`UnifiedAgentsSection`. Range-diff confirms unchanged lifecycle policy:
exact-key action-time authority, Unknown versus Offline, rejected
shutdown retains record/memberships, and separate local/provider/owner
gates. Main's exact-key profile routing survives. Both test-only CI
synchronization repairs (natural toast expiry and bounded stderr wait)
are byte-identical to the prior head. All ten original
authors/messages/DCO/material coauthor trailers are preserved;
configured signing policy was not changed.

Fresh checks on the rebased candidate:
- TypeScript, Biome on 26 changed TypeScript files, differential
file-size gate, and diff whitespace: pass.
- Focused production-hook/card/profile units: **58/58**.
- Fresh E2E build, availability/deletion browser: **11/11**, no retries.
- Main exact-key profile cases plus failed-DM send/startup retries:
**6/6**, no retries.

Previously reviewed full Desktop/buzz-agent package and mutation
evidence is reused for unchanged behavior; no ceremonial full suite, new
native/provider test, or `just ci` pass is claimed. Local configs,
dependency links, and historical artifacts are preserved.

Hosted observation: **MERGEABLE**, **BLOCKED / REVIEW_REQUIRED**, no
new-head formal review. [CI
33699735990](https://github.com/block/buzz/actions/runs/33699735990) is
running (including Rust and Desktop lanes), not a completed success. DCO
and required Security aggregate passed at the observation; the separate
Codex advisory review was skipped. No completed failing check or new
inline feedback observed. Historical approvals are not new-head
approvals. No reviewer/security authorization or merge action was
performed.

---

## Feature summary and retained pre-rebase evidence

## Summary

In Buzz Desktop, an agent could look online just because it had been
started or deployed, even when there was no current sign it was
connected. Cards and profiles now show availability from the agent's
relay presence rather than a saved launch record, so you can distinguish
an online agent from one that was merely deployed.

Agents cards and profiles use presence reported through the shared
server (the relay). A successful presence read with no online agent
shows Offline; failed/disconnected evidence shows unknown, rather than
retaining a misleading cached Online state.

Lifecycle actions remain separate. An offline agent may still have a
Shutdown action because the deployment record exists. Shutdown reports a
**request**, not proof the process stopped. Offline does not imply that
starting a duplicate agent is safe, and Online does not promise a
response.

### Related issue

Independent base: `main`; no stack parent or child among the
replacements. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](f4bb2ed44e/docs/agent-availability.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

The same saved provider-backed agent, with only authored presence
changing. These are mock-browser states, not a before/after deployment
or live relay transport test; production UI is unchanged by the later
fixture repairs.

**No online presence:** gray dot, existing Shutdown control retained.

![Offline presence does not remove the deployment lifecycle
control](https://raw.githubusercontent.com/block/buzz/ed68b1b4597dde47118b7591b1e8ebf3702447ca/pr-7127--offline-deployment.png)

**Online presence:** green dot, same lifecycle control.

![Authored Online presence changes availability without changing the
deployment](https://raw.githubusercontent.com/block/buzz/ed68b1b4597dde47118b7591b1e8ebf3702447ca/pr-7127--online-deployment.png)

[Capture
details](https://github.com/block/buzz/pull/7127#issuecomment-5482264824).
To check manually, compare runtime-only transitions with presence
updates, then disconnect/fail the presence read and verify it does not
stay Online. A Shutdown request should not immediately claim confirmed
termination.

#### Historical pre-rebase evidence and limitations (df7fad6a)

Lifecycle production source remains
**`f4bb2ed44e5a989d93c5f51e93c0bbd2dca941be`**. Current published head
is **`df7fad6ae65dda78508317186a95522d1bb22ed9`**: the prior browser
synchronization at `b78d093e` plus an additive two-file Rust
test-harness synchronization described below. No production bytes,
dependency/configuration files, or prior commits were changed; no
rebase. Current live main `0dbd036f5bff33e7ade75e7639f3218d424a6e73` has
identical failing-test/toaster/send-flow source; the causal browser
comparison used latest successfully tested main
`04babf02655440b4dfd37f2e2df605ead0a030d8`.

**Lifecycle/deletion correction:** both Agents and actual profile
deletion now pass the shared exact-key availability reader, not raw
cached data. It reads the canonical query state and connection at action
time, including after awaited channel discovery.
Failed/disconnected/pending evidence and unqueried persona siblings are
unknown; successful missing means Offline only for a requested key.
Successful background refetch cache remains usable; settled failure
revokes it. No second cache or per-row polling was added.

Provider record + channel + Online/Away/**unknown** awaits shutdown
submission before local removal; rejection preserves record/membership
for retry. Established Offline preserves intentional no-request removal.
No route preserves warned local removal. Local agents retain native
stop-before-remove, independent of presence. Profile consent now
describes a shutdown **request**, not remote deletion or guaranteed
termination. Existing ownership and force gates are unchanged.

**Verified, reused exact-candidate validation:** the independently
approved eleven-file patch (SHA-256
`2f69fe12ef0420e62dea1fd8db28cfa22cde5eecaf8080e656310a3e60d0cf86`) was
committed without byte changes. Desktop **5,921 passed, 0
failed/skipped**, including **26 new mounted production hook/IPC
regressions**; rebuilt availability browser suite **11/11 passed, no
retries**, including four actual profile Delete journeys. Desktop check
(existing 4 warnings/5 infos), typecheck, production/protected-feature
artifact matrix, differential file-size/policy and diff checks passed.
No blanket rerun or new full-repository `just ci` is claimed for this
frontend correction.

Production regressions cover cached Online **and Offline**
failure/disconnection, genuine missing/Offline, pending, successful
inflight refetch versus settled error, retained reader, error during
awaited channel discovery, unqueried persona sibling, shutdown
rejection/order/cancel, no route and local authority. Browser fixtures
use safe mock IPC and a retained provider receipt, not a real
deployment. Three restored mutation controls fail: unknown → skip
shutdown (**15** regressions), Agents raw-cache reader (**6**), actual
profile raw-cache caller (**1 browser journey**, false removal on failed
cached Offline). Independent review approved the exact frozen bytes and
added **4/4 cached-empty failure/disconnection probes** across both
callers. This is local independent approval, not formal GitHub/A Team
clearance.

The prior native propagation/poll-count defects remain closed ([earlier
response](https://github.com/block/buzz/pull/7127#issuecomment-5512278755)).
The prior hover-popover correction at `b55423f6` remains covered by the
full 11-journey browser run: pending/failed/disconnected means no badge
or accessible status, genuine missing/Offline retains an Offline badge.
Its earlier fallback-restoration mutation failed as expected (badge
count 1 rather than 0); that historical witness is reused, not rerun.

**Reused unchanged native/system boundary:** local `just ci` at
`c59067d8` passed workspace/Tauri fmt/clippy, static/policy checks, Rust
unit recipe, native workspace **3,159 passed / 20 ignored**, Web build
and **2,019 mobile tests**. No native implementation changed in this
lifecycle correction. These are historical boundary results, not
new-head native/live certification. [Parent
CI](https://github.com/block/buzz/actions/runs/33650549130) passed with
**14 retry-recovered browser flakes**, not retry-free. Old-head
CI/reviews are not current-head clearance.

**Hosted gates:**
[CI33662151103](https://github.com/block/buzz/actions/runs/33662151103)
on `f4bb2ed4` **FAILED**: smoke shard1 had 322 pass, one failure, one
retry-recovered flaky, two skipped. The failed first-DM retry test timed
out on all three attempts because the error toast intercepted Send. That
failure is preserved, not waived; the scoped test repair below is
published as `b78d093e`.
[CI33668171165](https://github.com/block/buzz/actions/runs/33668171165)
on `b78d093e` subsequently **FAILED** the Rust unit budget regression
described below. Both original failures remain visible; neither was
retried to green. Exact `f4bb2ed4` and `b78d093e` APPROVED reviews cover
unchanged reviewed bytes, not formal approval of the new head. Fresh
exact-head CI and the established automated technical rereview are the
next gates for `df7fad6a`. Historical deletion responses remain
([5092381800](https://github.com/block/buzz/pull/7127#issuecomment-5513759316),
[5092391193](https://github.com/block/buzz/pull/7127#issuecomment-5513763497)).
No formal review dismissed. The [security
notice](https://github.com/block/buzz/pull/7127#issuecomment-5482231278)
and latest-push maintainer/codeowner policy remain separate actionable
gates: eligible Block organization members own current-range
authorization. No merge/security authority exercised.

**CI causal repair (`b78d093e`, test only):** the error `Message failed
to send: Mock first DM send failed.` is deliberately injected by the
existing fixture. CI screenshot and retry trace show the bottom-right
Sonner notification over the actual enabled Send button. `fill()` leaves
the pointer parked there; Sonner pauses its 4-second lifetime while
hovered. A fast run can click before animation settles (unchanged local
test passed in 2.7s; two actual tested-main CI cases passed first
attempt in 3.3s), which does not disprove the failure. Independent
controlled browser runs on `f4bb2ed4` and tested main `04babf` both
reproduced the same toast hit-test at Send `(1203,627,32,32)`,
persistent hover beyond 4s, and intercepted ordinary click with no
second send. Moving the real pointer to the editor allows natural expiry
and successful ordinary retry, preserving all original
DM-channel/recipient assertions. This same synchronization already
exists in the neighboring agent-startup-failure test.

The one-file correction keeps the visible error assertion, scopes its
toast locator, moves the pointer back to the editor and observes normal
toast removal (bounded 10s) before retry. No forced click, direct toast
dismissal, mocked clock, CSS override, skipped test, production behavior
change, or new backend mock. Six focused browser executions pass
(first-send/startup-failure, three repeats each, no retries); the
held-toast control fails on original bytes at the Send click while the
exact repaired test passes. Biome and diff checks pass. Reuse unchanged
5,921 Desktop / 11 availability browser / four independent probes above;
no semantic production change warrants repeating those suites. Original
failed CI attempt/retries, local fast pass, deliberate failing control
and all traces remain in `WORK_LOGS/AVAILABILITY_CI_B9210A40`. Browser
evidence is mock-IPC Chromium, not native/live-relay certification. The
UI still temporarily overlays Send while a notification is hovered; the
test exercises its real move-away/expiry recovery, not immediate
click-through.

**Rust CI causal repair (`df7fad6a`, test only):** [original Rust / Unit
Tests failure,
job100375291370](https://github.com/block/buzz/actions/runs/33668171165/job/100375291370)
tested GitHub merge `223dee91a396d8cb4ebf18b9b8559e5a54951235`.
`context_recovery_budget_exhaustion_surfaces_the_error` failed at
`regressions.rs:2756` in **0.091s** because its immediate stderr
snapshot lacked `context recovery budget spent`. ACP context-error
assertions had already passed. The captured prefix shows all three
budgets **32768 → 16384 → 8192 bytes**, above the 4096-byte floor, and
ends during the third attempt. This is **not evidence of floor
exhaustion**. The collector is an independent Tokio task; a stdout
response is not a stderr barrier. Recovery, harness and test blobs were
identical across the compared base/head/merge parents; no production
regression was implicated.

The shared test Harness now provides a bounded event/condition wait,
registering for collector notifications before reading the buffer to
avoid lost wakeups. The budget and adjacent terminal floor assertions
wait for their own diagnostic and retain the matching snapshot. The
budget test still requires the provider's ACP context error and exactly
three recovery rungs, now corroborated by **exactly four provider
calls** and no floor diagnostic. Timeout remains a real failure with
captured stderr. No fixed sleep, weaker assertion, skip,
provider-limit/logging change, dependency/config edit, or production
change.

**Deterministic causal control:** the same real agent/HTTP-provider/ACP
scenario holds only stderr collection behind a one-shot gate until after
stdout responds. The old immediate snapshot fails the original budget
assertion (intentional exit101); the repaired wait explicitly remains
Pending while held, then passes after release. No scheduler-speed
assumption or fixed sleep. This reproduces the observation race under
controlled delay, **not the exact historical CI schedule**. A
missing-diagnostic test proves the wait actually times out. Original
failure and deliberate failing-control logs/patch remain in
`WORK_LOGS/RUST_TRIAGE_06E32D2D` and `WORK_LOGS/RUST_SYNC_BC5758B9`.

**Final candidate validation:** focused recovery **9/9**, floor **1/1**,
absent-diagnostic timeout **1/1** pass. One full touched-package run,
`cargo test --locked -p buzz-agent`: **695 passed, 0 failed, 1 existing
ignored**, including all **54 regressions**. Local nextest was
unavailable, so this uses the repository-supported cargo-test fallback,
not a claim of nextest reproduction. `cargo fmt --check`, package-scoped
Clippy all-targets with `-D warnings`, differential file-size/policy and
diff checks pass. Previously reviewed availability production and the
Desktop/browser evidence above are unchanged and reused; no all-native
blanket rerun. The test-only delta was self-reviewed against collector
ordering, timeout and falsification evidence. Existing production
approval remains valid for those bytes; exact-new-head technical/CI
clearance is not assumed. The required **Security aggregate** is
distinct from optional Codex advisory feedback; no security
authorization, human review contact, or merge was requested.

**Remaining policy limits:** shutdown submission is not harness
acceptance or process termination; confirmed Offline/no-route local
removal may leave a remote process; route discovery is best effort,
membership cleanup uses `Promise.allSettled`, and multi-instance
deletion is sequential/non-atomic. No distributed singleton,
provider-health, tenant-switch cancellation, live relay TTL or packaged
WebView/VoiceOver certification is claimed. The pre-existing DM-header
raw-presence fallback (`ChannelScreenHeader`/`useActiveChannelHeader`)
remains outside this repair and uncertified. Screenshots above remain
historical mock-browser illustrations, not new deletion or native
transport evidence.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-02 21:53:15 -04:00
Logan Johnson
44d19f59a5 fix(desktop): preserve spacing after multi-word mentions (#7128)
🤖

## Summary

In Buzz Desktop, choosing a multi-word name and immediately continuing a
sentence could swallow the space after the mention: `Hey @Alice
Chenhello`. This keeps the separator, so the same action produces `Hey
@Alice Chen hello` without moving the caret or repairing the name by
hand.

The editor recognizes the complete selected label, including its
internal spaces, and settles the autocomplete caret after the trailing
separator. Deliberately moving left or clicking inside the label still
lets you edit there; this is not a rule that forces every caret to the
end of a mention.

### Related issue

Independent base: `main`. Child:
[#7133](https://github.com/block/buzz/pull/7133), whose disambiguated
labels also contain spaces. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](4fe451d9c2/docs/mention-editor.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

Select an existing member named Alice Chen, then type `hello`
immediately. Repeat after ArrowLeft or clicking inside the mention:
typing should follow your chosen caret position.

Mock-browser captures, not live remote-agent evidence:

#### Immediate typing preserves the separator
Choosing the complete label then typing produces `Hey @Alice Chen
hello`.


![separator](https://raw.githubusercontent.com/block/buzz/7258fe2d9f276b93f4d304ac2ac47c450f104157/pr-7128--separator.png)

#### Deliberate caret movement is respected
After ArrowLeft, typing edits at the chosen caret rather than forcing
the caret back beyond the separator.


![intentional-caret](https://raw.githubusercontent.com/block/buzz/7258fe2d9f276b93f4d304ac2ac47c450f104157/pr-7128--intentional-caret.png)

[Original screenshot
publication](https://github.com/block/buzz/pull/7128#issuecomment-5482715543);
immutable image URLs and captions retained here.

#### Evidence and limitations

**5,801 desktop tests**, **45 focused editor tests**, both new browser
regressions, the browser-test build and static/type/size checks passed.
[Applicable CI
passed](https://github.com/block/buzz/actions/runs/33421534320).

The broader browser run had **132 passes / 6 failures**: two
clipboard-origin setup failures and four generic caret-formatting
failures also reproduced on unchanged main. Full local `just ci` stopped
at three native timing/probe failures; a same-head native rerun passed
**3,005 tests** with 18 existing ignores. This is not a full local-CI
pass. The change fixes insertion and caret behavior, not duplicate-name
recipient selection, discovery or invitation.

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-02 21:51:45 -04:00
Will Pfleger
c328202cb0 docs(nip-fi): document Git smart-HTTP credential exemption (#7268)
Amend NIP-FI HTTP ingress with an explicit Git smart-HTTP
credential-helper exemption. The exception covers method binding,
endpoint-URL binding, and the `payload` tag requirement for `info/refs`,
`git-upload-pack`, and `git-receive-pack`, while preserving per-request
NIP-FI assertion, key pairing, and deny-map enforcement.

The spec records Git's credential-protocol limitation, the required
compensating controls, and the rule that this exception is limited to
these endpoints and is superseded by per-request signing.

Related: [PR #7264](https://github.com/block/buzz/pull/7264)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-02 20:34:24 -04:00
Will Pfleger
47d068e210 feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259)
## What

Two new agent-facing capabilities in `buzz-cli`:

### 1. `buzz gifs` command group (agent KLIPY picker path)

Agents can now search and share GIFs via the relay's authenticated KLIPY
proxy without holding a provider credential.

```bash
buzz gifs search                         # trending GIFs
buzz gifs search --query "celebration"   # search GIFs
buzz gifs share --slug <slug>            # report selection to provider Recents
```

Output is a JSON array of GIF objects. Paste the `cdn_url` field
directly into `buzz messages send --content` — sending a GIF is a plain
message containing the CDN URL, no special send-path handling.

**Implementation details:**
- Gates on NIP-11 `supported_extensions` containing `buzz-gif` and
`gif.provider == "klipy"`
- Uses relay-relative paths from the NIP-11 `gif` descriptor — no
hardcoded paths; safe-path validation mirrors
`desktop/src/features/gifs/api.ts`
- New `post_json_authed` helper in `BuzzClient` handles NIP-98-signed
JSON POSTs and 204 No Content responses
- `customer_id` derived as `SHA-256(secret_key_bytes || '\0' ||
relay_url_bytes)[..16]` → 32 hex chars: stable, relay-scoped, not
computable from public data, no storage needed
- `locale` defaults to `$LANG` (stripped of encoding suffix) or `en_US`

### 2. NIP-30 custom emoji tags on outgoing messages

`buzz messages send` now automatically attaches `["emoji", shortcode,
url]` tags for any `:shortcode:` patterns in the content that resolve in
the workspace palette — identical to the desktop composer behavior.

```bash
buzz messages send --channel <uuid> --content "hello 👋 everyone 🎉"
# → event carries ["emoji", "wave", "..."] and ["emoji", "tada", "..."] tags
```

**Implementation details:**
- Hand-rolled single-pass scanner (no new dependency) implementing
`:([a-z0-9_-]+):` case-insensitively with canonical lowercase output —
mirrors `desktop/src/shared/lib/customEmojiTags.ts` exactly
- Zero extra relay round-trips when content contains no `:` character;
one `query` when candidates exist but none match
- Palette fetch reuses the existing `union_custom_emoji` logic from
`commands/emoji.rs`
- `build_message` in `buzz-sdk` gains a new `emoji_tags: &[Vec<String>]`
parameter (additive — all existing callers pass `&[]`); NIP-30 tag
attachment lives in the SDK alongside `imeta` tags
- MCP send path (`buzz-acp`) continues to pass `&[]` and is not
affected; the MCP gap is noted in a comment

## Files changed

| Crate | File | Change |
|-------|------|--------|
| `buzz-cli` | `src/commands/gifs.rs` | New — search + share handlers,
NIP-11 gating, tests |
| `buzz-cli` | `src/commands/mod.rs` | `pub mod gifs` |
| `buzz-cli` | `src/lib.rs` | `Gifs(GifsCmd)` variant, dispatch arm,
inventory test update |
| `buzz-cli` | `src/client.rs` | `post_json_authed` helper |
| `buzz-cli` | `src/commands/emoji.rs` | `scan_shortcodes` +
`resolve_emoji_tags_for_content` + tests |
| `buzz-cli` | `src/commands/messages.rs` | Emoji scan + tag injection
in `cmd_send_message` + seam tests |
| `buzz-cli` | `README.md` | `buzz gifs` section + emoji-in-messages
note |
| `buzz-sdk` | `src/builders.rs` | `build_message` gains `emoji_tags`
param + tests |
| `buzz-acp` | `src/pool.rs` | Update `build_message` call site (`&[]`)
|
| `buzz-acp` | `src/setup_mode.rs` | Update `build_message` call site
(`&[]`) |
| `countdown-bot` | `src/main.rs` | Update `build_message` call site
(`&[]`) |

Relates to: https://buzz.block.builderlab.xyz — buzz-team channel thread
on agent GIF/emoji support

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-02 20:03:38 -04:00
Arjun Mahanti
ac5a18697c feat(desktop): add persistent Bestie experience (#7223)
## Summary

- add a persistent, community-scoped Bestie designation for local
managed agents
- surface the designated agent in the sidebar, agent library, profile
actions, message toolbar, and draggable floating shortcut
- bloom the floating shortcut into a lightweight compact composer that
reuses the normal DM timeline, reactions, presence, and send behavior
- support message handoff with a bounded snapshot and a full Buzz thread
link so the agent can retrieve the complete conversation

## UX details

- the floating avatar and expanded panel stay above app chrome and drag
as one aligned surface
- closing the expanded panel returns it to its top-right anchor
- each mini-composer opening starts visually fresh while messages sent
during that opening remain conversational
- the designated Bestie's duplicate DM entry is hidden from the regular
DM list
- Bestie actions are suppressed inside the mini timeline to avoid
recursive handoff

## Reliability and maintainability

- preserve existing retention database paths across upgrades
- serialize assignment and deletion, clearing matching assignments
across community scopes before an agent is removed
- fence async conversation resolution against workspace and assignment
changes
- validate stale assignments against existing local agents before hiding
DMs
- share lightweight assignment state across agent cards and keep
protected-feature behavior out of the shared timeline API

## Testing

- `pnpm --dir desktop test` — 5,886 tests passed
- `pnpm --dir desktop exec tsc --noEmit`
- `pnpm --dir desktop exec biome check ...`
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
--all-targets -- -D warnings`
- focused native retention and Bestie assignment/command tests — 34
passed
- `VITE_BUZZ_BESTIE=1 pnpm --dir desktop build:e2e`
- `VITE_BUZZ_BESTIE=1 pnpm --dir desktop exec playwright test
--project=smoke tests/e2e/bestie.spec.ts`
- pre-commit and differential pre-push hooks

## Rollout

The UI remains gated by the `bestie` build feature. Screenshots covering
setup, empty, assigned, floating, and message-handoff states are
included in the PR discussion.

---------

Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-02 17:56:17 -04:00
Will Pfleger
6f6093243e fix(desktop): harden profile batch and thread-reply fetches against relay slowness (#7188)
Three client gaps turn transient relay failures into permanent UI
degradation. Under a slow or rate-limited relay:

1. A cold channel's profile batch exhausts its single retry and leaves
raw npubs + broken mention chips until the user manually kicks the
channel.
2. A thread opened from a notification trusts a successful-but-empty
reply read as authoritative and never retries.
3. A rate-limited `CLOSED` on a history subscription immediately rejects
the caller rather than retrying after the rate-limit window.

All three are addressed without changing global query defaults or the
happy-path behavior.

## Changes

**Fix 1 — cold profile batch resilience** (`useUsersBatchQuery`,
`desktop/src/features/profile/hooks.ts`)

Override `retry: 3` with exponential backoff and error-gated
`refetchOnWindowFocus: (query) => query.state.status === "error"`,
scoped to this query only. The global defaults (`retry: 1`,
`refetchOnWindowFocus: false`) are intentional for other queries and are
unchanged. After the retry budget exhausts, a window-focus event (e.g.
channel-switch) recovers the query automatically — but only when it is
already in an error state, preventing unnecessary refetches for
successful batches.

**Fix 2 — stale-empty thread reads** (`useThreadReplies.ts`,
`ChannelScreen.tsx`)

Add optional `expectedEventId` parameter. When a completed paged fetch
does not contain the expected event, throw
`ThreadExpectedEventMissingError` so React Query's built-in retry
machinery handles it rather than caching an authoritative empty.
`ChannelScreen` passes `threadScrollTargetId` (the notification-linked
reply ID) as `expectedEventId`.

When notification routing changes `expectedEventId` while the same
thread root is already mounted (same query key), an explicit
`invalidateQueries` in a `useEffect` triggers a fresh validation pass.
The `useEffect` is declared after `useQuery` so TanStack's internal
options-update effect installs the new `queryFn` closure first; the
refetch therefore uses the current `expectedEventId` rather than the
previous null closure. For the cold-start race (target arrives before
the first page returns), the effect detects `fetchStatus === "fetching"
&& status === "pending"` and calls
`cancelQueries().then(invalidateQueries)` so the obsolete in-flight
response cannot settle as authoritative before the new target's
validation closure is active.

The query-fn tracks consecutive fetch attempts per target. On attempt 3,
it adds the target to `exhaustedTargetsRef` before calling
`loadThreadReplies`. `loadThreadReplies` sees the target in the
exhausted set and returns the fetched replies directly rather than
throwing — the terminal attempt always resolves to success. No
re-entrant scheduling: the resolution is synchronous inside the query
function itself. Deleted/moderated targets never lock the thread in a
terminal error surface.

**Fix 3 — CLOSED recovery for history subscriptions**
(`relayClosedRecovery.ts`, `relayClientSession.ts`,
`relayClientShared.ts`, `relayGateBoundary.ts`)

On a rate-limited `CLOSED` the subscription previously rejected the
caller immediately. Store `filter` and `timeoutMs` on
`HistorySubscription`, then on rate-limited `CLOSED` re-register under a
fresh `subId` and defer `sendReq` until the rate-limit window clears —
matching the live-sub recovery design already present in
`relayClosedRecovery.ts`. Bounded to 3 attempts; exhausted retries
reject immediately so callers are never left waiting indefinitely. A new
op-timeout guards the retry REQ against a non-responding relay; when the
op-timeout fires it sends `CLOSE` for the rotated `subId` (matching the
behavior of the original timeout path) so the relay releases the slot
rather than counting it against the per-connection cap.

## Tests

- `relayClosedRecovery.test.mjs`: behavioral fake-clock tests for
history-sub retry, 3-attempt exhaustion, op-timeout CLOSE send +
late-EOSE non-regression, rejecting-`closeSubscription` swallowed
without unhandled rejection, wiring source assertion (fails if
`relayClientSession.ts` drops the `closeSubscription` callback) — 18
tests
- `useThreadReplies.test.mjs`: `loadThreadReplies` unit tests
(throw/exhaustion-guard); behavioral hook tests via real
`QueryClientProvider` + `renderHook`: exhaustion-resolves-to-data,
settled null→target change retries on missing-target page and lands
target data (fails if `invalidateQueries` is removed OR if `useQuery` is
moved after the `useEffect`), cold-fetch cancel-then-invalidate (gated
fetcher — released after rerender, stale empty discarded, replacement
fetch settles with target); ChannelScreen wiring source assertion — 9
tests
- `profileBatchResilience.test.mjs`: source assertions for `retry: 3`,
`retryDelay`, error-gated `refetchOnWindowFocus`, and unchanged global
defaults — 2 tests

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-02 17:42:45 -04:00
Will Pfleger
187df22252 docs(nip-fi): adopt deny-until-TTL and extend enforcement to HTTP ingress (#7254)
Spec revision following two decisions: Option B admin deny (2026-09-02)
and the HTTP ingress ruling (2026-09-02). Revises `docs/nips/NIP-FI.md`
only. Follows #7214 (merged spec v2).

## What changed

### Admin disconnect: session-only → deny-until-TTL

The disconnect operation proceeds in two steps, in order:

1. Insert a memory-resident deny entry keyed by `(iss, target_pubkey)`
with absolute expiry `until` — atomically combined with the `(iss, jti)`
replay-identity reservation as one all-or-nothing mutation. If the deny
set is at capacity (per-issuer bound), the relay rejects `503`; neither
the jti nor the deny entry is recorded, and the caller may safely retry
the same signed command.
2. Close all live WebSocket connections for the target pubkey,
synchronously.

The single atomic admission mutation (jti reservation + deny-entry
insertion) lives inside `VerifyCommandJwt` step 7, after all pure
authorization checks. The endpoint only closes sessions on success. This
ordering ensures a capacity failure leaves no state behind and makes the
retry-safe 503 contract implementable.

The deny set is RAM-cache only — no durable storage, no schema changes.
The same operational posture as the JWKS snapshot. A relay restart MAY
forget active entries; the issuer SHOULD re-push still-active deny
entries on observed restart (same publish/cache pattern as JWKS). If the
issuer stops issuing assertions and re-push completes before any
expired-entry reconnection attempt, residual exposure after restart is
bounded by `max(0, min(exp, iat + maximum_assertion_age) - now)`. If the
issuer continues issuing or re-push does not complete in time, that
formula does not apply and access may continue beyond it.

**`until` claim:** Required on the disconnect command JWT. Because an
assertion accepted at the future-skew boundary (`iat <= now + skew`)
remains valid until `iat + maximum_assertion_age`, the latest possible
authority deadline is `now + skew + maximum_assertion_age`. The relay
enforces `until <= now + skew + maximum_assertion_age`. A value above
this ceiling rejects `400`; a past `until` still closes live sessions —
absent an active same-key entry it creates no future denial, while an
active entry remains unchanged under the merge rule.

**Capacity and eviction (per-issuer):** The relay MUST bound the deny
set size **per issuer**. Capacity exhaustion under one issuer MUST NOT
cause rejection of another issuer's commands; the `503` capacity check
is evaluated against the command's own issuer bound. Implementations
MUST evict only expired entries; when an issuer's partition is at
capacity and all entries are still active, the relay MUST reject the new
command `503` without removing any existing entry. There is no LRU
eviction of active denies.

**Issuer-global deny:** The deny entry applies to admission across all
communities served by the relay under that issuer. Identity-level
revocation is intentionally not community-partial.

**Cross-replica propagation:** In a deployment with multiple relay
processes, the deployment MUST propagate both the session-close and the
deny entry to every process serving admissions for the issuer's
communities. The mechanism is deployment-defined (e.g. the existing
inter-process message bus, same posture as JWKS convergence).
Propagation is asynchronous with no protocol-level completion bound. The
issuer re-push duty is the recovery path for lost propagation, exactly
as for relay restart.

**Response shape:** A successful disconnect responds `{"disconnected":
true}` regardless of how many sessions were closed. No session count is
returned; a count would aggregate activity across communities and
constitute an information leak.

**Admission procedure:** Step 5 registers the session's proven `k`
before the deny-set check (new step 6) — ensuring any connection that
straddles a concurrent disconnect is caught by one side or the other.
`FI-TRACE-DENY-SET` oracle covers the per-issuer capacity rule and the
straddling termination requirement.

### HTTP ingress enforcement

Without explicit enforcement, a protected HTTP surface (bridge, invites,
media, git) with NIP-98-only authorization allows a principal holding an
active key to mint fresh NIP-98 events indefinitely — NIP-98 proves key
possession only, not identity. Without assertion verification there is
no expiry bound; the key remains valid for as long as it is accepted.

**Pairing rule:** in enforce mode, a protected HTTP request MUST carry
both:

```
Authorization: Nostr <base64-NIP-98-event>
Nostr-Federated-Identity: Bearer <compact-JWS>
```

The NIP-98 pubkey MUST equal the assertion `nostr_pubkey` claim.
Missing, mismatched, or invalid evidence of either kind denies, fail
closed.

**Verification:** reuses `VerifyAssertion` unchanged — offline, same
JWKS, same claim requirements, same denial classes.

**Per-request:** HTTP is sessionless; every request re-verifies. No
session lifetime, no cached admission. The cumulative residual bound
applies per request.

**Deny-set applicability:** the deny-until-TTL entry introduced above is
consulted per HTTP request identically to WebSocket admission.

**Protected surface:** deployment-configured set of routes, fail-closed
default (unclassifiable routes treated as protected). No normative route
names in the spec.

`FI-TRACE-HTTP-INGRESS` oracle added. Security considerations updated
with HTTP ingress bypass analysis. NIP-98 source reference added.

### Other changes

- `authorization_denied` rejection table row updated to "active deny-set
entry for pubkey".
- Discovery: `maximum_residual_upstream_revocation_seconds` remains
`null` — the deny-until-TTL model is best-effort RAM state and provides
no unconditional finite revocation bound.
- Rejection and privacy: explicit sentence for HTTP denial path.
- Client-attached transport: opening sentence generalized to cover both
WebSocket and HTTP.

## Scope

Single file: `docs/nips/NIP-FI.md`. No code changes.

References #7214. Channel: buzz-enterprise-identity-spec-v2
(#a6fe0b1c-987a-43c5-a974-71ee36678d78).

---------

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-02 17:24:14 -04:00
Taylor Ho
b6dc533b7c fix(composer): align wrapped inline chip fragments (#7242)
**Category:** fix
**User Impact:** Wrapped channel and mention chips in the chat composer
now align continuation text with the chip edge while keeping the icon on
the first line.

**Problem:** Plain composer decorations used absolute icons plus cloned
icon-sized padding, so every wrapped fragment inherited an empty icon
gap and the icon aligned against the union of all lines. **Solution:**
Keep the icon in the first fragment's inline flow and restore normal
chip padding on continuation fragments, while explicitly leaving the
separate wrapping Buzz-link and sent-message rendering paths unchanged.

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

**desktop/src/shared/styles/globals/composer.css**
Scopes in-flow icon geometry and normal continuation padding to plain
composer mention and channel decorations, excluding wrapping atom-link
chips and preserving the human-icon vertical correction.

**desktop/tests/e2e/mentions.spec.ts**
Adds a rendered narrow-composer regression that checks two fragments,
static icon geometry, first-line icon space, and continuation-line
alignment to ordinary chip padding.

</details>

## Reproduction steps

1. Open a channel in Buzz Desktop.
2. Narrow the chat composer enough for `#all-replies` to wrap.
3. Confirm the channel icon occupies only the first line and `replies`
starts at the chip's normal left padding rather than an icon-sized
inset.
4. Send or view a long inline Buzz chip in the message list at a
constrained width.
5. Confirm its icon remains attached to the leading fragment and its
remaining label continues cleanly on following lines.

## Screenshots

**Composer — wrapped `#all-replies` channel reference**

![Wrapped channel reference in the narrow dark-theme
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/7242/composer-wrapped-channel.png)

**Message list — existing wrapped inline-chip rendering preserved**

![Wrapped repository chip in a sent message at constrained
width](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/7242/message-list-wrapped-chip.png)

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-09-02 14:08:12 -07:00
ravarora2
91ab9d31b8 Add operation-aware database pool acquisition metrics (#7195)
## Why

Buzz already reports coarse writer/reader database checkout waits, but
those
signals cannot explain which startup or serving operation is blocked by
pool
pressure. That makes rollout diagnosis and postmortems ambiguous: a
readiness
probe, NIP-42 authentication, authorization check, reconnect history
repair,
event write, and background maintenance can all wait on the same pool
while
appearing identical.

This PR implements Package 2A of the pod-handoff plan: operation-aware
pool
borrow causality. It is observability-only; it does not change
configured pool
sizes, SQL semantics, transaction ordering, or timeout policy. Physical
DNS/TCP/TLS/Postgres authentication and session initialization remain
the
separate Package 2B boundary.

## Metric contract

The final contract separates three questions:

| Question | Metric |
|---|---|
| How long did checkout wait? |
`buzz_db_pool_acquire_duration_seconds{pool_role,operation}` |
| How did the attempt end? |
`buzz_db_pool_acquire_attempts_total{pool_role,operation,outcome}` |
| Who is waiting now for a tracked operation? |
`buzz_db_pool_waiters{pool_role,operation}` |

Outcomes are `success`, `timeout`, `error`, and `cancelled`. Operations
are
`bootstrap`, `readiness`, `tenant_resolution`, `authentication`,
`authorization`, `subscription_history`, `event_write`, and
`maintenance`.

Only these eleven pool/operation pairs are constructible:

```text
writer/bootstrap                 reader/bootstrap
writer/readiness
writer/tenant_resolution
writer/authentication
writer/authorization             reader/authorization
writer/subscription_history      reader/subscription_history
writer/event_write
writer/maintenance
```

The duration histogram intentionally does not carry `outcome`. Result
data
remains available on the terminal counter for historical counts and
rates,
without multiplying the expensive histogram family. Nine finite buckets
plus
`+Inf`, sum, and count produce 12 duration series per valid pair.
Together with
four outcome counters and one waiter gauge, the new-family ceiling is
exactly
187 raw Prometheus series per pod, asserted from the production
exporter.

The existing coarse acquisition families remain temporarily for
dashboard
compatibility while the new series are validated in staging.

The operation-specific waiter family covers the explicitly routed,
deployment-critical operations above; it is not a census of every
possible
SQLx checkout in the process. The dashboard pairs it with SQLx pool
active/idle/max gauges for whole-pool capacity context, and treats a
missing
operation series as unknown rather than healthy zero.

## What changed

### Cancellation-safe acquisition ownership

- Add writer- and reader-specific typed operation APIs so invalid label
pairs
  cannot be constructed and store modules cannot emit reader labels.
- Own every polled acquisition with one RAII terminal guard.
- Record exactly one duration and terminal outcome for success, timeout,
error,
  or cancellation.
- Emit nothing for a future that is created but never polled.
- Balance the operation-specific waiter count exactly once on every
terminal or
  dropped future.
- Periodically refresh every expected waiter pair, including healthy
zero, so
missing telemetry is not presented as zero. Reader pairs are emitted
only
when a distinct read pool is configured; a writer-only pod cannot
fabricate
  healthy reader-zero state.

### Production attribution

Route the deployment-critical acquisition paths through caller-owned
semantic
entry points, including:

- writer and reader bootstrap;
- the real post-#7149 readiness acquisition and deletion-catalog
validation;
- tenant resolution and community lifecycle checks;
- NIP-42 allowlist authentication;
- membership, moderation, invite, operator, Git, agent-owner, and policy
  authorization;
- operator community create/list/archive/unarchive, reverse host/channel
tenant
  resolution, and REQ row-community conformance lookups;
- writer/reader subscription history, feed, thread, and routed fallback
paths;
- primary and command event writes, replaceable events, mention
indexing,
  reaction/channel/member/archive side effects, and thread metadata;
- push matching, usage rollups/leadership, replica-fence startup and
recurring
probes, periodic reconciliation, channel/deletion reapers, partitions,
and
  other bounded maintenance/bootstrap paths.

Shared helpers now accept caller-owned intent or expose named semantic
variants
instead of assigning one misleading operation to every caller. No known
P0 path
uses `other`.

### Readiness and size-one-pool correctness

- Rebase on the post-#7149 readiness implementation and instrument the
actual
`Db::readiness_check` acquisition rather than the superseded ping-only
seam.
- Acquire once for deletion-catalog validation and run its queries on
that
  connection, preserving the shared readiness deadline.
- Scope the channel-roster catalog checkout before the behavior probe so
a
  size-one writer pool cannot self-deadlock during startup verification.

### Exporter, documentation, and CI

- Register metric HELP/type/unit metadata through the production
Prometheus
  builder.
- Configure dedicated checkout buckets at 1ms, 5ms, 10ms, 25ms, 50ms,
150ms,
  500ms, 1s, and 3s.
- Add a production scrape-contract test for exact names, labels,
buckets,
  valid pairs, sensitive-label exclusion, and the 187-series ceiling.
- Add source mutation guards for the P0 semantic entry points and
raw-checkout
  bypasses.
- Add an exact backend-integration CI selector for the production
attribution,
  cancellation, readiness, and size-one-pool PostgreSQL tests.
- Document the frozen label vocabulary, valid combinations, semantics,
and
  cardinality budget in the Helm chart README.

## Dashboard intent

The new Stage 2 row in **Buzz Startup & Rollout Safety** is
deployment-first:

- baseline-versus-candidate attempts, failure rates, cancellation rates,
and
  maximum wait by operation;
- outcome counts and percentages over time by SHA/ReplicaSet;
- acquisition wait heatmap, average, and maximum through the rollout;
- historical waiter pressure beside writer active/idle/max context;
- per-pod postmortem drilldown, including terminated pods;
- a smaller current-waiter table with explicit stale/missing semantics.

Percentile widgets remain disabled until Datadog metadata confirms
percentile
support for the new distribution. Current gauges use no fill,
interpolation, or
`default_zero`; missing means unknown.

## Risk assessment

Moderate. The patch touches many database acquisition call sites, but
preserves
the selected physical pool and executes the same SQL on the acquired
connection. The main risks are incorrect semantic attribution,
cancellation
double-counting, and a helper accidentally acquiring twice. Typed APIs,
production-method PostgreSQL tests, source guards, the raw scrape
contract, and
the size-one-pool regression cover those risks.

No tenant, community, user, pubkey, event, channel, SQL, URL, pod,
version,
ReplicaSet, or request-controlled value is emitted as an application
metric
label. Deployment identity is supplied by infrastructure enrichment.

## Verification

- `cargo fmt --all -- --check` — passed.
- `cargo clippy -p buzz-db -p buzz-relay --all-targets --all-features --
-D warnings`
  — passed.
- `cargo test -p buzz-db` — 122 passed, 0 failed, 263 ignored;
  source-contract integration test: 3 passed, 0 failed.
- Focused relay compatibility, metric-contract, and readiness tests —
passed.
- `scripts/test-postgres-test-discovery.sh` — passed.
- Full `buzz-relay` package run from the identical tree reached 1,015
passes;
the six media-test failures all stopped in their shared local PostgreSQL
setup with `Sqlx(PoolTimedOut)` because Docker/PostgreSQL was
unavailable.
The same six failed in isolation, while every changed exact test passed.
- Exact implementation head:
  `f92910b353086e9edf85918ca5f72190edbbe22f`.
- Exact multi-architecture staging image:
  `dev-sha-f92910b353086e9edf85918ca5f72190edbbe22f-run-33607968668-1`

(`sha256:161712c8ed2e265a15df9b63e02248d5973481f875ff129d7d2ae78a09d487a2`).
- Focused staging GitOps PR:

<https://github.com/squareup/builderbot-platform-core-infrastructure/pull/299>
— merged after renderer, inventory, infrastructure test, Kargo, Semgrep,
and
Intersect gates passed; the source/generated-artifact diff was exactly
two
  image lines.
- Exact GitHub head reports 47 terminal checks: 35 successful and 12
  intentionally skipped. PostgreSQL, unit, lint, security, both server
cross-compiles, backend integration, relay E2E, desktop, mobile, image,
  Helm, Semgrep, zizmor, and DCO gates are green.
- Datadog readback identifies two exact-image pods,
  `buzz-d79c8d8f7-ckv2l` and `buzz-d79c8d8f7-qzqdp`, in ReplicaSet
  `buzz-d79c8d8f7`; both report the full source SHA above.
- Both pods report all eleven allowed pool/operation waiter pairs at
current
zero, with no invalid pair. The acceptance window observed nonzero
success
  receipts for readiness, tenant resolution, authorization, subscription
history, event write, and maintenance, and no timeout, error, or
cancelled
outcome. Maximum observed wait was about 101 ms for maintenance and 50
ms
  for reader subscription history.

The main **Buzz Startup & Rollout Safety** dashboard now has a live
Stage 2
database row with eight widgets and nineteen fully scoped queries. Final
readback preserved all seven top-level groups, found zero under-scoped
Row 6
queries, and confirmed the tracked-operation waiter boundary in the
panel
descriptions.

Generated with Codex.

---------

Signed-off-by: Ravneet Arora <rarora@squareup.com>
2026-09-02 17:04:34 -04:00
Logan Johnson
83e5962984 fix(desktop): keep explicit agent profiles bound to their exact key (#7131)
🤖

## Summary

In Buzz Desktop, clicking a message from stopped agent A could open
running agent B—and B's controls—because both shared a persona (an agent
definition). This now opens the author you clicked and only that agent's
own controls, so you can inspect an old message without being redirected
to a different running agent.

An explicit public key—the identifier for one agent—now stays exact
across message authors, members, DMs, deep links and Instances rows,
including stopped, archived and relay-only agents. Local controls come
only from a matching local record for that key. A relay-only A cannot
borrow B's Start/Stop/Edit controls or configuration.

Deliberately opening a **persona** is different: it can still select a
representative that respects archived instances or offer Start when none
remains. The change removes competing historical-persona redirects
rather than adding another identity exception.

### Related issue

Independent base: `main`; no stack parent or child among the
replacements. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](9c4b6523ce/docs/agent-profile-identity.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

Synthetic Playwright mock-bridge state. After screenshots exercise this
independent profile extraction (`df6612b1`); no availability or
cloud-marker implementation is included.

#### Before: historical A redirects to running B
Unchanged main product code (`bc006f67`) with the same updated
historical-message fixture fails: clicking Earlier Parity Agent opens
Current Parity Agent and its Stop control.


![01-before-historical-redirect](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--01-before-historical-redirect.png)

#### After: historical A opens A
The clicked author remains Earlier Parity Agent, with A's public key and
its own Start control. The current sibling is not substituted.


![02-after-historical-exact](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--02-after-historical-exact.png)

#### Exact relay-only A while local sibling B exists
A's public key and owner-scoped profile are visible; no local
Start/Stop/Edit/Add control or sibling definition is borrowed.


![03-exact-relay-with-local-sibling](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--03-exact-relay-with-local-sibling.png)

#### Explicit persona navigation may select local B
Deliberately opening the persona selects its local representative, with
B's key and legitimate Stop/Restart/Edit controls.


![04-explicit-local-persona](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--04-explicit-local-persona.png)

#### Explicit persona without an instance may offer Start
This is a deliberately opened persona, not a relay-only key turned into
a persona surface.


![05-explicit-persona-without-instance](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--05-explicit-persona-without-instance.png)

[Original screenshot
publication](https://github.com/block/buzz/pull/7131#issuecomment-5482310890);
all five immutable image URLs and captions retained here. The final
documentation-only commit does not change this UI. These are synthetic
browser fixtures, not live runtime health evidence.

To check manually, open an old message from stopped A while same-persona
B is running; compare the displayed key and controls. Then open the
persona itself and verify that representative selection still works.

#### Evidence and limitations

**5,793 desktop tests**, **56 profile/archive browser cases**,
type/static/size checks and repository-wide `just ci` passed. The
historical-message regression fails on unchanged main by opening B
instead of A. [Published-head CI
passed](https://github.com/block/buzz/actions/runs/33422207592).

The [advisory security
check](https://github.com/block/buzz/actions/runs/33422240973) timed out
without a result; it is not a passing check. No availability,
cloud-marker, discovery or mention-routing change is included. These
screenshots do not establish remote delivery, agent execution or
termination.

#### Security authorization history (audit, not clearance)

The [security
gate](https://github.com/block/buzz/pull/7131#issuecomment-5482300054)
remains visible and unresolved. Existing authorization-request comments
were posted by `loganj`: [old-head
request](https://github.com/block/buzz/pull/7131#issuecomment-5482311163)
for `df6612b1db5a6f8d128cef955fd66a80b6828cb8` at 2026-08-31 17:55:11
UTC, then [current-head
request](https://github.com/block/buzz/pull/7131#issuecomment-5482320424)
for `9c4b6523ceaef0f3d92906fcdb5d9a3b9ede7e17` at 17:55:57 UTC. The
existing [issue-comment workflow
run](https://github.com/block/buzz/actions/runs/33422240973) ended
cancelled after the previously reported timeout; it did not produce a
completed security review. Latest exact-head Run/Post Codex jobs are
skipped, not security approval. Historical comments remain available at
their original links; consolidating their audit here does not withdraw
authorization or clear the gate. An authorized security workflow owner
must arrange the missing exact-range result.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-02 16:20:49 -04:00
Logan Johnson
434dafe958 fix(desktop): discover authenticated owned relay agents (#7122)
🤖

## Summary

An agent you own could be missing from **New message → To:** and
**Channel members → Add people and agents** on a machine that has never
managed it. This PR lets those existing lists find your agent without
requiring a shared channel first. Desktop now checks records proving you
own it, rather than looking only at agents in channels you've already
joined.

**No new screen or control is added.** For example, an agent with
verified ownership and **Who can send instructions → Only me (default)**
can now appear even with no shared channels. Each screen still applies
its existing access rules; this does not make every discovered agent
selectable everywhere.

| Screen / control | Before | After this PR alone |
| --- | --- | --- |
| **New message → To:** recipient picker | An owned agent absent from
this machine and shared-channel bot lists could be missing. | Its named
**agent** row can appear; selecting it adds a recipient chip. This is
recipient selection, not a guarantee that a later message will reach or
wake the agent. |
| **Channel members → Add people and agents** | The same agent could be
missing from **Not in this channel** search results. | Its row can
appear with the existing **Add** button. If you can add members, that
button submits the existing channel-membership request; finding the row
alone changes no membership. |
| **Stream / forum composer → @ suggestions** | An owned agent already
in the channel under an ordinary member role could be missing from agent
suggestions. | Its actual membership is recognized without requiring the
bot role. Agents not managed on this device still need membership in
that channel. |
| **Pulse → Agents** | An agent absent from both local management and
the server's agent list was omitted from the count and author lookup. |
The count and feed's author lookup can include it; notes appear only if
it has published them. |

Being listed does **not** mean the agent is online, add it to a channel,
or grant local Start/Edit controls. For agents not managed on this
device, global **Search** still excludes those configured for “Only me”,
and DM @ selection is not added here. DM @ selection and message-driven
nonmember invitation are addressed in
[#7124](https://github.com/block/buzz/pull/7124); the standalone forum
**Invite / Cancel** flow is in
[#7125](https://github.com/block/buzz/pull/7125).

<details>
<summary>Ownership and membership checks</summary>

A discovery lead is not proof: the latest agent profile must have a
valid signature and exactly one valid ownership attestation—the owner's
signed link to that agent. Its response policy must be signed by that
verified owner; an invalid latest policy cannot restore an older
permission. Membership comes separately from the latest server-signed
roster, including removals.

Existing profile cards, owner labels and agent-avatar shapes also use
this stricter verification: malformed or forged evidence must not supply
ownership/agent classification on its own. Valid ownership was already
recognized; no profile-picture or badge design changes.

Attestation time conditions apply to the signed event's timestamp, not a
live expiry timer. Existing legacy compatibility and builds requiring
verified owner policy retain their respective rules. Discovery and
sending remain separate operations, not an atomic permission check.

</details>

### Review corrections

- When runtime and owner policy overlap, **explicit online/away/offline
from the verified latest runtime is retained**. Policy still supplies
ownership/permissions; claimed runtime membership is not restored.
Missing/unrecognized status stays unknown, and invalid latest policy
cannot revive runtime permissions.
- Discovery without runtime evidence is now **unknown**, not offline:
native conversion, both IPC adapters, Pulse, Projects and
profile/session consumers preserve that distinction. Unknown has no
status dot and is not promoted to a deployed/running agent.
- Both relay-only picker paths retain the authenticated owner, including
the existing **managed by you** label. The analogous global Search
projection is fixed without changing its existing “anyone” filter.
- Authorized stored profile activity remains visible when liveness
becomes unknown/absent or the active turn ends. History reads do not
start a live subscription, grant access, or imply current availability.

### Related issue

Independent base: `main`. Child:
[#7124](https://github.com/block/buzz/pull/7124), then
[#7125](https://github.com/block/buzz/pull/7125). Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](3a56d17824/docs/owned-agent-discovery.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

Current candidate: `3a56d17824522580fe04cae463b54f4c7ba66021`, a
four-file native/test/doc runtime-status repair atop published
`ae23c1c9680a881cee7eed94e259bf15bf8ce3f7`. Branch ancestry is main
`1c8321cd08feb597f8bcff5195c21148fb3e98ed`; refreshed main
`0e878664b08cdf7fb2d89d940bc2aa92cdc485f7` adds only the independent
CI-workflow split. Read-only mergeability succeeds; this is not a tested
merged-tree claim.

**Local CI attempt and continuation (not an uninterrupted green run):**
the new exact-head `just ci` passed formatting/static checks, workspace
and Tauri clippy, workspace Rust tests, **5,910 desktop tests**, desktop
production build and Tauri check. Its native main target finished
**3,073 passed / 1 failed / 19 ignored** (exit 101):
`cheap_discovery_reports_absent_before_any_forced_probe` saw a
process-global login-shell counter of 2 instead of 0. The counter
includes unrelated version/adapter probes whose tests do not hold the
failed test's PATH mutex; no managed-agent discovery implementation
changed in the runtime repair. The unchanged failing test then passed
**three isolated invocations**. Only the failed native workspace lane
was retried with `RUST_TEST_THREADS=1 just desktop-tauri-test`: **3,074
main-target tests passed / 19 ignored**, all additional workspace
targets passed (exit 0). The previously unrun `just web-build
mobile-test` tail then passed (exit 0; **2,019 mobile tests**). Earlier
successful lanes were reused; no source/guard changes or blanket CI
rerun. The original failure and all diagnostic/retry logs are retained.

- **71 native `nostr_convert` tests pass**, including seven new
production merge regressions: online/away/offline, missing/invalid
status, policy-only, status-less latest replacement and forged latest
replacement. Before production repair, those seven yielded **4 failures
/ 3 passing controls**.
- Reused frontend evidence from `ae23c1c9` (frontend is unchanged):
Desktop TypeScript and isolated E2E build pass; **9 browser tests / 0
retries**, covering both relay-only picker journeys and seven adjacent
stop-control regressions. Real UI with mock Tauri IPC, not live
relay/native webview.
- Earlier `ae23c1c9` local `just ci` passed without failures, including
3,067 native main-target tests / 19 ignored and 2,019 mobile tests; not
substituted for the new source gate above.
- Reused unchanged repair evidence: **17 real-store/hook history
regressions**, **161 focused tests**, and independent **9 mounted
owner/bot/identity revocation/regrant transitions** with zero hook-phase
native calls. The regression was falsified before repair (14 failures, 3
controls).
- Signed local-server fixtures cover discovery with no local/shared
record, ordinary-role membership, forged ownership, invalid signatures,
duplicate authentication, wrong-owner/latest-invalid policy, revoked
membership and wrong destinations. These establish native data checks,
not a live agent response.

GitHub checks and renewed technical/security review must apply to the
current published head; earlier-head green checks are not
replacement-head proof. Local source review is not formal
code-owner/latest-push approval or exact-range security authorization. A
green security workflow with substantive review skipped is not security
clearance.

### Screenshots

#### Relay-only picker evidence —
`ae23c1c9680a881cee7eed94e259bf15bf8ce3f7`

These cropped rows come from the two real production picker journeys in
[`owned-agent-discovery.spec.ts`](ae23c1c968/desktop/tests/e2e/owned-agent-discovery.spec.ts),
using mock Tauri IPC with **no local agents and no user-search
duplicate**. The fixture supplies verified-owner data and unknown
availability; the browser test checks its presentation, not native
signature verification. Both exact-tip journeys pass without retries. No
live relay, native webview, invitation, delivery or wakeup is claimed.

Before the repair, both relay-only candidate constructors discarded the
owner, so the existing “managed by you” label was absent. These are
after-repair captures; no before image was captured.

#### New Message → To
The relay-only agent retains its authenticated owner label.


![new-message-owner](https://raw.githubusercontent.com/block/buzz/536c6d3c90776cf85b1d5ce58666f2c8ad518829/pr-7122--new-message-owner.png)

#### Channel members → Add people and agents
The matching result retains “managed by you” beside the existing Add
action; the test does not click Add or claim membership changed.


![member-add-owner](https://raw.githubusercontent.com/block/buzz/536c6d3c90776cf85b1d5ce58666f2c8ad518829/pr-7122--member-add-owner.png)

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-02 16:18:54 -04:00
Will Pfleger
0dbd036f5b feat(agents): harness-agnostic effort write path and spawn bridge (#4625)
## Summary

Genericizes the agent effort write-side so Goose participates in the
same canonical effort contract as buzz-agent. A spawn bridge translates
the canonical key to whatever the target harness expects at launch time.
Read/write/spawn paths all derive their vocabulary from runtime metadata
rather than a hardcoded buzz-agent list.

## What changed

### Rust — config bridge + spawn path

- `apply_spawn_effort_env` in `effort.rs`: production command-boundary
seam — writes baked env, runs the effort projection, strips per-runtime
suppress set, and emits exactly one projected key.
- `apply_effort_to_spawn_command` in `runtime.rs`: thin wrapper
returning a `#[must_use] EffortApplied(())` token (private field —
unforgeable outside the function). `spawn_agent_child` calls it as `let
effort = apply_effort_to_spawn_command(...)` and passes `effort` to
`spawn_with_effort_proof`. Deleting the call is a compile error:
`effort` is undefined at the `spawn_with_effort_proof` site. Deleting
`apply_spawn_effort_env` inside the wrapper turns the
production-sequence tests RED.
- `apply_record_field_updates` in `agent_models_update.rs`: returns
`Result<RecordFieldsApplied, String>` (`#[must_use]` token).
`update_managed_agent` calls it as `let applied =
apply_record_field_updates(...)?` then passes `applied` to
`stamp_record_updated_at`. Deleting the call is a compile error:
`applied` is undefined at the `stamp_record_updated_at` site.
- Unknown/custom-runtime passthrough: `apply_effort_launch_to_command`
skips the suppress loop when `preserve_passthrough && value.is_none()`,
preserving ambient ACP sentinels.
- `EnvVarGuard`: prior value stored as `OsString` (`var_os`) so
non-Unicode values are restored exactly on Drop. A single
`PROCESS_ENV_MUTEX` in `managed_agents/mod.rs` is shared by
`lock_path_mutex()` and `lock_env_mutex()` — any two tests calling
either helper are mutually exclusive with each other. Tests in other
modules (`app_state_tests`, `agent_config_tests`, `reader_tests`)
maintain their own independent locks and are not in this domain.
- Dead-code: `strip_effort_keys_from_command` marked `#[cfg(test)]`;
import path in `effort_cmd_tests.rs` fixed.
- Windows CI fix: platform-gated variants for inherited-env tests.

### TypeScript — renderer + model cleanup

- `AgentConfigFields` orphan-model cleanup effect: the
`isHarnessNativeEffort` early-return was skipping the model clear on
provider→Custom transitions. Refined to: return early only when model is
already null; clear model once while preserving the harness-native
effort key (Carl P2).
- Provider-empty convergence: when model is null and effort is native,
the cleanup effect returns early (nothing to clear) — prevents spurious
`onConfigChange` loop.
- `EffortSelectField` / `humanizeEffortLabel`: runtime-native option
labels title-cased (`off` → `Off`) with raw canonical values preserved
for round-trip fidelity.
- `AgentConfigFields`: drives effort renderer from
`selectedRuntime.effortCanonicalValues` (harness-native path) or the
model/provider catalog (buzz-agent/provider path), selected by
`isHarnessNativeEffort`.

### Docs

- `desktop/src/features/agents/AGENTS.md` item 14: updated from deleted
`persistAgentEffortLevel` direct-write contract to the shipped
Save-gated `update_managed_agent.effortLevel` path. Consistent with
`EffortPickerField`'s own doc comment.

### Tests

- `agent_models_update_tests.rs`: seam tests via
`apply_record_field_updates` — non-local rejects, local set/clear,
ordering invariant, ACP-sentinel sweep.
`record_field_updates_persist_effort_to_disk` (renamed from the prior
false-claim name) drives load→apply→stamp→save→load via a mock AppHandle
+ tempdir, asserting `effort_level` persists to disk. Manual HOME/XDG
restore replaced with RAII `EnvVarGuard` (panic-safe, `OsString`-exact).
- `effort_cmd_tests.rs` / `effort_tests.rs`: production-sequence seam
tests via `apply_effort_to_spawn_command`. Spawns `/usr/bin/env` to
verify child's real env. `EnvVarGuard` for panic-safe restore. Windows
twin using `cmd /c set`.
- `effortAutoClear.test.mjs`: five mounted stateful journeys via
`AgentConfigFields` with `useCustomSelect=true`. Covers: custom trigger
shows "Off" at mount; provider-empty mount is a stable fixed point;
provider→Custom switch converges; stale Anthropic model cleared on
Custom switch with Goose effort preserved (Carl P2 regression);
Settings-style Save/reread preserves effort.
- `agentDefaultsEditor.test.mjs`: two full Save/Next journey tests
through the real production parent trees. Both start with
`GOOSE_THINKING_EFFORT: "low"` and operate the real Popover-based effort
control (click trigger → click "off" option) before Save/Next, asserting
zero writes after selection. The `set_global_agent_config` stub captures
the submitted payload; each test asserts raw `GOOSE_THINKING_EFFORT:
"off"` in the captured config. The stub stores its canonical response
from the actual payload; the fresh remount's `get_global_agent_config`
returns that stored object (not a hand-written fixture), then asserts
"Off" shown. The `DefaultConfigStep` test starts with `isDirty: false` —
the real-control effort selection calls `onConfigChange → updateDraft →
isDirtyRef=true`, making the `commit()` on Next load-bearing.

## Mutation evidence

- Delete `let effort = apply_effort_to_spawn_command(...)` call from
`spawn_agent_child` → compile error: `error[E0425]: cannot find value
`effort`` at `spawn_with_effort_proof` site.
- Delete `let applied = apply_record_field_updates(...)?` from
`update_managed_agent` → compile error: `error[E0425]: cannot find value
`applied`` at `stamp_record_updated_at` site.
- Delete `apply_spawn_effort_env` from inside
`apply_effort_to_spawn_command` wrapper →
`production_sequence_goose_inherited_collision_resolved_in_child` RED.
- Revert `isHarnessNativeEffort &&` guard in cleanup `useEffect` to bare
`if (isHarnessNativeEffort) return` → stale model not cleared → Carl P2
regression test RED.
- Remove `isHarnessNativeEffort ||` from the nothing-to-clear condition
→ provider-empty mount emits `onConfigChange` → loop test RED.
- Remove `isHarnessNativeEffort` branch in
`AgentConfigFields.tsx:634-636` → both `agentDefaultsEditor.test.mjs`
mount assertions fail: trigger shows "Select" instead of initial effort
label.
- Remove `preserve_passthrough` guard in
`apply_effort_launch_to_command` →
`production_sequence_custom_inherited_acp_sentinel_survives` RED.
- Drop `GOOSE_THINKING_EFFORT` from the `set_global_agent_config` stub
payload → payload assertion in `agentDefaultsEditor.test.mjs` fails
(`undefined !== "off"`) → RED (verified).
- Remove the effort-select dirtying steps from the `DefaultConfigStep`
test (so `isDirty` stays false) → `commit()` is a no-op → write-count
assertion after Next fails (0 instead of 1) → RED.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-09-02 14:25:30 -04:00
Will Pfleger
04babf0265 chore(db): drop Phase-A NIP-FI relay-side authority ledger (#7221)
Removes the dead relay-side authority ledger introduced by migrations
0041 and 0042, and the dead `require_attested_key` verifier knob from
`buzz-auth`. Both are unreachable by design under NIP-FI spec v2 (#7214,
squash `d4420eb47`), which makes OSS Buzz stateless for identity: the
relay neither stores nor verifies an authority chain.

## What changes

**`migrations/0044_drop_nip_fi_ledger.sql`**
Drops all fifteen NIP-FI ledger tables and their trigger functions using
`CASCADE` to resolve the circular deferred FK between
`identity_bindings` and `identity_lifecycle_history`. Drops proceed in
FK dependency order: selectors → history/bindings →
enrollment_policies/receipts → parallel drop of auth tables. Restores
`community_write_fence_excluded_table` to its pre-0041 body (removes
NIP-FI table names from the exclusion array).

**`schema/schema.sql`**
Removes the NIP-FI section (~1885 lines of tables, functions, and
triggers) and updates `community_write_fence_excluded_table` to match.

**`crates/buzz-db/src/runtime/migration.rs`**
- Updates the `embedded_migrator_contains_consolidated_initial_schema`
sanity check: count 43→44, adds 0044 assertion block (verifies `DROP
TABLE` statements and absence of NIP-FI names from `schema.sql`).
- Removes ~2580 lines of NIP-FI Postgres integration tests (all
`#[tokio::test] #[ignore = "requires Postgres"]` from the 0041/0042
behavioral coverage).
- Removes the `extract_excluded_table_array` drift check (0042 body no
longer matches `schema.sql` by design).
- Adds `migration_0044_drops_populated_nip_fi_ledger_cleanly`: runs
migrations to 0042, seeds rows in `authorization_operation_receipts` and
`authorization_invalidation_domains`, then runs to 0044 and verifies all
fifteen NIP-FI tables are absent.

**`crates/buzz-auth/src/nip_fi/config.rs`**
Removes `require_attested_key: bool` from `IssuerPolicy` — field,
constructor parameter, accessor, and its contribution to
`derive_assertion_policy_id`.

**`crates/buzz-auth/src/nip_fi/verifier.rs`**
`parse_nostr_pubkey_claim` no longer takes a `policy` parameter. The
`None` (absent claim) arm now returns
`Err(VerifierError::ClaimRejected)` unconditionally instead of
conditionally on `policy.require_attested_key()`.

**`crates/buzz-auth/src/nip_fi/verifier/tests.rs`**
- Removes `missing_nostr_pubkey_denies_under_attested_key_policy` (the
sole `require_attested_key: true` call site).
- Removes `false,` from all eleven `IssuerPolicy::new` call sites.
- Injects `nostr_pubkey` by default in `mint_signed_by` (spec v2
requires it unconditionally).
- Updates `valid_access_token_verifies` to assert
`asserted_key().is_some()`.

**`crates/buzz-auth/src/nip_fi/startup/tests.rs` + `jwks/tests.rs`**
Removes `false,` from all `IssuerPolicy::new` call sites and adds
`nostr_pubkey` to all token-minting helpers.

## Verification

- Fresh-DB migration run to head: all migrations apply cleanly in
sequence.
- Populated-0041/0042-DB migration through 0044: seeds rows in live
NIP-FI tables, verifies all fifteen are dropped without error.

Closes the dead-code inventory item from the spec-v2 cleanup plan
(channel `48374f48`). Follows #7214.

---------

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-02 13:56:41 -04:00
Will Pfleger
2af9773d62 fix(acp): replace real user name in base prompt mention example (#7250)
Replace the real user name in the shared ACP mention guidance with the
fictional `Alice Smith` example. Preserve the exact-display-name and
no-inference instructions while avoiding prompt priming from a real user
identity.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
2026-09-02 13:12:25 -04:00
Luke Tornquist
0e878664b0 ci: split CI into reusable workflows (#7168)
## Why

The monolithic CI workflow is a frequent merge-conflict hotspot.
Splitting cohesive domains into same-repository reusable workflows keeps
one centrally filtered entry point while letting Rust, desktop,
relay/PostgreSQL, client, and security CI evolve independently.

## What

- Keep `ci.yml` as the only push/pull-request orchestrator with
unchanged concurrency and path detection.
- Move 18 execution jobs into five `workflow_call`-only domain workflows
without changing their runners, steps, matrices, caches, artifacts,
permissions, or timeouts.
- Keep the relay artifact producer with desktop integration, the
complete PostgreSQL lane, and relay E2E consumers.
- Preserve all 12 existing required GitHub Actions contexts through
lightweight top-level compatibility gates, so the repository ruleset
does not need to change.
- Update the Rust-cache contract to follow Unit Tests into
`_ci-rust.yml`.

## Risk Assessment

CI-only change with moderate workflow-orchestration risk. The main risks
are reusable-workflow output propagation, skip behavior, and visible
check naming; the old required names remain explicit top-level jobs, and
the draft will stay open until an exact-head GitHub Actions run and
independent review are complete.

Generated with Codex

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Signed-off-by: tornquist <tornquist@squareup.com>
2026-09-02 12:23:16 -04: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