mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
e76c81968b65b0755b83efdd59dc3375c59ddf40
2530 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e76c81968b |
refactor(db): split channel membership store (#6782)
## Current reconstructed head Exact base: `codex/issue-7-roster-test-fixtures` at `5df440c411be9705eb29a57f0c41f7239767e007` Exact head: `codex/issue-7-channel-membership-store` at `8ad0782ee311f5f51b714494ce750c5937f127cc` This current head removes `crates/buzz-db/tests/store_ownership.rs`; no replacement path-sensitive ownership test is introduced. Apart from removing that complete test-file diff, the production patch is byte-for-byte identical to the previously reviewed slice. This remains part of tracker #2 and the #17/#19 acceptance work. Independent exact-head review from a separate clean Blox workstation found no issues. Current-head evidence passed formatting, strict `buzz-db` clippy, 111 non-PostgreSQL library tests with 200 PostgreSQL tests ignored, the observability source test, relay consumer compilation, exact ownership/unique-span review checks, and 3 channel and 19 membership PostgreSQL tests on native PostgreSQL where applicable. ## Why Complete the channel ownership slice of [tracker #2](https://github.com/TheSentinel454/buzz/issues/2) and [domain issue #7](https://github.com/TheSentinel454/buzz/issues/7) while preserving the runtime/store boundary established by #6660 and #6668. This child stacks on the test-only fixture prerequisite #6819 above #6777 and carries forward PR #6700's membership/replacement lock timing without changing lock or transaction behavior. ## What - Keep channel lifecycle, metadata, TTL advisory locking, and lifecycle tests in `channel.rs` - Move membership/roster records, SQL, advisory-lock helpers, `Db` methods, focused tests, and datastore spans to a dedicated `channel_members.rs` - Preserve existing `buzz_db::channel::*` paths with compatibility re-exports while exposing the dedicated module - Move the four roster-fence PostgreSQL tests out of `lib.rs` ## Stack - Exact base: codex/issue-7-roster-test-fixtures at 21d1b265c133292e6707e766cd4204e6a43f08af ([#6819](https://github.com/block/buzz/pull/6819)) - Exact head: codex/issue-7-channel-membership-store at 25138bfd6588e046170dbdbc4ed953bdc3cf7ed1 - Tracker: https://github.com/TheSentinel454/buzz/issues/2 - Domain: https://github.com/TheSentinel454/buzz/issues/7 - Test/span acceptance: https://github.com/TheSentinel454/buzz/issues/17 and https://github.com/TheSentinel454/buzz/issues/19 ## Non-goals - No SQL, schema, retry, timeout, lock ordering, transaction boundary, or client-visible behavior changes - No change to channel TTL lifecycle ownership merely because lifecycle bootstrap writes an owner membership row - No store traits, domain-handle redesign, broad `PgExecutor` migration, raw pool accessor, new crate, or directory-wide reorganization - No changes to, retargeting of, or merge action on PR #6700 or #6777 ## Risk Assessment Moderate review surface, low semantic risk. The file split is large, but method signatures, SQL, bind order, membership and replacement lock namespaces, transaction boundaries, and span names remain unchanged. Compatibility re-exports preserve existing `buzz_db::channel::*` consumers. ## Blox Verification Author workstation: `buzz-tornquist-issue-2-store-stack` (`2046520`), exact head `8376e19d0da3ec77550590cd91cc3dfe284d95d6`. - `cargo fmt --all --check` — passed - `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` — passed - Native PostgreSQL channel lifecycle suite — 3 passed - Native PostgreSQL membership/roster suite — 17 passed; two pre-existing ignored-test fixture failures reproduced identically on the untouched parent `2de5444`: `large_roster_reconciliation_candidates_respect_snapshot_count_and_signer` and `locked_member_snapshot_blocks_post_capture_membership_mutation` both receive the migration-0032 `23514` invalid-`p`-tag rejection. This extraction intentionally does not fold a test-behavior fix into the move. - `cargo test -p buzz-relay --lib -- --test-threads=1` — 908 passed, 48 ignored; the existing load-sensitive mesh demo test returned 504, matching the #6700/parent baseline - `cargo test -p buzz-relay --lib api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo -- --exact --test-threads=1` — passed Independent exact-head review: `buzz-tornquist-pr-6782-review` (`2048397`) found no remaining critical, important, or minor issues. The full implementation review also independently reproduced both stated PostgreSQL fixture failures on the exact parent and passed the relay library suite (909 passed, 48 ignored). Generated with Codex ## Superseded pre-comment restack verification PR #6700 merged before publication completed. This layer was restacked onto current main through the exact parent named above; the final cumulative tip is 2ddcc8a29f95c8c8c9cb87bb6cf59335f2190fae. Cumulative author gates passed: formatting and diff checks; buzz-db and buzz-relay all-target clippy with -D warnings; DB lib 111 passed / 200 ignored; ownership 22/22; observability 1/1; the full isolated PostgreSQL domain matrix; and relay lib 910 passed / 49 ignored. - Workstation: `buzz-tornquist-pr-6782-final-review` (`2057620`), fresh shallow checkout - Base: `c60e793eadde79d9eab9f48bbb2ede0ad4831f9b` - Head: `fa09b6c81c4db3b3e1940a2117a97ab2186e49f7` - Findings: none Reviewed both commits in `base..head`. Channel lifecycle/metadata, TTL transitions, and their lock rationale remain in `channel.rs`; membership authorization, roster fencing/snapshots, membership advisory locking, membership records, and focused tests move together to `channel_members.rs`. SQL, transaction, and lock sequences are preserved. Verification: format and diff checks passed; `buzz-db --all-targets` clippy passed with `-D warnings`; DB lib tests passed (111 passed, 200 PostgreSQL tests ignored); ownership (2/2) and observability (1/1) guards passed; native PostgreSQL 17 passed 3 channel lifecycle tests plus 19 membership/roster tests with migrations 1-32 successful; relay lib test target compiled successfully. Final worktree was detached at the exact head and clean. Complete evidence archive SHA-256: `81ae374095f649ca7a25d8b9a4fc864257b7925d1b44657a69ca111523adf36e`. ## Comment-addressed restack Review follow-up on #6777 removed only the low-value replaceable ownership source test. This PR was restacked onto its rewritten parent; its production patch is unchanged. - Exact base: `21d1b265c133292e6707e766cd4204e6a43f08af` - Exact head: `25138bfd6588e046170dbdbc4ed953bdc3cf7ed1` - Final cumulative tip: `6fa2f104d42c6ba85bdf62e7ccb74ceaf4a84f67` - Per-layer patch-ID and tree audits confirm this PR’s production diff is unchanged from its pre-comment head. - Cumulative Blox gate: formatting and diff checks; strict `buzz-db`/`buzz-relay` Clippy; DB lib 111 passed / 200 ignored; ownership 21/21; observability 1/1; every moved PostgreSQL test; relay lib 910 passed / 49 ignored. - Independent re-review at this exact head: no findings; fresh exact-parent/head Blox review passed fmt/diff, strict Clippy, DB lib 111 passed / 200 ignored, current ownership/observability guards, 3 channel plus 19 membership PostgreSQL tests, and relay compilation. Signed-off-by: OpenAI Codex <codex@openai.com> Co-authored-by: OpenAI Codex <codex@openai.com> |
||
|
|
953a1798b2 |
feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776)
The closed, provider-neutral contract layer at the root of the NIP-FI federated-identity dependency graph — Phase A, PR 1 of the plan. It has no dependencies on any other PR and defines no database schema, migration, runtime JWKS fetching, binding resolution, enrollment, or request/proof binding; those belong to later PRs. ## What this adds - **Multi-issuer assertion-policy config** (`IssuerRegistry`, `IssuerPolicy`) keyed by exact `iss`. Identity is issuer-qualified `(iss, sub)` throughout — equal `sub` under different `iss` are distinct identities. The subject coordinate is fixed to the JWT `sub` claim (`SUBJECT_CLAIM`), never configurable, so no deployment can seal a mutable attribute like `email` as identity. Issuer URL and audience remain deployment configuration. - **The two deterministic semantic contract identities** — `AssertionPolicyId = H(canonical assertion-policy contract)` and `TransportContractId = H(canonical transport contract)` — derived by length-prefixed, domain-separated SHA-256 so a semantic change moves exactly its owning ID while benign JWKS rotation never changes policy lineage. Set-valued policy inputs (audiences, algorithms, subject-class values, scope capture) are canonicalized before derivation, so the ID is invariant under permutation and duplication. Config fields that a freshness class never reads are rejected at construction (an `offline-jwt` policy cannot carry `maximum_status_age`), so the canonical encoding stays total over valid configs and semantically identical policies always derive one ID. - **The single canonical verifier** (`FederatedAssertionVerifier`, `FI-INV-16`) producing the origin-sealed, provider-neutral `VerifiedAssertion` normalized result. Its constructor is crate-private, so unverified claims cannot be promoted into authority. The issuer→JWKS authority is entirely crate-owned: `AssertionKeySet` has no public constructor and `IssuerKeySource` is sealed, so no downstream crate can relabel one issuer's keys as another's. The authenticated key set is bounded (`MAX_JWKS_KEYS`) and the bound is folded into `AssertionPolicyId`, so an unbounded attacker-controlled `kid` scan cannot be driven; every snapshot requires a finite positive hard deadline. - **Sealed revalidation dependencies** — `RevalidationDependencies` carries the key-snapshot hard deadline and a `ConfidentialAssertion` handle to the exact compact JWS (no `Debug`/`Display`/`serde` leak, sole read path `compact_jws()`), so a changed snapshot can revalidate the same evidence: a retained key revalidates, a removed key denies. - **The privacy-preserving four-class denial contract** (`DenialClass`, `FI-INV-13`) with the byte-exact Nostr text, HTTP status, body, `Content-Type`, and `WWW-Authenticate` values fixed by the spec's rejection table. An unreadable required current dependency maps to `authorization_unavailable`/503, never to rejected evidence, and rejected evidence never masquerades as a 503 at either end of the pipeline: all bounded, dependency-independent checks (compact structure, header, signature shape, policy, algorithm, token class) precede key-source lookup, and all offline validation (token-class, key, signature, audience, claims, time) completes before any status-witness deferral. A wrong-`typ` or structurally malformed token, or malformed/invalidly-signed input naming a current-status issuer, therefore denies with `evidence_rejected`/403 rather than being reported as a 503 availability signal. ## Corrections applied to the mined source Mined from the `buzz-auth` verifier core in [#1476](https://github.com/block/buzz/pull/1476) and corrected to the settled spec (the merged `docs/nips/NIP-FI*.md`, [#5946](https://github.com/block/buzz/pull/5946)), which settled after #1476 was written: - Token-class selection with exact `typ` enforcement. Two classes are offered: `at+jwt` and `nip-fi+jwt`. There is deliberately no generic/absent-`typ` "named compatibility" class: it cannot be proven disjoint from an OIDC ID token by claim presence alone (an issuer can mint an ID token carrying `client_id`), and the only authenticated discriminator is `typ`, which such a mode declines to constrain. - OIDC ID-token denial — denies even when `iss`, `aud`, and `sub` match, via exact `typ` mismatch against every accepted class. - The fixed `nostr_pubkey` claim accepted only as lowercase hex of exactly one 32-byte key; bech32 and other aliases deny. - Resource-owner / client-subject ambiguity denial and required `client_id` for `at+jwt`. - JWK admissibility: a key's `use` and, when present, `key_ops` must authorize signature verification (a key restricted to other operations such as `encrypt` is rejected), and the selected JOSE algorithm is bound to the key's required family and curve (ES256↔EC/P-256, ES384↔EC/P-384, EdDSA↔OKP/Ed25519, RS/PS↔RSA). The JWK `alg` is advisory; the actual key material is what signs, so a JWK declaring a matching `alg` over mismatched material (a different family or curve) is rejected before signature verification. - Spec-exact time arithmetic (`now < exp`, `iat <= now + skew`, `now < iat + maximum_assertion_age`, equality at expiry is expired). `exp`/`iat`/`nbf` accept finite integer or fractional RFC 7519 `NumericDate` values with checked, overflow-safe conversion; non-finite and out-of-range values deny. ## Verification In-crate tests in `crates/buzz-auth/src/nip_fi/verifier/tests.rs` sign real ES256 assertions against a fixed test key and cover: the happy path, exact-wire-text for all four denial classes, deterministic and semantic contract IDs, canonicalization invariance, token-class enforcement including ID-token denial, JWK `key_ops` rejection, `nostr_pubkey` hex handling, time bounds, and multi-issuer selection (same subject across distinct issuers yields distinct identities and policy IDs). They also cover the round-3 contracts: the key-set bound (oversized rejected, at-bound accepted, empty rejected), fixed-`sub` identity (identity is `sub` not a configured `email`; a token without `sub` denies), offline-before-deferral (invalid-signature, wrong-audience, malformed-claim, and expired under a current-status issuer each deny 403/`evidence_rejected`), and the revalidation contracts (dependencies carry the deadline and exact JWS; a retained key revalidates, a removed key denies). And the round-4 contracts: algorithm↔key-material binding, covered exactly per accepted algorithm (a table-driven matrix asserts every policy-acceptable algorithm — ES256, ES384, EdDSA, RS256/384/512, PS256/384/512 — matches only its required family/curve and rejects every other, so each mapping mutation fails individually; and an ES256 token against P-384, RSA, and Ed25519 material each deny `InvalidKey`); malformed evidence classified before key lookup (wrong-`typ`, two-segment, four-segment, empty-signature, and non-base64url-signature tokens against an empty source deny 403 not 503); `maximum_status_age` inapplicable under offline-jwt rejected at construction while current-status still requires it; and fractional `NumericDate` (finite fractional `iat`/`exp`/`nbf` within bounds verify, non-finite and out-of-range deny). Compile-fail doctests guard the sealed authority seam (`AssertionKeySet::new`, `IssuerKeySource`) and the absence of the named-compatibility token class. ## Notes - `Co-authored-by` attribution is preserved for the mined author. The #1476 `buzz-auth` commits are authored by Cea Stapleton Cordasco (the salvage map records #1476 under Franco; the git history on that PR is Cea's — surfacing for the attribution/closure record). - Amends `docs/nips/NIP-FI.md` to remove the "named compatibility access token" class and its `FI-TRACE-TOKEN-CLASS` oracle reference, so the normative spec matches the two-class root implementation (a generic/absent `typ` cannot be proven disjoint from an OIDC ID token). Removal-only, no neighbor redesign; a `docs/nips/` grep confirms no dangling cross-reference to the class remains. - Adds `jsonwebtoken 10.4.0` (`aws_lc_rs`) as a workspace dependency. Dependencies: none. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
b49675894b |
fix(desktop): resolve exact typed mentions on space (#6862)
## Summary - Convert an exact, authorized manually typed `@Name` into the existing visual mention chip when plain Space is pressed. - Reuse autocomplete selection so outbound mention pubkeys, agent styling, and address behavior remain identical. - Preserve partial names, longer multi-word names, duplicate-name ranking, modifier keys, and IME composition. Before: https://github.com/user-attachments/assets/2bccc0eb-5c36-4554-8a2d-d67e41d85e64 After: https://github.com/user-attachments/assets/12859538-031d-44b0-ab38-40e1fb25c256 ### Related issue None found. Originating Buzz conversation: channel `5efbefaf-f478-4574-927e-32f28df07f09`, thread `563cb6709454b924405caa07f120ba9b88f074242fc3ee168686da76038b303f`. ### Testing Validated at `7f30db7b54e484cb510bd0a616db98a49ce1b6ae`: - `pnpm test` — 5,562 passed - `pnpm check` - `pnpm typecheck` - `pnpm check:file-sizes` - `pnpm build:e2e` - `pnpm exec playwright test tests/e2e/mentions.spec.ts --project=smoke --reporter=line` — 74 passed --------- Signed-off-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9733863516 |
perf(desktop): restore project context during startup (#6939)
## Summary Project channels previously appeared as ordinary channels until the community-wide project enumeration completed, delaying the contextual right rail most noticeably on larger relays. - Restore the last fully validated, relay-and-identity-scoped project collection immediately while keeping live relay data authoritative. - Resolve the active channel's project home through scoped `#buzz-channel` queries instead of waiting for the complete project scan. - Keep snapshots aligned with community removal and relay reconnect invalidation. ### Related issue None found. ### Testing - Pre-push `file-size-check`, `desktop-check`, `desktop-typecheck`, and `desktop-test` - Project enumeration, snapshot persistence, and relay invalidation unit coverage - Targeted Playwright scenario: project sidebar rows open the home channel and nest extra channels No numeric startup benchmark was captured; this draft validates the cache and scoped-query behavior while leaving timing measurement for review. --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> |
||
|
|
350caf1de3 |
fix(desktop): lift right auxiliary pane above shared header backdrop (#6966)
## Summary The sidebar close (`X`) and `Edit` controls disappeared for in-channel side panels on `main`. They stayed clickable but were visually washed out. [#6901](https://github.com/block/buzz/pull/6901) added `isolate` to `RightAuxiliaryPane` so project workspace sheets slide over an open thread cleanly. The pane carries no `z-index`, so isolating it makes the whole pane subtree paint at stacking level 0 — below the channel's sibling `z-30` shared-header backdrop in split layout. That translucent blur strip then washes out the pane's own `z-40` header chrome, where `X`/`Edit` live. The backdrop is `pointer-events-none`, so the controls remained clickable but invisible (matching the reported videos). Threads, channel settings, and in-channel agent panels all route through this wrapper, so all three were affected; the standalone Agents-nav panel does not render inside `ChannelPane`, so it had no backdrop and worked. ## Fix Add `z-31` to the pane's `aside` — above the `z-30` backdrop, still below the `z-41` thread drawer/sheet overlays. `isolate` stays, so #6901's sheet-over-thread layering is preserved and both behaviors coexist. ## Regression coverage `tests/e2e/auxiliary-pane-close-visibility.spec.ts` opens a split-layout thread from inside a channel and asserts the pane establishes its `isolate` stacking context and that its `z-index` outranks the shared-header backdrop. It fails on the pre-fix tree (pane `z-index` is `auto`) and passes with `z-31`. #6901's `project workspace sheet stays independent from an open thread` spec still passes. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>mobile-v0.15.0-rc.2 |
||
|
|
80177e4c8e |
fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962)
Codex CLI 0.149.x can leave a PTY descendant holding
`openai/codex-action`'s inherited stdio after the review turn completes.
The action's runner waits on the child's stdio *streams closing* rather
than on process exit
([`runCodexExec.ts`](https://github.com/openai/codex-action/blob/86365089/src/runCodexExec.ts#L322)),
so the `Review pull request` step never returns — the job idles until
its 30-minute `timeout-minutes` kills it and the already-written review
result is discarded.
Every `Run Codex Security Review` job since the workflow merged has hung
this way: the final JSON result and `tokens used` count are the last log
lines, with no step-end marker. Heavy runs (`gpt-5.6-sol` at `max`
effort, ~295k tokens) sit firmly in the failing regime.
Upstream:
[openai/codex-action#150](https://github.com/openai/codex-action/issues/150),
fixed in Codex CLI 0.150.0 by
[openai/codex@bf3eb2e](
|
||
|
|
a7c7414a7f |
feat(desktop): implement 30178 team catalog backend (#5112)
Implements the Rust/backend half of 30178 team catalog sharing on the community catalog. No TS callers yet — this is PR 1 of 2; [#3995](https://github.com/block/buzz/pull/3995) (stacked here) adds the parse layer, hooks, `CommunityCatalogDialog`, and e2e tests. ## What this adds **Projection builder** (`team_catalog.rs`): `build_team_catalog_event/content` produces a 30178 event from a team + member definitions. Size contracts: 192 KiB total ceiling, per-field bounds (name 256 B, text 4 KiB, system-prompt 16 KiB, avatar URL 32 KiB for projection fields; https URLs additionally validated at 2,048 bytes). Avatar handling: oversized raster data URLs downscaled to fit; oversized built-in avatars silently omitted; oversized https URLs rejected with a named error. **Share/unshare/tombstone** (`commands/teams/pending.rs`): `prepare_team_publication_at` retains a signed 30178 head for the flush loop; share state is relay-scoped (one community can share while another does not). `refresh_or_retract_shared_head_at` rebuilds or tombstones the head immediately on team/member edit — no stale publication until the next boot. Both the 30178 catalog and 30176 team tombstones are signed with a `created_at` that strictly dominates the retained head's (read inside the delete transaction), so a future-dated head cannot survive its own deletion under the relay's `created_at <=` soft-delete gate. **Serialized-publisher share command** (`commands/teams/sharing.rs`, `managed_agents/persona_events.rs`): `set_team_shared` publishes the retained head through `flush_pending_events_at` rather than submitting the prepared event directly. A direct submit ran outside `managed_agents_store_lock` and raced `delete_team`: the delete atomically purges the head's retained row and enqueues a newer 30178 tombstone in one transaction, and a delayed direct submit could land the old shared head *after* that tombstone. Because 30178 replacement has no deletion watermark, the deleted team would go publicly live again. Routing through the flush is necessary but not sufficient, because the flush is not itself a single publisher: several call sites (the 30s sweep, this share toggle, managed-policy updates) invoke it concurrently, and each invocation has an await gap between its per-row re-read and its relay POST. A second flush could publish the tombstone in that gap while an earlier flush's delayed POST lands the just-purged head after it. The flush now acquires a per-scope publisher mutex — keyed by the canonical retention `db_path`, which already *is* the durable scope identity (hashed normalized relay URL + owner pubkey) — and holds it across its entire invocation: snapshot, per-row re-read, POST, and `mark_synced`. Serialized-per-scope flush ⟹ within one scope the only interleavings are head-before-tombstone (the head lands first, then is dominated by the later tombstone) or purged-row-skip (the delete committed first, so the re-read skips the head) — a purged head can never publish after its tombstone. The lock is a `LazyLock<Mutex<HashMap<PathBuf, Arc<tokio::sync::Mutex<()>>>>>` static rather than an `AppState` field: it keeps the invariant at its acquisition site and out of the size-ratcheted `app_state.rs` (precedent: `agent_models_databricks.rs`'s `AUTH_GATE`), and the std-mutex map guard is released before the async guard is awaited so it never spans an await point. Keying per scope — rather than one process-global lock — means a stalled or hostile relay in one community can no longer block publication in every other community, and each per-row submit is additionally wrapped in a `tokio::time::timeout(PUBLISH_TIMEOUT = 60s)`: `submit_signed_event_at_with_keys` first waits on the process-wide admission gate (up to 300s on a 429) and then POSTs on the app-wide `http_client`, which leaves reqwest's connect/read/total timeouts unset, so a relay that accepts the connection and never finishes the response would otherwise pin the lock forever. A timeout takes the same `Err`/`continue` path as a relay rejection — the row stays pending for the next 30s sweep and a timed-out tombstone keeps its replacement deferred this pass — so a live admission gate now surfaces as timeout-pending rather than a held lock, the correct durable behavior since the sweep retries. `publicationStatus` (Published/Queued) is derived by re-reading the retained row's pending flag after the flush. The previously-unused `relayMessage` field is dropped from `SetTeamSharedResult`: the flush loop swallows every per-event relay rejection to its own log and only surfaces local DB faults (which the status re-read already propagates), so the field was permanently `null` on this path. #3995's `tauriTeams.ts` mapping and `useTeamActions.ts` log branch drop with it. **Domination-aware flush** (`managed_agents/persona_events.rs`): a strictly-dominating tombstone can be signed past the relay's ingest acceptance window (`MAX_TIMESTAMP_DRIFT_SECS`, ±900s from server time). Republishing such a byte-frozen event verbatim from the pending queue lets it age out of the window and be rejected forever, stranding the head live. The flush loop is now domination-aware for every retained kind:5 (covering both 30176 and 30178). For a pending tombstone with floor `f` (= the retained row's own `created_at`): - `f <= now` → re-date and re-sign at `now` (mirrors the existing archive-request re-sign branch; `mark_synced` stays keyed to the untouched retained row, so a re-date can't mask a concurrent edit), - `now < f <= now + 900` → publish verbatim at `f`, inside the window, - `f > now + 900` → skip the sweep; the event stays pending and its replacement keeps deferring (via `failed_tombstones`) so out-of-order retraction remains impossible, converging as the wall clock advances. No path emits an event the gate rejects, and a boundary reject self-heals through the submit-error requeue. Durable across offline gaps of any length. **Atomic adopt** (`commands/teams/adopt/`): `add_team_from_catalog` re-fetches and signature-verifies the head from the relay, then plans and commits a multi-entity add across two store writes with byte-exact rollback on any failure (crash window between writes explicitly retained). `plan_add` resolves full member provenance (owner, d-tag, member-key, projection-hash), reuses a recipient's own local built-in only when the published slug matches and the reuse hint's `projection_hash` — recomputed from the member's own embedded fields at the parse boundary and rejected on mismatch — equals (case-insensitively, matching the boundary's hex tolerance) the recipient's local built-in hash, and reactivates deactivated copies on re-add. Because the boundary already proves the hint hash describes the reviewed projection, a publisher cannot pair a real built-in's slug + hash with arbitrary reviewed fields to make adoption install the recipient's built-in in place of what was shown. `commit_stores` snapshots both stores before writing and restores them on failure. The commit and the retention enqueue are sequenced inside `commit_and_enqueue`, the sole route to a durable adoption commit: once the store write succeeds it enqueues a pending 30175 for every member copy the add wrote or reactivated and a pending 30176 for the team, so a crash before the next boot reconcile cannot lose the only adopted copy. A provenance match on an already-active copy is retained too (not just a reactivation): a recovery retry after a crash between the persona write and post-commit retention finds the copy active with no 30175 row, and `plan_add` short-circuits once the team row exists, so this reuse branch is the only place that retry can re-enqueue the orphaned member head. Retaining unconditionally is conservative, not exact — an active copy still referenced by a standalone managed agent can already hold a live head, and re-retaining only bumps it monotonically; reused built-ins are handled separately and never reach this branch. A byte-identical reused built-in and an idempotent replay write nothing and enqueue nothing; a failed commit propagates and enqueues nothing. Enqueue is best-effort per row (the boot reconcile is the backstop). The frontend refreshes via the `useAddTeamFromCatalogMutation` query invalidation in #3995, so no `agents-data-changed` emit is needed here. **Startup reconcile** (`event_sync.rs`): `reconcile_team_catalog_heads_at` walks all retained 30178 heads at boot: republishes heads whose content changed, tombstones heads whose team or member was deleted, skips unshared heads and unchanged content. Multi-team continuation — all shared teams processed in one pass. **Cross-device catalog retention** (`commands/personas/inbound.rs`): both recovery paths above — the boot reconcile worklist and the interactive `refresh_or_retract_shared_head_at` — key off a retained 30178 row and guard-return without one. A second device therefore never retained the owner's own catalog head published from another device, so its later edit or delete could never supersede or retract that discoverable head. The inbound reconcile now retains an inbound 30178 head as this device's publication witness through `retain_inbound_catalog_witness`, a self-gating dispatcher invoked unconditionally on the production non-deletion path: newest-wins via `retain_inbound_event`, arrival-scoped, no local JSON store, and deliberately **no** refresh or republish on arrival — a 30178 arrival is either this device's own echo or the other device's publication, and rebuilding on either would make two devices ping-pong identical heads. Retention advances the witness and stops. The tombstone router accepts a kind:5 covering a 30178 coordinate, so an inbound deletion purges the retained head on the receiving device (the covered-head purge already happens inside `commit_inbound_tombstone_with_store`; a 30178 head has no local record to remove). After a successful inbound persona/team upsert, this device refreshes the affected shared heads so the community catalog tracks the inbound edit (persona edit → every team whose resolved members include it, resolving the local persona `id` by d-tag; team edit → that team's head); after an inbound tombstone, a team deletion retracts its 30178 coordinate and a persona deletion refreshes the teams that listed it. The refresh is idempotent across devices: `refresh_or_retract_shared_head_at` skips the publish when the rebuilt projection is byte-identical to the retained head and still shared, so the editing device's own published head triggers no churn republish on the receiving device. **Executable-text concealment gate** (`team_catalog.rs`, `definition_validation.rs`): `validate_team_catalog_content` — the single chokepoint both the publish builder (`build_team_catalog_content`) and the adopt parser (`team_catalog_content_from_event`) funnel through — now rejects invisible, default-ignorable, and bidirectional-override characters (e.g. U+200B, U+2066, U+202E) in every field delivered verbatim to the ACP harness or rendered as reviewed identity in the catalog UI. This is the same invariant the persona catalog already enforces at its own parse boundary (`persona_catalog::parse_agent`); the 30178 boundary was the outlier. Member `display_name` + `system_prompt` go through `validate_agent_definition_text` per member (exact parity with `parse_agent`: display-name rule with no layout controls, prompt rule allowing `\n`/`\t`); `name_pool` entries take the display-name rule since they are minted verbatim as instance display names; team `instructions` take the visible-text rule with layout controls allowed, since they reach `BUZZ_ACP_TEAM_INSTRUCTIONS` multiline. The team `name` takes the display-name rule (no layout controls) and the `description` takes the visible-text rule with layout controls allowed, since both are rendered verbatim in the catalog UI as reviewed identity. `validate_visible_text` is exposed `pub(crate)` from `definition_validation.rs` and re-exported via `managed_agents`. A signed, shared, current head can no longer smuggle concealed control characters into executable configuration or reviewed catalog text through either the publish or the adopt path; emoji (VS16/ZWJ) names and multiline instructions still pass. **Types**: `TeamRecord` and `AgentDefinition` extended with `shared`, `catalog_source`, `team_catalog_source` fields. All commands registered in `lib.rs`. ## Tests - `team_catalog/tests.rs`: projection, size contracts, member-key stability, tombstone rollback, fixture matrix - `adopt/tests.rs`: head verification, store planning, provenance, rollback - `team_catalog/tests/concealment.rs`: the chokepoint rejects default-ignorable (U+200B) and bidi controls (U+2066, U+202E) in member `display_name`, `system_prompt`, `name_pool`, team `instructions`, and the team `name`/`description`, on both the publish and adopt paths; an emoji-bearing display name and multiline instructions still pass, so no legitimate team becomes unshareable - `team_catalog/tests/reuse_hint.rs`: a member pairing a real built-in's slug with that built-in's genuine `projection_hash` but carrying unrelated reviewed fields is rejected at the parse boundary, so adoption can never substitute the recipient's built-in for the reviewed projection; an honestly-stamped built-in reuse hint (including an uppercase form of its true hash) still passes the boundary. Removing the boundary recompute lets the tampered member validate, proving the test discriminates the substitution class - `adopt/tests/reuse.rs`: `reusable_builtin` reuses a local built-in for an exact-match hint (one record, no copy), reuses it just the same when the genuine hash is uppercased (case-insensitive, matching the boundary — one record, not two), and falls through to an authoritative embedded copy when the hash does not match. Comparing the hash case-sensitively turns the uppercase case red (two records instead of one reused) - `adopt/tests/concealment.rs`: a signed, shared, current head carrying a bidi override drives the `add_verified_team` sequence (verify+parse → `plan_add` → `commit_and_enqueue`) through real temp stores and a real retention scope, asserting the head is rejected AND the personas store, teams store, and retention rows are all left byte-unchanged. Stripping the concealment call at the chokepoint turns it red — the parse then succeeds and both stores are written, proving the test discriminates a validate-after-write regression, not just an error return - `adopt/tests/retention.rs`: adoption drives `commit_and_enqueue` through a spy commit + a real temp-dir retention scope and asserts persisted pending rows — commits-then-enqueues (30175 per minted member + 30176 team), a failed commit enqueues nothing, an idempotent replay skips both the commit and the enqueue, a reused built-in retains only the team, a reactivated copy is re-retained, and a partial-commit crash recovery (active member copy with no retention row, team row absent) re-enqueues the orphaned member's 30175. Deleting the enqueue inside the seam turns these red — the wiring, not just the helper, is protected - `sharing/tests.rs`: publish/queue lifecycle plus three concurrency gate tests. (a) `concurrent_flushes_never_land_the_head_after_its_tombstone` prepares a share, runs a concurrent delete's purge+tombstone, flushes the tombstone to a recording relay, then releases the delayed share and asserts the purged 30178 head is never published after its tombstone and no pending row survives (removing the lock turns it red — the relay sees the resurrected head after the tombstone). (b) `a_stalled_scope_does_not_block_publication_in_another_scope` pins one scope's flush mid-POST on a stalled relay and runs a second scope's flush to completion, asserting it publishes without waiting (re-globalizing the key turns it red). (c) `a_stalled_relay_releases_the_publisher_lock_within_the_bound` proves a never-completing POST returns within `PUBLISH_TIMEOUT`, leaves the row pending, and releases the lock so a subsequent same-scope flush proceeds (removing the timeout turns it red). `test_relay_rejection_stays_durably_queued` asserts queued + still-pending rather than a relay-message string, matching the flush-routed contract where the rejection text is no longer surfaced - `pending/tests.rs`: share/unshare/tombstone lifecycle, edit refresh/retract, tombstone timestamp domination, typed outcomes, cross-device catalog convergence — device B retains device A's inbound head then supersedes it on a member edit and tombstones the coordinate on a delete, a byte-identical rebuild is a no-op (`Noop`, `created_at` untouched), and an inbound retention alone queues no outbound publish (the no-ping-pong guard). Neutralizing the inbound retention leg turns the supersede/tombstone regressions red — B stays blind (`Noop`, no dominating tombstone) — proving they discriminate the load-bearing leg - `catalog_reconcile_tests.rs`: a signed kind:30178 head is driven through the real production entrypoint `reconcile_inbound_persona_event_blocking` over a `MockRuntime` `AppHandle` (retention scope resolved from the handle's `app_data_dir` under an overridden `$HOME`/`$XDG_DATA_HOME`), asserting the arrival witness is retained at the owner coordinate with `pending_sync=false`, stored verbatim, and no outbound publish is queued. An early return for `KIND_TEAM_CATALOG` immediately before the production `retain_inbound_catalog_witness` invocation turns it red, proving the seam under test is the production dispatch path and not a test-only shim - `pending/tests/gate.rs`: flush driven through a stub relay that logs every `POST /events` with its accept/reject status and enforces the real ±900s ingest gate, for both 30176 and 30178 — within-window publish+dominate; beyond-window stays-pending with **zero POSTs** (the gate never receives a rejectable event); and the delayed/offline-retry case where a tombstone signed strictly past a then-future head has aged more than 900s into the past, so flush must re-date to `now` to publish. The reversal check — restore the byte-frozen replay in `persona_events.rs` and the delayed-retry and zero-POST assertions go red — is what proves the suite discriminates the fix from the rejected implementation - `teams/tests.rs`: 30176 tombstone timestamp domination and no-head fallback - `event_sync_team_catalog_tests.rs`: reconcile scenarios including multi-head continuation - 26 shared JSON parity fixtures (`tests/fixtures/team_catalog_content/`) consumed by both this PR's Rust tests and #3995's TS tests ## Durable ordering, deletion reconcile, and relay-contract alignment The catalog paths above sit on the shared inbound/deletion retention seam. This PR makes that seam's ordering structural rather than conventional, adds a negative-side (deletion) counterpart to the existing positive-side boot backstop, and aligns inbound resolution with the relay's actual soft-delete and NIP-33 winner rules. **Preflight-then-commit (`commit_inbound_with_store`, `commands/personas/inbound.rs` + `retention.rs`).** A named primitive runs the fallible store mutation first and advances the durable retention head only on success; an event that loses preflight returns `Skipped` without touching the store. The persona/team upsert arms and the inbound kind:5 removal path all route through it, so no inbound arm can advance the head ahead of the store write it represents. The managed-agent arm keeps its own preflight (its runtime transition must not run for a skipped event) and still advances the head only after `save_managed_agents`. **Atomic + monotonic tombstone helpers (30175 / 30176 / 30177).** The three ordinary tombstone helpers (`commands/personas/pending.rs`, `commands/agents_pending.rs`, `commands/teams/mod.rs`) each read the prior head, sign, delete, and retain inside one `BEGIN IMMEDIATE` transaction, and sign the kind:5 with a `created_at` that strictly dominates the prior head (`monotonic_created_at(prior_head)`). This mirrors the 30178 `tombstone_team_catalog_coordinate` precedent. The three siblings deliberately duplicate the `BEGIN IMMEDIATE` shape rather than sharing a helper this round — the shared-helper consolidation is deferred to the persona-tombstone follow-up PR where the flush-replay class already lives. **Deletion reconcile (`event_sync.rs`, negative-side counterpart of the positive-side boot backstop).** The positive side already reconstructs missing retained heads at boot from surviving disk records. Deletion is the asymmetric gap: an atomic tombstone helper preserves the head on failure, but boot reconcile enumerated disk records — deletion already removed them — so an orphan retained head was never tombstoned. The deletion reconcile enumerates retained 30175/30176 heads and tombstones only genuine orphans (a retained head with no matching disk record), routing through the now-atomic helpers. It reconciles only against a successfully parsed store: a truncated, malformed, or wrong-shape JSON store fails loudly (never triggers a tombstone); a missing file is treated as empty. Managed agents (30177) are excluded by design: their inbound sync retains a head *without* minting a local disk record (agents carry device-local secrets that can't come from a relay event), so a retained 30177 head with no matching record is the normal cross-device state for every agent created on another device — not a lost deletion. Sweeping it would tombstone and archive another device's live agents at boot. Agent deletion-retry therefore stays a pre-existing gap owned by the direct delete path (tracked in Follow-ups). **Inbound relay-contract alignment (`retention.rs`, `commands/personas/inbound.rs`).** Two inbound rules now match the relay: - *Equal-second tie-break.* `inbound_event_outcome` previously treated every equal timestamp as stale; the relay resolves equal `created_at` by lowest event id. Preflight now matches the relay tuple — strictly newer timestamp wins, equal timestamp resolves by lower event id, an exact echo skips — so two devices authoring different same-second successors converge instead of one silently republishing an event the relay refuses. - *Covered-head resolution on inbound kind:5.* `reconcile_inbound_tombstone` previously consulted only the retained kind:5 row, so a historical tombstone replayed after a newer recreation deleted the recreated record and never purged the covered head. Preflight now resolves both the tombstone row and the covered `(target_kind, owner, d_tag)` head: a target head strictly newer than the tombstone preserves JSON and skips; an actually-covered head is removed from disk first (fallible), then the tombstone row commit and covered-head purge happen atomically. A JSON-save failure advances neither, so the identical event stays retryable. **Atomic agent-archive coupling (`commands/agents_pending.rs`).** The 30177 tombstone and the 9035 archive request now enqueue in one `BEGIN IMMEDIATE` transaction, with the archive's `persona_id` derived from the retained head (`persona_id_from_head`) rather than the deleted record, where it survives the tombstone as owner-signed historical alias data. The standalone `archive_managed_agent_pending` production callers are removed — there is no double-enqueue path. Unlike personas/teams, this coupling is not re-enqueued by the boot deletion reconcile (30177 is excluded, above): a crash after the disk-authoritative record is removed but before this transaction commits leaves agent deletion-retry a pre-existing gap owned by this direct delete path (tracked in Follow-ups). ## Follow-ups `publish_prepared_persona` (`commands/personas/sharing.rs`) has the identical direct-submit-outside-the-lock race for 30175 persona heads that this PR fixes for 30178 team heads. It predates this work and is tracked in the separate persona-tombstone follow-up PR, not folded here. The app-wide `http_client` leaves reqwest's connect/read/total timeouts unset (`app_state.rs` builder configures only pool options). This PR bounds the team-publish call site with a `tokio::time::timeout`, but every other consumer of the shared client remains exposed to a non-responding endpoint. A client-wide default timeout is the broader fix; it is pre-existing on main and affects all consumers, so it is out of scope here. Managed-agent deletion-retry: the atomic 30177 tombstone + 9035 archive is best-effort, and unlike personas/teams it is deliberately excluded from the boot deletion reconcile (a device-local-absent 30177 head is the normal cross-device state, not a deletion). A crash between the delete's store write and its retention enqueue therefore has no boot backstop for agents. The durable fix is a local deletion intent written before the record is removed; deferred rather than folded into this round. Stack: this PR → [#3995](https://github.com/block/buzz/pull/3995) Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
ae0ecd9a67 |
feat(model-capabilities): humanize Databricks UC model families (#6955)
Chat-eligible Unity Catalog models from the Gemini, DeepSeek, GLM, Grok, Llama, Qwen, Gemma, and Inkling families were rendering as raw FQNs such as `system.ai.gemini-3-5-flash` in the agent model picker. #6918 surfaces these models, but the capability manifest carried no records for most of them and the registry-label path could not strip their Unity Catalog or `goose-` prefixes. I added a display-only `label_family_tokens` list and `databricks_v2` exact records for the sixteen previously unlabeled endpoint stems. All 26 target FQNs and their `goose-` aliases now resolve to curated labels through `databricks_registry_label`, while capability resolution continues to use only the existing `family_tokens` (`claude-`, `gpt-`, and `kimi-`). This keeps label discovery from changing capability profiles for unrelated model IDs. Each new record includes reconciliation metadata against the pinned [models.dev catalog](https://models.dev/api.json). Nine records adopt first-party effort evidence that the Databricks MLflow Chat transport can express: Gemini 3.5 Flash, Gemini 3.5 Flash Lite, Gemini 3.6 Flash, Gemini 3 Pro Image, DeepSeek V4 Flash, DeepSeek V4 Pro, GLM-5.3 Flash, Grok 4.6, and Inkling. The remaining seven deliberately retain the `databricks_v2/concrete_unknown` fallback because upstream evidence is absent, identifies a non-reasoning model, or provides only toggle/token-budget controls that `reasoning_effort` cannot represent. The normative corpus adds the sixteen exact records, four Unity Catalog alias probes, and two capability-isolation probes. Existing base vectors remain unchanged, and Rust and TypeScript consume the same strict manifest and corpus. Related: #6918 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Thufir <7ebdb0b67dab08a570b9faf7bbada97535673b4ccaba2cbd546ad3ba84c87fa6@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Thufir <7ebdb0b67dab08a570b9faf7bbada97535673b4ccaba2cbd546ad3ba84c87fa6@buzz.block.builderlab.xyz> |
||
|
|
be8d7756d5 |
feat(agent): discover Databricks Unity Catalog models (#6918)
## Summary Add Unity Catalog model-service discovery to `buzz-agent` Databricks v2, preserve full Unity Catalog FQNs through ACP model discovery, route model services through MLflow chat, and add the optional `DATABRICKS_MODEL_FILTER` visibility filter. The resilience follow-up bounds each catalog attempt across headers, bounded body reads, and JSON parsing; retries transient 499/5xx, transport/body failures, malformed JSON, and attempt timeouts; and emits only safe catalog/status diagnostics. ## Validation The live validation and test gates below were run on the pre-rewrite tip `1cf6c66e7c5bf9a00c357bda24df48e3beeb310b`. The rewritten tip `0364cf44f3eb5cfbd5771b0a0dd7868ce0ad2c19` has identical trees for both commits, so the attribution rewrite changes no tested code. - 31 catalog tests passed - 19 Databricks OAuth/routing integration tests passed - `cargo check -p buzz-agent` passed - `cargo clippy -p buzz-agent --all-targets -- -D warnings` passed - `cargo fmt --check` passed - `git diff --check` passed - Desktop Tauri validation passed through the repository-supported setup path, including 2,777 library tests and auxiliary terminal suites The full `buzz-agent` package has one separately reproduced pre-existing failure: `regressions::cancel_kills_inflight_tool_via_mcp_notification`, failing on both baseline `eb8e97cfe` and the follow-up worktree. No Databricks path reaches that test. The live validation proves discovery and ACP exposure, not inference against every listed model. --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
57216c942f |
test(db): use canonical channel roster fixtures (#6819)
## Summary
Fix two PostgreSQL channel-roster test fixtures so they satisfy
migration 0032's canonical kind-39002 fence before the
channel/membership store extraction moves them.
This is a test-only prerequisite in the issue #2 extraction stack. It
emits the canonical four-field `p` tags with authoritative roles, keeps
the stale-history mutation after insertion, and gives the colliding
other-tenant fixture its own matching roster.
## Stack
- Exact base: codex/issue-6-finish-replaceable-store at
|
||
|
|
69096c9a8d |
preserve channel description paragraph breaks (#6946)
**Category:** fix **User Impact:** Channel descriptions now keep their paragraph structure everywhere people and agents read them. **Problem:** Multi-paragraph channel descriptions were flattened in the desktop interface and in agent context, making operational instructions harder to scan and potentially changing their meaning. A running agent harness could also continue using a stale description after someone edited it. **Solution:** Preserve authored line and paragraph breaks across the desktop surfaces, and render descriptions as bounded, indented agent-context blocks with semantic delimiters escaped. Prompt-visible channel metadata now refreshes before each turn with cached fallback, while one shared desktop derivation keeps the header and channel intro consistent. <details> <summary>File changes</summary> **crates/buzz-acp/src/pool.rs** Refresh prompt-visible kind-39000 metadata with one bounded request per turn so edits reach running harnesses, while retaining cached metadata as a degradation fallback. Add regression coverage for description edits and update request-count fixtures for the refresh lifecycle. **crates/buzz-acp/src/prompt_framing.rs** Add semantic text escaping for untrusted values embedded inside paired prompt sections. **crates/buzz-acp/src/queue.rs** Render multi-paragraph descriptions as indented context blocks, preserve blank lines, retain the 500-character limit, and escape semantic delimiters. Add helper- and full-prompt tests for paragraph preservation, UTF-8-safe truncation, and adversarial input. **desktop/src/features/channels/lib/channelDescription.ts** Introduce one shared channel-detail derivation and preserve newlines between status context and authored detail text. **desktop/src/features/channels/lib/channelDescription.test.mjs** Cover shared field precedence, multiline descriptions, status-prefix separation, and fallback behavior. **desktop/src/features/channels/ui/ChannelPane.helpers.ts** Use the shared channel-detail derivation for the channel intro instead of maintaining a second field order. **desktop/src/features/channels/ui/ChannelPane.helpers.test.mjs** Verify description text wins consistently over legacy purpose text when both are present. **desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx** Preserve paragraph whitespace in editable and read-only channel summaries and allow enough lines for compact multi-paragraph descriptions. **desktop/src/features/channels/ui/ChannelManagementSheetRows.test.mjs** Render both channel-summary variants and assert the multiline whitespace and four-line clamp contract. **desktop/src/features/messages/ui/ChannelIntroBlock.tsx** Display description line and paragraph breaks in the empty-channel intro. **desktop/src/features/messages/ui/ChannelIntroBlock.test.mjs** Add render-level coverage for the intro's multiline whitespace contract. </details> ## Reproduction steps 1. Run the desktop app and create an empty channel. 2. Give it a description with two paragraphs, including a blank line and a multi-line second paragraph, then save and reopen channel management. 3. Confirm the empty-channel intro preserves the blank line and subsequent line breaks. 4. Confirm the channel-management summary shows the paragraph structure for both an editor and a read-only member rather than a detached ellipsis. 5. Hover the channel title and confirm its tooltip keeps the description's line breaks. 6. Add an agent, send one prompt, edit the description, then send another prompt. Confirm the next generated agent context contains the updated description as an indented multiline block. ## Screenshot A real-app screenshot of the multiline channel intro and channel-management summary is attached in the PR comments below. Closes AIDA-1980. --------- Signed-off-by: tulsi <tulsi@block.xyz> |
||
|
|
0808ab485c |
fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825)
## Problem
`buzz channels create --template` fails with a bare-pubkey
duplicate-instance error when a persona has more than one live instance.
When duplicate instances share the same display name and avatar, the
error gives the operator no signal to identify which instance is stale.
## What changed
**`crates/buzz-cli/src/commands/channels.rs`** (boundary) + one
visibility line in **`crates/buzz-cli/src/commands/users.rs`** (`fn
presence_subject` → `pub(crate)`), plus a relay behavior change in
**`crates/buzz-relay/src/api/bridge.rs`** and a supporting test in
**`crates/buzz-pubsub/src/presence.rs`**.
### Relay: presence-lookup failure is now surfaced as a failure
`synthesize_presence` previously collapsed a Redis `get_presence_bulk`
error into `unwrap_or_default()` → HTTP 200 `[]`, making a backend
outage indistinguishable from an authoritative all-offline snapshot. It
now returns `Option<Result<Vec<Value>, (StatusCode, Json<Value>)>>`:
`None` means the request is not a presence query (fall through),
`Some(Ok)` is a real snapshot (an empty vec is an authoritative
all-offline result), and `Some(Err)` is a backend failure surfaced as a
non-2xx response. The event tag-parse and sign paths were converted from
`.ok()?` fall-throughs to explicit `Some(Err(...))` for the same reason
— a silent fall-through would have reintroduced the fake-empty-success
anti-pattern. The function takes `pubsub` and `relay_keypair` directly
rather than the whole `AppState`, which keeps the Redis-error seam
unit-testable without live infrastructure; its single production caller
was updated accordingly, and no other caller consumes
`synthesize_presence`.
### Hint fetch architecture
**`assemble_roster_resolution<F, Fut>`** — post-fetch stage extracted
from `build_roster_resolution` as an injectable-fetcher async generic.
It contains: archive-filter-based duplicate detection, the conditional
`fetch_hints` call, and `finalize_roster_resolution` delegation.
Accepting the fetcher as a closure makes the wiring directly testable
without a relay.
`build_roster_resolution` reduces to: gather slugs → `tokio::join!(scan,
archive)` → delegate with the real `fetch_candidate_hints` closure.
**Zero hint queries on the happy path**: `assemble_roster_resolution`
only invokes the fetcher when duplicate live instances exist after
archive filtering. Untrusted archive snapshot (`Err`) conservatively
treats all found instances as live.
When duplicates exist, `fetch_candidate_hints` runs the presence
(kind:40902) and profile (kind:0) queries concurrently, bounding **each
independently** through `join_bounded_queries`: a per-query 3-second
timeout maps to `Err`, so a lookup that completes is never discarded
because its sibling hung. On total failure or timeout, bare pubkeys are
printed promptly.
### Trusted-snapshot boundary — `hints_from_results`
The relay drops the Redis presence key when an identity goes offline, so
the bulk presence snapshot never returns an event for an offline
instance — precisely the stale duplicate an operator needs flagged.
`hints_from_results` therefore treats a *trusted* presence snapshot as a
**complete snapshot** for the requested pubkeys: it seeds all of them
`offline`, then overlays returned statuses.
A response is trusted only when `trusted_presence_snapshot` validates
**every** array element as the actual snapshot contract: each must parse
as a complete signed `nostr::Event` of the presence-update kind carrying
exactly one `p` tag whose subject is one of the requested pubkeys.
Requiring the *sole* `p`-tag subject — the exact value `build_hint_map`
later reads via `presence_subject` — is what stops a mixed-tag event
(`[["p","<unrequested>"],["p","<requested>"]]`) from passing the gate
yet overlaying a different subject downstream. Any element that is not
such an event — a vacuous object like
`{"pubkey":"…","content":"online"}`, `[{}]`, `[null]`, an event of the
wrong kind, one for an unrequested subject, or one carrying more than
one `p` tag — makes presence enrichment untrusted: nothing is seeded
`offline`, so absence is never falsely inferred as offline. A failed,
timed-out, or non-array presence response is likewise untrusted. In
every untrusted case the successful profile sibling still contributes
its hints.
### Response-to-map conversion — `build_hint_map`
Sync production function taking the offline seed plus raw
presence/profile event slices. Relay-signed presence events carry the
agent pubkey in the `p` tag (not the event author); `build_hint_map`
calls `presence_subject` from `users.rs` (now `pub(crate)`) to resolve
the correct key.
### Hint display — `format_candidate`
Appends `[online/offline, profile updated YYYY-MM-DD]` (or subset) to
each candidate pubkey in the error. `profile_updated_at` names the field
correctly: kind:0 is replaceable state that desktop republishes on
rename and profile reconciliation, so `created_at` reflects the last
profile update, not provisioning time. Date formatted via
`chrono::DateTime::from_timestamp`; out-of-range timestamps omit the
date. Missing entries fall back to bare pubkeys.
### Cardinality rule
`apply_cardinality_rule` remains pure and relay-free; zero/one/many
semantics unchanged.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
745ff6066c |
Fix Codex security review authorization (#6913)
The Codex security review workflow now authorizes automatic reviews from the live pull request instead of the webhook snapshot. This prevents a stale author association from sending an internal PR down the external-contributor invalidation path. Internal PRs with no previous review now exit without attempting a comment or label mutation. The jobs that update PR comments and labels use `pull-requests: write`, and the review job runs only after the trusted prepare step emits `authorized=true`. Regression tests cover stale author associations in both directions, the no-op invalidation path, and the mutation-job permission contract. Checked with `just ci`, `just security-review-check`, and `actionlint .github/workflows/codex-security-review.yml`. The failing `Mark Previous Review Stale` check on this PR runs the pre-fix `pull_request_target` workflow from `main`. GitHub will not execute this branch’s trusted workflow until it is merged; the new workflow contract and state-machine tests exercise the fix before then. Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
0ccf934b88 |
fix(db): disable heartbeat vacuum truncation (#6898)
## Why `replica_heartbeat` is updated continuously, but autovacuum heap truncation can take an `ACCESS EXCLUSIVE` lock whose replay cancels concurrent hot-standby reads. Truncating a fixed, single-row table provides no meaningful space benefit. ## What - Add migration 0034 to set `vacuum_truncate = false` on `replica_heartbeat` - Keep the desired-state schema aligned for fresh installations - Extend migration coverage so both paths retain the reloption ## Risk Assessment Low — this disables only autovacuum's heap-truncation phase for a single-row table; normal vacuum cleanup remains enabled. The tradeoff is retaining at most the table's otherwise-truncatable tail pages. ## References - [Investigation thread](https://sq-block.slack.com/archives/C0B2WT43BDH/p1787760831375989) - [PostgreSQL table storage parameters](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS) Generated with Codex --- **Update Aug 27, 2026:** Hardened fresh `pgschema` bootstraps after review. - Renamed the shared post-apply script to `reconcile-schema-after-pgschema.sql` and made it restore the heartbeat reloption and singleton row idempotently. - Added live catalog and row assertions that stop bootstrap when the database does not match the required state. - Updated every repository `pgschema apply` caller, including the desktop release smoke path. - Added a regression test that requires future `pgschema apply` callers to run reconciliation. - Documented the `pgschema` DML and storage-parameter limitation in `AGENTS.md` and at the reconciliation entry point. - Merged current `main` and renumbered this migration to 0034 after `main` added migration 0033. --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> |
||
|
|
c363ce13c0 |
chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663)
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [rui314/setup-mold](https://redirect.github.com/rui314/setup-mold)
([changelog](
|
||
|
|
525007af4b |
chore(deps): update dependency vitest to v4.1.11 (#6667)
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [vitest](https://vitest.dev) ([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`4.1.10` → `4.1.11`](https://renovatebot.com/diffs/npm/vitest/4.1.10/4.1.11) |  |  | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>vitest-dev/vitest (vitest)</summary> ### [`v4.1.11`](https://redirect.github.com/vitest-dev/vitest/releases/tag/v4.1.11) [Compare Source](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11) ##### 🐞 Bug Fixes - Revive global concurrency limit for test lifecycle \[backport to v4] - by [@​sheremet-va](https://redirect.github.com/sheremet-va) and [@​hi-ogawa](https://redirect.github.com/hi-ogawa) in [#​10992](https://redirect.github.com/vitest-dev/vitest/issues/10992) [<samp>(5146d)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/5146df80b) - **browser**: - Encode iframeId in tester iframe URL \[backport to v4] - by [@​sheremet-va](https://redirect.github.com/sheremet-va), **Pduhard** and **Claude Opus 4.8** in [#​10955](https://redirect.github.com/vitest-dev/vitest/issues/10955) [<samp>(10b2c)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/10b2cd201) - Trigger playwright/chromium gc on lower disk availability \[backport to v4] - by [@​hi-ogawa](https://redirect.github.com/hi-ogawa), **Hiroshi Ogawa** and **OpenCode** in [#​10951](https://redirect.github.com/vitest-dev/vitest/issues/10951) [<samp>(9851d)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/9851dbc41) - **mocker**: - Restrict redirect mocks to the fs allowlist \[backport to v4] - by [@​sheremet-va](https://redirect.github.com/sheremet-va) in [#​10974](https://redirect.github.com/vitest-dev/vitest/issues/10974) [<samp>(fe5a1)</samp>](https://redirect.github.com/vitest-dev/vitest/commit/fe5a11d3c) ##### [View changes on GitHub](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
30596d3add |
chore(deps): update dependency @tanstack/react-virtual to v3.14.10 (#6666)
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@tanstack/react-virtual](https://tanstack.com/virtual) ([source](https://redirect.github.com/TanStack/virtual/tree/HEAD/packages/react-virtual)) | [`3.14.9` → `3.14.10`](https://renovatebot.com/diffs/npm/@tanstack%2freact-virtual/3.14.9/3.14.10) |  |  | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>TanStack/virtual (@​tanstack/react-virtual)</summary> ### [`v3.14.10`](https://redirect.github.com/TanStack/virtual/blob/HEAD/packages/react-virtual/CHANGELOG.md#31410) [Compare Source](https://redirect.github.com/TanStack/virtual/compare/@tanstack/react-virtual@3.14.9...@tanstack/react-virtual@3.14.10) ##### Patch Changes - Updated dependencies \[[`a0a411e`]( |
||
|
|
82e748085e |
chore(deps): update ubuntu:24.04 docker digest to 33ceb71 (#6664)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ubuntu](https://hub.docker.com/_/ubuntu) ([source](https://git.launchpad.net/cloud-images/+oci/ubuntu-base)) | container | digest | `561618e` → `33ceb71` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zOS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
9176e34405 |
fix(projects): allow owners to delete agent projects (#6533)
## Summary - expose project deletion when the current user directly owns the project or owns/manages its author agent - publish a viewer-signed, project-only tombstone after fetching the exact live head, then detect concurrent replacement - honor relay-authorized owner-signed tombstones in desktop project read models ## Testing - pre-push hook: desktop Biome, TypeScript, 5,358 desktop tests, Rust tests, Tauri checks, and file-size ratchet passed at `a36f7a4cf` - `cargo check -p buzz-test-client --tests` - relay E2E coverage added as ignored; not run against a live relay --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz> |
||
|
|
27c1a7c91b |
Fade expanded video controls on hover (#6926)
## Summary - fade expanded video controls out when the pointer leaves the video - restore controls on video hover or keyboard focus while keeping touch controls visible - respect reduced-motion preferences ## Testing - focused expanded-video hover Playwright test - full video attachment E2E suite (12 tests) - desktop check and typecheck - desktop unit tests (5,674) --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
b3baa56ba8 |
fix(db): exclude kind:30179 ciphertext from brownfield FTS (#6822)
## Problem Kind `30179` (NIP-PMA private managed agent — NIP-44 ciphertext carrying the agent nsec, env vars, prompt) joined `AUTHOR_ONLY_KINDS` in #4593, but only **fresh** installs stopped indexing it: - `migrations/0008` installs the positive FTS allowlist only when `events` is empty; - `migrations/0014` wraps the retained brownfield expression for `30350` alone; - `schema/schema.sql` still carries the legacy negative skip-set without `30179`. So a relay upgraded in place keeps tokenizing `30179` ciphertext into `events.search_tsv`. Found by Wren while reviewing #4999 (which activates publication of 30179 from Desktop). No readable leak: `/query` applies `search_hit_accepted` + `event_visible_to_reader` before serialization and live foreign searches against builderlab returned `[]` — this is the storage-layer privacy invariant (`docs/nips/NIP-PMA.md` deployment step 2) plus wasted FTS work. ## Fix - `migrations/0033_private_managed_agent_fts.sql` — same shape as 0014: capture the current generated expression via `pg_get_expr`, drop/re-add `search_tsv` wrapped with `kind = 30179 → NULL`. Every other kind keeps whatever policy the database already had (fresh allowlist or brownfield skip-set). Rebuilds the GIN index; no heap-wide policy rewrite. - `schema/schema.sql` — add `30179` to the desired-state skip-set. ## Tests - `populated_upgrade_preserves_search_policy_except_for_private_kinds` (renamed from `…_push_leases`): provisions the **legacy negative expression** (migrations 1–7), inserts kind 1 / 30179 / 30350 rows, checkpoints after 0014 (30179 still searchable, 30350 not), then runs to head and asserts 30179 is NULL. **Proven red** with the 0033 file removed: `left: [(1, Some(true)), (30179, Some(true)), (30350, None)]`. - `embedded_migrator_contains_consolidated_initial_schema`: count 32→33, asserts 0033 shape and `schema.sql` parity. - `buzz-search/tests/fts_integration.rs` setup applies 0033 so the FTS tripwires run against the real chain. ## Verification (HEAD |
||
|
|
4d77175c78 |
fix(client): resurface hidden DMs from live activity (#6885)
## Summary Hiding a DM row and then receiving a new message from that person never brought the row back — the reader was left with only an Inbox notification. This restores the row from live activity, client-side, with no relay or database changes. - Subscribe to hidden-DM activity **channel-scoped** over the hidden-DM id set (`#h`), re-registering whenever the set changes. The relay's `fan_out_scoped` only routes channel-scoped events (every kind:9 DM carries a `channel_id`) to channel-scoped subscriptions, so a community-global `#p` filter receives nothing. Hiding a DM never drops membership, so `#h` authorization holds. Because every delivered event is already for a hidden DM the reader belongs to, eligibility no longer needs a self `p` tag — untagged CLI/agent DMs resurface too — and no per-event visibility fetch is required. - On qualifying activity, invoke the existing open-DM action (`41010`) to reopen the hidden one-to-one or group DM, validating that the relay reopened the same channel. - Preserve the existing relay-synced hide snapshot (`41012` → `30622`); hide state still syncs across devices. - Let Inbox reopen a hidden DM before navigating to its message, with a busy state on the row and context-menu "Open in channel" action while a reopen is pending. - Coalesce resurface attempts per channel: a concurrent message that arrives while a reopen is in flight triggers a retry on failure instead of being marked handled and dropped. - Write exact dm-visibility query keys on open/hide mutations. This is the client-only replacement agreed in the Buzz design thread and supersedes #6713. The accepted limitation is that resurface only fires while a client is running — a message that arrives while all clients are closed does not restore the row on next launch (the `30622` snapshot has no watermark); Inbox click-through is the safety net for that case. Addresses #6316. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> |
||
|
|
c856be0fb9 |
fix(desktop): keep the draft space when typing right after a mention pick (#6875)
## Problem
Desktop Smoke E2E flaked on `mentions.spec.ts` ("typing a mention before
existing text does not interleave spaces"): picking a suggestion
mid-draft and typing immediately could swallow the draft's own space,
producing `hello @bob abcworld` instead of `hello @bob a world`.
With `hello world` drafted and ` @bo` typed after `hello`, picking the
suggestion leaves `hello @bob world` — the mention's trailing space
followed by the draft's space. Chromium reconciles that whitespace run
by re-emitting it as a replacement (`replace(" " -> " a")`), anchored at
*either* edge of the run. The settling-time text-input redirect only
recognized a caret or a rewrite anchored at the chip edge, so the other
shapes fell through to ProseMirror's destructive default and ate the
separator.
The race pre-dates this branch and reproduces on `main`.
## Fix
`mentionHighlightExtension.ts` now redirects on the invariant instead of
on a specific rewrite shape. While autocomplete is settling the user
cannot have a selection (a selection cancels settlement), so a
whitespace-only replacement adjacent to a mention's trailing space is
always the browser normalizing whitespace — never an intentional delete.
The handler keeps every space already in the document and inserts only
the typed characters after the token, whichever edge the rewrite is
anchored at. Non-whitespace replacements still fall through as real
edits.
`insertPosForMentionTextInput` / `mentionTextInputInsertPos` become
`insertionForMentionTextInput` / `mentionTextInputInsertion`, returning
`{insertAt, text}` so the handler can rewrite the payload, not just the
position.
## Tests
- Helper-level cases for all four anchors (caret at chip edge, caret
past the trailing space, run rewritten from the chip edge, draft space
rewritten alone), plus space/NBSP variants and a non-whitespace
replacement that must be left alone.
- A plugin-level test drives the real `handleTextInput` prop and falls
back to ProseMirror's default when it declines — exactly as the browser
does with the return value — so it fails against the previous production
code rather than depending on winning a timing race in a headless
browser.
Desktop unit suite passes on pre-push (5558/5558).
---------
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
b622003f74 |
broker: define the agent-to-broker action contract (#6742)
Addresses the interface half of #6467. This is the contract for how a keyless agent asks a host to act on its behalf — and only that. No host, no transport, no signing, no Desktop code; the only implementation is a test double. An agent that holds no secret key still has to read its channels, answer a mention, react, keep its profile, address its memory, and create the agents it needs. This PR defines the vocabulary for those requests and the rules both sides must follow. **#6467's open question, answered: per-operation, not "sign this".** A raw signing primitive tells the host *who* is asking but not *what for*, so the only policies it can express are all-or-nothing. A closed set of named operations lets a host serve channel reads while refusing agent creation, and gives any later policy layer something to attach to. The cost is that adding an operation means changing the contract — which is the point. Signing, publishing, and the auth/encryption NIPs are deliberately *not* operations; they are mechanisms inside whoever holds the key, and a test pins that none of those names resolve. **Nine operations in v1:** read a channel (whole channel, one thread, or mentions-only — these differ by filter, not permission, so they share one operation), post, reply, react, set profile, derive a storage address, and create / update / delete agents. Presence and typing are deferred as housekeeping a host may decline regardless; streaming is deferred, so waking on a mention is a polled mentions-only read. Each operation has typed, validated arguments and a typed outcome. **What the contract enforces structurally rather than by instruction:** - *Retry means identical bytes.* A request is validated and serialized exactly once and the transport only ever sees that frozen form. There is no way to re-render it, so a retry can't drift and be mistaken for a conflicting request. - *Read cursors are opaque.* Host-issued strings, validated for shape only, never parsed or compared. The host owns ordering and paging stability instead of being locked into timestamps. - *Reads return signed Nostr events.* A keyless agent verifies authorship and content locally and trusts the host only for completeness and authorization. Reply ancestry and mentions are derived from the signed tags, so nothing can disagree with the signature. - *Validation is the only door to a response.* The transport primitive hands back an unjudged envelope; the only response a caller can actually obtain is one already checked against the request it answers — request id correlates, the outcome's operation matches, identifiers and cursors are well-formed. A mismatch counts as "no answer", not a failure verdict. The raw primitive can't be invoked from outside the crate (pinned by compile-failure checks), so skipping that step isn't an option. - *Authority is never asserted by a body.* The envelope has no requester, owner, scope, or credential field; all of that comes from the session the host authenticated. Ownership of a created agent is implicitly the requester, never a field — which is what lets agents own agents while the chain still ends at a human. - *No secret crosses the wire.* Agent creation returns a pubkey and handle, never the minted key. Every payload type rejects unknown fields, and a test pins the exact JSON key set of all eighteen argument and outcome types, so no field can be added without a reviewer touching that table. Two honest limits: a string can physically hold secret text, so keeping secrets out of message content is host policy; and nothing stops a host from *holding* keys — it just may not hand them over. - *Null is never legal; omission is the only spelling of absence.* Every optional member rejects an explicit null. Otherwise null and omitted would be indistinguishable, and a contradiction check on the response envelope could be skipped by sending one (a real hole in an earlier revision). Coverage is derived by walking every path in the real fixtures, so new optional members are covered automatically. Serializers that emit null by default (Go without `omitempty`, Python's default dump) need configuring — called out in the module docs. **Wire mapping.** One POST endpoint, JSON body is the frozen request, opaque bearer credential in the auth header. Every verdict the host reached — including failure and including a rejected credential — comes back as a well-formed envelope with 200, because a second copy of the verdict in the status line could only disagree with it. Clients still try to parse an envelope regardless of status (intermediaries may remap) and fall back to the status only when none is present. A rejected credential is a host verdict carrying the promise "the action did not run"; transport errors are reserved for "nothing can be concluded about side effects." Binding the credential to a specific agent and conversation is the host's job and deliberately unenforceable here. **Relationship to #6543.** Envelope, request-id rules, protocol version, the three-way succeeded / failed / indeterminate result, error codes, and the agent-management argument shapes carry over. Changed deliberately: pubkeys are a validated type rather than bare strings, "capability" became "action" throughout, and a new "unsupported" error lets a host decline a best-effort operation without it reading as a fault. Both PRs land in the same module path — whichever merges second needs a hand reconcile, which I own as the author of #6543. **Validation at `7c853e0fb`.** 299 tests in the sdk crate (full package run), workspace tests green apart from the pre-existing relay timeout flake, which I reproduced on unmodified `main`. Clippy with warnings denied, fmt, warning-free docs, and the pre-push gate all pass. Review findings were closed with mutation tests — each fix verified by reverting it and watching a named test fail. One of those exposed a gap in my own coverage (response-side fields weren't pinned); that's fixed and floored separately from the request side so neither can mask the other. **Size.** About 5,400 lines across 8 files, roughly half tests. Every source file is under the repo's 1,000-line cap except the test file; #6759 splits it mechanically, is stacked on this branch, retargets to `main` after merge, and lands before PR 2 of the stack. --------- Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Signed-off-by: Bradley Axen <baxen@squareup.com> Signed-off-by: ss-core-02 <cca2c695c61d409321106cc399009c2cc674d72cc8900b2999a68fe54577a411@buzz.block.builderlab.xyz> Co-authored-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: ss-core-02 <cca2c695c61d409321106cc399009c2cc674d72cc8900b2999a68fe54577a411@buzz.block.builderlab.xyz> |
||
|
|
cada3028ce |
fix(desktop): keep project sheets independent from threads (#6901)
## Summary Project workspace sheets now slide in from the right as an independent layer over an open conversation thread instead of replacing the thread or rendering inside its panel. Closing the sheet restores the unchanged thread, Escape applies only to the topmost surface, and the covered thread's resize divider no longer leaks through the overlay. ### Related issue Related: #6602. No matching issue found. ### Testing - `pnpm --dir desktop exec biome check src/features/channels/ui/RightAuxiliaryPane.tsx tests/e2e/project-commit-detail.spec.ts` - `pnpm exec playwright test tests/e2e/project-commit-detail.spec.ts --project=smoke --grep "project workspace sheet stays independent from an open thread"` - Pre-push desktop formatting, file-size, typecheck, and unit-test checks --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> |
||
|
|
cdab765748 |
Add gated security reviews (#6816)
Add a gated Codex security review for Buzz pull requests. Reviews run automatically for Block organization members. External PRs require a `@buzz-security-review` comment from a Block member, and every new external push invalidates the previous result. Codex reviews the PR SHA in a read-only, network-restricted job with no write GitHub token. A separate trusted job validates and sanitizes structured findings before posting them. The workflow uses the dedicated `CODEX_REVIEW_API_KEY` secret. --------- Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
ceb8ba605f |
fix(desktop): accent-colored mention badges that count thread mentions (#6900)
**Category:** fix **User Impact:** Sidebar mention numerals now match the user's theme/accent color and appear for mentions inside threads, not just top-level channel messages. Hover-to-preview on channel rows is unchanged. **Problem:** Follow-ups to #6696: 1. The mention numeral used a fixed red `--notification` token that ignored the user's theme and accent selection. 2. The numeral read `appBadgeCount`, which deliberately excludes threaded replies (Home's badge subtotal counts those toward the Dock badge). Mentions inside threads therefore never surfaced a numeral on the channel row — only the generic thread-activity dot. **Solution:** 1. The badge now reuses the primary accent pair (`bg-primary` / `text-primary-foreground`), the same treatment as the DM count badge, so it follows whatever accent the user applies — including the pinned neutral accent of the Buzz themes. The now-unused `--notification` tokens and Tailwind utilities are removed. 2. Non-DM rows now count every unread mention/broadcast — threaded or top-level — via a new `highPriorityCount` channel projection, replacing the boolean `highPriorityUnread` across the TS contract, the Tauri SQLite store, the e2e mock bridge, and the native test rig. The Dock/app badge projection (`appBadgeCount`) is unchanged, so Dock totals do not double-count thread activity that Home already counts. The hover-to-preview channel activity popover is untouched; e2e coverage now pins the combination (thread mention → numeral, no dot, popover still opens and lists the mentioning reply). <details> <summary>File changes</summary> **desktop/src/features/channels/useUnreadChannels.ts** Non-DM sidebar counts switch from `appBadgeCount` to the mention/broadcast-inclusive `highPriorityCount`; the high-priority channel set derives from the same count. **desktop/src/features/sidebar/ui/SidebarSection.tsx** Badge drops the fixed notification classes and inherits the accent-following default. **desktop/src/shared/styles/globals/theme.css / desktop/tailwind.config.js** Remove the unused fixed `--notification` tokens and semantic utilities. **desktop/src/shared/api/tauriObservedUnread.ts / desktop/src-tauri/src/observed_unread.rs** Projection contract: `highPriorityUnread: bool` → `highPriorityCount: u64`, aggregated per unread high-priority event. **desktop/src/testing/e2eBridge.ts / desktop/src/features/channels/observedUnreadNativeRig.mjs** Mock/native-rig projections mirror the new count field. **desktop/src/features/channels/observedUnreadNative.test.mjs** Native badge-lane test seeds a high-priority event to match the numeral's new semantics. **desktop/tests/e2e/badge.spec.ts** Top-level mention test now asserts the badge equals the applied accent (seeded `#22c55e` under an adversarial theme). New test: in-thread mention shows the numeral, no dot, and the hover preview popover still lists the mentioning reply. **desktop/tests/e2e/channel-activity-popover.spec.ts / desktop/tests/e2e/thread-unread.spec.ts** Seeded fixtures that mention the user now expect the numeral (which subsumes the dot). </details> ## Reproduction steps 1. Pick any theme + accent color in Settings → Appearance. 2. Receive a top-level message mentioning you in an inactive channel → the row shows a numeral pill in your accent color. 3. Receive a reply **inside a thread** that mentions you in an inactive channel → the row shows the numeral (previously only a dot). 4. Hover the row → the channel activity preview still opens and lists the mentioning reply. 5. Switch accents/themes → the pill re-colors accordingly; the Dock badge count is unchanged from before. ## Verification - `tsc --noEmit`, Biome, `check:px-text` — clean - Desktop unit tests: 179/179 channel-suite; full suite has one pre-existing failure from #6590 (`useRetainedProjectGitViews.test.mjs` loader error, fails on clean `main`) - Tauri Rust: `observed_unread` tests + clippy + fmt — clean - Playwright smoke: badge (19), channel-activity-popover (10), thread-unread (13), plus channels/inbox/stream/messaging/mentions suites — all green ## Screenshots Screenshots are posted in the PR discussion using immutable repository-hosted image URLs. Signed-off-by: tulsi <tulsi@block.xyz> |
||
|
|
79df468255 |
Add Buzz benchmark evaluation layers (#6823)
## Summary - classify every Buzz-native benchmark task as `regression` or `workflow` - add metadata-backed `--layer` selection with k=1 regression and k=3 workflow defaults, while preserving explicit `--attempts`/`-k` - split an unqualified mixed Buzz dataset run into two Harbor jobs so each layer gets the correct default - document the layer contract and cover metadata, selection, defaults, overrides, invalid input, and command construction --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
eb8e97cfe7 |
fix(desktop): show edited head content in thread panel (#6887)
## Why Fixes #6886. In the channel timeline an edited message shows its newest body, but opening its thread panel showed the stale pre-edit body until the async thread-aux backfill landed (and permanently if it raced or missed). ## Root cause `desktop/src/features/messages/lib/independentThreadPanel.ts` built `events = [head, ...replyEvents]`, discarding the head's aux overlay events (edits `kind:40003`, deletions, reactions) already present in the channel window. `formatTimelineMessages` only applies an edit when the edit event sits in the same array as its target, so the thread head relied solely on `withThreadAux` to re-supply it. ## What - Carry the head's non-content aux events (via `!isTimelineContentEvent`) from the channel window into the thread head's event array, deduped against `replyEvents` by id. - Thread head is now byte-identical to the main timeline the instant the thread opens, independent of the async aux fetch. - Restricted to non-content kinds so reply content rows never leak in. ## Tests - Unit regression `independentThreadPanel.test.mjs` (6 cases): red pre-fix (`"two PRs"`), green post-fix (`"these PRs"`). - e2e `thread-head-stale-edit.spec.ts`: delays thread-replies aux to force the channel-window-vs-thread-aux divergence, then opens the thread via the reply button and asserts the panel head shows the edited body. Verified red pre-fix (stale `message-thread-panel` head), green post-fix. - Full desktop unit suite: **5562 pass / 0 fail**. `biome` + `tsc --noEmit` clean. Origin: Buzz channel `fix-reply-thread-old-view-bug` (#464b982f-fc05-42c8-918f-409e1f05a1df). --------- Signed-off-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz> Co-authored-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz> |
||
|
|
03bfc837cb |
fix(desktop-tooltip): increase surface contrast (#6897)
**Category:** fix **User Impact:** Tooltips now use a higher-contrast surface, making their labels easier to read across desktop themes. **Problem:** Tooltip surfaces could blend into hovered content, making labels difficult to distinguish. **Solution:** Use the existing popover surface and foreground tokens for clearer visual separation and theme-aware contrast. <details> <summary>File changes</summary> **desktop/src/shared/ui/tooltip.tsx** Updates shared tooltip colors to use higher-contrast semantic surface and foreground tokens. </details> ## Reproduction steps 1. Run the desktop app and hover a control that displays a tooltip. 2. Compare the tooltip against the content beneath it in both light and dark themes. 3. Confirm the tooltip surface remains clearly distinguishable and its label is readable. ## Screenshots | Before | After | | --- | --- | |  |  | ## Validation - `pnpm build:e2e` - `pnpm exec playwright test tests/e2e/tooltip-semantics.spec.ts --project=smoke` — 4 passed - `pnpm exec biome check src/shared/ui/tooltip.tsx tests/e2e/tooltip-semantics.spec.ts` - Pre-push desktop checks, typecheck, and unit tests - `git diff --check` - Full smoke project attempted; 345 of 1,222 tests passed without failure before the 10-minute local execution limit. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
8f3566ad9e |
Deduplicate ACP thread prompt context (#6706)
## Why This is a small isolated follow-up to [#6503](https://github.com/block/buzz/pull/6503): remove two proven duplicate prompt fragments without adopting its broader framing experiment or changing Buzz-native routing and identity behavior. ## What - Remove parsed `parent=<id>` only when it exactly duplicates parsed `root=<id>`; retain `root=<id>`, nested parents where parent differs from root, raw event tags, event IDs, author identity, mention fields and rules, and the explicit `--reply-to` instruction. - Remove the full-history retrieval hint only when complete, untruncated conversation or thread context is included. Thread context must include its root and cover every event rendered in the batch; mixed-thread batches and root-missing windows retain retrieval guidance. Context that is absent, previously delivered, or truncated also retains retrieval guidance, and the context header, count, and messages remain unchanged. ## Risk Assessment Low — the change is confined to Buzz ACP prompt rendering and preserves all routing, identity, event, and retrieval information outside the two exact duplicate cases. ## Testing - `cargo test -p buzz-acp` — 804 unit tests and 9 lifecycle integration tests passed on the clean rerun - Focused mixed-thread, same-thread, and missing-root prompt regressions passed - The timing-sensitive `keepalive_resets_idle_past_deadline` test passed in isolation after one unrelated full-suite flake, then passed in the clean full rerun - `cargo clippy -p buzz-acp --all-targets -- -D warnings` - `cargo fmt --all -- --check` - `just file-size-check` - Repository pre-push gate at `1b88793574e74e22f00a1fd19bfe9aa8e86150fa` — file-size, branch, Rust unit-test, and Desktop Tauri lanes passed - `git diff --check` Generated with Codex Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
01c87a350b |
Apply access policy when reusing channel agents (#6838)
Reused managed identities now apply their intended inbound-author policy before attachment or startup. An explicit policy wins, then the linked persona policy, then `owner-only`. This covers both channel-agent provisioning and the direct composer autocomplete path. If the policy update fails, attachment, startup, and message send stop. The production Playwright regression starts with a persona-linked agent set to `anyone` with a stale allowlist, then verifies the update happens before membership and startup and that the stored policy is `owner-only`. A small resolver test covers explicit, persona, and default precedence. This closes the stale-policy entry described by [Project Loupe issue #497](https://github.com/project-loupe/audit-buzz/issues/497). The separate Welcome Team reuse path is outside this PR. Checked with the 5,502-test desktop suite, `pnpm check`, `pnpm typecheck`, `pnpm check:file-sizes`, and the focused Playwright composer regression. --------- Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
4f554b6385 |
feat(sidebar): prioritize unread DMs in overflow navigation (#6842)
**Category:** improvement **User Impact:** Users can see which unread direct messages are waiting offscreen and jump to the most relevant one first. **Problem:** The sidebar overflow control treated every offscreen unread conversation alike, so an unread DM could be hidden behind channel activity with no visible clue about who was waiting. **Solution:** Prioritize the first offscreen unread DM, preview up to three senders with avatars, and keep the arrow, avatars, separator, and message count intact while only the trailing phrase truncates in narrow sidebars. <details> <summary>File changes</summary> **desktop/src/features/sidebar/lib/useUnreadOverflow.ts** Adds ordered DM preview data to unread-overflow calculations while preserving the sidebar's existing visibility and direction rules. **desktop/src/features/sidebar/lib/useUnreadOverflow.test.mjs** Covers DM prioritization, ordering, offscreen filtering, and channel-only fallback behavior. **desktop/src/features/sidebar/ui/AppSidebar.tsx** Passes unread DM previews into the lower overflow control and navigates to the preferred DM before the next unread channel. **desktop/src/features/sidebar/ui/MoreUnreadButton.tsx** Renders the frontmost-first avatar stack, accessible DM target label, middot separator, and message-count treatment. **desktop/src/features/sidebar/ui/MoreUnreadButton.test.mjs** Locks the three-avatar cap, stacking order, preferred target, fallback, accessible copy, and truncation structure. **desktop/src/shared/ui/UnreadPill.tsx** Contains the primary pill and allows only its trailing label to ellipsize under width pressure. **desktop/src/shared/ui/UserAvatar.tsx** Allows the sidebar preview stack to identify each rendered avatar in regression coverage. </details> ## Reproduction steps 1. Open enough sidebar conversations that unread items fall below the visible area. 2. Leave one or more offscreen DMs unread, along with unread channels. 3. Confirm the bottom overflow pill shows up to three overlapping DM avatars followed by a middot and the total new-message count. 4. Select the pill and confirm it navigates to the first offscreen unread DM rather than the nearest unread channel. 5. Resize the sidebar from 420px through 300px to 220px and confirm the arrow, avatars, middot, and full numeric count remain visible; only `new messages` may ellipsize. 6. Repeat with channel-only unread overflow and confirm the control keeps its channel fallback behavior without avatars. ## Screenshots Built-UI matrix with 100px of visible space beyond the sidebar edge. All images are hosted through `pr-screenshots`. | State | 220px min | 300px default | 420px max | Result | | --- | --- | --- | --- | --- | | Channel-only · 13 |  |  |  | Full label throughout. | | Channel-only · 999 |  |  |  | Full label throughout. | | One DM only · 1 |  |  |  | Full label throughout. | | One DM + channels · 99 |  |  |  | Full label throughout. | | Two DMs only · 2 |  |  |  | Full label throughout. | | Two DMs + channels · 47 |  |  |  | Full label throughout. | | Three DMs + channels · 128 |  |  |  | 220px: `128 new messa…`; full at 300/420. | | Three DMs + channels · 999 |  |  |  | 220px: `999 new mess…`; full at 300/420. | | Three DMs + channels · 9,999 |  |  |  | 220px: `9,999 new mes…`; full at 300/420. | **Matrix result:** 27/27 states remain contained. Exactly 3/27 states ellipsize—all are three-avatar cases at 220px. The arrow, all avatars, middot, and complete numeric value remain visible; only the trailing `new messages` phrase yields. ## Validation - Desktop unit suite: 5,517 passed - TypeScript: passed - Biome on all seven changed files: passed - Pre-push desktop checks, typecheck, tests, file-size, and branch-skew gates: passed - 27-state built-UI matrix capture: 27/27 contained; only the three 220px three-avatar states truncate, and only the trailing phrase - `git diff --check`: passed --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@users.noreply.github.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> |
||
|
|
de188ebf06 |
feat(projects): add agent and CLI project-home support (#6590)
## Summary - inject bounded project-home identity and repository context into managed agent sessions - add project-aware CLI flows for creating projects, repositories, issues, and related channels - match project homes through the existing relay query surface, then filter channel metadata client-side This is Part 1 of the channel-first Projects stack. Part 2 contains project creation and model foundations. ## Testing - `cargo fmt --all -- --check` - `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings` - `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test ignored - full pre-push gate passed ## Post-Deploy Monitoring & Validation - validate project-home agent context and project-aware CLI commands against a staging relay - healthy signals: project context matches the active channel, explicit repo coordinates remain stable, and normal channels receive no project block - failure signals: cross-channel project context, duplicate project creation, or commands targeting an unrelated repository; mitigate by reverting this PR --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> |
||
|
|
22cdda441d |
feat(desktop): restore message quick reactions (#6892)
## Summary - restore three learned quick-reaction shortcuts to the desktop message action rail - keep Add reaction (`SmilePlus`, `h-4 w-4`) immediately after shortcuts and left of the restored divider - preserve Reply, Copy link, More, custom emoji rendering, accessibility labels, and responsive behavior - hide shortcuts and the divider below `sm`, while keeping Add reaction available ## Tests At `24c4732c2341b846dccf4ae8fdc2455f700fc44d`: - `pnpm exec biome check src/features/messages/ui/MessageActionBar.tsx tests/e2e/message-copy-link.spec.ts tests/e2e/custom-emoji.spec.ts` - `pnpm exec tsc --noEmit` - `pnpm test` — 5,567 passed - `pnpm build:e2e` - `pnpm exec playwright test tests/e2e/message-copy-link.spec.ts tests/e2e/custom-emoji.spec.ts --project=smoke` — 19 passed - `pnpm exec playwright test tests/e2e/reaction-order.spec.ts --project=smoke` — 2 passed - pre-push: file-size-check, desktop-check, desktop-typecheck, desktop-test Vogue design review: **SHIP**. ## Screenshot Restored desktop message action rail: three learned quick reactions, Add reaction (`SmilePlus`), divider, Reply, Copy link, and More.  Signed-off-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz> Co-authored-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz> |
||
|
|
f177f4909d |
Use paired tags for standing & per-turn context (#6701)
## Why PR #6503 bundled a broader context compiler with several prompt-shape and telemetry changes, making it difficult to isolate whether paired XML-like top-level boundaries improve agent behavior. This keeps the follow-up experiment focused on framing only. ## What - Replace top-level standing and per-turn bracket headers with paired tags while preserving inner section content and event/message framing - Carry only metadata already present in dynamic headers as attributes, such as thread counts, truncation, event type, and batch count - Preserve every section body verbatim so the model executes the exact bytes reviewed in Desktop - Keep routing metadata, event content, batching, and delivery behavior unchanged - Teach Desktop diagnostics to parse the new framing while retaining compatibility with archived bracket-framed sessions; ambiguous paired boundaries render as the complete captured prompt ## Risk Assessment Medium — the change is limited to agent prompt framing and diagnostic parsing, but it affects both standing context and every dynamic agent turn. Bodies and delivery behavior remain unchanged, and compatibility parsing remains for prior sessions. ## References - Follow-up to #6503, scoped only to the paired-tag framing hypothesis ## Update — 2026-08-24 - Addressed review feedback by removing entity encoding/decoding from the execution and diagnostic paths. - Added regressions for literal closing tags, placeholders, entity-looking text, ampersands, and authored boundary whitespace. - Verified `cargo test -p buzz-acp` (805 unit + 9 integration), Desktop tests (5,412 before the main sync; 5,439 in the pre-push gate), Desktop typechecking, and Tauri clippy/tests. - Live local relay startup and message ingestion passed. The live ACP reply step is blocked locally because installed Goose 1.0.23 no longer provides the documented `goose acp` command and no alternate ACP adapter is installed. --- **Update Aug 25, 12:13 EDT:** Extended paired tags to per-turn top-level sections. - Converted context, conversation/thread context, event batches, and steer/interrupt boundaries without changing their inner bodies. - Preserved existing header metadata as attributes only; added no new prompt metadata. - Verified 806 Buzz ACP unit tests plus 9 integration tests, 5,441 Desktop tests, Desktop typechecking and formatting, and all differential pre-push lanes. --- **Update Aug 25, 13:58 EDT:** Addressed framing review feedback. - Updated operational base instructions to reference the emitted `<context>` section and added a regression guard against stale `[Context]` instructions. - Restored batched steer/interrupt cardinality in Desktop diagnostic titles with focused coverage for both paths. - Verified 807 Buzz ACP unit tests plus 9 integration tests, 5,442 Desktop tests, Desktop typechecking and formatting, and every differential pre-push lane including Tauri clippy/tests. --- **Update Aug 25, 15:37 EDT:** Hardened legacy transcript compatibility. - Semantic standing-context parsing now requires paired framing at the start and complete input consumption. - Historical bracket-framed personas keep literal balanced tag examples in their original section; malformed or mixed current framing displays as the complete captured prompt. - Added regressions for the reported legacy persona case and trailing unframed text; verified 60 focused parser tests, 5,444 full Desktop tests, formatting, and typechecking. --- **Update Aug 25, 16:31 EDT:** Preserved slash-command prompt block boundaries. - Desktop now sets aside a valid connector-facing slash-command block only when the following block begins recognized paired framing, parses the semantic blocks normally, and retains the command as visible Prompt metadata. - Added an end-to-end transcript regression using the production three-block ACP shape, including the user bubble, author pubkey, event ID, and grouped prompt context. - Verified 113 combined transcript tests, 5,445 full Desktop tests, formatting, typechecking, and all applicable pre-push lanes. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
583af02299 |
fix(cli): preserve signatures in event reads (#6884)
## Summary
- preserve string Nostr `sig` values in normal `messages get`, `messages
thread`, `messages search`, and `feed get` output
- omit absent or non-string signatures instead of fabricating empty
values
- keep compact output at exactly `{id, content, created_at}` and update
CLI/managed-agent contract docs
## Validation
- `cargo test -p buzz-cli --lib` (367 passed at final head
`a2498faa0e07f90efc0ab0165606ba3b048327c7`)
- release candidate live-relay matrix: 49/49 normal events independently
recomputed to their NIP-01 IDs and passed BIP-340 verification; all four
compact commands preserved exact three-key output and ordering
- `git diff origin/main...HEAD --check`
- independent adversarial review: clear, no findings
## Compatibility
The normal JSON change is additive for valid relay events. In-repo
consumers use tolerant field lookup. Compact output is unchanged and
locked by regressions for both message and feed formatting.
Generated by Carl, an automated contributor working with Wes.
---------
Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
|
||
|
|
ef0d202568 |
refactor(db): finish replaceable event store extraction (#6777)
## Why Finish the replaceable-event slice of [tracker #2](https://github.com/TheSentinel454/buzz/issues/2) and [domain issue #6](https://github.com/TheSentinel454/buzz/issues/6) without disturbing the runtime/store boundary established by #6660 and #6668. PR #6700 has merged; this PR now targets current main containing its replacement lock and transaction observability. ## What - Move `Db::replace_addressable_event`, its SQL, lock/transaction instrumentation, and focused addressable/parameterized replacement tests from `lib.rs` to `replaceable.rs` - Preserve the transaction-required parameterized API, replacement ordering, rollback semantics, mention indexing, and exactly one datastore span per public operation - Keep implementation and focused PostgreSQL tests co-located; follow-up review removed the dedicated replaceable ownership source guard as low value ## Stack - Exact base: main at |
||
|
|
dcee330b5c |
Fix Admin feedback filter overflow (#6825)
## Why Long community hostnames can make the native Community select exceed the Admin feedback filter grid, pushing the Received and Status controls outside the filter bar and creating horizontal overflow. ## What - Allow filter labels and selects to shrink within their grid tracks - Add a browser regression test covering an unusually long community hostname ## Risk Assessment Low — this is limited to Admin feedback filter sizing and preserves the existing desktop and mobile grid layouts. ## Before The Community select expands past the filter bar and pushes the Received and Status controls outside it.  ## After Long community names remain contained, keeping all four controls inside the filter bar.  ## References - Verified on Blox at the local checkout revision: Admin checks passed and all 9 Playwright tests passed Generated with Codex |
||
|
|
db514b153a |
fix(desktop): stop pulsing addressed agents on send (#6873)
## Summary - Stop replaying the selected-agent pulse when a remembered agent remains addressed across a send. - Preserve the existing pulse for genuine selections and shake feedback for failed sends. ### Related issue Reported in Buzz: buzz://message?channel=2745947d-f905-4f31-8a3f-fc4485d3c36c&id=0cffab376186be9cc4e7f29027352cc1c7941aead48b3fed948f36181f4a59af ### Testing - `cd desktop && pnpm test` (5,556 passed) - `cd desktop && pnpm typecheck` - `cd desktop && pnpm check` - `cd desktop && pnpm build:e2e` - `cd desktop && pnpm exec playwright test tests/e2e/persistent-agent-audience.spec.ts --grep 'always-mentioned agents remain selected|failed always-mentioned send'` (2 passed) This changes animation timing only; the regression is covered by Playwright assertions on the pulse and shake versions, while a static screenshot is unchanged. Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
4f529a45f2 |
fix(desktop): prioritize sidebar channel status (#6861)
## Summary - Treat the sidebar temp-channel icon as a fallback behind working, mute, numeric unread, and thread-unread indicators. - Keep the header affordance and the sidebar temp icon when it is the only row status. - Extend the ephemeral-channel browser scenario across working, idle, collapsed-sidebar, and unread states. ## Before / After | Before: five temp icons compete with duration and unread states | After: all five duration and unread states remain clear | | --- | --- | |  |  | ## Testing - `node --import ./test-loader.mjs --experimental-strip-types --test "src/**/*.test.mjs"` (5,556 passed) - `tsc --noEmit -p desktop/tsconfig.json` - `biome check desktop/src/features/sidebar/ui/SidebarSection.tsx desktop/tests/e2e/channels.spec.ts` - `vite build --mode e2e` - Isolated Playwright run of `create ephemeral stream shows sidebar and header affordances` (1 passed) ## Duplicate check No matching open PR or issue found for `ephemeral sidebar icon`. Signed-off-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz> Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz> |
||
|
|
e8172b5bac |
feat(desktop): hyperlink selected composer text on link paste (#6684)
## Summary Pasting a link while composer text is selected now turns that text into a hyperlink instead of replacing it — the behavior people expect from every other editor. - `resolveExactLinkPaste` recognizes a clipboard payload that is exactly one supported link (bare or `<…>`-wrapped `http(s)`, or a Buzz deep link, canonicalized through the existing composer-link attribute resolver) and returns the href to apply. - The paste handler applies that href as a `link` mark over the current selection, collapses the caret to the end of the newly linked range, clears stored marks so subsequent typing isn't linked, and refocuses the editor. Edge cases handled: - **No link mark available / selection can't carry marks** — falls through to the existing replace-with-node path rather than silently dropping the paste. - **Selection contains a composer message-link chip** — keeps the old replacement behavior instead of nesting a link over a chip. - **Whitespace in the clipboard payload** — `unwrapExactHttpLink` now rejects any text containing whitespace, so multi-line or "text plus URL" pastes are not treated as an exact link. - **Empty selection (caret paste)** — unchanged. - **Idempotent re-paste** of an already-wrapped link is consumed rather than doubling up. ## Testing `desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs` gains coverage for exact-link resolution (per-shape accept/reject cases, Buzz canonicalization) and for the paste handler: linking selected text, canonicalizing Buzz links over a selection, idempotent wrapped-link paste, fall-through when the selection can't carry marks, all-selection collapse, chip-containing selections, and preserved caret-paste behavior. Full pre-push gate ran green on this branch: `file-size-check`, `desktop-check`, `desktop-typecheck`, and `desktop-test` (5425 tests, 0 failures). ## Demo Pasting an `https://` URL over selected composer text turns the selected words into a hyperlink (instead of replacing them); text typed immediately after is not linked: https://github.com/user-attachments/assets/bbc93574-dd9d-4b1b-bad0-53307b6e887c --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> |
||
|
|
52621c09be |
chore(release): release Buzz Desktop version 0.5.20 (#6839)
## Buzz Desktop release v0.5.20 - **Frozen main:** `8471049c430073474939336dfc6aa98272bc8762` - **Reviewed candidate:** `95154bee4034ca7a40b33095c2ddbde8c9aa1614` - **Previous desktop release:** `desktop-v0.5.19` - **Proposed immutable tag:** `desktop-v0.5.20` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
8471049c43 |
feat(desktop): add KLIPY GIF search to composers (#5554)
## Summary - Supersedes #1913 with a KLIPY-hosted URL implementation. - Adds KLIPY GIF search and trending results to desktop message and forum composers. - Keeps selected GIFs hosted by KLIPY; Buzz stores only the external URL and media metadata (no imeta tag, since relays only accept hash-backed local `/media/` entries). - Aligns the Emoji/GIF picker with Buzz's standard segmented control, theme surfaces, and motion behavior. ## Relay-to-provider boundary - The relay proxies KLIPY search/share so the `BUZZ_KLIPY_API_KEY` never reaches the desktop; the key stays server-side behind a redacted `Debug` impl. - The dedicated GIF `reqwest` client sets `redirect::Policy::none()`. Because the API key rides in the request path, following a provider `3xx` could replay a key-bearing URL to an attacker-chosen host (an SSRF/key-disclosure primitive). With redirects disabled, a `3xx` returns as a non-success status that the handlers map to a generic `502`; the `Location` target is never read or forwarded. - Admission reuses the established NIP-98, membership, replay, and per-pubkey rate-limit gates, with an upstream response-size cap and allowlisting so KLIPY error bodies never cross the relay boundary. ## Accessibility - Under `prefers-reduced-motion: reduce`, the picker grid renders a static provider poster (a normalized `jpg` asset) instead of the animated preview, or a named static placeholder when no poster is available. It reacts to preference changes while mounted. `no-preference` keeps the animated preview. - Selected GIFs carry their title through `ImetaMedia`'s `displayLabel`, so the composer thumbnail, preview dialog, editor, lightbox, and remove control all derive one non-empty accessible name instead of an empty `Attachment ` label. Ordinary hashed uploads keep their existing hash-derived names. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> 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: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> |
||
|
|
e8cd7516e6 |
fix(desktop): respect automatic mention preference after send (#6837)
**Category:** fix **User Impact:** Disabling automatic agent mentions now keeps one-time agent mentions out of the next message draft. **Problem:** A successfully sent inline agent mention was treated as eligible for post-send restoration even when **Automatically mention agents** was disabled, so the agent immediately reappeared in the composer. **Solution:** Gate the send-success restoration path on the live preference while leaving explicitly pinned agents and enabled automatic mentions unchanged. <details> <summary>File changes</summary> **desktop/src/features/messages/ui/MessageComposer.tsx** Skips the automatic post-send mention restoration path when the preference is disabled. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Reproduces disabling the toggle before sending an inline agent mention and verifies the next composer is empty while the outgoing mention still reaches the agent. </details> ## Reproduction steps 1. Enable **Automatically mention agents** in the composer mention options. 2. Disable it again. 3. Compose and send `@Agent test` using the inline mention picker. 4. Confirm the message still mentions the agent, but the cleared composer does not repopulate `@Agent`. 5. Re-enable the preference and repeat; confirm the mention is restored for the next message. ## Testing At `a179a2d00a6ac5cf0bc4a1c7c1ef3d5dfa9d0875`: - Full desktop unit suite: 5,501 passed. - Desktop check and typecheck passed in the pre-push hooks. - Desktop file-size ratchet passed in the pre-push hooks. - Full `persistent-agent-audience.spec.ts` Playwright file: 17 passed, including preference off (empty composer), preference on (restored mention), and the existing address-undo journey. ## Visual validation | Automatic mentions off | Automatic mentions on | | --- | --- | | After sending a one-time mention, the outgoing message is preserved and the next composer stays empty. | After sending, the agent remains addressed and is restored in the next composer. | |  |  | <details> <summary>Original regression</summary> With automatic mentions disabled, sending `@Alia test` still repopulated `@Alia` in the cleared composer.  </details> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> |
||
|
|
820a858997 |
fix(release): attribute desktop candidates to the operator (#6831)
The desktop release tooling hardcoded one contributor's personal identity into every release candidate commit. `scripts/prepare-desktop-release.sh` committed the candidate with a `git -c user.name='Wes' -c user.email='wesbillman@users.noreply.github.com'` override, and `scripts/desktop_release.py` `validate` required the candidate author to be exactly `Wes <wesbillman@users.noreply.github.com>` plus a matching `Signed-off-by` trailer. That leaked from Wes's working setup into the validation contract in #3568, so a release cut by any other operator was falsely attributed to and signed off by Wes (as happened on #6828). ## Change - `prepare-desktop-release.sh`: drop the `-c` identity overrides so `git commit -s` uses the operator's own configured identity to author and sign off the candidate. The automation `Co-authored-by` trailer is unchanged. - `desktop_release.py` `validate`: replace the exact-Wes checks with structural ones — the commit author must be non-empty, the body must contain a `Signed-off-by` trailer whose name and email match the commit author (honest DCO), and the existing automation `Co-authored-by` regex check stays. Failure messages remain specific. - `test-desktop-release-candidate.sh`: the fixture candidate now commits under the harness's own identity, and a new negative case rewrites the author to a mismatched identity and asserts the validator rejects it. Release authorization is bound to the merged PR via the GitHub API in `scripts/verify-desktop-release-merge.sh`, never the commit author field, so this does not weaken the trust model. `RELEASING.md` and `.github/workflows/desktop-release-candidate.yml` reference no author identity and need no change. Verified locally: `scripts/test-desktop-release-candidate.sh` passes, including the new sign-off/author-mismatch rejection. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
cae7f826b3 |
fix(ci): check out source in docker.yml merge job (#6833)
## Problem Every `docker.yml` run on `main` has failed since [#6781](https://github.com/block/buzz/pull/6781) merged. That PR added a "Create deployment eligibility predicate" step to the `merge` job, which runs `jq` against `$GITHUB_WORKSPACE/scripts/create-deployment-eligibility-predicate.jq`. But the `merge` job has no `actions/checkout` step — the workspace is empty, so `jq` can't open the file and exits `2` (`jq: Could not open ... No such file or directory`). The `build` and `qualify` jobs each check out the source; `merge` never needed one until this step was added. ## Fix - `.github/workflows/docker.yml`: add `actions/checkout` (same pinned SHA as the other jobs, `df4cb1c` / v6.0.3) as the first step of the `merge` job. - `scripts/test-relay-image-eligibility-workflow.sh`: guard the regression by asserting the `merge` job checks out the source before building the predicate. ## Verification CI cannot exercise the `merge` job on a pull request — the job is gated `if: github.event_name != 'pull_request'`, so it only runs on push to `main`. The proof is the root cause (missing checkout for a step that reads a repo file) plus the eligibility workflow test: `scripts/test-relay-image-eligibility-workflow.sh` passes with the fix and fails with the new guard's specific message (`merge job must check out the source before building the eligibility predicate`) when the checkout is removed. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
7a1b7d8e09 |
chore(release): release Buzz Desktop version 0.5.19 (#6828)
## Buzz Desktop release v0.5.19 - **Frozen main:** `ee6ca5fa28bce04dfecb6717de65b08a57f2ac47` - **Reviewed candidate:** `ab691bcdaeccaa6698a2199beaba3f6e93daae81` - **Previous desktop release:** `desktop-v0.5.18` - **Proposed immutable tag:** `desktop-v0.5.19` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
ee6ca5fa28 |
Remove public relay signing key fallback (#6729)
Require `BUZZ_RELAY_PRIVATE_KEY` on every relay startup and remove the shared public fallback key completely. For local development, `just bootstrap` now generates a random key once in the gitignored `.env` file. Re-running bootstrap preserves that key, so relay-authored events retain the same identity across restarts. The local relay recipes reload `.env`, while isolated CI and E2E launchers pass explicit per-run keys. Startup fails immediately when the key is missing or invalid, before connecting to Postgres or Redis. Deployed relays continue to receive their stable key from the existing chart-managed secret. Validated with: - `./scripts/test-ensure-local-relay-key.sh` - `cargo test -p buzz-relay --bin buzz-relay` - `cargo clippy -p buzz-relay --bin buzz-relay -- -D warnings` - `cargo fmt --all -- --check` - `shellcheck scripts/ensure-local-relay-key.sh scripts/test-ensure-local-relay-key.sh` Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
22f32c99e2 |
docs(nest): make commit attribution policy-neutral (#6707)
## Summary - replace the generated Nest's unconditional human author/sign-off rules with portable guidance that separates authorship, material co-authorship, DCO certification, and cryptographic signing - defer attribution to repository-local policy, forbid inferred or guessed identities, and require inspection of every outgoing commit - bump the Nest template version so existing installations refresh, with regression coverage for fresh generation and upgrade preservation ### Related issue None found. Related runtime identity work exists in #6177, but this PR is intentionally limited to the generated Nest guidance and its refresh behavior. ### Testing - `bin/just desktop-tauri-clippy` - `bin/just desktop-tauri-test` - `bin/just file-size-check` - `git diff --check` - pre-push hooks: `push-head-scope`, `branch-skew`, `file-size-check`, and `desktop-tauri-checks` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> |