Commit Graph

2582 Commits

Author SHA1 Message Date
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
Will Pfleger
d4420eb47f docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
Rewrites `docs/nips/NIP-FI.md` as a stateless spec and deletes the five
companion profiles (NIP-FI-CONF, NIP-FI-DELEG, NIP-FI-EDGE,
NIP-FI-LIFECYCLE, NIP-FI-MODEL) that presuppose relay-side state.

## What changes

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

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

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

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

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

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

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

### Companion profiles deleted

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

## What does NOT change

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

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

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

---------

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

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

## Why

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

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

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

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

## Behavior

### Readiness evaluation

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

### Ordered publication and shutdown

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

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

Shutdown without dependency evaluation records only:

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

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

### Listener and response contract

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

## Prometheus contract

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

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

### Closed label sets

`reason`:

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

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

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

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

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

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

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

## Postgres production seam and CI

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

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

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

## Tests and verification

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

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

## Risk assessment

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

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

## Operational notes

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

## Non-goals

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

## References

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

---------

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

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

### Related issue

Follow-up to closed #6546.

### Testing

- Focused Tauri preset and desktop logo tests
- TypeScript typecheck

Generated with Codex

---------

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

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

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

## How

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

## Tests

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

## Scope / follow-ups

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

## Stack

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

---------

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

## Summary

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

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

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

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

### Related issue

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

### Testing

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

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

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

### Limits

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

---------

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

## Links

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

---------

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

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

## Why

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

## Testing

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

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

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

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

### Why?

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

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

### How is it tested?

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

---------

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

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

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

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

## What changed

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

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

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

## Tests

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

---------

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

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

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

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

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

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

### Related issue

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

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

### Testing

At exact head `0a09a2f3716b0378546b66be2302b750ed4330b9`:

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

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

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

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

## What

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

## Changes

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

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

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

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

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

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

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

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

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

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

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

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

### Invariant coverage

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

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

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

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
2026-09-01 13:00:40 -04:00
klopez4212
ac4aa94b05 Hide download action on voice notes (#7182)
## Summary

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

Follow-up to #6978.

## Testing

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

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

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

## What

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

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

## Risk

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

Remaining operational risks:

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

## Verification

Author Blox workstation `2020088`:

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

GitHub exact final head `92c231e48f299ea2af23817763f5ae80ac013d68`:

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

## References

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

Generated with Codex.

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

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

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

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

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

Verification at local branch head
`60086c1f6ee34a193c342255a5e8e6293b60e988`:

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

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

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

## Update — August 25, 2026 restack

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

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

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

## Update — August 25, 2026 final hosted verification

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

---------

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

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

## Testing

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

## Screenshots

Focused snapshots are attached below.

---------

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

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

## What

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

## Risk Assessment

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

## Testing

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

## Stack Info

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

Generated with Codex

---------

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


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

## Summary

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

## Root cause

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

## Verification

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

---------

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

## Summary

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

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

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

### Related issue

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

### Testing

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

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

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

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

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

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

---------

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

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

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

Follow-up to #6955.

---------

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

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

## Two internally-ordered migrations

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

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

## Durable, immutable ledger posture

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

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

## Authorization shape and cardinality contracts

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

## Mined vs. new

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

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

---------

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

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

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

## How it is turned on

Off by default. Operators opt in with one setting:

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

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

## Key design decisions

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

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

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

## Not in this PR

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

## Testing

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

---------

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</details>

### Reproduction Steps

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

### Screenshots / Demo

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

### Verification

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

---------

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

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

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

## Build contract

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

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

## Why this is separate

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

## Non-goals

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

## Verification

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

---------

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

**Category:** fix

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</details>

<details>
<summary>Validation</summary>

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

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

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

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

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

</details>

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

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

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

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

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

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

---------

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

## Summary

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

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

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

## Trust boundary

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

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

## Why this PR

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

## Validation

At exact clean commit `fe5b55619fe44176343eefb4cb7fe180df45a7d8`:

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

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

## Exact-head local relay + ACP proof

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

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

Cases:

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

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

## Deployment and residual risk

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

---------

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

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

### Related issue

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

### Testing

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

### UI evidence

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

---------

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

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

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

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

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

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

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


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

---------

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

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

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

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

## What

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

## Risk Assessment

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

## References

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

---------

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

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

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

Testing:

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

---------

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

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

## What

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

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

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

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

## Behavior change to note

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

## Testing

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

Closest existing PR/issue: none found.

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

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

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


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

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

Generated with Codex

---------

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

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

## Before

Default Ongoing/Public state:

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

Type dropdown open, showing the extra selection click:

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

## After

Default Ongoing/Public state:

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

Temporary/Private state with the Expires after row visible:

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

## Verification

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

---------

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

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

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

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

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

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

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

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

## Why

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

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

## Scope

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

---------

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

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

### Related issue

None found.

### Testing

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

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

---------

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

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

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

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

## Changes

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


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

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

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


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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</details>

## Reproduction Steps

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

## Validation

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

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

## Screenshots/Demos

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

---------

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

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

## Root cause

One underlying bug, two victims:

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

## What

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

## Verification

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

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

Fixes #6872
Related #6662

---------

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

## Summary

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

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

## Matched performance results

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

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

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

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

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

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

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

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

### Measurement boundaries

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

## Validation

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

## Limits / follow-ups

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

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

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-28 15:38:03 -06:00
Tom Brow
c432a111ca feat(mobile): push notifications MVP (#6269)
This PR implements MVP, iOS-only,
[NIP-PL](8d2d0ff5ad/docs/nips/NIP-PL.md)-compliant
push notifications.

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

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

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

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

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

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

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

## Push-time flow

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

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

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

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

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

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

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

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

## Known limitations

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

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

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

## Validation

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

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

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

## Independent pre-reviews

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

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Jordan Mecom <jm@squareup.com>
2026-08-28 15:15:09 -06:00
Luke Tornquist
a3730784fc refactor(db): extract domain stores from database runtime (#6987)
## Summary

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

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

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

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

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

### Exact base and head

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

### Related issue

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

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

### #17 / #19 acceptance

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

### Non-goals

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

### Risk

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

### Testing

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

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

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

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

---------

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

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

## Owner catalog sync pipeline

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

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

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

## Data layer

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

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

## Hooks

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

## CommunityCatalogDialog

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

## TeamShareDialog

Publishes and unshares team catalog entries via `set_team_shared`.

## e2e + screenshots

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

## Follow-ups

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

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

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

### Related issue

None found.

### Testing

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

---------

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

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

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

### Related issue

Related: #6939

### Testing

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

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-28 10:52:03 -07:00