mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
5142cf6b2a0f44686eeda1ed07905ee3560fd78e
2866 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5142cf6b2a |
test(desktop): move CSP guards from Rust to the desktop JS suite (#8172)
## Summary Moves the packaged-app CSP guards from `desktop/src-tauri/tests/csp.rs` to `desktop/src/app/contentSecurityPolicy.test.mjs`. No production code changes beyond one added `export`. `csp.rs` never touched Rust app code. All seven tests parse `tauri.conf.json` and compare CSP strings. The MediaPipe check also `include_str!`'d `desktop/src/features/profile/lib/animatedAvatarCapture.ts` and pulled the `MEDIAPIPE_WASM_BASE` literal out by splitting the text. That made a Tauri test depend on a frontend file. With path-filtered CI (#8156), a change to only that `.ts` file would skip the check. The JS version imports `MEDIAPIPE_WASM_BASE` directly (it's now exported) and reads `src-tauri/tauri.conf.json` from disk. The `desktop/**` CI filter covers both files, so the check runs in Desktop JS whenever either one changes, and the filter needs no cross-boundary special case. The seven tests and their assertions carry over one-for-one, along with their rationale comments. **What got simpler:** we delete a 204-line Rust integration test, the `include_str!` of a frontend file, and the hand-rolled string-literal scraper, and the CI path filter doesn't need to know about this file. ### Related issue Addresses the CSP coupling raised in review of #8156. Related: #4614 (introduced the guards). ### Testing - New suite on Linux: 7/7 pass. - I falsified each test by mutating the real policy or code, one mutation at a time. Each mutation fails exactly the matching test, and the suite passes again once restored: - moved `MEDIAPIPE_WASM_BASE` outside `@mediapipe/` - dropped `'wasm-unsafe-eval'` - dropped the MediaPipe scope from `script-src` - added a bare `https://cdn.jsdelivr.net/` origin - added `'unsafe-inline'` - removed `img-src` - dropped `ws:` from `connect-src` - made the pinned-source predicate always pass - Blox `just ci` at tree `5664c6ce` (same tree as head `f798f94b`): - Passed: fmt, clippy, desktop/Tauri/web checks, security-review/file-size checks, `desktop-test` (6,819 + 139 jsdom), `desktop-build`, `desktop-tauri-check`, `desktop-tauri-test`, `web-build`. - `test-unit`: 94/95 suites pass. The two failing `buzz-acp` `git_bootstrap` tests fail because the workstation's Git 2.43 predates the credential `authtype` protocol. They pass 4/4 with Git 2.51, and this PR touches no `crates/`. - Not run: `mobile-check`/`mobile-test`, because the workstation can't reach pub.dev. No `mobile/` changes. - Hosted CI green: https://github.com/block/buzz/actions/runs/37801901659. This PR selects the Tauri lanes because it deletes `src-tauri/tests/csp.rs`. Later edits that only touch the constant select Desktop JS only. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
99924a5a69 |
fix(desktop): repair the Windows notification shim's false denial on read (#5600)
Addresses #2445. Closely related to #2483 -- see "Relationship to #2483" below; I am happy to close this in favour of that one. ## The problem On Windows the notification plugin's injected shim decides its permission at startup like this ([`guest-js/init.ts:12-18`](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/notification/guest-js/init.ts)): ```ts if (window.Notification.permission !== 'default' || __TEMPLATE_windows__) { return await Promise.resolve(window.Notification.permission === 'granted') } return await invoke('plugin:notification|is_permission_granted') ``` `__TEMPLATE_windows__` is replaced with the literal `true` on Windows, so the `||` short-circuits and the shim compares its own freshly-initialised `'default'` against `'granted'` -- `false` -- and stamps **`denied`**. The line that would ask the backend is unreachable, and the backend would have said yes: `permission_state()` returns `PermissionState::Granted` unconditionally on desktop. So on every launch of the Windows app, `window.Notification.permission` is `denied` and nothing in the OS is denying anything. ## Why the toggle is only half of it `getDesktopNotificationPermissionState()` trusts that value, so `refreshPermission()` reports `denied` at mount, and this effect in `hooks.ts` runs before the user touches anything: ```ts React.useEffect(() => { if (settings.desktopEnabled && (permission === "denied" || permission === "unsupported")) { setSettings((current) => ({ ...current, desktopEnabled: false })); } }, [permission, settings.desktopEnabled]); ``` It writes `desktopEnabled: false` and persists it. Measured on a real install: with alerts successfully enabled and `desktopEnabled` persisted as `true`, a clean relaunch comes back with it persisted as `false`. That means fixing only the enable path leaves a Windows user re-enabling alerts after every restart. ## What this changes One place: `getDesktopNotificationPermissionState()` repairs the bogus value instead of reporting it. ```ts if ( window.Notification.permission === "denied" && isTauri() && isWindowsPlatform() ) { try { return await requestDesktopNotificationAccess(); } catch { return window.Notification.permission; } } ``` `requestPermission()` is the only call that reaches the backend, and on Windows it raises no prompt, returns `Granted`, and rewrites the shim's cached value -- so this is self-limiting: the next read short-circuits as `granted` and never requests again. There is a test for exactly that. Repairing at the read rather than at the toggle fixes every consumer at once, including `sendDesktopNotification()`, which gates delivery on `=== "granted"` and would otherwise drop notifications at boot. - `denied` stays terminal on macOS, on Linux, and on ordinary Windows web pages. - A denial that survives the request is still reported as `denied`. - A rejected request falls back to the current value instead of propagating; callers do not handle rejections today. - Adds `isWindowsPlatform()` to `shared/lib/platform.ts`, alongside the existing `isMacPlatform()` / `isLinuxPlatform()`. - No new dependencies. No changes to `hooks.ts` or to notification delivery. ## Verification on a real Windows 11 install Buzz Desktop 0.5.9 release build, Windows 11 Home 10.0.26200, WebView2 151.0.4129.72, attached over CDP. Clean boot, nothing injected: ``` window.Notification.permission -> "denied" localStorage[...].desktopEnabled -> false (written by the effect above) await window.Notification.requestPermission() -> "granted" window.Notification.permission -> "granted" new window.Notification("Buzz", { body: ... }) ToastNotificationManager::History .GetHistory("xyz.block.buzz.app") -> 3 -> 4 (real toast delivered) CreateToastNotifier(...).Setting -> Enabled ``` The OS was permissive throughout: no `ToastEnabled` override, no notification policy keys under HKCU or HKLM, and the app present under `HKCU\...\Notifications\Settings\xyz.block.buzz.app`. Full detail in [this comment](https://github.com/block/buzz/pull/2483#issuecomment-5255972424). To be precise about scope: this confirms the mechanism the patch relies on, measured on a stock release build. The patched build itself is covered by the unit tests below, not by a packaged Windows run. ## Tests `pnpm test` -- full desktop suite. New cases in `lib/desktop.test.mjs`: - Windows Tauri repairs the shim's false denied state on read - the repaired state is cached, so later reads do not request again - a denial that survives the request is reported as denied - denied stays terminal outside the Windows Tauri app (Windows web, Linux Tauri, Linux web) - granted is returned untouched and never triggers a request Plus `shared/lib/platform.test.mjs` for `isWindowsPlatform()` across `Win32` / `Win64` / `Windows` and `MacIntel` / `Darwin` / `Linux x86_64`. ## Relationship to #2483 #2483 fixes the same root cause at the toggle: it retries the request inside `setDesktopEnabled`. That makes the toggle work, which is what #2445 reports, and its `ensureDesktopNotificationPermission` helper is a nicer shape than an inline condition. The difference is where the repair happens. Because #2483 leaves the mount-time read as `denied`, the effect quoted above still turns the setting off on every launch, so the fix does not survive a restart. Repairing at the read covers the toggle path too, since `setDesktopEnabled` calls `refreshPermission()` first and finds `granted`. The two overlap: both add `isWindowsPlatform()` to `platform.ts`, so whichever lands second needs a trivial rebase. If maintainers prefer #2483's shape, the equivalent of this PR is to call its helper from `refreshPermission` as well as from `setDesktopEnabled` -- I am glad to close this and send that instead. I did not want to push changes into someone else's open PR uninvited. ## Once this lands #2445 can close. I am running a per-machine launcher workaround for a small Windows team until then, which this makes unnecessary. Signed-off-by: VSCteam <144167621+Joxyko@users.noreply.github.com> |
||
|
|
394d4a434b |
fix(mobile): normalize relay URL before disabling push (#8201)
Communities joined with a `ws://` or `wss://` relay URL cannot disable push: the opt-out path passes that URL directly to the HTTP-only NIP-11 fetcher, fails before publishing the inactive lease, and leaves the durable retry pending. This was reproduced in USB logs from TestFlight 0.20.1 (49). Normalize the URL with the existing push relay HTTP helper before fetching metadata, matching the revocation outbox's handling. No settings UI changes. Related: #8199 removes the status text but does not fix this synchronization failure; no duplicate fix found. ### Validation Four regression cases exercise the real community deactivator through its HTTP boundary. The WebSocket cases fail with the device's exact exception before the fix; all four pass afterward. All 37 focused community/revocation tests and targeted Flutter analysis pass. An initial full mobile run had three failures (recount timing and native export timeout/order); all three passed in isolated reruns. The full mobile checks and test suite passed on retry. The local repository-wide gate stopped during Rust compilation without a final result; a full local pass is not claimed. [GitHub CI](https://github.com/block/buzz/actions/runs/37865048873) passed, including mobile tests, Android build, Swift tests, iOS simulator build, and unsigned iOS release build. [Codex review](https://github.com/block/buzz/pull/8201#issuecomment-6071795750) reported no major issues. The installed TestFlight build is unchanged. After this fix lands, validate that upgrading the same installation clears its existing pending opt-out and leaves the relay lease inactive, without toggling notifications back on. Signed-off-by: Tom Brow <tomb@block.xyz>mobile-v0.22.0-rc.1 |
||
|
|
16eb0b6685 |
feat(buzz-db): run push enqueue and channel TTL refresh in app code (#8135)
## Summary Moves the producer side of the two event follow-up triggers into application code. The triggers stay in place, so both producers run until a later change drops the triggers after this ships to every pod. Builds on #8130 (`AdmittedTx`). Refs BUZZ-176. ### Push match enqueue New `store::event_follow_up::enqueue_push_match` replaces the `events_enqueue_push_match` trigger (`schema/functions/enqueue_push_match_job.sql`, migration 0023) and matches it exactly: - Same kind allowlist (9, 40002, 45001, 45003), now `PUSH_MATCH_KINDS`. - Takes the same SHARED `buzz_push_gate:<community>` advisory lock as its own statement, so under READ COMMITTED the lease check gets a snapshot taken after the lock is granted. Lease activations still take it EXCLUSIVE in `push.rs`. - Same lease predicate (`active AND endpoint_enabled AND expires_at > now`), then `INSERT push_match_queue ... ON CONFLICT DO NOTHING`. - Fails closed: any error rejects the event, as the trigger did. It runs right after the INSERT, only when the insert affected a row. The consumer side (claims, `lease_until`, attempts, reaper) is already app-owned and is unchanged. ### Channel TTL refresh `AdmittedTx` now records every channel that received an event (except kind 9007 and channel-less events). `AdmittedTx::commit()` refreshes their `ttl_deadline` as the last statement before COMMIT, which keeps the commit-time behavior of the deferred `events_refresh_channel_ttl` trigger (migration 0024): - One savepoint per channel, in sorted order, under the same SHARED `buzz_channel_ttl:<community>:<channel>` lock. - Runs the same `UPDATE` with the same `archived_at`/`deleted_at` filters as its own statement after the lock, so its snapshot follows the lock grant. Its `ttl_seconds IS NOT NULL` qual filters a permanent channel before any tuple lock, so a permanent channel's row is never locked. An ephemeral channel's row lock is held through the remaining refreshes and the COMMIT round trip (the trigger held it only inside COMMIT). - Lock keys come from one builder per protocol (`channel::channel_ttl_lock_key`, `push::push_gate_lock_key`), shared by the SHARED and EXCLUSIVE sides. The shared push-gate wait is observed through `observe_advisory_lock(LockType::PushGate, …)`. - On failure it rolls back the savepoint, logs a warning, and increments `buzz_db_channel_ttl_refresh_failures_total{reason}`, where `reason` is `lock_timeout` or `error`. The event still commits, matching the trigger's `EXCEPTION WHEN OTHERS`. A cancelled statement (`57014`) still rejects the transaction, because plpgsql never caught that either. - While both producers run, an app refresh that hits `lock_timeout` is counted, and then the trigger retries it inside COMMIT and may succeed. So during the dual window the failure counter can over-report and the worst-case wait doubles. Both go away when the retirement PR drops the triggers, after which the counter is exact. ### Call sites Every production `INSERT INTO events` calls `event_follow_up::after_admitted_insert`: - `insert_event_on` - `insert_event_with_thread_metadata_tx` - `LockedMemberSnapshot::replace_member_event` - `replace_addressable_event` - `publish_nip43_membership_locked` - `accept_lease_event` (kind 30350, a no-op today, kept so the policy has no exceptions) `replace_parameterized_event_in_transaction_impl` inserts inside the `parameterized_replace` savepoint. It enqueues the push job inside the savepoint, so a rollback to that savepoint also drops the job, and records the TTL channel only after the savepoint is released. The two rolled-back verification probes (`verify_floor_guard_behavior`, `verify_channel_roster_fence_behavior`) are named exceptions, because they never commit. ### Source policy `tests/observability_source.rs` adds `every_production_event_insert_runs_the_follow_up_hook`. It scans every `crates/*/src` and fails on any function that inserts into `events` without the hook. Matching is normalized (letter case, whitespace, `\` continuations, `\n`/`\t`/`\r` escapes, optional `public.`). The file is masked once first: comments (`//`, nested `/* */`) and string, raw-string (including `br"`/`cr"`), and char literals become spaces. `#[cfg(test)]` markers, `fn` signatures and the hook are all found in that masked view, so a marker in a comment (even a multiline block comment) strips nothing, an `fn` line inside a multiline string doesn't split a function, and a commented-out or quoted hook doesn't count. SQL is still matched in the original text. A table test pins the masker's output for each literal form. A fixture test pins what the scan rejects (no hook, push without TTL, a hook in a line comment, block comment, or string, lowercase or line-broken or escaped SQL, writers after test-only items or under comment mentions) and what it ignores (writers in `#[cfg(test)] mod`). A floor of seven hooked writers catches a scan that silently stops seeing writers. ### Tests `src/store/event_follow_up_postgres_tests.rs` is the contract suite from the BUZZ-176 discovery work, ported to the `AdmittedTx` API. It is a crate-internal `#[cfg(test)]` module because it runs `SET LOCAL` timeouts and in-transaction reads through the crate-private `AdmittedTx::conn()`. It runs on both the desired and migration schemas, in two arms: - **dual**: production during the migration window, with the triggers and the app code both running. - **app-only**: drops both triggers in the disposable database (and asserts no partition clones remain), the way the retirement migration will. The trigger-only baseline is the base branch, where the same suite passed before this change. The suite also pins the two branches that differ from "swallow everything": a `lock_timeout` failure increments `buzz_db_channel_ttl_refresh_failures_total{reason="lock_timeout"}` exactly once, and a statement cancel (`57014`) during the refresh rejects the event with no row, job, or deadline change. ## What got simpler Nothing yet. This adds a parallel producer during the migration window. It is what lets the follow-up PR delete two plpgsql functions, two triggers, and the "keep in sync with migrations/00xx" duplication between `schema/functions/` and `push.rs`. ## Verification Rebased onto `main` @ `745501962`. All runs used a Linux cloud workstation with the pinned toolchain (rustc/clippy 1.95.0, hermit), and the tree was clean before and after each run. The final head, `c5a06a82`, changes only `crates/buzz-db/tests/observability_source.rs` compared with `1eb11bde` (via `fbc9a598`, `7c0baa41`, `9199992c` and `d405b2f6`). Production source is identical between the two. | Check | SHA | Result | |---|---|---| | `cargo fmt --check`, `cargo clippy -p buzz-db --all-targets -D warnings` | `fbc9a598` | pass | | `cargo clippy` (workspace) | `1eb11bde` | pass | | source policy (`--test observability_source`), fmt, buzz-db clippy | `c5a06a82` | 17/17, pass, pass | | `cargo test --locked -p buzz-db` (non-PG) | `c5a06a82` | 148 lib + 17 policy + 5 doctests | | source policy (`--test observability_source`) | `fbc9a598` | 16/16 | | `cargo test --locked -p buzz-db` (non-PG) | `fbc9a598` | 148 lib + 16 policy + 5 doctests | | contract suite (`store::event_follow_up_postgres_tests`, desired/migration × dual/app-only) | `1eb11bde` | 4/4 | | buzz-db PG full | `1eb11bde` | 464/464 | | buzz-relay PG | `1eb11bde` | 378/378 | | GitHub CI | `fbc9a598` | 53 pass, 0 fail (Codex security review pending) | **Falsifications** (each mutation made the named test fail, then was reverted): - dropping the `relay_members` hook → policy names `publish_nip43_membership_locked` - no-op push producer → app-only arms fail, dual arms pass (trigger covers) - skipping the TTL refresh → app-only fails on the deadline; dual fails on the failure-metric assertion - swallowing `57014` → all four arms fail - dropping the metric increment → all four arms fail - unanchored `#[cfg(test)]` match, or no escape handling → fixture test fails - a `#[cfg(test)]` constructor that skips admission, added inside `impl AdmittedTx` (indented or as a column-0 `mod`) → `admitted_tx_is_constructed_only_by_admitting_constructors` fails; putting the strip back into the constructor scan → its fixture test fails - at `7c0baa41`: `accept_lease_event`'s hook commented out and the real call made through an alias → policy names `accept_lease_event` (the same mutation passes under the `fbc9a598` policy); hook matching on unmasked text → the comment/string fixtures fail - at `9199992c`/`d405b2f6`: no nested block-comment depth, no string-escape skip, no `br"`/`cr"` raw-literal prefix, or no raw-string branch at all in the masker → the follow-up fixture test fails - at `c5a06a82`: finding `#[cfg(test)]` in the unmasked text → `writer_after_block_commented_cfg_test` is no longer reported; finding `fn` signatures in the unmasked text → `writer_after_raw_string_example` is no longer reported; no `cr` raw prefix → the masker table test fails on `cr"` - unhooked writers appended to `side_effects.rs` (after test-only items, under a doc-comment mention, with an escaped newline) → policy names each **Live relay E2E**: an independent reviewer ran this at `1eb11bde` against a real relay and CLI, with PostgreSQL 17 and Redis 7, the follow-up triggers absent, and no partition clones. - A kind 9 on a TTL channel advanced the shortened deadline by about 580 s. On a permanent channel, the deadline stayed NULL. - Each accepted event produced exactly one pending `push_match_queue` row with the same event id. - An exact signed replay was a no-op. - While the exclusive TTL lock was held, the event and its job stayed invisible and the deadline didn't change. After release, both persisted once. - Roster create/join/add/remove kept SQL and wire state in step (1→2→3→2), with one current 39002 head. - NIP-33 replace, duplicate, stale, failure and retry cases all passed. Limits: the push lease was a SQL fixture, and push consumers were disabled. Kind 30023 is global, so channel-scoped TTL on the parameterized path is covered only by the PG contracts. No live relay rerun was done at `fbc9a598`, because that delta is test-only. **Latency** (measured at pre-rebase `f8380927`, same production delta; debug build on loopback, 1,000 samples per cell, EVENT→OK): head app-only p50/p99 3.80/4.58 ms (permanent) and 3.87/4.64 ms (TTL), against base trigger-only 3.49–3.53/4.00–4.03 ms and 3.56–3.62/4.30–4.46 ms. About +0.3 ms p50, from the extra client round trips (lock, UPDATE, savepoints, push lock/insert) that the triggers ran server-side. One debug loopback run, not a production p99. Not measured: contention from many writers on one hot TTL channel, where each writer now waits on the channel row lock for about two more client round trips (RELEASE SAVEPOINT and COMMIT) than with the trigger. In review, a burst of 8 concurrent posts to one TTL channel committed with no deadlock or lock timeout. Watch send p99 and `pg_locks` waits on `channels` during rollout. **Known pre-existing, not touched here:** `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` is flaky when Redis is present. It returns a 504 in 4 of 5 runs at base `745501962` and 2 of 5 at `fbc9a598`, always at the same assertion. No CI lane selects it. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
b6d97a69f0 |
fix(mobile): keep thread content visible while positioning replies (#8134)
Opening a mobile thread could blank the original message while the app positioned replies. This affected empty and populated threads, including links to a specific reply. Keep one original-message widget mounted and usable throughout positioning. It follows a measured placeholder in the reply list, which becomes visible only at its final position. This preserves media and reaction ownership, supports scrolling through tall originals, and excludes the head from input and accessibility when it leaves the viewport. ### Validation - Added production-path frame regressions for empty, short, long, and deep-linked threads; two video lifecycle cases verify exactly one initialization and disposal, including completion after navigation away. - Added coverage for dragging, mouse-wheel and accessibility scrolling, removal of offscreen and app-bar-covered head semantics, and scrolling to the interactive bottom of a 70-line original message. Removing the accessibility exclusion or restoring the raw viewport-top boundary fails both long-thread regressions; restoring the duplicated head fails the lifecycle test. - Mobile dependency setup, analysis, the full mobile test suite, and unconfigured-push tests passed. The native iOS simulator debug build passed after integrating main, including the conflict resolution for the new Buzz icon API. The current tree passed 3,076 mobile tests (four skipped) and three unconfigured-push tests, and the updated native simulator build passed. - Exercised empty, three-reply, forty-reply, and older-reply deep-link flows on an iPhone 17 Pro simulator against an isolated local relay with a disposable identity. Confirmed correct head accessibility coordinates and absence when offscreen. The local posterless-video fixture rendered its attachment fallback; native decoding success is not claimed. Resource ownership is covered through the production preview-loader seam. [Updated simulator captures](https://github.com/block/buzz/pull/8134#issuecomment-6047098230). - Added delayed-hydration regressions for mouse-wheel and explicit accessibility scrolling through the persistent head. Both preserve a visible reading anchor across hydration and viewport resizing; removing the user-scroll lifecycle callbacks makes both fail. Repeated empty, populated, and older-reply deep-link opening with the updated native build against the retained isolated relay. - Entered and sent a reply from the disposable simulator identity against the isolated local relay. The app displayed the reply and cleared the composer, and the relay database confirmed persistence. Software keyboard open/close remains unverified because the simulator did not display it. - Agent review feedback was addressed. Personal mobile testing is not applicable under the author's standing exception. Post-merge validation: repeat empty, populated, and deep-linked thread opening in the integrated mobile candidate, including keyboard open/close and sending a reply before release. buzz-review-completed --------- Signed-off-by: Tom Brow <tomb@block.xyz>mobile-v0.21.0-rc.2 |
||
|
|
e9269cbdf6 |
fix(mobile): simplify notification settings (#8199)
Keep notification settings as a separate card with a single “Notifications” toggle. Remove the section heading, bell, all subtitles (including permission/capability and pending opt-out messages), and the iOS settings button. Toggle callbacks, capability visibility, persisted opt-in/out, and relay synchronization are unchanged. The disable failure is being investigated separately. This intentionally removes the pending warning rather than claiming to fix synchronization. Tom explicitly requested removal of the immediate opt-out synchronization warning and all other status text; this is an intentional exception to the pending-feedback guidance in VISION_MOBILE.md. [Codex’s finding is answered against that agreed scope](https://github.com/block/buzz/pull/8199#discussion_r4225426842). No new tests. Delete tests for the removed permission/settings UI and remove obsolete display assertions from existing recovery tests while preserving their opt-out synchronization coverage. Closest related issue: [#7826](https://github.com/block/buzz/issues/7826). No duplicate cleanup PR found. ### Validation Mobile formatting/static analysis passed. The full existing mobile suite passed (3,216 tests; 4 skipped), plus the three unconfigured-build tests and gateway recipe checks. Existing opt-out recovery tests exercised the switch, persisted tombstone, snapshot synchronization, and successful relay retry. Before/after renders use the existing Flutter widget fixture with synthetic data and mocked native navigation. No simulator or TestFlight run was performed. After merge, validate the settings row and on/off flow in the next iOS candidate; remote disable failure remains a separate investigation. GitHub CI also passed the mobile tests, unconfigured-build tests, and gateway recipe checks. The [security review](https://github.com/block/buzz/actions/runs/37863885299) completed successfully. The [Android build](https://github.com/block/buzz/actions/runs/37863554077/job/113605025305) passed. The iOS simulator build, native accessibility checks, and unsigned iOS release build passed. [All required CI checks](https://github.com/block/buzz/actions/runs/37863554077) are green. ### Before and after Headless Flutter widget fixture, with synthetic data and mocked native navigation. | Before | After | | --- | --- | |  |  | Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
4db7bb0e7f |
fix(push): stop backfilling messages on lease activation (#8178)
Remove the 120-second message scan from push lease activation and renewal. Subsequent eligible messages continue through the existing database trigger. The lease protocol, schema, trigger, and advisory-lock order remain unchanged. Related enqueue-gating work: #2145. No duplicate open PR or issue found. ### Validation - All 18 PostgreSQL push persistence tests pass against isolated desired-state databases after review changes. - The existing concurrency test verifies activation waits for an in-flight insert and a subsequent message queues once. The retention fixture activates its lease before inserting its message. - Formatting and PostgreSQL test discovery pass. The earlier full local run passed repository lint/static checks, native desktop pre-push checks, and every non-ACP integration step. - Broad local checks hit unrelated ACP idle/keepalive timing failures. A serial retry passed two affected tests but still failed the keepalive deadline test. The pre-push hook was skipped with author authorization after its other checks passed. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
acf1ede202 |
docs(nip-ap): add teams and kind:44300 instructions versions (#8191)
🤖 This PR adds two things to NIP-AP: a Teams section, and `kind:44300` instructions versions, an owner-private history of each agent's and each team's instructions. Today instructions are edited in place, so there's no way to see past edits, tie a behavior change to an edit, or roll back. **Teams.** A team is identified by owner, community, and team id, using the same id grammar as the `kind:30178` `d` tag. The section defines what membership means (scoped by owner and community, not channel membership, follows the team's current instructions), how team instructions combine with an agent's own at launch, and the existing rule that an agent in several teams can't start if their instructions differ. How a client stores its private team records stays client-defined, so this doesn't standardize discovering another client's private teams. **`kind:44300`.** A regular event, so every save is a new event and a subject's events are its history. Each event names exactly one subject: an agent (`p`) or a team (`t`). Content is NIP-44 encrypted to the owner as `v1:` plus the raw text, which keeps empty text representable and caps `content` at 218,548 bytes for the 128 KiB text limit. Current is the newest version, and history pages with the `until` + `before_id` cursor so versions saved in the same second are never skipped. Restore and Reset to template append a new version. Launch order for an agent's own instructions is its current version, then the linked `30175` prompt, then a definition-less `30177`. The relay keeps it author-only on every read surface, excludes it from search, and applies the subject filter before `LIMIT`. Deletion requests for these versions must carry `["k", "44300"]` and get the same author-only treatment, since they reveal version ids and deletion times. A `30178` share is a snapshot: saving a version doesn't change an existing share. **Changes to existing spec text:** - Removed the sentence saying `kind:30176`'s body is untouched "so device sync keeps its contract". The current desktop client doesn't sync teams through `30176`. - Reworded the `30178` `d` tag rationale so it no longer refers to a `30176` head. - The "30175 beats 30177" rule now says a current `44300` version beats both for an agent's instructions. - Replaced the stale search bullet. It claimed no shared-gated kind is in an FTS allowlist; search indexes those kinds, and each result passes the per-event visibility check before delivery. Relay and desktop implementation follow in separate PRs once this is approved. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> Co-authored-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> |
||
|
|
8f66f7051c |
feat: raise hosted community limit to fifty (#8186)
## Summary - Raise the stock relay active ownership quota from 5 to 50 for creation and ownership transfer. Raise the absolute lifetime quota from 20 to 50 reserved hosts; completed owner deletions still permanently consume lifetime capacity to bound create/delete host squatting. - Honor the owner-list `can_create` projection across Desktop settings, create flow, and onboarding instead of imposing a client-side count. Display server `quota_used` / `quota_limit` when available; when the projection is missing, let the server's `limit_reached` decide. - Cover 49→50 and 50→51 admissions, deleted-owner lifetime exhaustion, and rendered create gates under 5/50, 0/50 exhausted, and missing projections. ## Verification - Desktop full `pnpm test` and `pnpm typecheck` pass after the 50-cap update; `cargo fmt --all -- --check` passes. Four targeted Playwright cases and `pnpm build:e2e` passed for the original dynamic-quota change at the previous commit, before changing test fixtures from 20 to 50. - `cargo test -p buzz-db --lib` cannot compile here: macOS dyld rejects the `sqlx_macros` dylib with `mis-aligned LINKEDIT string pool` on this host before the crate's tests run. CI must validate the DB package. ## Rollout / manual validation - Confirm `BUZZ_MAX_COMMUNITIES_PER_OWNER` is unset or 50 on **every** production relay replica; this per-process cached override takes precedence over the stock default. - Confirm Builderlab's list/create/transfer proxy passes the relay's `quota_used`, `quota_limit`, `can_create`, and `limit_reached` without its own 5-community gate. Its backing implementation is not in this repo and was not verified here. - Human test: with an owner at 49 live communities, create the fiftieth, verify the fifty-first returns `limit_reached`; transfer into an owner at 49 then at 50 and confirm the same boundary. Check a deleted-owner account at 0 active/50 lifetime displays the generic limit and disables creation. This manual check remains outstanding. --------- Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
a8fe2b612c |
feat(db): advance an empty partition catch-all under bounded locks (#7993)
## Summary Adds bounded, serialized advancement of an empty right-edge partition catch-all for `events` and `delivery_log`. The repaired catalogs currently end in an empty catch-all from 2027-01-01, and the merged manager (#6515) only audits them. Without this change, January writes land in the catch-all, after which only an operator can move them. When `BUZZ_PARTITION_MANAGER_ADVANCE_ENABLED=true`, each relay keeps six dedicated monthlies after the current month. It does this at startup and on every audit interval by replacing the empty catch-all with canonical monthlies and a new canonical `<table>_p_future` after the horizon. From today's catalogs the first run creates January–March 2027 and moves the catch-all to April 2027. ## Safety design - **Refuse before locking.** The following return `operator_required` without taking any table lock: - a populated catch-all, or one whose occupancy is unknown; - a catch-all with child-only triggers, which the replacements would not inherit; - a misaligned bound; - a `DEFAULT`, anomalous or pending-detach child; - a name collision. - **One winner.** A schema-scoped `pg_try_advisory_xact_lock` is taken before any DDL. Losers report `skipped_locked` and never issue DDL. The winner re-plans from a fresh audit before taking any table lock and checks for collisions. A runner that lost a race therefore sees the committed layout as a no-op, not as colliding names. - **Writer lock order.** First `ONLY` the parent is locked `ACCESS EXCLUSIVE` under a 2 s `lock_timeout`. Then each foreign-key counterpart (today only `communities`) is locked in OID order `SHARE ROW EXCLUSIVE NOWAIT`, then the catch-all `NOWAIT`. - The counterparts are read again once the parent is locked, and that set is the one locked. Any FK DDL on or to the parent conflicts with the parent lock, so the set can't change after that. An FK committed while maintenance waited is therefore locked `NOWAIT` too, so attaching never waits on it under the parent lock. The read before the lock remains as a cheap early refusal. - This is the order an insert takes locks (parent, then the FK check on `communities`), so maintenance cannot deadlock with ingest. The internal manual runbook used to lock `communities` first; it now uses this order too. - `SHARE ROW EXCLUSIVE` is the strongest lock the DDL takes on `communities`; measured on PG 16 and 17, the catch-all `DROP` takes none, as long as the catch-all's only foreign keys are those inherited from the parent. A catch-all with its own foreign key, or one that another table's foreign key references, is refused: once before any lock, and again after the catch-all lock, where the check is authoritative. Readers and other writers' `FOR KEY SHARE` checks proceed. A concurrent `communities` writer makes the run fail fast with `lock_timeout`. - A `40P01` deadlock victim is also reported as `lock_timeout`. - Parents that are referenced by a foreign key, or that reference an inherited table, are refused, including when such a key commits while maintenance waits on the parent. - **Re-plan under lock.** The catalog is re-audited under those locks. The transaction proceeds only if the plan is unchanged. - **Bounded.** `statement_timeout` 5 s, `idle_in_transaction_session_timeout` 5 s, and a 10 s client deadline per table. A deadline-abandoned connection is closed, never returned to the pool, and the server rolls back anything uncommitted. If the deadline fires after `COMMIT` was sent, the change may have landed; the next audit reports the truth. The orphaned backend keeps its locks until its in-flight statement ends, at most `statement_timeout`. - **Verify before commit.** The changed catalog is re-audited inside the transaction for bounds, canonical kinds, trigger parity and catch-all emptiness, and serving safety must not regress. It also checks that every new child has valid, ready clones of each parent index and validated clones of each PK/unique/FK constraint. - **Audits tolerate a concurrent advance.** Catalog rows come from the statement snapshot, but `pg_get_expr` renders from current catalog state. So an audit that overlaps another relay's `DROP` of a catch-all can see a partition that no longer exists. The read-only audit detects this (a NULL bound, or `42P01` from the emptiness probe) and retries up to twice with a fresh snapshot. - **Fail fast on audit probes.** If another relay's audit is probing the catch-all's emptiness, the catch-all `NOWAIT` lock fails and the run reports `lock_timeout`. The parent lock is then released immediately, and the next runner or interval completes the advance. Uncovered-month creation (`BUZZ_PARTITION_MANAGER_CREATE_ENABLED`, default `true`) now runs through the same locked transaction, replacing the unlocked per-month `CREATE`. That flag remains the kill switch for all automatic partition DDL: with it off, advancement is disabled too. With advancement disabled (the default) the periodic loop stays read-only and startup behaves as before, apart from the locking. A runner that planned DDL always re-audits before returning, even when a peer did the work, so it never reports the pre-maintenance layout. A serialized re-plan that finds only disabled work reports `skipped_disabled`. ## Behavior changes - The relay audit horizon and the `buzz-admin partition-audit` default are now 6 months (`PARTITION_MANAGER_MONTHS_AHEAD`), up from 3. Until advancement is enabled, months already covered by the catch-all make `degraded` true. That correctly reports a runway under six months. - New metric `buzz_partition_maintenance_runs_total{table,outcome}` with outcomes `noop | skipped_disabled | created | advanced | skipped_locked | operator_required | lock_timeout | deadline | error`. `buzz_partition_create_attempts_total` keeps its per-month `created | skipped_covered | error` labels. `error` there still means a DDL or collision failure; `lock_timeout` and `deadline` runs add nothing to it, and only `buzz_partition_maintenance_runs_total` records them. - `ensure_future_partitions` (free function and `Db` method) is removed; `maintain_partitions` is the one entry point. - New advisory-lock label `lock_type="partition_maintenance"`. ## Rollout Enable `BUZZ_PARTITION_MANAGER_ADVANCE_ENABLED` per environment in BPCI: `bb-block-staging` first, then production. The staging canary proves a real advance because the six-month horizon already extends past January. Fallback: rerun the internal manual runbook, which uses the same parent-first lock order as of 2026-10-05. It keeps `ACCESS EXCLUSIVE` on the counterparts, because its detach/attach can drop FK triggers on them. ## Testing - `cargo test -p buzz-db --lib partition`: 17 unit tests. The 10 new ones cover: - plans: repaired layout, policy gating, no-op, refusal states including child-only triggers, December→January; - the create kill switch also stopping advancement; - the month cap at exactly 120; - per-month metric outcomes for transient failures versus DDL failures; - the mid-audit-drop retry classifier. - `cargo test -p buzz-db --lib partition -- --ignored` (PostgreSQL 17): 48/48 on 9 consecutive runs, including 17 new tests: - advance to the canonical layout, with routing and idempotent rerun; - replacing a canonically named catch-all in one transaction; - populated catch-all refused in under 1 s while the parent is locked; - a `communities` writer making the run fail fast and roll back; - a `communities` reader not blocking the advance; - an in-flight insert's FK check, and its write to `communities`, behind queued maintenance completing without a deadlock; - a foreign key committed while maintenance waits on the parent, with a writer on its new counterpart, failing fast as `lock_timeout` on that table instead of waiting under the parent lock; - a foreign key referencing `events`, committed while maintenance waits on the parent, being refused under the parent lock; - a catch-all's own foreign key, or a foreign key referencing it, being refused before the parent lock with the key intact, and a catch-all key committed while maintenance waits on the parent being refused after the catch-all lock; - a `40P01` deadlock victim being classified as `lock_timeout`; - parent contention timing out without blocking the other table; - held advisory lock skipping with no DDL; - concurrent runners converging, with at most a transient `lock_timeout`; - a stale audit after a concurrent advance being a no-op, and the losing runner returning the committed layout; - a re-plan that finds only disabled work reporting `skipped_disabled`; - an audit retrying when a listed partition is dropped mid-audit; - a catch-all with a child-only trigger being refused; - client deadline abandoning the attempt and closing the session (scoped to the test's own blocked backend); - catch-all name collision; - incoming foreign key refused. - Mutation checks: - restoring the old counterpart-first `ACCESS EXCLUSIVE` order fails the reader test (`lock_timeout`) and the writer test (a ~1 s deadlock wait); - dropping `NOWAIT` from the counterpart lock, or allowing a populated catch-all, fails the matching tests; - removing the parent-locked counterpart re-read fails the committed-FK tests (the parent is held 2.06 s waiting on the new counterpart), and ignoring its refusal fails the referencing-FK test; - locking the counterparts `SHARE ROW EXCLUSIVE` before the parent fails the writer test; - removing either catch-all foreign-key check fails its test. - Full PostgreSQL lane: 716/718. Both failures are timing-sensitive tests outside partition code, and each passes 3/3 when run alone: - `hanging_redis_peer_times_out_before_the_health_listener_binds`; - `readiness_check_cancellation_balances_waiter_and_inflight_connection`. - `cargo clippy -p buzz-db -p buzz-relay -p buzz-admin --all-targets -D warnings` and `cargo fmt --check` are clean. ## Related Refs #2396. This PR conflicts textually with #7994 (partition audit observability follow-ups) in `crates/buzz-db/src/store/partition.rs` and `crates/buzz-relay/src/main.rs`. Whichever PR merges second will be rebased onto the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Ian Oberst <ioberst@block.xyz> Co-authored-by: Ian Oberst <ioberst@block.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>mobile-v0.21.0-rc.1 |
||
|
|
5a93e862ee |
fix(chart): own the Datadog version label on every workload Pod (#7876)
## Summary
Every chart-managed Pod now carries a chart-owned
`tags.datadoghq.com/version` label derived from the deployed image,
instead of a value each wrapper restates once per workload.
Covered Pod templates:
- relay Deployment (`templates/deployment.yaml`)
- pairing relay (`templates/pairing-relay.yaml`)
- storage-accounting CronJob
(`templates/storage-accounting-cronjob.yaml`)
- operator jobs, including the deletion drain
(`templates/_operator-jobs.tpl`)
**Source order:** `image.tag`, then `image.digest`, then
`Chart.AppVersion` (`buzz.imageVersionSource`). Tag-first is deliberate:
- The rendered value matches the `sha-<commit>` pins wrappers already
carry, so upgrading to this chart does not roll any Pods.
- An image promotion only has to write `image.tag` + `image.digest`. No
per-workload label edit is needed.
Exact runtime identity stays exact in two places. The image reference is
`repository@digest` when a digest is pinned.
`BUZZ_STORAGE_SNAPSHOT_CODE_SHA` (persisted as `code_sha`) is still
digest-first.
**Label safety:** `buzz.imageVersionLabel` maps any OCI tag or digest to
a valid label value.
- Valid values already in Datadog's normal form (lowercase, no `__`)
pass through byte for byte, so `sha-<hex>` and `1.2.3-rc.4` are
unchanged. Datadog lowercases tag values and collapses `__`, so any
other value is hashed rather than allowed to report the same version as
a different revision.
- A digest keeps 63 hex characters.
- Anything else is replaced by the first 63 hex characters of its
SHA-256.
- Exactly 63 lowercase hex characters is reserved for the hashed and
digest forms, so a rendered label cannot be fed back as a tag to make
two revisions collide.
- When both tag and digest are set, the label names the tag even if they
disagree; the digest still pins what runs.
**Precedence:** a `tags.datadoghq.com/version` supplied through any
workload's `podLabels` is ignored. Every other label, including
`tags.datadoghq.com/env` and `/service`, passes through unchanged.
Overriding instead of failing means a wrapper with a stale pin is
corrected by the upgrade itself. The chart bump and the wrapper cleanup
don't have to land together.
**What got simpler:** one image identity instead of N. Wrappers delete
their per-workload version pins. Image promotion tooling no longer needs
a "mirror the tag into a label path" feature. One shared helper
(`buzz.podLabelsWithVersion`) replaces four `podLabels` passthroughs.
Chart `0.1.10 → 0.1.11`, with an `artifacthub.io/changes` entry.
Documented in the chart README ("Datadog version label") and
`docs/deployment-identity.md`.
Note: a vendor-namespaced label now renders unconditionally in an OSS
chart. This is in tension with
[VISION_SOVEREIGN.md](../blob/main/VISION_SOVEREIGN.md). The label is
inert without the operator-supplied `tags.datadoghq.com/service`/`env`,
and the chart already ships vendor-specific defaults (e.g.
`sidecar.istio.io/inject`).
### Testing
Run on a Blox (Linux) workstation at tree `41634b7ca` (helm v3.16.4,
helm-unittest 0.8.2, the CI versions):
- `helm lint deploy/charts/buzz`: 0 failed
- `helm unittest deploy/charts/buzz`: 12 suites, **85/85 passed**
New `tests/version_label_test.yaml` checks the relay, pairing relay, and
deletion drain: tag wins over a stale `podLabels` pin, the label is
stamped with no `podLabels`, and digest-only images get a label-safe
value. `tests/storage_accounting_test.yaml` covers the same cases for
the CronJob, plus the `Chart.AppVersion` fallback.
Mutation checks:
- Reverting any one template to the plain `podLabels` passthrough fails
tests.
- Switching the source order to digest-first fails tests.
- Restoring the case-insensitive passthrough fails the three
Datadog-normalization tests (uppercase, `__`, uppercased 63-hex).
No Rust, desktop, web, or mobile sources changed.
## Linear tracking
Related work: [BUZZ-215](https://linear.app/squareup/issue/BUZZ-215).
---------
Signed-off-by: tornquist <tornquist@squareup.com>
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
chart-v0.1.11
|
||
|
|
7e8aac4403 |
fix(mobile): polish channel management and iOS Settings (#8171)
## Summary Use native iOS sheets for channel editing and Add Members, with profile avatars, matching search, and destructive removal dialogs. Canvas content becomes a tappable preview with Add/Remove actions. Fix the Settings availability menu and clipped copy confirmation. Align channel action messages with chat styling and keep agent identities distinguishable across selection, attribution, and profile surfaces. ### Related issue No duplicate found in the existing mobile Settings/channel issues and PRs. ### Testing - Flutter analysis, formatting, file-size checks, focused collision regressions, and the full mobile test suite passed. - Production UIKit picker simulator check passed: distinct visible/accessibility labels, avatar updates, selected rows, and pubkey callbacks. Wired into Mobile Swift CI. - Original UI was built and tested on the physical iPhone with author confirmation; profile/community data preserved. - Review fixes await renewed author device confirmation before restoring `buzz-review-completed` and ready-for-review status. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
1ac0a1271b |
fix(mobile): preserve Markdown attachment destinations (#8168)
## Summary Fix mobile “Image unavailable” errors caused by angle-bracketed Markdown URLs. Preserve attachment destinations across images, galleries, videos, and file links, including spaces, parentheses, and optional titles. ### Related issue Related: #8086 (attachment filenames). ### Testing - `just mobile-install mobile-check mobile-test` - Regression coverage decodes PNG/JPEG/GIF/WebP/BMP and verifies gallery viewers, video routing, and authenticated file links. - Native device playback not tested. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
836e7d0d06 |
feat(desktop): browse and moderate every community in the Admin Console (#8027)
The Admin Console only ever saw the community the desktop happened to be connected to. With #8011's community reads, the console can now browse every community on the relay and, for signed staff, moderate any of them by name without switching connections. 🤖 ## What changes - **Communities tab.** Lists every live community on the relay, searchable by host prefix with "Load more" paging. The connected community is pinned at the top by exact host match. Report rows get a community badge that opens that community. - **Community page.** Reports, Restrictions, Members and Actions, each scoped to the community on the page. The Actions form has no host field: it always acts on the page's community, and the TypeScript host normalization is gone from the Actions tab. - **Native reads** (`admin_list_communities`, `admin_search_members`, `admin_get_member`, `admin_get_event`). Every request validates the explicit community host and NIP-98-signs the real method and URL. Failures come back as a structured error (`status`, `bodyComplete`, `bodyEmpty`, `code`), so the UI can tell an older relay's empty 404, a coded `event_not_found` and a transport failure apart, and never says "not found" unless the relay did. - **Explicit-host lifts.** `admin_lift_restriction` sends the page's community host, not the active relay's. It freezes the relay and signer the intent was reviewed under, and refuses to send if either has changed. - **Pending actions survive navigation.** A panel-level `DirectActionsProvider` holds the frozen intent, so switching communities or tabs mid-confirm doesn't lose it. Only Discard drops it. A signer change remounts the controller and drops it. - **Fenced lookups.** Member and event lookups are keyed by origin, signer, community, target and a generation counter, so a late answer for one community can't unlock Review in another. - **Shared member picker** (`MemberSearchResult`, also used by Add Member), showing the truncated npub so members with the same name can be told apart. Delete accepts a note link and previews the message before Review. Relay staff are blocked before Review. - **Disabled admin auth is read-only.** With `BUZZ_ADMIN_AUTH=disabled` the Communities tab and community pages still render, and report badges still open them, but every ban, timeout, delete, add-member and lift control is disabled. The settings badge says the console is read-only and that acting needs `nip98`. Staffing stays hidden. - #7904's protections in the Actions tab are kept: frozen intent, `requestId`, Retry, `notSent` and the key-backup guard. - **Readable load errors.** Admin list and detail loads show the relay's error message (for example "request is not authorized"), never the raw JSON envelope. - **Failed kicks reload the report.** A resolve that fails with `enforcement_failed` reloads the report, so its failed action and "Cancel & reopen" appear immediately. - **Key backups never leave in a search.** Every admin API request URL passes the NIP-49 key-backup guard before it is signed or sent: community and legacy reads, mutations, the connection probe (before its first, unsigned request) and feedback-attachment fetches. The community and member search boxes, including the directory's Load more, refuse a pasted `nsec` or `ncryptsec` anywhere in the input without calling the relay. - **`just admin-seed` for multi-community testing.** Besides the local community, it seeds `beta.localhost:3000`, `gamma.localhost:3000` and 50 filler communities, with named members, a ban and a timeout in each named community, a report with Kick available, and a message for the delete preview. Rerunning it restores all of that to the starting state. It refuses, writing nothing, while a moderation action is unfinished on a seeded report. - The bodyless `GET /events/{id}` preview is added to the events-URL egress inventory. It sends no body, so it carries no key material. ## Tests Native (Rust): - `empty_404_and_405_read_as_complete_and_empty`, `coded_404_carries_the_relay_code`, `truncated_and_over_cap_bodies_are_incomplete`, `transport_failure_has_no_status`: the structured read error tells an older relay, a coded refusal, a cut-off body and a transport failure apart. - `a_401_retry_is_signed_by_the_same_keys`, `lift_signs_the_send_and_401_retry_with_the_checked_keys`: the 401 retry never switches keys. - `read_urls_validate_the_explicit_host_and_encode_the_query`, `lift_url_sends_the_page_host_not_the_active_relay`, `list_url_carries_host_and_cursor_and_refuses_a_changed_relay`, `lift_refuses_before_sending_on_signer_change_or_bad_host`: requests target the explicit host and refuse to send after a relay or signer change. jsdom: - `actions-pending-nav`: a pending intent survives a community change, and only Discard drops it. - `actions-fenced-lookup`, `fenced-load`: a late lookup for an old key never shows under, or unlocks, the new one. - `actions-same-ids`: the same pubkey and event id are read separately in each community. - `actions-community-change-drops-member`, `actions-page-host`, `actions-success`, `actions-identity`: the form acts only in the page's community, and confirm sends the frozen intent. - `actions-preview-errors`, `actions-delete-link`, `actions-staff-target`, `actions-disabled-auth`: preview errors, delete by note link, the staff block, and community pages rendering read-only with every action control disabled under disabled admin auth. - `communities-directory`, `communities-unsupported`, `community-badge`, `community-badge-disabled-auth`: the directory, older relays, and report badges, which open community pages under disabled admin auth too. - `restrictions-lift-signer-change`. The existing restriction tests now run on the community page and use `admin_lift_restriction`. Mutation checks: removing the pending-navigation guard turns `actions-pending-nav` red, and dropping the host from the fence key turns `actions-fenced-lookup`, `actions-same-ids` and `fenced-load` red. Ungating restriction lifts on `canMutate` turns `actions-disabled-auth` red. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
a918e605ca |
docs(nips): add NIP-FA federated identity adapter spec (#8148)
🤖 Moves the enterprise identity adapter's wire contract into a new normative spec, `docs/nips/NIP-FA.md` (Federated Identity Adapter), so third-party adapters have a spec to conform to. The Desktop-only material stays in a guide, renamed from `docs/enterprise-identity-adapter.md` to `docs/federated-identity-adapter.md` to match the NIP's name. - `NIP-FA.md` holds browser login, code exchange, session check, relay assertion (including the denial table), NIP-FI assertion transport, security considerations, and privacy. Agent-key assertions are out of scope. - `docs/federated-identity-adapter.md` is a short Desktop guide for the client in block/buzz-app: build configuration (`BUZZ_BUILD_ENTERPRISE_AUTH_*`), what is implemented today (build config, discovery gate) versus planned (browser login, session check, assertion fetch, profile projection), and the vendor-specific headers adapters must not use as session credentials. Endpoints, fields, headers, and denial codes are unchanged. The NIP converts lowercase requirements to BCP 14 keywords and pins rules the old guide left open: - `/v1/session` reports a bad session as 401 `session_required` / `session_expired`; clients discard the session only on those, and keep it and retry with backoff on 429, 5xx, and network failures, matching the assertion endpoint. - The assertion `aud` is `https://` plus the canonical `relay_url` authority (`wss://c.example.com:8443` → `https://c.example.com:8443`), and adapters issue dedicated `nip-fi+jwt` assertions. Relay authorities on port 80 are not supported. - Codes are single-use and short-lived, only `S256` challenges are accepted, and adapters accept only `127.0.0.1` and `[::1]` callback hosts. - After code exchange the client must call `/v1/session` and compare the RFC 3339 `expires_at` instants before committing the login. - `session_token` is sent only to adapter endpoints, never to a relay, and no credential is logged. - NIP-98 proofs carry exactly one `method` and one `payload` tag; retries honor `Retry-After` and surface failure after bounded attempts; IPv6 relay hosts have a canonical form. - The relay-proof header example is scoped to NIP-98-protected HTTP, with WebSocket (NIP-42 AUTH) and Blossom (kind 24242) deferred to NIP-FI. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
7afdc2aac9 |
feat(relay): record when a read-state account starts (#8157)
## Goal Add `started_at` to read-state accounts from the day the table is created. This prevents users seeing ALL their channels as unread when they first install a client update that starts using these APIs --------- Signed-off-by: hMB2 <0400eb0da32afbbf16cdd1dee6ffd243587e8f3d6a2823fe7dfa95d16b002b7d@buzz.block.builderlab.xyz> Signed-off-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz> Signed-off-by: hMurderbot <219c58afff8ac2c76e4e527a329aa61c2ebbb896786b1916d4f9cca5140ecd04@buzz.block.builderlab.xyz> Co-authored-by: hMB2 <0400eb0da32afbbf16cdd1dee6ffd243587e8f3d6a2823fe7dfa95d16b002b7d@buzz.block.builderlab.xyz> Co-authored-by: hMurderbot <219c58afff8ac2c76e4e527a329aa61c2ebbb896786b1916d4f9cca5140ecd04@buzz.block.builderlab.xyz> Co-authored-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d34c9c2c9b |
ci: shorten the PR critical path (desktop lanes, smoke shards, parallel relay artifacts) (#8156)
## Summary The PR critical path on main push [run 37635224099](https://github.com/block/buzz/actions/runs/37635224099) (`e097c4299`) was about 42 minutes: - **Desktop Core** took 41 min. It ran frontend checks, two clippy graphs, the Tauri suite, the mesh-llm suite and every compiled-flag state one after another. The compiled-flag step alone took 16m51. - **Relay artifact producer → Desktop E2E Integration** took about 31 min. The producer built the relay binaries (5m07), the backend integration archive (4m41) and the PostgreSQL archive (8m18) one after another. - **Desktop Smoke E2E** took 19–24 min per shard across 4 shards, with 82 min of tests in total. This PR attacks all three. ### 1. Run each compiled-flag state once (`Justfile`) | Previous step | Cost | Why it's redundant | |---|---|---| | Clean state: full `buzz_lib` suite (`--lib`) | 3m27 run | `desktop-tauri-test` runs the same suite in the same compile state one step earlier. When `BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY` is unset, the expected value already defaults to `false` in non-owner-only builds (`runtime/test_fixtures.rs`, `agents_tests.rs`). | | Internal state: auto-connect and owner-only built separately | 1m22 + 1m06 + 56s across three recompiles | Internal release packaging always sets both capabilities together, so building owner-only without auto-connect tests a configuration that never ships. The extra 56s happens because a bare `cargo test <filter>` builds every target, not just the `--lib` harness. | | Demo state: `cargo test --workspace` | ~30s more than `--lib` | `buzz-terminal` and the `tests/*.rs` integration tests don't read any `BUZZ_BUILD_*` variable or `buzz_lib`, so they ran the same binaries a second time. | A fourth cost was hidden: every recipe invocation spent ~1m20 recompiling `buzz-desktop` to the *same* unit hash `desktop-tauri-test` had just built. `_ensure-sidecar-stubs` ran `touch` on every sidecar stub on each run. `tauri-build` emits `rerun-if-changed` for each `externalBin`, so the new mtimes reran `build.rs` and recompiled the crate. Stubs are now created only when they're missing. This also speeds up any local `desktop-tauri-*` recipe run back-to-back. After this change: - **Clean state:** runs only the two compiled-flag assertions (`compiled_flag_matches_expected`, `compiled_policy_matches_expected`) with `expected=false`. No recompile. - **Internal state:** one compile with `BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY=1` and `BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY=1`. It runs both assertions with `expected=true`, then the full `buzz_lib` suite. The suite now runs with auto-connect enabled as well, which matches the shipped internal build more closely than before. - **Auto-connect-only state (assertions only):** auto-connect `true`, owner-only `false`. The two capabilities are documented as independent. Without this state, a `build.rs` block that read the other capability's variable would pass, because every other state sets both or neither. - **Demo state:** one compile with only `BUZZ_BUILD_DEMO_SLUG`. It asserts the slug and asserts both capabilities stay `false`, then runs the full `buzz_lib` suite. - The recipe clears the three toggled `BUZZ_BUILD_*` capability variables and any caller `BUZZ_TEST_EXPECTED_*` values up front. Each state exports its own values in a subshell. Every invocation uses `--lib`, so each state compiles one test harness. - Each ignored-assertion run requires exactly one passing test per name filter. Previously, a renamed or un-ignored assertion matched nothing and passed silently. - The 32-character demo-name rejection check is unchanged. The full-suite reruns in the internal and demo states are kept on purpose. They are the only coverage that the whole suite holds up in those two shipped build shapes. Estimate: the compiled-flag work goes from about 17 min to about 11. ### 2. Split Desktop Core into parallel, path-gated lanes (`_ci-desktop.yml`) - **Desktop JS:** `desktop-check`, `desktop-test` and `desktop-build`. These compile no Rust, but some node:test cases read relay and DB crate sources to pin shared constants (for example `relayReconnectReplay.test.mjs`). So the job runs on desktop *and* Rust changes, in parallel with the Tauri lanes. `lefthook.yml` notes this as a deliberate local deviation. - **Desktop Tauri (core | mesh | compiled-flags):** a matrix of parallel Rust lanes. - `core` and `compiled-flags` share the existing `desktop-core` Rust cache key, so they start warm. `compiled-flags` restores it read-only. - `mesh` keeps its own cache because `mesh-llm` pulls a different dependency graph. - **Smoke E2E and the Windows frontend build** compile no Rust and read no crate sources, so they no longer run for Rust-only changes. - **Smoke E2E goes from 4 to 10 shards.** Playwright shards by test count, not duration. A simulation from the per-test durations in run 37635224099 reproduces the observed 4-shard split exactly. With 10 shards, the slowest shard runs about 11 min of tests. - **The `Desktop` gate is stricter.** Each selected job must succeed and each unselected job must be skipped, so a mis-gated job can't pass silently. The required check context (`Desktop`) is unchanged. `scripts/ci-required-gates.test.mjs` pins each job's `if:` to the gate's selection expression, and checks which jobs a Rust-only, desktop-only, Tauri, push, and empty selection pick. - **Path-filter fixes:** - The `desktop` filter now includes `_ci-desktop.yml`, `ci.yml` and `Justfile` (which holds the desktop recipes), so edits to them run the frontend lanes. - The `rust` filter now matches `Justfile`. It still named the pre-rename `justfile`, so a `Justfile`-only change selected no Rust suites. - `scripts/ci-selection.test.mjs` gains `Justfile` and desktop-workflow scenarios, and the shared-workflow scenario now expects `desktop`. - **Dead step removed:** the failure-only Playwright artifact upload in the old Core job never had anything to upload, because that job never ran Playwright. ### 3. Build relay artifacts in parallel (`_ci-relay.yml`) - **The producer is now a 3-way matrix** (`relay-binaries`, `backend-integration-tests`, `postgres-tests`). - Each lane has its own exact-source cache entry and uploads `desktop-e2e-relay-<artifact>`. - Consumers download `desktop-e2e-relay-*` into `target/ci`, so their paths are unchanged. - The required `Desktop E2E Relay` context still reads the producer job's aggregate result. - **Rust cache saving:** all lanes restore the existing Rust cache key, but only the widest lane (`postgres-tests`) saves it. Otherwise the fastest lane could overwrite it with a narrower target dir. - **Desktop E2E Integration goes from 2 to 3 shards.** Shard labels now use `strategy.job-total`. ### 4. Bound degraded apt steps (`scripts/ci-apt-retry.sh`) - With more shards, more runs hit the occasional degraded Ubuntu mirror. On this PR, a Tauri lane ran `apt-get install` for 27 minutes: apt's `Acquire` timeouts kept firing, but apt kept re-queuing the requests and never failed. Separately, three Smoke shards stalled silently in `playwright install-deps` (no output after `InRelease`) until the job timeout. Healthy apt steps across four recent runs finished in at most 238s. - Every apt-backed step in Desktop and Integration now runs through `scripts/ci-apt-retry.sh`. The script writes the apt options to `apt.conf.d`, so Playwright's own apt-get calls get them too. It kills each attempt after 7 minutes (90s for `apt-get update`). Between attempts it stops any `apt-get` or `dpkg` the attempt left running, repairs dpkg under its own 60s deadline, and retries up to 3 times. The stop matters for `playwright install-deps`, which runs `sudo sh -c "apt-get update && apt-get install ..."`: sudo puts its command in a new process group, so the deadline's signal stops `sh` and apt-get survives holding the dpkg lock (seen on Smoke shard 8 of run 37696815557, and reproduced with real sudo). - Downloaded `.deb`s are kept between attempts, so a retry resumes where the last one stopped. Against a mirror that stays degraded, this bounds each wrapped command at about 25 minutes rather than recovering it. The Tauri lanes wrap two commands, `apt-get update` (about 8.5 minutes worst case) and `apt-get install` (about 25), so about 34 minutes together. Job timeouts are sized to fit that bound plus the job's own work: Tauri 55m (lanes take up to ~15m), Integration 40m, and Smoke 45m. A degraded mirror therefore ends with the wrapper's error instead of a silent job timeout. - **The retry has already fired on this PR.** In [run 37703795342](https://github.com/block/buzz/actions/runs/37703795342), `playwright install-deps` on Integration shard 1/3 hit the 420s attempt deadline. The second attempt started about 1.4s later, downloaded and configured the real packages, and succeeded. The step took 526s (8m46), and that shard took 17m24 while the other two took about 5m. Healthy installs have taken at most 238s, so 420s leaves headroom, and a slow mirror costs one attempt instead of the job. - Between attempts the script stops **every** `apt-get`/`dpkg` on the machine, not only the ones it started. That's intentional: on a hosted runner any other one would hold the same dpkg lock. It assumes an ephemeral runner, and the script says not to use it on a self-hosted runner or a developer machine. - `scripts/ci-apt-retry.test.mjs` covers the following with a fake `sudo`: success, retry, the attempt budget, a stalled attempt, a stalled dpkg repair, an apt-get that escapes the attempt's process group (on a retry and on the final attempt), and usage. It needs Linux (GNU `timeout`, `flock`, `setsid`, procps), so it skips on other platforms. CI runs it on Ubuntu. ### What got simpler - **Compiled-flag recipe:** two full-suite runs instead of three, six cargo invocations instead of eight, and sidecar stubs no longer invalidate the desktop build cache. - **Path selection:** each desktop job declares only the paths that actually affect it. The stale `justfile` filter and a dead artifact step are gone. - **Apt options:** set once in `apt.conf.d` by one script, instead of as repeated `-o Acquire...` flags on each apt call. - **Shard counts:** the shard count is written once per matrix, not repeated in names and commands. ### Cost - **More runners:** a full run uses more runner jobs. Smoke E2E goes from 4 to 10 jobs, Desktop Core from 1 to 4, the relay producer from 1 to 3, and Integration from 2 to 3. - **Fewer runners on single-area PRs:** desktop-only PRs skip the Rust lanes, and Rust-only PRs skip Smoke E2E and the Windows build. - **One extra Rust cache entry:** `desktop-mesh`. ### Related issue None found. ### Testing Blox, compiled-flag recipe at `77b9874b`: - [x] Recipe passes with output redirected to a file. The log keeps all 4 state headers, the ignored assertions ran 2/2/2/3, and both full suites passed (`3423 passed`). - [x] A second back-to-back run shows no clean-state `Compiling buzz-desktop`. - [x] Mutation tests all fail as required: - `build.rs` auto-connect reads the owner-only var: rc=101. - The reverse swap: rc=101. - Rename `compiled_policy_matches_expected`: rc=1. - Wrong owner-only expectation: rc=101. - [ ] `just ci` Workflows: - [x] `actionlint` on `_ci-desktop.yml`, `_ci-relay.yml` and `ci.yml`: clean. - [x] `scripts/test-ci-required-context-isolation.sh`: passes. - [x] GitHub Actions: runs [37701003713](https://github.com/block/buzz/actions/runs/37701003713) and [37703795342](https://github.com/block/buzz/actions/runs/37703795342) took 27m09 and 28m51. Before this PR, PR runs had a median of 40.0m (n=21) and main pushes took about 42m. These are observations across mixed runner images, not a measured causal speedup. - [x] Selection, gate and apt suites (74/74 on Linux), `actionlint` and `bash -n` on Blox at tree `db9725fe`. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
bbcbd7ce13 |
Preview Tabler icons in the mobile app (#8109)
## Summary Preview buzz-app's Tabler icon style in the mobile app, replacing app-drawn Lucide glyphs across channels, messages, activity, search, and settings. A shared `BuzzIcons` mapping uses the bundled Tabler 3.46.0 outline font, matching buzz-app's version, with its MIT license and source documented. Native UIKit controls retain SF Symbols. The former 300/500 navigation glyphs use Tabler's standard outline; existing selection colors and labels remain. ### Related issue No matching Tabler mobile migration issue or PR found. Related native iOS controls: #8055. ### Testing - Pre-push mobile analysis, formatting, file-size checks, and the full mobile test suite passed. - 194 focused widget tests passed, including channels, home, search, settings, and message content. - Light and dark community-settings renders inspected. - Signed iPhone Release build succeeded; installed and launched as Buzz Tabler on a physical iPhone. - Full `just ci` attempted; stopped during Rust clippy compilation because the Mac ran low on disk space. Full repository validation is not claimed. Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
c6340a45a0 |
fix(deletion): give the scheduled drain the cold-start DB connector (#8146)
## Summary The scheduled `buzz-admin deletions drain` Job never gets past its first database connection in a hosted staging deployment. Every run exits 5 after exactly 3 seconds with `pool timed out while waiting for an open connection`. The drain starts in a fresh pod with no mesh sidecar, and that pod's first connection can take longer than the relay's 3-second acquisition budget. `connect_store()` in `buzz-deletion` inherits that budget from `DbConfig::default()`, with no retry. In the same pod, the same command run 15 seconds later succeeds. TCP to Postgres, Redis, and S3 from a sidecar-less pod is fine. #7770 fixed the same failure for `buzz-admin storage-snapshot`, but only inside `buzz-admin`. This PR moves that connector into `buzz-db` as `Db::connect_cold_start` and uses it from both run-once workers: - **Pool settings:** a 30s acquisition budget, and no idle spare connections (`min_connections: 0`). - **Retries:** startup timeouts and transient transport errors retry at most twice, after 2s and 5s, so startup waits at most 97s. - **No retry:** configuration, authentication, TLS, and protocol errors fail immediately, and the error message stays free of credentials. - **Per-worker settings:** each worker keeps its own pool size. Storage-snapshot stays at 1 connection, and deletion stays at `BUZZ_DB_POOL_SIZE` (default 20). Each worker also keeps its session timeouts. - **Startup events:** the stderr events carry a worker prefix. Storage-snapshot still emits `storage_snapshot_db_connect_*`, and the deletion worker now emits `deletion_db_connect_*`. - **Error text:** the exhaustion message is now `{worker} database startup failed after …`. For storage-snapshot that reads `storage_snapshot database startup failed …` instead of `storage snapshot database startup failed …`. Nothing in this repo matches on that string. - **Session-setup failures:** SQLx retries a failed `after_connect` hook (writer floor guard, session timeouts, isolation assertion) until the acquire deadline. Those failures therefore surface as `timeout`, are retried, and fail closed after at most 97s instead of immediately. Relay pool defaults are unchanged. The deletion CLI's operator commands (`list`, `approve`, …) use the same connector. They normally run inside the warm relay container, where the first attempt succeeds. **What got simpler:** there is now one cold-start connector instead of a `buzz-admin`-private copy that the deletion worker couldn't reach. `storage_snapshot_startup.rs` is gone, and its unit tests moved with the code. The two Postgres startup tests now share one harness with the drain's tests (`postgres_storage_snapshot.rs` → `postgres_cold_start.rs`). ### Related issue Follow-up to #7770 (same failure, other worker) and #7827 (the drain CronJob). No duplicate found. ### Testing All at head `1545de7b5`: - **GitHub CI:** green on attempt 1, including `PostgreSQL Domain / PostgreSQL Tests`. - **Blox:** - fmt, `clippy --workspace --all-targets -D warnings`, and the `buzz-db` / `buzz-admin` / `buzz-deletion` tests pass. - All four `postgres_cold_start` tests pass, each against a freshly migrated database. - Falsification: with main's `connect_store` restored, the drain retry test fails. - **Agent review:** two rounds. All findings are fixed or disclosed; the error-text change is listed above. - **Agent exercise:** real `buzz-admin deletions list` / `deletions drain` against local Postgres 17 behind a fault proxy. - **5s delay:** succeeds on attempt 1 (3/3). Main fails at about 3.5s. - **Dropped handshake, then 5s delay:** attempt 1 fails with `UnexpectedEof`, then a 2s backoff, then attempt 2 succeeds (3/3). - **Wrong password, missing database or malformed URL:** exit 5 after one attempt. - **Persistent rejection:** exit 5 after three attempts, with 2s and 5s backoffs. - The deletion queue stayed empty throughout. - **Not covered:** an approved deletion, real S3 / Pod Identity, TLS and session-setup failures, and a live 97s stall. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
745501962e | refactor(schema): fan desired-state schema out to one file per table (#8131) | ||
|
|
f8bf2c529c | refactor(db): type-level admitted event-write transactions (AdmittedTx) (#8130) | ||
|
|
70d2ca7760 |
fix(mobile): tolerate unrelated NIP-11 relay metadata (#8160)
Relay capability additions such as `read_state_snapshot` currently make mobile push discovery reject otherwise valid NIP-11 documents. This blocks descriptor-dependent enrollment, lease updates, and remote revocation. Read unrelated top-level metadata permissively while preserving validation of the advertised `nip-pl` extension and the complete push descriptor. Each returned descriptor value is covered by compatibility tests; missing or malformed required push fields still fail. ### Validation - [Codex code review](https://github.com/block/buzz/pull/8160#issuecomment-6049355700) and [security review](https://github.com/block/buzz/pull/8160#issuecomment-6049486587) completed without findings on the final code. - Added 24 regression cases covering read-state metadata, arbitrary future metadata, HTTP discovery, missing/malformed push fields, and invalid extension advertisements. The six compatibility/discovery cases fail on unchanged main and pass with the fix. - All 55 focused descriptor, bootstrap, capability, and revocation tests pass. Mobile formatting and analysis pass. - Full mobile gate passes: 3,063 tests passed and four skipped with a configured gateway, plus all three tests for an unconfigured gateway build and the gateway recipe checks. - [Current-head CI](https://github.com/block/buzz/actions/runs/37706738403) passes, including Flutter tests, Android debug APK compilation, the complete iOS simulator app, and the unsigned iOS release build. - Production HTTP descriptor discovery against live personal-relay metadata passes without enrollment or relay changes. - Device enrollment, delivery, notification tap, and opt-out remain to be exercised in the updated iOS candidate. No relay or device state was changed. buzz-review-completed --------- Signed-off-by: Tom Brow <tomb@block.xyz>mobile-v0.20.1-rc.1 mobile-v0.20.0-rc.2 |
||
|
|
1972b7d256 | feat(admin): add community archive commands (#7107) | ||
|
|
d742fc5f30 | Use Silo for bundled object storage (#8023) | ||
|
|
032198ce43 |
feat(relay): add community reads to the admin API (#8011)
The in-app Admin Console needs to work on any community the relay
serves, not only the one the desktop is connected to. This adds four
read-only routes under `/api/admin/v1` that the stacked desktop PR
builds on, puts every admin route under one explicit access rule, and
binds every admin request's signature to its real method, which fixes
signed `HEAD` on reads.
## Access rule
| Mode | Moderation reads | Writes | Staffing (`/operators`) |
|---|---|---|---|
| `nip98` (default) | signed, any staff role | signed, role-checked |
signed Operator |
| `disabled` | served to anyone who can reach the relay | `403` | `403`
|
"Moderation reads" means every read: reports, feedback and attachments,
restrictions, and the four community routes below. `nip98` behavior is
unchanged, and the Operator API (`/operator/*`) is untouched.
**Existing `BUZZ_ADMIN_AUTH=disabled` deployments start serving the four
new reads** (community directory, member search, member lookup without
staff status, and stored events by ID) with no config change. The
startup `WARN`, `docs/admin/README.md`, `.env.example`,
`deploy/compose/.env.example` and the `just admin` comment list exactly
what disabled mode exposes, say it should be reachable only by people
allowed to see all moderation data in every community, and say the whole
relay port must be private: the admin API is on the relay's main
listener and its hostname check uses the caller's `Host` header. `just
admin` binds the relay to `127.0.0.1` unless `BUZZ_ADMIN_AUTH` is
exactly `nip98`, so an unset, padded or misspelled value stays local.
In code, `authorize()` returns `AdminAccess`, either `NetworkTrusted`
(disabled mode, no identity) or `Staff(AdminPrincipal)` (`nip98`, any
rostered Operator or Moderator). Each route makes one check: **view**
(`authorize()` succeeding), **act** (`AdminAccess::act()`, must be
`Staff`, then the existing role checks) or **operator**
(`AdminAccess::operator()`, must be an Operator). Act and operator
refuse `NetworkTrusted` with `403 this endpoint requires
BUZZ_ADMIN_AUTH=nip98`; no principal is ever created for an anonymous
caller. `every_route_makes_its_check_in_both_auth_modes` lists every
mounted admin route with its check and asserts the result in both modes,
and `every_route_is_listed` fails when `router()` mounts a route that
list does not name.
## Routes
All four live in `api/admin/reads.rs`; `mod.rs` only registers them.
Their queries reject unknown fields, like `ReportQuery`. The three
`?communityHost=` routes share `CommunityQuery`, which now rejects
unknown fields too, so the existing unban, untimeout and direct-action
routes that use it do the same; their desktop callers send only
`communityHost`. Every admin route that takes `limit` validates it with
the module's shared `limit()` helper, so an out-of-range value is `400
invalid_limit`.
| Route | Returns |
|---|---|
| `GET /communities?q=&cursor=&limit=` | `{items: [{id, host,
icon\|null}], nextCursor\|null}` |
| `GET /members/search?communityHost=&q=&limit=` | `{items: [{pubkey,
displayName\|null, nip05\|null, avatarUrl\|null}]}` |
| `GET /members/{pubkey}?communityHost=` | `{pubkey, profile:
{displayName, nip05, avatarUrl, about}\|null, role:
"owner"\|"admin"\|"member"\|null, banned, mutedUntil\|null, isStaff:
bool\|null}` |
| `GET /events/{id}?communityHost=` | `{id, authorPubkey, kind, content,
createdAt, deletedAt\|null, channelId\|null}` |
- **One community per read.** Community-scoped routes bind
`communityHost` through the tenant binder (`400 unknown_community_host`)
and read only that community's `users`, `relay_members`,
`community_bans` and `events` rows.
- **Directory.** It uses the binder's liveness rule (not archived, not
deleted, `deletion_state = 'active'`), so every listed host can be
opened. `q` is an escaped, case-insensitive host prefix of at most 255
characters (`400 invalid_query`). Results are ordered and paged by
`lower(host)`, which is unique, so the opaque base64url cursor carries
only the last host; a malformed cursor is `400 invalid_cursor`. `limit`
is 1 to 100 with a default of 50.
- **Member search.** It reuses `search_users`, which already escapes,
binds and scopes, with no second escaping layer. `q` is trimmed and must
be 1 to 100 characters (`400 invalid_query`), and `limit` is 1 to 50
(default 20). It searches community profiles, so former members appear.
- **Member lookup.** `isStaff` uses the same roster lookup as the
direct-action staff guard and is returned to signed staff only: it is
`null` in disabled mode, where the roster is never read. A roster lookup
failure is a 500, never "not staff".
- **Event preview.** It reads the event by `(community_id, id)` and
never returns a private kind (see below). A malformed id, an id stored
only in another community and a private kind are all `404
event_not_found`, the same answer as an absent event.
## Private events
Admin reads never reveal events the relay keeps private.
`admin_hidden_kinds()` combines the four private kind sets in
`buzz_core::kind`: `AUTHOR_ONLY_KINDS`, `RESULT_GATED_KINDS`,
`P_GATED_KINDS` and `SHARED_GATED_KINDS`. The whole shared-gated set is
hidden, including events tagged `["shared","true"]`; the shared tag is
never evaluated. Every HTTP response applies it the way it treats a
missing event: event preview is `404 event_not_found`, report detail
returns the report with `message: null` and `targetAuthorPubkey: null`,
and the report list returns a null target author.
Moderators can still act on these events, they just cannot read them.
Report detail is one query with the hidden-kind list as a parameter,
exposed two ways: `admin_get_report` hides private kinds and backs every
HTTP response (GET detail, the resolve response, reopen, cancel and the
reporter-notice worker), while `admin_get_report_for_enforcement` reads
the target unfiltered and is used only by the resolve handler's
enforcement load (the staff-target check and ban, timeout and kick
target derivation) and by the recovery worker for interrupted actions.
Its result is never serialized, so a ban from a report on a private
persona bans the real author while GET still shows nothing.
## Method binding
Axum serves `HEAD` through `GET` handlers, but the read handlers
authorized with a literal `"GET"`, so a correctly signed `HEAD` was
rejected. Admin reads now authorize through `authorize_read` and writes
through `authorize_write`, both of which use the request's real method
and full target, so no handler hardcodes a method string. A database
error behind any admin route is now logged before it becomes a 500.
## Directory query plan
`idx_communities_host` is on `lower(host)` in the default collation, so
the prefix `LIKE` is a filter, not an index condition. The plan walks
the index in order and stops once it has `limit` matches. A common
prefix is cheap (on local Postgres 16 with 21,359 communities, a first
page skipped 2,728 index rows in 3.9 ms), but a prefix with few or no
matches walks every index entry, so its cost grows with the number of
communities. A `COLLATE "C"` index would turn the prefix into an index
range; it is left out because the admin directory is low-traffic and the
cost is small at current scale.
The cursor condition is `lower(host) > COALESCE($2, '')`, with no `OR`,
so it remains an index condition under a generic plan:
```text
-> Bitmap Index Scan on idx_communities_host
Index Cond: (lower((host)::text) > COALESCE($2, ''::text))
```
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
|
||
|
|
f260ad7f95 |
fix(ios): use the shared glass capsule for thread titles (#8150)
The iOS thread header currently renders an unbacked “Thread” label over scrolling messages. Supply the channel display name as its subtitle so the header uses the same two-line glass capsule as channel conversations. The title remains noninteractive and inherits the shared sizing, truncation, text scaling, and accessibility behavior. Channel and DM display names update with channel metadata; missing metadata keeps the capsule without inventing a subtitle. App-bar construction moves into a sibling file to keep the thread page within the file-size limit. ### Validation - [CI run](https://github.com/block/buzz/actions/runs/37654016581): mobile tests, native iOS build, and all other executed jobs passed. The overall workflow reports a GitHub internal server error; GitHub rejected a retry. Required checks passed or were skipped. - [Codex review](https://github.com/block/buzz/pull/8150#issuecomment-6042500880) completed without major findings on the current head. - Mobile dependency setup, formatting, file-size checks, and analysis passed. - Full mobile suite: 3,039 tests passed, four skipped; all three unconfigured-push tests passed. - Widget regressions exercise real thread-page native parameters for channel/DM labels, renames, missing metadata, and disabled title interaction. - Native iOS 26.5 simulator check confirms the shared capsule renders “Thread” above “engineering,” with a single noninteractive accessibility header. Before/after captures use synthetic content. [Before/after simulator captures](https://github.com/block/buzz/pull/8150#issuecomment-6042451532) ### Post-merge validation During the mobile release baking period, open threads in channels and DMs on a device, including long channel names and larger text settings; verify the bubble stays clear of Back and has no tap action. Human app-test confirmation is not applicable under the standing Buzz mobile exception. buzz-review-completed Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
6d07a29137 |
perf(relay): bound fleet usage metrics collection (#7176)
## Why
The usage poller pays memory and query costs proportional to community
count even when per-community emission is disabled. Every pod loads the
full host map, and the leader materializes all per-community aggregates
before suppressing their labels. That makes telemetry collection itself
a scaling constraint.
## What changed
- Fleet-only usage metrics are now the default. `all` remains available
as an explicit rollback mode.
- In fleet-only mode, the poller skips the community host map and all
community-sized query results.
- Exact community counting is replaced with the PostgreSQL planner's
table-size estimate.
- Stock totals are collected as one fixed row, scanning each stock table
once per hour.
- 1d/7d/30d active-user totals come from one bounded 30-day scan per
day.
- Fleet database snapshots run only on a proved read replica, with a
telemetry-specific freshness budget and server-side statement timeouts.
If no such replica is available, the collection is skipped; it never
falls back to the writer.
- Successful fixed-row snapshots are cached and re-emitted on each poll
tick, so gauges stay scrape-visible without raising query frequency.
- `buzz_usage_snapshot_available{family}` reports whether the latest due
refresh of that family succeeded. After a skipped or failed refresh, the
last values stay visible (with `buzz_usage_snapshot_age_seconds`) but
the family reads unavailable until a 60-second retry succeeds.
- The `all` rollback collector publishes the same availability gauges,
so dashboards gated on availability keep working during a rollback.
- Each due refresh outcome is applied by `apply_stock_refresh` /
`apply_activity_refresh`, the single production mapping from `Ok(Some)`
/ `Ok(None)` / `Err` to cache, availability, skip counter, and retry;
the regression test drives that mapping directly.
- `buzz_usage_snapshot_age_seconds` is emitted in the `all` rollback
too, so age-based alerts keep firing through a rollback.
- Every fleet telemetry attempt that completes lands in
`buzz_db_route_decision`; a query error on a proved reader records
`skipped/replica_error`. An attempt abandoned at the relay deadline
records no route decision and appears only as
`buzz_usage_query_skipped_total{reason="timeout"}`. The shared
`proved_reader` warnings no longer claim "routing to writer", because
the telemetry caller never does (serving callers still record their
writer decision in the route counter).
- The PostgreSQL production-method matrix now exercises
`usage_fleet_stock_snapshot`, so the new `reader/maintenance` pool pair
is proven to be emitted by a real Db method.
- Each fleet collection, reader proof included, runs under a relay-side
`tokio::time::timeout` (10s stock, 20s activity, both above their
server-side `statement_timeout`). A reader that stops answering expires
into the same failure mapping with
`buzz_usage_query_skipped_total{reason="timeout"}`, so it cannot stall
the leader's poller, hold the advisory lock past a liveness check, or
leave a frozen snapshot reporting available.
- Fleet telemetry reader checkouts (`ReaderOperation::Maintenance`) opt
into SQLx `close_on_drop` before their first post-acquire await, so an
abandoned collection's connection is closed within SQLx's 5s
`CLOSE_ON_DROP_TIMEOUT` instead of going through the unbounded
return-to-pool ping while it holds a slot in the reader pool that
serving reads share. Serving reads keep pooled reuse; the cost is one
reader reconnect per fleet refresh.
- On leader demotion, cached snapshots and availability are cleared, and
a fresh collection is forced after the lock is reacquired.
- Fleet-only mode passes no host map to the worker-snapshot storage
emitter. It therefore omits community attribution instead of reporting
every byte as `buzz_storage_unmapped_community_bytes`.
- Lifetime stored-message and active-channel totals are no longer
queried in fleet-only mode. Event throughput remains available through
the existing stored-event counter.
- No test-selector changes: main's `config::tests::` selector in `just
test-unit` and the `run-tests.sh unit` fallback already runs the new
replica-budget config test, and the new fleet schedule, availability,
and storage tests run through the existing bin `tests::` and
`storage_sweep::tests::` selectors.
- Added seeded PostgreSQL contract tests and operator docs covering
rollout, rollback, availability, the no-replica behavior, and leader
election.
No migrations, tables, indexes, or new persistent storage are added.
### What got simpler
The relay-side storage half of the original branch (totals-only S3
listing and `buzz_storage_community_breakdown_available`) was dropped.
Main moved S3 listing out of the relay in #7543 and #7845, so that code
was dead on arrival. The rework adds no storage code path; it only gates
attribution on the presence of a host map.
## Risk
Medium. The default changes which telemetry is emitted:
- In fleet-only mode, `buzz_communities_estimated` replaces the exact
`buzz_communities_total`.
- `buzz_total_messages` and `buzz_total_active_channels` are not
refreshed in fleet-only mode (also called out in the rollout steps of
`docs/usage-metrics.md`).
- Deployments without a verified reader (including local compose and
single-node self-hosts) skip database-derived fleet snapshots instead of
querying the writer, and report both families unavailable.
- Multi-pod dashboards must filter database and storage series to
`buzz_usage_poller_is_leader == 1`.
Setting `BUZZ_USAGE_METRICS_PER_COMMUNITY=all` restores the legacy
collector for temporary rollback.
## Verification
At head `1879ee3aedf4227942120a3b521c8d35fc7839fe` (tree `78477e74`;
merges main `da651ad6`; the PR's diff against main is byte-identical to
`1613823f`'s, minus the Justfile and `run-tests.sh` selector lines that
main's `config::tests::` selector now subsumes):
- On Blox: `cargo fmt --all -- --check` and `cargo clippy -p buzz-db -p
buzz-relay --all-targets -- -D warnings` pass. Relay bin `tests::`
34/34, `config::tests::` 55/55, `storage_sweep::tests::` 10/10 and
`store::usage::tests` pass, as do the four `fleet_` and lock PostgreSQL
tests that use scratch databases, including
`abandoned_fleet_collection_releases_its_reader_slot`.
- Against a freshly migrated PostgreSQL database:
`store::usage::postgres_tests::` 14/14,
`storage_sweep::postgres_tests::` and `buzz-db --test
observability_source` 14/14 pass. The new reader code passes main's
crate-wide typed-acquisition and serving-table policy checks.
- E2E at `1613823f` is reused for this head. It ran in an isolated relay
against a real PostgreSQL primary and streaming standby. In 2/2 runs a
stock read went dark mid-result; the deadline abandoned it at about 10s,
the reader pool's active and size counts dropped to zero about 1s later,
and a fresh activity read and the scheduled stock retry succeeded on new
backends while the dark session stayed open. The `all`-mode and
healthy-replica controls also passed. Main's merged changes are on the
writer path and in startup step timing, and touch none of the reader
checkout, the `close_on_drop` seam, the fleet poller or the deadlines.
At head `1613823fa8ef1a423bc1a499c57bc40d692fcd4f` (tree `12bc6d1b`;
test/docs-only on top of `57749418`), on a Blox workstation:
- `cargo fmt --all -- --check` and `cargo clippy -p buzz-db -p
buzz-relay --all-targets -- -D warnings` pass. Relay bin `tests::` 34/34
pass.
- New PostgreSQL test
`abandoned_fleet_collection_releases_its_reader_slot`: a one-slot reader
pool behind a loopback proxy that goes dark mid-session; the collection
is cut at a deadline, then a later acquire must succeed in under 8s on a
fresh connection. The dark call runs on a fresh `Db` with a cold Aurora
capability cache, so the first post-acquire await is the capability
probe. It passes in 3.4s. With `close_on_drop` removed, or moved below
the probe await, it fails with `PoolTimedOut` after the 15s acquire
timeout. The other three `fleet_` PostgreSQL tests that use scratch
databases pass.
- `each_fleet_family_is_abandoned_at_its_own_deadline` now has an outer
timeout: replacing the stock deadline with a bare `.await` fails it
(`Elapsed`) instead of hanging.
At head `fd194ec79d6c6796d6764564de225ad56485cfc7` (tree `79e94727`,
main merged at `932228936`), on a Blox workstation:
- `cargo fmt --all -- --check` and `cargo clippy -p buzz-relay -p
buzz-db --all-targets -- -D warnings` pass.
- Relay bin `tests::` nextest selection: 27/27 pass (plus
`store::usage::tests`), including
`fleet_refresh_outcomes_drive_availability_through_production_mapping`
and the new `stalled_fleet_collection_times_out_into_failed_refresh`
(paused time: two never-completing collections are abandoned at exactly
10s + 20s; both families read unavailable with cached values kept,
`reason="timeout"` skips, retry scheduled; the next tick runs and
recovers; no route decision is recorded for an abandoned attempt) and
`each_fleet_family_is_abandoned_at_its_own_deadline`. Compile-time
asserts keep each deadline above its `statement_timeout`.
- Falsifiability: replacing the stock deadline with a bare `.await`
makes the new test fail (the tick stalls); swapping the two deadlines
fails the per-family test; deleting `schedule.mark_stock_failure(now)`
from the `Ok(None)` arm fails the mapping test. Trees restored and
re-verified.
- At `970414db`: CI PostgreSQL Tests 809/809 including the
production-method matrix with `reader/maintenance`; Unit Tests ran
`finish_usage_read_records_every_attempt_in_route_decisions`.
- `just ci` / `just test` at `5b18914a` on Blox: not green for
environmental reasons (`mobile-check` could not reach pub.dev;
`_ensure-services` failed so service-backed suites did not run;
`buzz-acp git_bootstrap` fails identically on `origin/main`). PostgreSQL
and integration coverage comes from GitHub CI at this head.
E2E at the previous head `0a01e5f22` (isolated relay, real Postgres
primary plus streaming replica, worker storage snapshot, desktop
frontend in relay mode): with no reader, both families report
skipped/unavailable and route `skipped/disabled` (no writer queries);
fleet-only storage emits totals without `buzz_community_storage_*` or
unmapped bytes; `all` mode reports both availability gauges `1` with
per-community storage series; with a fresh replica, both fleet paths
route `replica/fresh` on `reader/maintenance` acquisitions; desktop
send/receive works. The cached success → due failure → recovery
transition was not reached live (hourly/daily cadence); it is covered by
the seam test above.
The original branch also recorded live verification at `aa3e7023`,
before this rework: reduced-idle cache re-emission, two-relay leader
takeover with demoted availability clearing, the no-reader skip with
60-second retry, `all` rollback restoring per-community series, and a
desktop smoke test. Those runs predate the availability and storage
changes and are not claimed for this head.
## Linear tracking
Related work: BUZZ-167.
---------
Signed-off-by: tornquist <tornquist@squareup.com>
Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
|
||
|
|
c96d707f6e |
fix(ios): let channel titles use available navigation space (#8137)
Channel titles could truncate while space remained between the navigation buttons. The title was sized before inheriting its displayed fonts, and longer names also hit a fixed 240-point cap. Use Auto Layout and stack views for the title, subtitle, avatar, and presence indicator. UIKit now updates the layout when fonts change without resetting the title frame. The navigation bar constrains the bubble between its buttons; short names remain compact and long names use the available space before truncating. [Before/after simulator captures](https://github.com/block/buzz/pull/8137#issuecomment-6041101824) ### Validation - All 97 native iOS tests passed on an iPhone 17 Pro running iOS 26.5. Coverage includes inherited text size, increasing and decreasing text size, 320/393/600-point widths, right-to-left avatar placement, presence indicators, and title accessibility/taps. - Exercised the production native header in the simulator and captured the full channel name with its member count after the refactor. No relay/community connection was needed. - [CI passed for the Auto Layout refactor](https://github.com/block/buzz/actions/runs/37644234496), including mobile tests, Android debug, iOS simulator, native pagination accessibility, and unsigned iOS release builds. Mobile push checks also passed. - The earlier broad local gate hit ACP deadline and desktop subprocess timing failures; all failed tests passed on retry, including the complete serialized ACP suite. Remaining checks and builds passed. - Agent review completed. Human app-test confirmation is not applicable under the author's standing Buzz mobile exception. ### Post-merge validation During the mobile release baking period, exercise long channel names at default and larger text sizes on narrow and wide screens; verify Back, Huddle, and title taps remain accessible and do not overlap. buzz-review-completed --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
da651ad672 |
buzz-db: purge retention-free deletions in the app and index mentions in the event transaction (#8128)
## Summary Part 1a of removing database objects that exist only in migrations. This PR moves into the app the work two migration-only triggers do today. It doesn't drop anything. On migration-built databases (production), `purge_soft_deleted_nip_rs` (0009/0011) and `purge_soft_deleted_buzz_mesh_status` (0019) turn a NIP-09 soft delete of a NIP-RS read-state event (kind 30078) or a mesh-status heartbeat (kind 30003) into a hard delete, and delete its mentions. `schema/schema.sql` has neither trigger, so on desired-state databases (CI) those rows remain as soft-deleted. The two schemas behave differently today. Changes: - `soft_delete_by_coordinate` and `soft_delete_event_and_update_thread_in_tx` now hard-delete matching NIP-RS and mesh-status rows and their `event_mentions` rows in the same transaction, before the generic soft delete. The classification predicate is the same one the 0011 and 0019 triggers use. - `insert_event_with_serving_write_guard` now indexes mentions inside the event transaction, under a savepoint, instead of after commit. A mention failure still only logs a warning. A concurrent deletion can no longer leave a mention row behind, and `guard_event_mention_live` will no longer be needed. The other post-commit `insert_mentions` callers write kinds 48101 and 13534 and a test helper, never 30078. - Both soft-delete `UPDATE`s exclude the same predicate (`AND NOT COALESCE(..., false)`). A retention-free row is either purged or left live, never soft-deleted, even when a replacement races the deletion under READ COMMITTED. The purge runs only for kinds 30078 and 30003. - While the 0011 hard-delete fence is still installed, the purge sets the transaction-local `buzz.nip_rs_hard_delete` opt-in. 1b removes it, along with the fence. **Rollout:** the triggers stay in place on purpose. Pods still running N-1 during a deploy depend on them. A follow-up (1b) will add a migration that drops the 5 functions and 5 triggers, remove the `buzz.nip_rs_hard_delete` opt-in code, and add the catalog-parity test. 1b should merge only after this PR is deployed everywhere. **What got simpler:** once 1b lands, NIP-RS and mesh deletion will follow one rule owned by the app, the same on both schemas, and the hidden trigger work will be gone. This PR adds about 130 lines to `event.rs`. A known gap for 1b: the replace path's Rust mesh classifier is stricter than the SQL predicate, and 1b has to make one of them authoritative before the triggers are dropped. 1b will then delete 5 triggers and 5 functions, plus the opt-in plumbing in `replaceable.rs` and `deletion.rs`. ### Related issue BUZZ-175, BUZZ-177 (Linear). ### Testing Blox, head `a2dac90646ca64fb3f961ffda292e3da976deffc`: - `cargo fmt --check`, `clippy -D warnings` (`buzz-db`, `buzz-relay`), and test discovery: all pass. - New tests: 4 of 4 pass. - `nip09_deletion_purges_retention_free_rows_and_mentions`, desired-state and `migration_schema_` variants. On the migration schema, a 13-shape table checks that the app and the triggers classify each shape the same way. A separate case covers a mesh row with a NULL `d_tag`. - `coordinate_deletion_racing_nip_rs_replacement_never_soft_deletes_read_state`, a deterministic lock interleaving. - `serving_write_guard_insert_indexes_mentions_in_event_transaction`: mentions are indexed in the event's own transaction (checked via `xmin`), and a mention failure leaves the event live. - Falsification: the race test fails against the pre-fix `event.rs`. The mention test fails with the merge-base post-commit `runtime/mod.rs`. Both NULL-`d_tag` variants fail with `COALESCE` removed. - `buzz-db` PostgreSQL lane (`scripts/postgres-test-run.sh -p buzz-db --lib --tests`): **409 of 409 passed** in an unscoped run. One box hangs on `writer_pool_rejects_non_read_committed_database_default` (an environment issue; it also hangs there at merge-base), and there 408 of 408 others passed. - E2E against a real relay on both schemas: 18 of 18 signed deletion cases, the serving-write mention path (success, pre-commit invisibility, isolated failure, recovery), and browser mention, send, reload and delete. - CI at `a2dac906`: all checks pass. The `PostgreSQL Tests` lane needed one rerun: the first attempt hit a known flaky test, `replica_fence::sample_writer_waits_for_shared_floor_lock_before_sampling_time`, which fails the same way on `main` at `001290a34`. buzz-review-completed (agent review and agent E2E complete; human test waived by Luke Tornquist) --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
e097c42996 |
feat(relay): time post-metrics-bind startup steps (#8132)
## Summary
Relay pods on large deployments can take over 5 minutes to open their
health port, and right now nothing shows which startup step is slow.
Startup timing ends at `metrics_bind`, and the slow work all happens
after it.
This PR times every startup step from Postgres connect through public
listener bind. It does not change startup behavior.
- Each step logs `Startup phase started` and `Startup phase finished`,
with `phase`, `elapsed_ms`, and `status` (`succeeded` / `degraded` /
`failed`, the same values `buzz_process_lifecycle` records use).
- Gauge `buzz_startup_phase_current{phase}` is `1` while a step runs and
`0` after it ends. A pod that is still starting shows exactly which step
it is on.
- Gauge `buzz_startup_phase_seconds{phase}` records each finished step's
duration.
- The startup NIP-43 membership reconcile logs `NIP-43 startup
reconciliation progress` every 5,000 communities, with `done`, `total`,
`republished`, `failed`, and `elapsed_ms`.
These are gauges rather than counters on purpose. Each value is written
once per boot, and a monotonic-counter scrape drops a series' first
sample, so a one-shot counter never shows up.
## Contract change
`deploy/charts/buzz/README.md` ("Early-startup telemetry contract") said
the early lifecycle phases are log-only. That still holds: `crypto_init`
through `metrics_bind` are unchanged and emit no metrics.
The README now also documents the post-bind steps. The guard in
`boot_lifecycle.rs` now allows exactly the two `buzz_startup_phase_*`
families. It still rejects any other startup, boot, or lifecycle family,
and it also rejects any exported label value that names an early phase.
## Design
- `crates/buzz-relay/src/startup_steps.rs` adds a closed `StartupStep`
vocabulary (18 steps) and a `StepTimer` guard.
- A step ends exactly once:
- `finish()` → `succeeded`
- `degrade()` then `finish()` → `degraded` (a non-fatal error branch)
- dropped without finishing → `failed` (covers `?` early returns and
panics)
- The NIP-43 and large-roster reconciles now return `ReconcileSummary {
repaired, failed }`. A sweep where any item fails ends its step
`degraded`, and the final log line includes the `failed` count. NIP-FI
warm ends `degraded` when any issuer did not warm. The deprecated
`reconcile_nip43_membership_snapshots` wrapper still returns `repaired`.
- Steps that are disabled by config (migrate, audit connect, NIP-FI
warm, git probe, NIP-43 reconcile) are not timed, so their series never
appear.
- The gauges are set once, so they age out after the existing gauge idle
timeout (2700 s with default intervals; logged as `idle_timeout_secs`).
A step that runs longer than that loses its `_current` series. The
README says so.
- Progress logging runs only for the `Bootstrap` reconcile. The 60 s
maintenance sweep stays quiet.
## What got simpler
Little got simpler in the code: this mostly adds instrumentation. One CI
gap closed along the way: the 54 `config` tests and the 9 non-ignored
`boot_lifecycle` tests, which ran in no CI lane, now run in `test-unit`.
The instrumentation is scoped as small as I could make it, and it is the
evidence needed for the follow-up that should delete code: moving the
per-community NIP-43 reconcile off the pre-bind path, after which the
900 s startup-probe override can go back to the chart default.
## Testing
- `startup_steps` unit tests (`DebuggingRecorder`):
- a running step is current and has no duration
- a finished step clears current and records its elapsed time
- dropped and degraded steps clear current and record a duration
- log capture: `finish`, `degrade`+`finish`, drop, and panic each emit
exactly one `Startup phase finished` record with the right `status`.
Mutating `degrade()` to a no-op and removing the ends-once guard makes
these tests fail.
- the vocabulary is unique and shares no names with the early lifecycle
phases
- Tests that capture logs or check callsite interest now share
`test_support::tracing_dispatch_lock()`. These are the `startup_steps`
helpers, two `telemetry` tests, and the `config` and `bridge`
log-capture helpers. tracing-core caches each callsite's interest across
the whole process. Without the lock, the new panic test lost its record
in 60 of 150 parallel runs of `telemetry:: startup_steps:: config::`.
With the lock it failed 0 of 300. Removing the lock from `gauges()`
brings the failures back (53 of 100 runs).
- `nip43_progress_logs_every_interval_inside_the_startup_sweep_only`
checks the cadence at 79,942 communities (15 progress lines, none at the
start or end) and table-tests that the maintenance sweep never logs
progress.
-
`boot_lifecycle::successful_main_emits_complete_lifecycle_and_reports_current_startup_step`
boots the real relay binary against a TCP peer that never speaks
Postgres. It asserts that `/metrics` shows
`buzz_startup_phase_current{phase="db_connect"} 1` and no finished-step
durations, and that no early phase leaks into metrics.
- **CI routing.** `just test-unit` picks relay tests by hand-maintained
filter. Before this PR, CI's filters selected none of the
`startup_steps`, `telemetry`, `config` or non-ignored `boot_lifecycle`
tests above; the NIP-43 cadence test is picked up by the existing
`handlers::side_effects::tests::` clause. Its infra-free relay stanza
now selects `startup_steps::tests::`, `telemetry::tests::` and
`config::tests::` (anchored so `nip_fi_config` is not selected twice),
and a new `cargo nextest run -p buzz-relay --test boot_lifecycle` line
runs the non-ignored `boot_lifecycle` tests (they spawn the real binary
and need no Postgres or Redis; the Postgres cases stay `#[ignore]`d).
`scripts/run-tests.sh` mirrors both, running `telemetry` and `config`
single-threaded because they mutate process env.
### Manual check
1. `just relay` (or run `buzz-relay` against local Postgres and Redis).
2. Once the relay is up: `curl -s localhost:9102/metrics | grep
buzz_startup_phase`. Every step that ran has a `_seconds` value, and
every `_current` value is 0.
3. In the relay logs, `"Startup phase finished"` lines show each step's
`elapsed_ms`.
---------
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
|
||
|
|
bd1ff00e48 |
release: push gateway chart 0.3.5 (#8141)
Publish gateway chart 0.3.5 through the normal immutable release workflow. This release includes Deployment metadata annotations from [#8136](https://github.com/block/buzz/pull/8136), while preserving chart-owned policy markers and existing default manifests. Only the chart version and release notes change. The gateway binary appVersion remains 0.1.0. ### Validation Full gateway chart render suite and release-contract validation passed. The feature's full repository validation and exact-head owner review passed before merge. Publication will be verified from the OCI registry before advancing the wrapper dependency. `buzz-review-completed`: Agent validation passed; automated DCO finding was reconciled against the signed exact-head commit and green DCO check. Tom confirmed the human-testing rule is inapplicable to this chart-only release; personal testing is not claimed. Signed-off-by: Tom Brow <tomb@block.xyz>push-chart-v0.3.5 |
||
|
|
9f5a25a41e |
feat(push): support Deployment metadata annotations (#8136)
Consumers currently need a Kustomize patch to place secret-reloader annotations on the gateway Deployment. Add optional `deploymentAnnotations` so they can express that configuration through Helm values. The setting applies only to Deployment metadata. The chart's release-namespace and external-network-policy annotations remain reserved and cannot be overridden. ### Validation Full gateway chart render suite passes, including schema rejection of reserved keys and non-string values, preservation of external-policy ownership markers, and unchanged Pod annotations and migration hooks. Repository-wide `just ci` passed with `RUST_TEST_THREADS=1`. The first parallel run failed five unrelated timing/transport tests; all five passed isolated reruns and the full serialized run completed successfully. `buzz-review-completed`: Codex review is clean on the current commit and agent render validation exercises the change. Tom confirmed the repository human-testing requirement does not apply to this chart-only change; personal human testing is not claimed. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
b9e851d7be |
fix(buzz-pair): install rustls ring CryptoProvider before WSS (#7998)
## Summary - `buzz-pair` panics on any `wss://` relay (including the documented default `wss://relay.damus.io`) because no rustls `CryptoProvider` is installed. `ws://` is unaffected. - Sibling binaries already install ring explicitly (`crates/buzz-cli/src/lib.rs`) and take a direct `rustls` dep with the `ring` feature so a unified release build can still select a provider. - This applies the same two-line pattern to `buzz-pairing-cli`: direct `rustls` dep (`ring` + `std`) and `install_default()` at the top of `main()`, before any connection. Fixes #7927. ## Test plan - [x] Diff matches the established `buzz-cli` install pattern (same dep features, same `let _ =` swallow on double-install). - [x] `cargo build -p buzz-pairing-cli` and `buzz-pair source --relay wss://relay.damus.io` reaches "Waiting for target" without the rustls panic. - [x] `buzz-pair test-vectors` still exits 0 (no WSS, install is a no-op). --------- Signed-off-by: Bartok9 <259807879+Bartok9@users.noreply.github.com> |
||
|
|
5ff83d82f8 |
fix(desktop): address workflow deletions at the workflow's actual owner (#2669)
Fixes #2671. ## Problem Deleting an agent-created workflow from the desktop app appears to succeed, but the workflow keeps running and stays in the list. Since workflows are commonly created by agents, this makes most workflows undeletable from the UI. ## Root Cause `delete_workflow` builds the NIP-09 `a`-tag coordinate with the **caller's** pubkey: ```rust let builder = events::build_workflow_delete(&workflow_id, ¤t_pubkey_hex(&state)?)?; ``` kind:30620 is addressable by `(author, d-tag)`. For an agent-created workflow, `30620:<caller>:<workflow-id>` names a record that does not exist. The relay accepts the kind:5 (a self-addressed delete always passes validation) and deletes nothing; the UI reports success. Observed live: a delete of an agent-owned workflow produced `["a","30620:<human-pubkey>:<workflow-uuid>"]` while the kind:30620 event is authored by the agent — the coordinate can never match. ## Fix Fetch the workflow's kind:30620 event first (the same `#d` query `update_workflow` already uses) and build the coordinate from the event's author. Relay-side validation already authorizes the owning human for agent-authored coordinates via `is_agent_owner`. Deleting an unknown workflow id now surfaces `workflow not found` instead of silently publishing an unmatchable delete. This is the client half of the fix. The relay half is #2242: today the relay executes a-tag deletions scoped by the delete's signer, so a human-signed delete of an agent workflow still no-ops server-side. Without this change, #2242 alone won't help desktop users, because the client addresses a record that doesn't exist. Together they make delete work end to end. Related but deliberately not included: deleted workflows leaving their kind:30620 event queryable (ghost list entries) is #2390, with a relay-side fix open in #1340. ## Validation - `cargo test` in `desktop/src-tauri`: 1617 passed, 0 failed, 14 ignored. - `cargo clippy --all-targets` and `cargo fmt --check`: clean. - Live repro against the hosted relay: the stock client's kind:5 for an agent-owned workflow named the signer in the coordinate; the relay returned `accepted: true` and the workflow remained live and kept firing. - With the fix: the delete event carries `30620:<agent-pubkey>:<workflow-id>`; an owner-signed deletion of that exact coordinate was verified to remove the workflow record. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
003770cb79 |
fix(buzz-acp): bound sibling-gate lookup failures instead of caching them (#7216)
## Summary Fixes #5450. Supersedes #5459 (see "Related issue"). In `owner-only` / `allowlist` respond modes, the inbound author gate admits a sibling agent (same owner, proven by the NIP-OA auth tag on its kind:0) only after `check_sibling_via_profile` fetches that profile over REST (`POST /query`, 2 s timeout). The boolean result of that lookup was cached in `OwnerCache` **for the process lifetime, unconditionally** — so a timeout, a network error, or a non-2xx response was cached as "not a sibling". One relay blip at first contact and the agent silently ignores that teammate bot until restart. The owner never hits this path (`author == owner` short-circuits with no network call), which is exactly the observed shape: agents keep answering their owner while dropping every agent-to-agent mention. Two things kept it invisible: the author-gate drop was logged at `debug` (hidden by the default filter), and `RestClient::request_with_retry` returned non-retriable HTTP statuses (401/403/500) with no log line at all. ### What changed (`crates/buzz-acp`) - **Tri-state verdict.** `check_sibling_via_profile` returns `SiblingVerdict::{Sibling, NotSibling, Indeterminate}`. Only a *fetched* profile yields a cacheable verdict. A failed query, a malformed body, or a missing kind:0 (not published, or not yet replicated to this relay) is `Indeterminate` — nothing was attested. - **Bounded per-author retry instead of retry-per-event.** An indeterminate lookup fails closed for the current event and arms a retry deadline: 5 s, doubling per consecutive failure to a 60 s cap (`SIBLING_RETRY_BASE` / `SIBLING_RETRY_MAX`). Events inside the deadline are denied with no relay call; expiry re-arms exactly one lookup; a proven verdict replaces the deadline. This matters because the lookup awaits inline in the sole inbound-event loop — an unthrottled retry under a persistent `/query` outage lets one chatty sibling stall ingress for the owner too. With the backoff, a dead relay costs at most one inline lookup per author per minute. - **No wholesale cache clear at capacity.** Replacing an author already present never evicts. A new author at the 256-entry cap evicts exactly one entry — the longest-expired retry record if any, else an arbitrary entry — so proven verdicts and the other authors' backoff tiers (their `failures` counters) survive saturation. - **Diagnosability.** The author-gate drop logs at INFO; `request_with_retry` WARNs on non-retriable HTTP statuses. ### Reviewer notes - Fail-closed security posture is unchanged: an unverifiable author is still dropped. Only what is *remembered* about that author changes. - A fetched profile with no verifying owner attestation is still a cached `NotSibling`, so a stranger spamming an owner-only channel doesn't cost a REST round-trip per message. A *missing* profile is retried under the backoff, which removes the need to restart a harness after a sibling's profile is published late. - When no expired retry record exists, the single eviction victim is an arbitrary map entry rather than LRU; per-entry access timestamps didn't seem worth it for a cap the fleet sits far below. Happy to change if you'd prefer it prefer throttled entries over proven ones. ### Related issue - Fixes #5450. - #5459 by @whyihaveyou introduced the tri-state verdict and the no-cache-on-indeterminate rule for the same bug. It has been open without review since 2026-08-10 and is now conflicting with `main`; it also retries the inline lookup on every event and leaves the wholesale `map.clear()` in place. This PR carries the same first half and adds the bounded retry and eviction behaviour that came out of review on our fork (yjc801/buzz#112). Credit for the diagnosis and the verdict design goes to that PR. ### Testing Nine regression tests in `author_gate_tests` bind the production seam (`is_owner_or_sibling` → `check_sibling_via_profile`) against loopback `/query` stubs: - unreachable relay ⇒ denied, **not** cached as a verdict - missing profile (`[]`) ⇒ denied, not cached, throttled under the backoff - fetched profile without a verifying tag ⇒ denied and cached `false` - kind:0 carrying a real `compute_auth_tag` attestation ⇒ admitted and cached `true` - flaky relay (500 until healed): a burst inside the deadline makes **no** relay calls, expiry re-queries once, recovery caches the proven verdict and ends throttling - backoff sequence 5 → 10 → cap, tracked per author - re-arming one author's deadline in a full cache keeps the other 255 proven verdicts - a new author at capacity displaces an expired record first, then exactly one entry - with two expired records at capacity, only the longest-expired is evicted and the survivor keeps its backoff tier `cargo test -p buzz-acp`: all unit and lifecycle tests pass on this branch; `cargo clippy -p buzz-acp --all-targets` and `cargo fmt --check` clean. Commit is DCO-signed. Live context that led here: on a hosted relay whose REST endpoint flapped for a day, every local agent that restarted with an empty cache stopped answering its teammates within hours while still answering the owner; two agents sat online with zero turns. Not live-verified post-fix upstream — verified by the tests above and by the fork's review rounds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Junchao Yan <yjc801@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
1a2ddf9eaa |
feat(relay): private read-state accessory API (#7906)
Buzz BFF: Private read-state accessory API (`/buzz/v1`) |
||
|
|
46dcc01873 |
fix(acp): keep agent-to-agent thread replies in the thread (#8124)
## Summary When an agent @mentions another agent inside a thread, `resolve_reply_anchor` returned `None`, so the receiving agent's `<context>` had no `--reply-to` instruction. A harness that follows the base prompt literally then posted its reply at the channel top level. Agent↔agent turns in a thread now anchor to the triggering event. The reply stays in the thread and can still nest (the reason #1281 left these turns unanchored). Top-level agent↔agent turns and all human-facing turns are unchanged. Native steering keeps the running turn's `<context>`, so a message steered into a turn anchored to its trigger would be answered under that trigger too. The prompt task now records whether its anchor is the trigger, next to its DM classification (`PromptDmClassification` → `PromptRouting`). The steer guard refuses native steering into a channel thread turn whose anchor is the trigger, or not yet recorded. That message takes the cancel+merge path, whose re-prompt carries its own `<context>`. Behavior change: a same-thread follow-up that arrives before the running turn's prompt is formatted (for example during `session/new`) now cancels and merges instead of steering natively, because the anchor is not known yet. Once the prompt is formatted, follow-ups to human-facing thread turns steer natively as before. ### Related issue Fixes #8122 ### Testing - `cargo test -p buzz-acp -- --test-threads=1` (BUZZ_* env cleared): 1046 passed. `cargo clippy -p buzz-acp --all-targets -D warnings` clean. Pre-push workspace `rust-tests` hook passed. - New regressions, each confirmed to fail when its guard is removed: - `human_follow_up_to_agent_thread_turn_cancels_and_merges`: a real dispatched turn with agent profiles served by the relay fixture. A human message in the same thread is not steered natively. - `test_human_merged_into_agent_thread_turn_anchors_to_root`: the merged re-prompt anchors to the thread root. - `channel_same_thread_follow_up_during_prompt_steers_natively` / `..._during_setup_cancels_and_merges`. - Live local (`buzz-relay` + `buzz-acp` at this head + `buzz-pi-acp`; agent B is a CLI identity with a NIP-OA auth tag): - B @mentions A in a human-rooted thread → A's `<context>` says `--reply-to <B's event>`, and the reply nests under B. On `main` the `<context>` had no reply instruction. - B asks A to `sleep 30` then reply; while A works, the human @mentions A in the same thread → `control signal sent to in-flight task` (cancel+merge), the re-prompt says `--reply-to <root>`, and both replies land at the thread root. --------- Signed-off-by: Leo <a3812943bac59c128e311ca05f83bf91113f179bb6fa9ca96c3d8efe15c330e6@buzz.block.builderlab.xyz> Co-authored-by: Leo <a3812943bac59c128e311ca05f83bf91113f179bb6fa9ca96c3d8efe15c330e6@buzz.block.builderlab.xyz> |
||
|
|
c6dd587e2a |
release: push gateway chart 0.3.4 (#8126)
Publish push gateway chart 0.3.4 through the normal chart release workflow. The chart version and implementation were merged in [#8113](https://github.com/block/buzz/pull/8113); this release PR records the release notes and triggers the immutable chart tag and OCI publisher. Publication is authorized. The pending security review of #8113 is a blocker for pinning the chart in the later infrastructure PR, not for this publication. No gateways or relays are activated. ### Validation The merged implementation passed the complete chart render suite, chart CI, and all 14 local wrapper tests. This release changes only the changelog; chart contents are unchanged from the validated implementation. Published OCI contents and digest will be verified after publication. --------- Signed-off-by: Tom Brow <tomb@block.xyz>push-chart-v0.3.4 |
||
|
|
73a2a05c97 |
feat(push): support platform-managed gateway runtime pods (#8113)
Platform-owned service meshes need runtime pod identity and a replacement network policy without patching rendered gateway manifests. Add runtime-only pod labels, service account and shutdown-grace settings, plus an explicit named replacement-policy mode. Migration isolation remains independent; the default render is unchanged. Chart 0.3.4 publication and live deployment verification follow in the separate release and deployment work. ### Validation Added tests cover runtime-only settings, reserved selector labels, minimum shutdown grace, replacement-policy acknowledgement and conflicting settings. The complete chart render suite and release-contract tests pass. Default rendering matches the current main chart byte-for-byte. Repository-wide `just ci` reached unit tests but failed in existing buzz-acp tests outside this chart change. Live mesh behavior has not been exercised. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
001290a34d |
chore(release): release Buzz Desktop version 0.5.27 (#8125)
## Buzz Desktop release v0.5.27 - **Frozen main:** `af5bb0af488784c3707e99c87e0675a729711a6e` - **Reviewed candidate:** `7a2fb3cc2d401e91e67d9819741d9025278cbec0` - **Previous desktop release:** `desktop-v0.5.26` - **Proposed immutable tag:** `desktop-v0.5.27` 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 <wpfleger@block.xyz> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
403c19f673 |
fix(desktop): show OpenAI-compatible endpoint settings in the baked-env UI (#8117)
## Summary `is_safe_to_reveal` (`desktop/src-tauri/src/managed_agents/env_vars.rs`) is the single default-deny allowlist for showing baked env values. It lists `DATABRICKS_HOST`/`DATABRICKS_MODEL`/`DATABRICKS_MODEL_FILTER` but not their OpenAI-compatible counterparts, so a build that bakes `OPENAI_COMPAT_BASE_URL`, `OPENAI_COMPAT_MODEL`, `OPENAI_COMPAT_API` shows them as `••••••`. Adds those three non-secret settings to the allowlist: an endpoint URL, a model id, and an `auto|chat|responses` enum. **No secret is revealed: `OPENAI_COMPAT_API_KEY` is deliberately not added and remains masked.** ### Related issue None found. ### Testing - New test `baked_env_openai_compat_endpoint_is_unmasked_but_key_stays_masked` covers the three revealed settings and asserts the API key stays masked; `baked_env_allowlist_is_case_insensitive` extended. - `cargo test --lib baked_env` in `desktop/src-tauri`: 11 passed. `cargo fmt` clean. - Not exercised in the running desktop UI. Signed-off-by: Tom Bedor <tbedor@squareup.com> Co-authored-by: Tom Bedor <tbedor@squareup.com> |
||
|
|
1e2281fd45 |
feat(buzz-agent): send User-Agent on LLM requests (#8116)
## Summary `buzz-agent` builds its LLM `reqwest::Client` without a User-Agent, and reqwest sends none by default, so OpenAI-compatible providers that identify clients by product token see buzz-agent as an anonymous caller. This sets `User-Agent: buzz-agent/<crate version>` on the shared LLM client in `crates/buzz-agent/src/llm.rs`. The version is `CARGO_PKG_VERSION`, the same value `agentInfo` reports over ACP. No request body or endpoint changes. Open point: the crate version is the workspace version (`0.1.0`), so every build reports the same version. A per-build version would need the desktop to pass one through at spawn time; this PR does not do that. ### Related issue None found. ### Testing - The test stub now captures the `User-Agent` request header; new `llm_requests_carry_buzz_agent_user_agent` asserts `buzz-agent/<version>` on a Chat Completions POST. - Verified red without the `.user_agent(...)` line (`left: None`), green with it. - `cargo test -p buzz-agent --lib llm::tests`: 194 passed. `cargo fmt` clean; the one clippy warning is pre-existing (`hints.rs:33`). - Not exercised in a built desktop app. Signed-off-by: Tom Bedor <tbedor@squareup.com> Co-authored-by: Tom Bedor <tbedor@squareup.com> |
||
|
|
af5bb0af48 |
docs(changelog): remove hand-written Unreleased section (#8123)
`CHANGELOG.md` is written entirely by `scripts/desktop_release.py`, but #7692 hand-added a `## [Unreleased]` section at the top. `generate` puts each new release block directly under `# Changelog`, which leaves `## [Unreleased]` sandwiched between the new block and the previous release. `validate` reads a release block up to the next `## v` heading, so it pulls the Unreleased lines into the new block and fails with `changelog block is not deterministic for recorded candidate base`. Every `just release-desktop` run is blocked until the section is gone. This removes the section. Its only entry (#7692, `036215636`) is already in the generated v0.5.27 changelog block, so nothing is lost. Signed-off-by: Will Pfleger <wpfleger@block.xyz> |
||
|
|
9c6fa14aa0 |
feat(desktop): offer Claude effort levels in agent edit and create (#8059)
Claude Code agents now get a thinking-effort dropdown in the agent Edit and Create dialogs, matching the one buzz-agent and Codex agents already have. ## Behavior - **Where levels come from:** Claude effort levels come from the Claude model data for the model that will actually run after Save, resolved in order: explicit model, then inherited default (linked definition or global), then adapter default. Stored Claude session levels are not used, because the stored session data does not record which model produced them. - **Edit:** for an agent linked to a definition, effort follows the definition's model, which stays authoritative. Save never sends a model for linked agents. - **Create:** effort is offered for local agents only and is stored on the instance. Allowed levels are recomputed on every render, so collapsing Advanced and then switching to a model without effort (e.g. Haiku) drops effort from the request. Non-local effort is rejected before any create side effects. - **Persistence:** `create_managed_agent` sets the picked effort when it builds the record and drops stale effort env aliases. Its body runs over any Tauri runtime, so a mock-app test drives the real create path and reads the record back from disk. That test runs in a child process with temp app-data and nest folders, and a test-only switch turns keychain use off so agent keys stay inline in the temp agent file and the OS keychain is never touched. - **Observer store:** live session-config captures are written to the store before the dialog refreshes; archived captures are read-only. - **Codex:** its existing session-native effort picker is unchanged. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
d0fd96fca6 |
BUZZ-175: Route serving event writes through the tenant lock (#7828)
## Summary
Route every serving event write through one community-admitted
transaction entry point. `Db::begin_event_write_transaction(community)`
is now the only `Db` constructor for event-write transactions: it takes
the shared community admission lock before returning, so callers that
use it cannot take domain or row locks ahead of tenant admission, and a
quiescing community rejects the write at entry. Database triggers and
commit-time fences remain the authoritative backstop.
This migrates normal, replaceable, roster, relay membership, reaction,
reminder, admin-delete, push-owned, artifact-accept, huddle-join
(48101), workflow-deletion, and command-executor event writes. Coupled
mention writes stay in the same transaction; pool-level
`insert_mentions` now opens its own admitted transaction.
**Review fixes (since `950d5f6`):**
- The unguarded event-write constructor is gone. Removed with it: the
deprecated `begin_transaction` alias, the test-only replica-floor
constructor, and `begin_community_write_transaction` (now identical to
the single constructor). Artifact accept, the huddle join, and both
workflow-deletion paths now admit before any domain or row lock;
`persist_command_event` no longer hand-rolls `guard_transaction`;
read-only `query_artifacts` no longer opens an event-write transaction.
- `insert_channel_head_checked` is now `insert_canvas_head_checked`: it
rejects non-canvas kinds before opening a transaction and always takes
the coordinate lock.
- Source policy now also scans `buzz-relay`: any function that hands a
transaction to an event-write helper must have opened it through the
admitted constructor. The scan drops each `#[cfg(test)]` item instead of
stopping at the first one: it tracks brace depth and ends the item on a
line ending in `;` or `}` (ignoring a trailing `//` comment, even after
a string that contains `//`), with a column-0 `}` as fallback. Fixtures
pin a unit struct, a multi-line static, a `const { … }` block, a struct
with fields, a module, and one-line items with trailing comments,
including after `"https://…"` and `"ws://…"` strings; a raw opener after
each must still be flagged. Known limit: braces inside string or char
literals or comments are still counted, and only `//` trailing comments
are recognized, so a test-only item with unbalanced braces in a literal
or comment, or with a trailing `/* */`, would hide what follows; none
exists today. The scan checks that admission is present; the per-path
PostgreSQL tests pin that it comes before domain locks.
- `just test-unit` (and the non-nextest fallback in
`scripts/run-tests.sh`) now runs `buzz-db --test observability_source`.
Before this, no CI job ran these source-policy tests: the unit lane ran
`buzz-db --lib` only and the PostgreSQL lane runs only ignored tests.
- Renamed or removed since `950d5f6`: `begin_transaction`,
`begin_community_write_transaction`, and the replica-floor test
constructor are gone; `insert_channel_head_checked` →
`insert_canvas_head_checked`; test helper `quiesce_community_for_tests`
now delegates to `main`'s `set_deletion_state`, which reads the real
`deletion_fence_generation` instead of hard-coding `'0'`;
`runtime::insert_mentions` is pinned to the admitted constructor by
`pool_level_insert_mentions_opens_the_tenant_local_chokepoint`.
**What got simpler:** four transaction constructors collapse to one, and
the hand-rolled admission guard is deleted. The compiler rejects callers
of the removed constructors, but a transaction opened from `Db::pool()`
can still reach the public `*_in_transaction` helpers; only the
source-policy scan and the database fences catch that until BUZZ-254.
**Behavior note:** #7932 made `insert_event` skip the transaction for
kinds that are not listener mentions. Every serving event write now has
to run in a transaction that holds the community admission lock, so
`insert_event` always opens the admitted transaction again. `main`'s
`soft_delete_event_and_update_thread` / `_in_tx` split and its
removal-marker guard are kept; the outer function now opens the admitted
transaction.
### Related issue
[BUZZ-175](https://linear.app/squareup/issue/BUZZ-175). Caller-migration
stage following #7706 (merged); rebased onto `main` at `5173fad66`.
Follow-up: [BUZZ-254](https://linear.app/squareup/issue/BUZZ-254) makes
event-write helpers require a type-level admitted transaction.
**Known debt, out of scope:** `create_channel`, `update_channel`, and
`insert_thread_metadata` write channel and thread rows (not events)
outside the admitted constructor.
### Testing
At `efe4a94623dd69973780a3c39f16dd972cf7cb76` on a cloud workstation:
fmt; `buzz-db` clippy (all targets, `-D warnings`);
`observability_source` 14/14; `just --dry-run test-unit` includes the
new step. Reverting just the end-of-item check to an earlier rule makes
a trailing-comment fixture fail.
At `eaf190ae59d1595741c3d19dc4c23ce492b5f4d4` (the commit after it only
changes this test file, `Justfile`, and `scripts/run-tests.sh`): full
GitHub CI green; on the workstation, clippy for `buzz-db` and
`buzz-relay`, ordinary `buzz-db` 154/154, `just test-unit` green, and
the PostgreSQL lane 805 of 806 (exception below). An end-to-end run
against a local relay and the built desktop app covered artifact accept,
huddle join, and workflow deletion for both a healthy and a quiescing
community, plus p-tag reaction rollback and replay of the same signed
event.
New tests: one fenced-community test per migrated path (artifact accept,
huddle join, both workflow deletions) puts the community into
`quiescing`, holds the lock the path takes next, and asserts rejection
at entry with nothing stored; a DB-free test pins the non-canvas
rejection.
Exceptions:
-
`runtime::postgres_tests::writer_pool_rejects_non_read_committed_database_default`
hangs on that workstation's local Postgres 16 at both this head and an
earlier `main` base, `f0eb5575f` (2/2 runs each). The test, `Db::new`,
and the pool setup are unchanged by this PR, and the test passes in
GitHub CI. Live traces put the hang in the client, not the database: the
1 s acquire timeout stops firing after sqlx's `after_connect` rejection
path under tokio's `current_thread` runtime. No database lock waits were
observed.
- Two `observability_source` assertions were already failing on `main`,
unnoticed because no CI job ran that test binary: a stale
`fetch_optional` pin after #8005, and lease-seam attribution that now
lives in the `bounded_serving_lease_sql` helper. Both are corrected
here, and the binary now runs in `just test-unit`.
Generated with Codex
---------
Signed-off-by: tornquist <tornquist@squareup.com>
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
|
||
|
|
9322289364 |
feat(mobile): follow the portable mention rules, including DMs (#8102)
🤖 ## Summary The mobile app's @mention chooser now follows the same rules as the desktop app (buzz-app). The same typed text finds the same people in the same order on both apps, and sending works the same way. - **Same matching and order.** Mobile uses the shared mention rules and runs their shared test cases. A multi-word search such as `@Mary J` still searches the community directory, so it can find "Mary Jane" even when she is not in the channel. - **DMs can mention people outside the DM.** The chooser marks them "not in DM". Nobody can be added to a DM, so the message sends without a prompt. Outside people go out as references, so they are not notified. This matches buzz-app #510. - **Channels still ask.** In a stream or forum, mentioning someone outside the channel asks "Invite" or "Do nothing" before the message sends. A channel with no type tag counts as a stream. - **Sessions do not search the directory.** Only session members appear. - **A failed directory search stays visible.** The chooser shows the error and a Retry button. Before, it closed. - **Search results stay in their community.** If you switch communities while a search runs, its people and its error do not show in the new community. The search starts again there. - **A failed send check keeps the draft.** Before a message with a mention sends, the app checks who is in the channel. If that check fails, the message stays in the compose bar and a notice offers Retry. Before, nothing was sent and nothing was shown. - **Invite stays in its community.** If you switch communities while the "Invite" question is open, Invite adds nobody. ### Screenshots The real compose bar widget, rendered by Flutter's test renderer with the app's fonts and theme. The data is sample data. These are not device captures. | DM: outside person is "not in DM" | Stream: outside person is "not in channel" | |---|---| | <img width="320" alt="DM chooser" src="https://github.com/user-attachments/assets/0e884518-2386-4cd1-9f19-16661aaa194e" /> | <img width="320" alt="Stream chooser" src="https://github.com/user-attachments/assets/5395e4e1-032e-459b-98c6-0126c060c4d9" /> | | Stream: send asks first | Session: members only | Search failed: Retry | |---|---|---| | <img width="260" alt="Stream ask on send" src="https://github.com/user-attachments/assets/2b988257-3c6a-445d-acfa-fca47b9752fe" /> | <img width="260" alt="Session chooser" src="https://github.com/user-attachments/assets/e193551f-f5bf-41d9-aa29-bc8665c643f9" /> | <img width="260" alt="Search failed with Retry" src="https://github.com/user-attachments/assets/e8c4b9bb-067e-4fe9-9fdd-4e5691bc66be" /> | ### Details - `mobile/lib/shared/mentions/mention_rules.dart` holds the rules: query syntax, match tiers, sort order, Space selection of an exact unique name, stable rows while results arrive, and `p` and `mention` tags. A `p` tag notifies the person. A `mention` tag is a reference and does not notify. - `mobile/test/shared/mentions/mention-rules.fixtures.json` is an exact copy of the buzz-app fixture file. `mention_rules_test.dart` runs every case. - Known difference: desktop gives the directory to a channel with an unknown explicit type (not stream, forum, DM or session). Mobile does not. No such type exists today. ### Related issue None found. Desktop counterpart: block/buzz-app#510. #7513 also edits `send_message_provider.dart` and may conflict. ### Testing `just mobile-install mobile-check mobile-test` passes: analyze clean, 2998 tests pass on `49c44653` (before rebase). The pre-push hook ran `just mobile-check && just mobile-test` again on the rebased head `de1ca080` and passed. Each review fix was mutation-checked: removing the fix makes its new test fail. --------- Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
166d383ca7 |
feat(mobile): read web app catch-up marks for unread state (#8103)
🤖 ## Summary The web and desktop app (`buzz-app`) saves a "caught up" mark when you scroll to the bottom of a channel and stay there, and another one for a thread. Mobile did not understand these marks. It relied on the separate per-message read marks that `buzz-app` also kept. `buzz-app` is about to stop keeping those per-message marks under the "caught up" mark (block/buzz-app#590), to save space in the synced read state. With this change, mobile reads the "caught up" marks with the same rules as `buzz-app`: - `activity:<channel>` reads ordinary top-level messages up to its time. It does not read thread replies, mentions, broadcasts or DM messages, because reaching the bottom of the channel does not prove you saw those. - `thread-activity:<root>` reads the replies in that thread up to its time. So a message you read in `buzz-app` stays read on mobile: in the app badge, the channel list unread state, the "new messages" divider when you open a channel, and the Mark read / Mark unread action. Mobile does not write these marks. It only reads them. ### Related issue None found. Companion to block/buzz-app#590. The old desktop app in this repo is intentionally not changed. ### Testing - New badge tests: `activity:` clears an ordinary top-level message, and does not clear a mention, a thread reply or a DM. `thread-activity:` clears replies in its own thread only. The two "clears" tests fail without this change. - New tests for the per-message read check and the "ordinary message" rule. - `flutter analyze`, `dart format` check and the full mobile test suite pass locally. - Not yet exercised on a device. --------- Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
1e67395fd1 |
docs(identity): tighten enterprise adapter contract (#8069)
Tightens `docs/enterprise-identity-adapter.md` to define the enterprise adapter contract implemented by kgoose in [cash-server#132770](https://github.com/squareup/cash-server/pull/132770) and the Buzz Desktop client in [buzz-app#562](https://github.com/block/buzz-app/pull/562). - Requires canonical `wss://host[:port]` relay URLs with a lowercase scheme, the lowercase ASCII (A-label) host form with no trailing dot, the default port omitted, and no trailing slash, path, query, or fragment. Adapters MUST configure relays in canonical form, compare `relay_url` against that exact form, and reject unknown relays with 403 `authorization_denied`. - Requires NIP-98 proof freshness of at most 60 seconds old and at most 5 seconds in the future, with the proof bound to the endpoint, method, and exact request body. - Rejects content-encoded request bodies with 400 `invalid_request`. - Caps assertions at `exp - iat <= 300` seconds and the adapter session expiry, requires `typ` `nip-fi+jwt`, and validates the echoed `nostr_pubkey`. - Defines refusal and retry handling: Clients MUST retry 429 and 503 with bounded backoff whatever the `error` code. Clients MUST treat any other status, a 400/401/403/413 response with a contract-undefined code, or a rejected `200` response including one it cannot parse, as a refusal: keep the session, show it as refused, and not retry automatically. Network failures retry with bounded backoff. This tightens the enterprise adapter contract restored in [#8064](https://github.com/block/buzz/pull/8064). --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>mobile-v0.20.0-rc.1 |
||
|
|
c9143ed8f7 |
chore(push): release gateway chart 0.3.3 (#8107)
Prepares publication of the Buzz push gateway chart 0.3.3, including the optional migration Pod annotations already merged in #8105. This PR adds release notes only; chart templates, values, and schema are unchanged from the reviewed implementation. Merging the `push-chart-release/0.3.3` branch triggers the normal release workflow: create `push-chart-v0.3.3`, validate the chart, then publish `oci://ghcr.io/block/buzz/charts/buzz-push-gateway:0.3.3`. Keep this PR in draft until publication is explicitly authorized. It does not deploy a gateway. ### Validation Focused migration annotation renders, release contract checks, and local chart packaging pass. The merged implementation passed chart CI and Jude's automated review in #8105. The earlier repository-wide local `just ci` run failed in unchanged ACP tests; it was not rerun for release notes. The registry manifest digest can only be verified after publication. --------- Signed-off-by: Tom Brow <tomb@block.xyz>push-chart-v0.3.3 |
||
|
|
fcf5f044b3 |
fix(desktop): use object-scale-down for inline markdown images (#4686)
Fixes #4384. A dim-less markdown image with no cached decode gets `DEFAULT_IMAGE_RESERVE = { height: 256, width: 384 }` and `useFixedReserveBox: true` in `resolveImageReserveBox`. The frame is that big on first view, and the inline `<img>` uses `object-contain` which scales UP to fill the frame — so a 46×20 shields badge renders as a giant blurry block. The same message renders at the natural size on the second view because `rememberDecodedImageDimensions` populates the cache, but the inconsistency between first and subsequent view is the bug. Switches `object-contain` → `object-scale-down` on `ProgressiveImage.tsx`'s shared `IMAGE_CLASS`. Both `object-contain` and `object-scale-down` scale down to fit, but only `object-scale-down` also refuses to scale up — a small image now sits centered in the reserve box on first view, never upscaled. The layout-shift guarantee for large images is preserved (they still scale down to fit the reserve), matching the issue's stated constraint. Chose option 2 ("Alternatively/minimally") from the issue over option 1 because: - It's a one-class-name change with zero new state, zero new effects, and zero risk to the existing layout-shift guarantee that the surrounding `useFrozenImageReserve` is explicitly designed to preserve. - Option 1 (collapse the frame on decode) is the issue author's preferred direction but requires deciding whether the layout shift is acceptable *shrink*-only — a direction question that should be confirmed with the maintainer first. The issue explicitly says "Happy to open a PR for option 1 if the direction is acceptable" — that's the sign-off step, not this PR. If maintainers want the full first-view-equals-second-view behavior, option 1 is a follow-up that builds on this base. Not added: a regression test. The current desktop test infra (`desktop/src/shared/ui/markdown/markdown.test.mjs` and siblings) doesn't cover `ProgressiveImage` directly; a DOM-style snapshot would need a new test harness. The change is one CSS class name; the visual regression is plain to eyeball in the screenshots CI already captures. Verified: - `pnpm check` (biome + file-sizes + px-text + pubkey-truncation) — clean - `npx tsc --noEmit` — clean Signed-off-by: kushaim <carlossilvajimenez@gmail.com> Co-authored-by: kushaim <carlossilvajimenez@gmail.com> |