Commit Graph

2668 Commits

Author SHA1 Message Date
Duncan
2e2f4df893 docs(agents): enumerate authoritative delivery-failure statuses in stale comments
agentSessionTypes.ts deliveryFailed doc and LifecycleActivity.tsx
PermissionDecisionButtons doc block still claimed every non-sent
control_result increments the failure token / re-enables buttons.
Update both to name the three authoritative statuses (no_active_turn,
channel_closed, no_channel) and note that channel_full is transient and
does not increment the token, matching the reducer and component comments
fixed in 965980093.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-02 15:21:26 -04:00
Duncan
51486b2638 Merge remote-tracking branch 'origin/main' into duncan/permission-policy
* origin/main:
  feat(agents): harness-agnostic effort write path and spawn bridge (#4625)
  chore(db): drop Phase-A NIP-FI relay-side authority ledger (#7221)
  fix(acp): replace real user name in base prompt mention example (#7250)
  ci: split CI into reusable workflows (#7168)
  fix(desktop): retain automatic mentions only in threads (#7144)
  feat: add databricks fable 5.1 model capabilities (#7213)
  docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
  feat(relay): add detailed readiness metrics (#7149)
  feat(desktop): add Pi agent preset (#7208)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

# Conflicts:
#	desktop/src-tauri/src/commands/agent_models_update.rs
#	desktop/src-tauri/src/commands/agents_deploy.rs
#	desktop/src-tauri/src/managed_agents/types/requests.rs
#	desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
#	desktop/src/shared/api/types.ts
2026-09-02 15:06:52 -04:00
Duncan
9659800934 fix(acp): classify channel_full as transient in transcript reducer
handlePermissionDecisionResult treated every non-sent/already_decided
status as an authoritative delivery failure, incrementing deliveryFailed
and triggering the PermissionDecisionButtons useEffect to call
setPending(null). With channel_full now keeping the retransmit orchestrator
alive, this caused two concurrent delivery loops for the same nonce: the
original retransmit loop still subscribed, plus a second loop from the
newly re-enabled button click.

Add channel_full as an early-return alongside sent/already_decided in the
reducer. Both channel_full producers (mixed owner-Full/sibling-Sent and
pure-Full) are transient queue-saturation conditions; the orchestrator
handles resend automatically and the card must stay disabled until the
retry settles or the deadline expires.

Update LifecycleActivity.tsx deliveryFailed prop doc and the re-enable
comment to name the three authoritative statuses (no_active_turn,
channel_closed, no_channel) and explain that channel_full does not
increment the token.

Tests:
- Fix buildTranscript_control_result_second_failure_increments_delivery_failed
  to use no_channel for the second failure (channel_closed would still work,
  but channel_full was the prior value and is now transient)
- Add buildTranscript_control_result_channel_full_does_not_mark_delivery_failed:
  transcript 79→80; mutation (removing the guard) → test red at expected
  undefined got 1
- Add test_channel_full_reducer_to_component_buttons_stay_disabled in
  LifecycleActivity.render.test.mjs: drives full pipeline acp_read →
  buildTranscript → click → channel_full through reducer → assert both
  buttons stay disabled + no second delivery starts; companion case
  (no_active_turn) asserts buttons DO re-enable; mutation → red at disabled
  assertion

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-02 14:31:57 -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
Duncan
a0309cdbd3 fix(acp): treat channel_full as transient; fix legacy-key retirement with colon-bearing sessionId
P1 (Desktop retransmit): channel_full is a queue-saturation signal emitted when
the owning read loop's queue is momentarily full. The previous code settled the
retransmit loop with 'failed' on this status, re-enabling the card for a manual
retry. The fix: treat channel_full as transient in retransmitPermissionDecision.ts
— the loop stays subscribed, the scheduler keeps firing, and the card stays
disabled until the loop either receives 'sent'/'already_decided' (acked) or the
deadline expires (expired/fail-closed). The owning loop's first-wins dedup
tolerates duplicate deliveries once the queue drains. Authoritative routing
refusals (no_active_turn, channel_closed, no_channel) still settle 'failed'.
NIP-AO.md updated to describe channel_full as transient rather than listing it
among the four failure statuses. Stale lib.rs and permission-request-card.tsx
comments updated to match.

P2 MINOR (legacy key colon safety): retireLivePermissionCardsForTurn cleaned up
pendingPermissions legacy keys via positional split(':') at index 2, assuming
sessionId never contains ':'. ACP SessionId is an unrestricted string. Fix:
match by the item's turnId field (which already carries the authoritative
identity) rather than parsing the key. No key-format change; existing entries
continue to be cleaned up correctly, and colon-bearing session identities no
longer leave stale legacy keys.

Tests: retransmitPermissionDecision.test.mjs split into channel_full-stays-active
(+2 tests: keeps loop/resends until acked; expires cleanly at deadline) and
authoritative-statuses-fail (3 statuses, unchanged contract).
agentSessionTranscript.test.mjs adds colon-in-sessionId regression test.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 20:04:27 -04:00
Duncan
5c8ec7a2ec fix(acp): scope permission-card retirement to terminating turn; fix fan-out false-ack
P1 (fan-out false-ack, lib.rs): The prior fix correctly gated nonce
recording and 'sent' status on all_tx_accepted (every tx-equipped loop
accepted). Adds the required regression test: saturate the owner queue
while leaving a sibling queue open, deliver a decision -> status must be
'channel_full' not 'sent', nonce not recorded; drain owner queue, retransmit
-> status 'sent', nonce recorded, owner loop receives the message.

P2 (channel-wide card retirement, agentSessionTranscript.ts): Replace
retireAllLivePermissionCards with retireLivePermissionCardsForTurn at both
turn_completed and turn_error/agent_panic call sites. With concurrent
thread-scoped turns, channel-wide retirement on turn B completing would
retire thread A's still-pending cards and delete their nonce indexes, making
a later A decision drop on an unknown nonce. Turn-scoped retirement confines
the backstop to cards whose turnId matches the terminating turn; falls back
to channel-wide only when no turn identity is present (legacy archive frames).

retireLivePermissionCardsForTurn added to agentSessionTranscriptPermissions.ts
with the same copy-on-write semantics as retireAllLivePermissionCards, scoping
both the itemsById scan (item.turnId === turnId) and the pendingPermissions
cleanup (ch:session:turn:id key format, parts[2] matches turnId).

Regression tests added (agentSessionTranscript.test.mjs):
- buildTranscript_turn_scoped_retirement_does_not_retire_sibling_thread_cards:
  thread A pending -> thread B turn_completed -> A still actionable -> A
  decision resolves correctly, including archive replay.
- buildTranscript_turn_error_scoped_retirement_does_not_retire_sibling_thread_cards:
  same invariant for the turn_error terminal path.

Mutation table:
- P1: restore any_sent semantics -> mixed-result test fails at 'sent'!=
  'channel_full' assertion.
- P2: restore retireAllLivePermissionCards -> both sibling-isolation tests
  fail because A's card is retired by B's terminal event.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 19:23:57 -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
Duncan
fbeb9fafb3 test(buzz-acp): assert reject option ID in reject_once applied write
The reject_once_resolves_entry_with_applied_write test previously
asserted only map-empty and write-count=1. Thufir's mutation of
permission_response_selected emitting a hardcoded opt-allow-once
demonstrated the test would stay green on a wrong-optionId regression.

Inspect the single applied write's payload and assert
payload[result][outcome][optionId] == opt-reject-once, mirroring
the first-wins test's payload assertion at the ACK path.

Mutation proof: forcing permission_response_selected to emit
opt-allow-once regardless of selection makes this test FAIL
at the optionId assertion.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 18:49:53 -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
Duncan
96c9b7f692 test(buzz-acp): close pass-3 gaps for F1/F3/F4 and MINOR desktop wording
F1: replace prefix-only context truncation with head/tail layout
(truncate_to_bytes_head_tail) so suffix differences survive even when the
context budget is small (10 bytes). Same-prefix commands like sameprefix-a /
sameprefix-b now produce distinct descriptions. Seam test rewritten to use
same-prefix commands and drive through build_sentinel_pending_payload,
asserting both extractor and sentinel descriptions differ.

F3: add reject_once_resolves_entry_with_applied_write — a separate client
that exercises the reject_once path and asserts resolution plus exactly one
applied write. Proves the symmetric rejection proof Thufir's accepts()→
allow_id-only mutation missed.

F4: add signed_observer_control_event_delivers_permission_decision — calls
handle_relay_observer_control_event with a real owner-signed, NIP-44-
encrypted kind-24200 frame, proving the full outer admission path (signature
check, owner-pubkey guard, freshness window, NIP-44 decrypt, type dispatch)
delivers to the in-flight mpsc without a live relay.

MINOR: fix Desktop overclaim — retransmitPermissionDecision.ts and
agentSessionTranscriptPermissions.ts comments changed from 'already applied'
to 'previously forwarded/delivery suppressed', matching pool.rs wording.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 18:19:39 -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
Duncan
9628c1e0de test(buzz-acp): strengthen pass-2 acceptance proofs for F1–F4 and routing
F1: reserve CONTEXT_RESERVE=10 bytes before capping title so a 200-byte
title no longer saturates context_budget to zero; two commands under the
same long title now produce distinct descriptions. Seam test upgraded to
deliver two different commands (cmd_a / cmd_b) under the same 200-byte
title and asserts assert_ne! on their descriptions — the saturation
mutation (title_cap_limit = DESCRIPTION_COMBINED_MAX_BYTES) makes this
red.

F2: fix false assertion text in the always-Uncertain bounded-exit test
(was claiming 'large RESOLVED_DELIVERY_WINDOW_SECS'; the injected 4s
delivery_window is what the mutation targets). Add two new old-expiry-
crossing tests:
  - resolved_edit_retransmitted_across_old_card_expiry_disconnect: entry
    resolved after its 1s card deadline; Uncertain on first attempt then
    Accepted; asserts >=2 identical-id publishes.
  - resolved_edit_retransmitted_across_old_card_expiry_lost_ok: delivery
    window starts after simulated old-expiry advance; per-attempt timeout
    sweeps and Accepted lands; asserts >=2 identical-id publishes.
Both assert only 1 publish when entry.deadline is used as delivery_deadline.

F3: restructure allow_always_and_reject_always_decisions_are_ignored_by_
read_loop into three sequential steps with fresh channels per step:
  step 1 allow_always -> entry still Pending, zero applied acp_write events
  step 2 reject_always -> entry still Pending, zero applied acp_write events
  step 3 allow_once -> entry resolved, exactly one applied acp_write event
CardActions::accepts()->true mutation resolves the entry on step 1, firing
the intermediate pending assertion.

F4: extend early_decision_first_wins to Reject + Reject-dup + Allow +
Allow-dup before ACK; still asserts exactly one applied write carrying
opt-reject. The is_none() guard mutation (unconditional overwrite) lets
the last Allow win — assertion goes red.

Routing: add two_read_loops_same_channel_nonce_mismatch_dropped_by_sibling
in acp.rs — two AcpClient instances share a channel, nonce_a's decision
is delivered to both read loops; client_a applies it, client_b silently
drops it (nonce mismatch); client_b then resolves on its own decision.
Removing the nonce guard makes client_b apply nonce_a's decision -> assert
fires.

pool.rs doc: 'has already applied' -> 'has already forwarded' to match
suppress-only semantics of recently_decided.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 17:40:07 -04:00
Duncan
c23daf8b64 fix(acp): correct wrapper-overhead and test structure for F1/F3 regressions
Two bugs surfaced by the full nextest run after the origin/main merge:

F1 — description wrapper-overhead off-by-two:
  wrapper_overhead was 3 ('(' + possible '…' + ')'), but the UTF-8
  ellipsis U+2026 is 3 bytes on its own, so the truncated form
  '(ctx…)' costs 1 + 3 + 1 = 5 bytes of overhead. Changing the
  constant from 3 → 5 keeps the combined description within
  DESCRIPTION_COMBINED_MAX_BYTES in both the truncated and
  non-truncated paths.

F3 — allow_always_and_reject_always_decisions_are_ignored_by_read_loop:
  read_until_response_with_idle_timeout takes the permission decision
  receiver via .take() and drops it on return. The original test
  called the loop three times with 200ms timeouts, causing the
  receiver to be dropped after the first call; subsequent perm_tx
  sends panicked with SendError. Restructured to pre-send all three
  decisions (allow_always, reject_always, allow_once) into the channel
  buffer before the loop runs once — the loop processes them in order,
  ignores the two non-ruled options, and resolves on allow_once,
  matching the pattern established in early_decision_first_wins.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 16:40:25 -04:00
Duncan
b144047c97 Merge remote-tracking branch 'origin/main' into duncan/permission-policy
* origin/main:
  feat(buzz-agent): add DatabricksAuthCoordinator single-flight OAuth (#5545)
  fix(desktop): preserve keyring identity during recovery (#7203)
  feat(mobile): prepare `buzz-push-gateway` for deployment (#7158)
  ci: relax file-size ceilings by surface (#6485)
  fix(mobile): isolate extension linker flags; complete iOS build in CI (#7187)
  chore(ci): lower Codex security review effort (#7179)
  fix(dev-mcp): extend shell timeout cap to 20 minutes and align outer budgets (#7185)
  fix(dev): keep the canonical profile when launching from desktop/ (#7143)
  feat(buzz-auth): add production NIP-FI federated assertion runtime (#7109)
  Hide download action on voice notes (#7182)
  ci: run PostgreSQL tests in isolated lane (#6730)
  Add voice notes to desktop messages (#6978)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-01 16:08:27 -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
Duncan
e5fb508b1b test(acp): fix bounded-exit test — small-step advance for cross-task channel interleaving
Under start_paused, a single large tokio::time::advance() moves the clock
but cannot flush cross-task channel interactions. The Uncertain path requires
test_pair_silent to drop ack_tx (channel-driven) so ack_rx resolves before
the 2s backoff sleep fires. A single yield_now() after a 34s advance was
insufficient for this ping-pong; replace with a 500ms-step polling loop that
gives both tasks a chance to run on each advance tick, matching the pattern
already established in ordinary_timeout_publishes_resolved_edit.

Also corrects the mutation-proof comment: the test constructs its own
delivery_deadline from a 4s window — the production RESOLVED_DELIVERY_WINDOW_SECS
constant is not involved; the real mutation is removing the deadline gate entirely.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 15:55:01 -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
Duncan
00a04c13b4 fix(desktop): correct display_name in content_matches_nip_ap_vector compaction
The sample_persona() refactor in the ratchet commit left out a display_name
override — sample_persona() returns 'Test Persona' but the NIP-AP vector
fixture asserts 'Test Agent'. Add p.display_name = 'Test Agent' alongside
the existing p.id override.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 12:51:42 -04:00
Duncan
d5343ef71a fix(desktop): restore extract_applied_permission_policy lost in merge, fix test callers
Three compile errors introduced by the origin/main merge at ed3a25c32:

1. agents_deploy.rs: extract_applied_permission_policy was dropped from the
   file during the merge; restore it from the pre-merge tree. The function was
   added in 0bc5683e5 and is still imported by agents.rs and called by
   provider_deploy.rs.

2. agents_deploy.rs: three test call sites for build_launch_block and
   build_launch_block_for_policy are missing the effective_permission_policy
   argument added in f4efd99d4. Pass None (no override, use resolved default).

3. permission_policy.rs: AgentDefinition test helper missing the description
   field added to the struct by main's cb3144999 ('add public descriptions to
   agent personas'). Pass None.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 12:29:34 -04: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
Duncan
3d925efe94 chore(desktop): reclaim ratchet headroom — extract duplicate types, compact test fixtures
Five files exceeded the file-size gate after the permission-policy additions and
main merge. Reclaim mechanism per file:

- types.ts (1028→923): Remove duplicate inline definitions of AgentPersona,
  CatalogSourceCoordinate, PersonaBehaviorInput, CreatePersonaInput,
  UpdatePersonaInput, SwitchManagedAgentModelStatus, and ControlResultFrame
  that were re-inlined by the merge. The re-export block already handles them;
  canonical definitions live in personaTypes.ts and permissionPolicy.ts. Add
  permissionPolicy field to AgentPersona and PersonaBehaviorInput in
  personaTypes.ts (canonical location) instead.

- agent_models_tests.rs (1015→990): Remove 24-line dead AgentDefinition struct
  literal immediately shadowed by serde_json::from_str re-assignment of the
  same variable.

- persona_events/tests.rs (1044→998): Replace two 20-27 line AgentDefinition
  struct literals in permission-policy tests with sample_persona() helper calls
  (field values verified to match assertions).

- discovery/tests.rs (1822→1819): Remove duplicate doc comment line and
  redundant doc on record_with (2 lines; already-over-cap file may not grow).

- readiness.rs (1742→1741): Trim one doc comment line (already-over-cap file
  may not grow).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 12:02:35 -04:00
Duncan
242dbf2680 fix(acp): close Thufir addendum gaps — unconditional first attempt, interactive delivery-seam test
F2 (retransmit structural fix): move delivery-deadline check to after the first
attempt. The first publish is now unconditional — an already-expired deadline
supplied by the caller (e.g. ordinary-timeout path where entry_deadline was past
at resolution time) no longer silently skips the one relay write that resolves
the card. Retries still consult the deadline.

F2 (test fixes): replace the channel-closed bounded-exit test with two focused
tests:
- retransmit_resolved_edit_unconditional_first_attempt_on_expired_deadline:
  supplies an already-expired deadline, verifies the kind-40003 event is still
  published. Mutation: revert !first_attempt guard → zero publishes.
- retransmit_resolved_edit_always_uncertain_bounded_exit: uses test_pair_silent
  (always Uncertain) under paused time, advances past the 4s delivery window,
  verifies the loop exits. Mutation: large RESOLVED_DELIVERY_WINDOW_SECS → hangs.

F3 (interactive delivery-seam): add _deliveryFn testability seam to
PermissionDecisionButtons and thread it through LifecycleActivity. Add
test_f3_interactive_delivery_seam_allow_once_and_reject_once_fire_delivery:
builds an acp_read transcript card (four option kinds), renders via
LifecycleActivity with a mock _deliveryFn, clicks allow_once button, asserts
delivery called with opt-allow-once + nonce-interactive; clicks reject_once
button in a fresh render, asserts delivery called with opt-reject-once.
allow_always/reject_always produce no buttons (ACTIONABLE_KINDS contract).
JS suite: 10/10 pass. Mutation: remove kind from ACTIONABLE_KINDS → button
absent → delivery assertion fails.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 11:41:18 -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
Duncan
d70558f576 fix(acp): implement expanded findings F1-F4 + routing hazard
F1 — Combined ≤200 UTF-8 byte description with known-key extraction
and secret-bearing key redaction. summarize_raw_input extracts
command > file/path keys > cwd > reason > compact-JSON-fallback from
rawInput; keys matching SECRET_KEY_PREFIXES are replaced with
"<redacted>" in the fallback and excluded from named paths.
DESCRIPTION_COMBINED_MAX_BYTES (200) replaces the independent
DESCRIPTION_RAW_INPUT_BYTES (120) budget so the combined title+context
string always fits in one sentinel field. Truncation appends '…'.
New tests: scalar/empty-object rawInput → title only; token/password
redaction; command priority over path; combined-bound invariant;
production-seam sentinel-level bound verification.

F2 — RESOLVED_DELIVERY_WINDOW_SECS raised 60→300s (aligned with
relay card-maximum and PERMISSION_ASK_TIMEOUT_SECS admission window).
New tests: bounded-exit on channel-close (terminal path); same event
ID across clones verified.

F3 — Cross-layer test: acp_read event with all four adapter option
kinds driven through buildTranscript reducer → LifecycleActivity
render, asserting exactly 2 buttons (allow_once + reject_once).
Rust read-loop coverage: allow_always and reject_always decisions are
silently ignored by the card_actions allowlist; only allow_once is
accepted and resolves the entry.

F4 — Existing early_decision_first_wins test retained; read-loop
coverage now includes the allow_always/reject_always rejection path.

Routing hazard (same-channel multi-thread) — handle_permission_decision_control
now fans out to ALL tasks with matching channel_id instead of finding
the first one. The nonce is unguessable so the owning read loop accepts
it while siblings drop it on mismatch. Two regression tests:
two_threads_same_channel_fan_out_routes_to_owning_thread (Thread B
installed first, Thread A's decision still reaches A) and
two_threads_same_channel_thread_b_not_stranded (Thread B receives its
own decision independently after Thread A's is handled).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 11:09:21 -04:00
Duncan
aa30ce6b83 fix(acp): implement Carl review findings F1–F4 with full behavioral tests
F1 (truthful operation context): `description_from_request_permission`
now appends a byte-truncated JSON summary of `rawInput` to the tool
name for buzz-agent v1/v2 paths, producing `"<tool>(<args>)"` so two
calls of the same tool with different arguments yield distinguishable
card descriptions. Codex v1.1.7 paths (command / reason) are unchanged.
New production-seam test drives `handle_permission_request` with a v2
payload and asserts the description field reaches the published kind-9
sentinel content. Tests cover file-path visibility, hostile markup,
control characters, byte truncation, and UTF-8 char-boundary truncation.

F2 (terminal-outcome delivery window): `finish_permission` now computes
`delivery_deadline = Instant::now() + RESOLVED_DELIVERY_WINDOW_SECS`
at resolution time instead of passing `entry_deadline` (already past
for ordinary timeouts). New test: ordinary-timeout path publishes at
least one kind-40003 resolved edit; would emit zero with old code.

F3 (observer surface two-option contract): removed `reject_always` from
`ACTIONABLE_KINDS` in `LifecycleActivity.tsx`; removed the
`hasPersistentGrant` badge and the "Permanent grant" advisory copy.
Updated all affected render tests and added a four-option contract test
proving only allow_once + reject_once render actionable buttons.

F4 (first-wins pre-ACK decision): guarded `entry.early_decision` with
`if entry.early_decision.is_none()` so only the first valid decision is
buffered. New regression test: Reject arrives first, Allow second, ACK
fires, observer confirms Reject is applied.

lib.rs: fixed three `TaskMeta` literal compile errors in test code
(missing `permission_decision_tx` / `scope` fields from main merge).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-09-01 10:46:08 -04:00
Duncan
ed3a25c325 Merge remote-tracking branch 'origin/main' into duncan/permission-policy
* origin/main:
  feat(desktop): add thread-scoped ACP session experiment (#6909)
  fix(desktop): scope composer autocomplete to focus (#6860)
  feat(desktop): add isolated named demo builds (#6407)
  fix(model-capabilities): humanize databricks goose model names (#7135)
  feat(db): add NIP-FI identity and final-admission schema foundation (#6994)
  feat(buzz-acp): give each channel thread its own agent session (#6732)
  docs: add review-proven failure-path & async-state rules to AGENTS.md (#7061)
  fix(desktop): back split thread headers (#7137)
  add public descriptions to agent personas (#7126)
  feat(desktop): add protected-build Bestie experiment (#6902)
  fix(relay): reject a frame on its own acknowledgement channel (#6961)
  fix(acp): wake agents from workflow messages (#6953)
  feat: render agent avatars as squircles (#7106)
  fix(ci): salvage Codex review output on PTY-shutdown hang (#7042)
  fix: retrieving cold memories; add regression task (#6950)
  Enforce NIP-OA authorization time bounds (#7004)
  feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229)
  feat(desktop): use segmented controls for channel creation (#6845)
  feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

# Conflicts:
#	crates/buzz-acp/src/config.rs
#	crates/buzz-acp/src/pool.rs
#	crates/buzz-acp/src/relay.rs
#	desktop/src-tauri/src/commands/agent_config_tests.rs
#	desktop/src-tauri/src/commands/agent_models_tests.rs
#	desktop/src-tauri/src/commands/agents_deploy.rs
#	desktop/src-tauri/src/commands/agents_tests.rs
#	desktop/src-tauri/src/commands/personas/delete_cascade_tests.rs
#	desktop/src-tauri/src/commands/personas/inbound/inbound_tests.rs
#	desktop/src-tauri/src/commands/personas/pending.rs
#	desktop/src-tauri/src/commands/personas/sharing.rs
#	desktop/src-tauri/src/commands/personas/snapshot/fidelity_tests.rs
#	desktop/src-tauri/src/commands/personas/snapshot/tests.rs
#	desktop/src-tauri/src/commands/personas/update/name_propagation_tests.rs
#	desktop/src-tauri/src/commands/team_snapshot/tests.rs
#	desktop/src-tauri/src/managed_agents/agent_events.rs
#	desktop/src-tauri/src/managed_agents/agent_snapshot_envelope.rs
#	desktop/src-tauri/src/managed_agents/agent_snapshot_tests.rs
#	desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs
#	desktop/src-tauri/src/managed_agents/discovery/tests.rs
#	desktop/src-tauri/src/managed_agents/effective_config/tests.rs
#	desktop/src-tauri/src/managed_agents/global_config/tests.rs
#	desktop/src-tauri/src/managed_agents/parallelism.rs
#	desktop/src-tauri/src/managed_agents/persona_events/tests.rs
#	desktop/src-tauri/src/managed_agents/personas/tests.rs
#	desktop/src-tauri/src/managed_agents/readiness.rs
#	desktop/src-tauri/src/managed_agents/runtime/test_fixtures.rs
#	desktop/src-tauri/src/managed_agents/runtime/tests.rs
#	desktop/src-tauri/src/managed_agents/spawn_snapshot/tests.rs
#	desktop/src-tauri/src/managed_agents/team_snapshot.rs
#	desktop/src-tauri/src/managed_agents/teams_tests.rs
#	desktop/src-tauri/src/managed_agents/types/requests.rs
#	desktop/src-tauri/src/managed_agents/types/tests.rs
#	desktop/src-tauri/src/migration_avatar_tests.rs
#	desktop/src/features/agents/AGENTS.md
#	desktop/src/shared/api/types.ts
2026-09-01 10:15:22 -04: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