mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
a96ace4e55baa9c943cfbbfc323502ff2eff05f3
227 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d79446316b |
fix(mobile): support DeviceHub in mobile-dev (#8404)
`just mobile-dev` fails on Xcode 27 machines that have DeviceHub instead of Simulator. Resolve the simulator frontend inside the selected Xcode installation: DeviceHub for Xcode 27, or Simulator for Xcode 26. Launch by full path so an older installed or running app cannot determine the choice. This extracts the still-needed DeviceHub portion of Wes's [#8330](https://github.com/block/buzz/pull/8330). The CocoaPods deployment-target work already landed in [#8379](https://github.com/block/buzz/pull/8379); this PR does not change Podfile or its lockfile. CI and pre-push mobile selection include the shared Justfile and mobile recipe regression script, so edits to either run the regression in the mobile gate. ### Validation - Nine production-recipe checks pass, covering side-by-side Xcode 26/27 selection with spaces in paths, missing apps, launch failure, and gateway argument forwarding. Mutations reject unqualified Launch Services lookup and removal of DeviceHub selection. - All 39 tests of the pinned paths-filter action pass, including the Justfile and recipe-test selection cases. Mobile formatting, analysis, and the full mobile suite pass. - Native macOS smoke checks exercised launch from the selected Xcode installation and reached the Flutter boundary. Flutter and identity setup were stubbed, so no app was built or installed. - The initial full local CI gate stopped on an unchanged ACP signal-cancellation test; that exact test passed in isolation. The initial pre-push run also hit missing dependencies and two unchanged desktop agent-discovery tests. Desktop lint and TypeScript checks passed after installing pinned dependencies. Publishing used the authorized local-hook exception. - Current-head [CI](https://github.com/block/buzz/actions/runs/38963831198) passed, including all nine recipe cases and native Swift validation. Owner approval and Codex code/security reviews are complete with no unresolved findings. buzz-review-completed --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
d61fe2071d |
Use native UIKit scrolling for iOS photo carousels (#8361)
Builds on the mobile polish merged in #8359; this PR now targets `main`. On iOS, inline photo galleries now use a UIKit collection view for dragging, deceleration, and snapping. Photo taps retain the existing full-screen viewer and reply/more actions; gallery scrolling continues to fade the message avatar. The final photo stops at the full message gutter rather than the smaller gap between photos. Android keeps the current Flutter carousel. Visible cells subscribe to continuing frames from Flutter's existing authenticated image providers, preserving animated GIF/APNG/WebP media. Request IDs fence late frames and cancel subscriptions on reuse/disappearance/disposal. A four-slot bound includes retired unresolved loads until their transport terminates. Shared media fetches now use abortable HTTP requests with a 30-second deadline through response-body completion, so stalled requests release their slots without leaving live transports behind; native eager precaching is disabled so it cannot bypass that bound. Frame conversion coalesces to the newest pending frame. Native cells expose photo labels and positions to VoiceOver, support RTL, and preserve the selected photo during resizing. Validation: - Mobile formatting and analysis passed. - Native simulator regression checks passed for geometry, RTL, snapping, taps, frame replacement, cancellation, and resizing. The native CI lane now runs the carousel executable. - Full mobile suite and pre-push gates passed, including two-frame GIF delivery, delayed-provider bounds/cancellation, real HTTP stalled-header/body recovery with four requests maximum, disposal, and unconfigured-push tests. - Signed iPhone Release build of the frame/subscription implementation passed. Physical iPhone testing covered the earlier carousel/gutter revision; the new animation/cancellation changes have local Flutter and native simulator coverage, not a new physical-device journey. Preview-only signing overrides preserve its bundle/Keychain group; production entitlements are unchanged. Local provisioning excludes production push/App Attest capabilities. - Full `just ci` was started but stopped after unrelated Rust/desktop compilation consumed about 5 GB; broad CI is incomplete. Device build required local Xcode 27 workarounds: iOS 16 deployment target and a generated swift-secp256k1 cache compatibility edit. Neither changes repository source. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
b77ba86262 |
perf(mobile): cut test wall time and run mobile CI lanes in parallel (#8347)
## Problem Mobile pushes and PRs are slow. - **Pre-push takes ~133s on a mobile-only change.** About 115s of that is `flutter test`, and the finish time is set by a few files that wait on real clocks. - `push_presentation_export_test` (75s) and `push_presentation_recovery_test` (19s) sleep through the full 7.75s retry backoff. - `channel_messages_provider_test` (70s) sleeps through recount and ownership retry backoff on real time. - **The required Mobile CI job takes ~15–16 min.** It runs tests (390–430s on a 4-vCPU runner, 2 test files at a time) and then the Android debug APK build (~400s, no Gradle cache) back to back. - **Mobile Swift takes 18–21 min.** The unsigned release build (~4.5 min) waits behind the simulator tests. Pre-commit is ~3s and isn't part of the problem. ## Change **Tests** (each one still asserts what it did before) - **Push exports.** Production retries wait through `debugPushPresentationRetryWait`, which defaults to `Future.delayed`. The export and recovery tests skip the real waits and assert the exact 250ms → 4s backoff schedule instead. Export fixtures sign each event once per file. - **`channel_messages_provider_test`.** Every test runs on `fakeAsync`. A file-local `test` wrapper keeps the 90 call sites unchanged. Timers fire in their real order, and a test that waits on work outside fake time fails instead of hanging. Raising the production retry bound (`attempt < 2` → `3`) still fails the 5 exhausted-retry tests. - **`signed qualified caller`.** This test mounts the page 48 times and opens the profile sheet twice per mount. It now disables the sheet animation; it asserts which pubkey and labels show, not motion. **CI** - **Mobile runs as five parallel matrix lanes:** checks (format, analyze, unconfigured push test, recipe test), three `flutter test --total-shards=3` shards, and the Android build. The required `Mobile` gate still reads the combined result. - **Android restores a Gradle cache,** which is saved only on pushes to `main`. - **Mobile Swift builds the unsigned release** in a parallel lane beside the native build, tests and simulator checks. ## Validation Measured locally on an 18-core Mac with warm caches: | | Before | After | |---|---|---| | `flutter test` (full suite) | 115s | 74s | | Summed per-test time | 411s | 307s | | `push_presentation_export_test` | 75s | 38s | | `channel_messages_provider_test` | 70s | 22s | | Pre-push, mobile-only push | 133s | 95s | - The CI lane timings come from this PR's own run. - `actionlint` passes, and so do `scripts/test-ci-required-context-isolation.sh` and `scripts/ci-required-gates.test.mjs`. - The pre-push hook (format, analyze, full `flutter test`) passed on push. ## Not in this PR - `channel_detail_page_test` (~55s) is now the local long pole. Splitting it into shard files would let it run in parallel, but it is one 16k-line library with `part` files, so that is a separate refactor. - Caching Hermit and pub on macOS would cut ~100s of setup from each Mobile Swift lane. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8222e99915 |
fix(mobile): keep thread references consistent across push and Inbox (#8332)
Follow-up to [#8294](https://github.com/block/buzz/pull/8294). Malformed trailing thread tags could make a notification open the right thread while Flutter indexed its reply elsewhere; uppercase references could similarly miss lowercase event IDs. Share one validated thread-reference parser between the mobile timeline, Inbox, and feed helper. It ignores malformed markers, keeps the last valid root/reply, and normalizes lookup IDs to lowercase. Native push parsing now uses the same casing. Existing thread fixtures use valid event IDs so they continue exercising real thread relationships. Add RunnerTests coverage for the production native-to-Flutter argument conversion, including buffered cold-start navigation and omitted optional thread IDs, and run it in the native CI lane. ### Validation - New parser and Inbox regressions cover malformed markers and uppercase references. Widget regressions open and highlight the exact reply for valid, uppercase, and trailing-malformed tags. - After integrating [#8106](https://github.com/block/buzz/pull/8106), Flutter analysis and the complete mobile suite pass: 3,337 tests, four existing skips. Integration preserves per-event read predicates and migrates incoming thread fixtures to valid IDs. An unchanged keyboard-history test also failed with main's original parser; recording reads before the scroll transition fixes its timing assumption while retaining visibility assertions. - Native package tests passed (127 tests), and both Runner bridge tests passed on an iOS simulator with Xcode 26.5. Removing the production `threadRootId` assignment made the bridge regression fail; restoring it passed. Native code is unchanged by the main integration. - [Current-head CI passed](https://github.com/block/buzz/actions/runs/38659307247), including Mobile and Mobile Swift. [Codex review](https://github.com/block/buzz/pull/8332#issuecomment-6181458528) and [security review](https://github.com/block/buzz/pull/8332#issuecomment-6181449235) found no issues on the integrated commit. [Owner approval](https://github.com/block/buzz/pull/8332#pullrequestreview-5526812957) covers the exact integrated commit. - Standards and spec reviews found no blockers, including the merge resolutions and test adjustments. Validation is scoped to mobile/native per VISION_MOBILE; unrelated repository-wide checks were stopped, not counted as passing. The bridge test covers serialization and buffering, not end-to-end APNs delivery or app launch. No new physical-device acceptance is claimed; routine post-merge dogfood remains human-owned. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
b3795236cc |
fix(relay): reject unreadable write targets as missing (#8249)
🤖 A sender who cannot use or read a referenced event, artifact or workflow now gets exactly the response it would get for an ID that does not exist. #8197 (merged) set this rule for deletions; this applies it to the other writes that look up a target, plus the workflow read and webhook endpoints. Artifact `create` and workflow definition saves are the exceptions, described below. Access is checked before any message that depends on the target, readability goes through `event_visible_to_reader` (including #8197's private-deletion rule), and soft-deleted targets keep matching missing ones. | Path | Checks, in order | Response for any unusable target | |---|---|---| | Reaction (kind 7) | target lookup → `event_visible_to_reader` → every channel gate for the derived channel, including archived | `invalid: reaction target event not found` | | Edit (kind 40003) | target in the edit's `h` channel and readable → author / agent-owner check, including an author since removed from a private channel | `invalid: edit target not found or not editable by you` | | Forum vote (kind 45002) | target in the vote's `h` channel and readable → kind check | `invalid: vote target event not found` | | Report by event (kind 1984 `e`) | `event_visible_to_reader` → token channel scope → channel membership or open | `invalid: report target event not found` | | Reply (NIP-10 and workflow `send_message` replies) | parent in the reply's channel and readable | `reply parent not found` | | Artifact update / delete / restore / move | head exists and is homed in `h` (move: in the source the sender can write) → revision, type, deleted and root checks | `conflict: artifact head unavailable` | | Workflow trigger (kind 46020) | workflow exists and sender is its owner | `invalid: workflow not found or not triggerable by you` | | `GET /workflows/{id}/runs` and `/runs/{run_id}/approvals` | workflow exists, has a channel, and the caller can read it | `404 {"error":"workflow not found"}` | | `POST /hooks/{id}` | workflow exists and the provided secret matches its stored secret → definition and trigger-type checks | `404 {"error":"workflow not found"}` | Two of these reject writes that used to be accepted: a reaction to an event the sender cannot read (such as someone else's author-only event), and a report on an event the reporter cannot read. Reports by pubkey and by blob are unchanged. In an archived channel, writes whose channel comes from their target (reactions and deletions) get that target's not-found response, and an artifact move out of an archived source channel gets `conflict: artifact head unavailable` like any other source the sender cannot write; every other write keeps `invalid: channel is archived`. A webhook call with an absent or wrong secret, or to a workflow without a webhook secret, now gets `404` instead of `401`/`400`. Artifact `create` and workflow definition saves still report a taken ID, as NIP-AR allows for artifacts. Removed messages, none of which any client depends on from these endpoints: `edit target event not found`, `must be event author to edit`, `target event belongs to a different channel`, `target event has no channel`, `parent event belongs to a different channel`, `parent event has no channel association`, `artifact home changed`, `workflow is not accessible`, `workflow is not channel-scoped`, the webhook's `authentication failed` and `webhook secret required but not configured`, `forbidden: not authorized to trigger this workflow` for non-owners (it remains for an owner whose channel authority lapsed), and `restricted: not a channel member` for an edit by an author removed from a private channel. `e2e_existence_oracle` checks each event write over HTTP and WS, and the workflow read and webhook endpoints over HTTP, against missing, soft-deleted, other-channel, private-channel, archived, author-only and wrong-kind targets as applicable, with a positive control per path. It runs in the Backend Integration job alongside `e2e_author_only_deletion`. `removed_author_edit_does_not_reveal_target` checks that a removed author's edits of their remaining message, their deleted message and a missing ID get identical answers. In the PostgreSQL suite, `head_outside_authorized_channel_matches_missing_head` covers the artifact transaction, `workflow_reply_to_unusable_parent_matches_missing` covers workflow replies through `RelayActionSink::send_message`, `scoped_token_report_does_not_reveal_target` and `scoped_token_reaction_does_not_reveal_target` cover channel-scoped tokens, and `channelless_workflow_runs_read_as_missing` covers the runs read for a workflow with no channel. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
ea24a3ee3d |
fix(ci): remove Detect Changed Paths timeout and deletion test race (#8300)
🤖 ## Summary This PR removes two CI flakes that failed relay-stack runs on 2026-10-13. Both passed when rerun. - **Detect Changed Paths timeout.** This first CI job had a 2-minute limit. Its steps take about 75 seconds, but GitHub sometimes finishes the job minutes after the last step. In three runs, every step had finished, but GitHub still cancelled the job at the limit, and all later jobs failed. The limit is now 10 minutes. - **PostgreSQL deletion test.** `approval_boundary_blocks_claim_until_exact_inventory_is_approved` sometimes could not claim an approved deletion request. This was a race in the test, not a product bug. How the test race happens: - The test sends an UPDATE that the database must reject. Then it claims the same row. PostgreSQL sends the error to the client before it rolls back the statement and releases its row lock. sqlx returns as soon as it gets the error. - The claim runs on another pooled connection with `FOR UPDATE ... SKIP LOCKED`. It can skip the row while the lock is still held, and return no claim. - The test now runs the rejected UPDATE on its own connection. Then it sends `SELECT 1` on that connection before it claims. That round trip completes only after PostgreSQL has finished the rollback. Production code does not change. A claim that skips a row locked by another transaction is correct behavior. I checked the other tests in this file. Only this test has a rejected write on the request row immediately before a `SKIP LOCKED` claim. In the other tests, a blocking `approve` call runs before the claim. ### Related issue N/A. No existing PR found. Failing runs: [timeout #8174](https://github.com/block/buzz/actions/runs/38468285648/job/115540525751), [timeout #8176](https://github.com/block/buzz/actions/runs/38468321774/job/115541550557), [timeout #8182](https://github.com/block/buzz/actions/runs/38468329504/job/115541197994), [deletion test #8182](https://github.com/block/buzz/actions/runs/38468329504/job/115550003726). ### Testing - I confirmed the order in the PostgreSQL 17 source: `EmitErrorReport()` (which flushes to the client) runs before `AbortCurrentTransaction()` in `tcop/postgres.c`. A BEFORE UPDATE trigger locks the row first (`GetTupleForTrigger` in `commands/trigger.c`). - I could not reproduce the failure locally: 0 failures in 410 runs, including runs on a CPU-limited PostgreSQL container. The fixed test passes against PostgreSQL 17. Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
99c5fc8bba |
ci(docker): publish qualified, sortable push gateway main tags (#8305)
## Summary This PR makes two changes to the push gateway image job (`push-gateway-merge` in `docker.yml`). Together they let staging auto-promote the gateway the same way it already auto-promotes the relay: 1. **Publish only after same-SHA CI qualification.** `push-gateway-merge` now needs `qualify` as well as `push-gateway-build`. This is the same gate the relay's `merge` job already uses: a successful `ci.yml` push run on the exact source SHA. Before this change, the gateway image was published even when main CI was red. That was safe while staging pinned the gateway by hand. It isn't safe once the newest main image deploys automatically. 2. **Publish the sortable `:main-<YYYYMMDDHHmmss>-<sha7>` tag on main pushes.** It's the same `type=raw` line the relay matrix gained in #8240. It uses the same `enable=` guard, so it applies only to `push` on `refs/heads/main`. A Lexical selector on `^main-\d{14}-[0-9a-f]{7}$` can then pick the newest gateway build from the tag list alone. `:sha-<7>` has no ordering. `:main`, `:sha-<7>`, and the release semver tags don't change. The gateway has no debug variant, so no `debug-main-*` tag is published. **Tradeoff:** a gateway image now appears only after `qualify` finishes, which takes as long as main CI. If main CI fails, no gateway image is published for that commit, and that includes `:main` and `:sha-<7>`. The relay already behaves this way, and it's the behavior we want for anything that auto-deploys. **What got simpler:** the relay and gateway images now share the same publication gate and the same tag set. Downstream tooling no longer needs a special case for the gateway's unqualified, unordered tags. **What stays different:** the relay's `merge` job also attests the `deployment-eligibility/v1` predicate (`docs/deployment-identity.md`); the gateway publishes no such attestation. That's intentional for now: the staging promotion guard selects the gateway by package and `main-*` tag and does not verify attestations, so the gateway warehouse must not be wired to require one. If promotion ever starts verifying eligibility, the gateway needs the same attestation first. ### Related issue N/A (staging rollout tooling). The downstream promotion-guard and warehouse changes land separately. The warehouse switches on only after this tag has been published. ### Testing - `scripts/test-relay-image-eligibility-workflow.sh` now pins both properties of the `push-gateway-merge` job (it runs in CI under "Relay image eligibility contract"). Both checks match whole active lines, so a commented-out copy can't satisfy them. - At `9b8c49600` it passes. - With the live `needs` line replaced by `needs: [push-gateway-build]` and a commented-out `# needs: [push-gateway-build, qualify]` left behind, it fails. - With the gateway `main-*` line commented out, it fails. - With `needs` reverted to `push-gateway-build`, it fails with `push gateway image must publish only after same-SHA CI qualification`. - With the `main-*` tag line removed, it fails with `push gateway image must publish the sortable main-<commit UTC>-<7> tag on main pushes`. - `scripts/test-release-ref-contract.sh` now counts the sortable `main-*` tag line on all three image legs (relay, relay-debug, gateway) instead of two. It passes at `9b8c49600`. - `bin/actionlint .github/workflows/docker.yml` is clean at `f6c03b6b5`. - PR runs don't push images. The first real `main-*` gateway tag appears on the first main push after merge, and I'll check the registry for it on that manifest. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
d6dc813290 |
ci(docker): publish a sortable main-<commit UTC>-<sha7> relay tag (#8240)
## Summary
Each push to main adds one more tag to the relay image manifest that
`docker.yml` already publishes:
- `:main-<YYYYMMDDHHmmss>-<sha7>` (UTC commit time)
- `:debug-main-<YYYYMMDDHHmmss>-<sha7>` on the debug manifest
`:main`, `:sha-<7>`, and the release tags don't change.
**Why:** `:sha-<7>` has no ordering, so a deploy tool that wants "the
newest main build" has to fetch metadata for every tag before it can
sort. Our staging promotion warehouse (Kargo, `NewestBuild`) does that
on every discovery cycle, against about 1,700 `sha-*` tags (4,800 total)
in `ghcr.io/block/buzz`. That's slow, it hits registry rate limits, and
one failed fetch aborts the whole cycle. The timestamp prefix makes the
tag sort lexically in build order. A `Lexical` selector with
`^main-\d{14}-[0-9a-f]{7}$` can then pick the newest build from the tag
list alone and fetch only the tags it keeps.
Details, checked against the pinned `docker/metadata-action` v6.1.0
(`80c7e94d`):
- `{{sha}}` is the 7-char short sha (`defaultShortShaLength = 7`), the
same value as `:sha-<7>`.
- `{{commit_date}}` formats in UTC by default. On a push it uses the
event's `commits[].timestamp`, so rerunning a commit produces the same
tag.
- `type=raw` inherits the flavor prefix, so the merge job's debug leg
emits `debug-main-…`. That's the same mechanism that produces
`debug-main` today.
- `enable=` limits the tag to `push` on `refs/heads/main`. Release tags,
rescue dispatches, and PRs never get it.
On main's last 40 commits, author time and committer time are the same
instant, and both increase monotonically. A commit pushed with a
backdated timestamp would sort below newer builds and just wouldn't be
auto-picked; the next push supersedes it.
**What got simpler:** this adds one tag line in each of the two places
that already mirror the tag matrix, plus header docs. In return,
downstream tooling no longer needs per-tag metadata fetches to find the
newest build.
### Related issue
N/A (staging rollout tooling). The downstream warehouse and tag-guard
changes land separately and don't switch over until this tag has been
published.
### Testing
- `scripts/test-relay-image-eligibility-workflow.sh` and
`scripts/test-release-ref-contract.sh` pass at `9ba1d8b7f`.
- PR runs don't push images (`push: false`), so the first real tag
appears on the first main push after merge. I'll check the registry for
`main-<14 digits>-<sha7>` and `debug-main-…` on that manifest.
---------
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
|
||
|
|
40c1c474e7 |
fix(relay): hide deletions of author-only events from other readers (#8197)
🤖 Opened by an agent (Hayt) for Will Pfleger. The relay keeps `AUTHOR_ONLY_KINDS` (event reminders `30300`, push leases `30350`, private managed agents `30179`) readable only by their author, but a NIP-09 deletion request (`kind:5`) for one of those events was readable by any community reader. The deletion carries no `h` tag, so ingest stores it with its target's channel (none), and every read gate classified it by its own kind, `5`. Another reader could read and count it by author, by id, live, or over HTTP, learning the deleted ids, how many, and when. A deletion is now exactly as private as the event it deletes: - **Classification by the deletion's own `k` tag.** `buzz_core::kind::is_author_only_event_kind` treats a `kind:5` that names an author-only kind in a `k` tag as author-only. Only the canonical decimal spelling counts (`"30300"`, not `"030300"` or `"+30300"`), so Rust classification, ingest and the admin SQL (which matches the tag text) always agree. Because it reads the deletion, not a target lookup, the deletion stays private after its target is soft-deleted or gone. It is generic over `AUTHOR_ONLY_KINDS`, so a kind added to the list later (for example `44300`) is covered automatically. - **Every read surface.** The shared per-event gate (`is_author_only_event`, used by WS REQ historical and lookup by id, COUNT fallbacks, HTTP `/query`, `/count`, search hydration and the thread window), live fan-out in `handlers/event.rs`, the COUNT fast-path guard (`filter_can_match_author_only_kinds` now treats any filter that can match `kind:5`, including unfiltered and id-only filters, as needing per-event filtering), the channel-window aux closure in the bridge, and admin event preview plus report list and detail (SQL suppression of `kind:5` rows with a tag whose first two elements are `k` and an author-only kind, matching the Rust classifier; soft-deleted rows included). - **Enforcement still reaches the author.** The admin report query takes the private-deletion `k` list from its caller next to the hidden kinds. HTTP-facing reads pass both; `admin_get_report_for_enforcement` passes neither, so report-directed bans, timeouts and action recovery still resolve the stored author of a reported private deletion. - **Ingest agreement.** `validate_standard_deletion_event` rejects a deletion of a stored (including soft-deleted) author-only event, by `e` or by `a` coordinate, unless it carries the matching canonical `["k","<kind>"]`. It also rejects an author-only `k` on a deletion of any other kind, so a public deletion can't hide itself from the readers who need it. A deletion whose target is itself a private deletion is rejected with `invalid: cannot delete a private deletion request`: it would otherwise be stored as a public event revealing the private deletion's id and timing, and NIP-09 gives a deletion of a deletion no effect. A deletion of a public deletion is unchanged. The `k` tag only narrows who can read the request; it grants no one new deletion authority. - **Unauthorized deletions learn nothing about the target.** On every delete-by-id path, no target-dependent error (its kind, the artifact and `k`-marker errors, the artifact-removal-marker error, or where it lives) is returned until every access check that could block the deletion has passed. A sender who fails any of them gets `invalid: deletion target not found or not deletable by you`, the same text and status as for a missing target. Errors that depend only on the request, never on the target, are unchanged: for example a `kind:9005` whose own `h` channel is archived still gets `channel is archived`, for a missing target and a real one alike. "May not delete" includes an author who has since been removed from the target's private channel, an author whose token is scoped to a different channel, and anyone deleting in an archived channel. The single-target rule (exactly one `e` or `a` tag) now runs before any target lookup, so every target faces the gates. For `kind:5` with an `e` target, ingest decides authority (effective author or the owning human of the authoring agent) first, then takes the channel from that same lookup, soft-deleted targets included, so tombstones face the same gates; an authorized re-deletion of a soft-deleted target now keeps the target's stored channel instead of being treated as global, so historical access checks, `#h` filtering and live routing all use that channel: readers outside a private channel no longer see it, and that channel's members and subscribers do. The token channel scope, scoped-token global-event, membership and archived checks then reject with the shared denial, all before `validate_standard_deletion_event` returns its artifact and `k`-marker errors. Other kinds, and `kind:5` with only `a` coordinates, keep their existing errors. For NIP-29 `kind:9005`, a target outside the `h` channel is denied like a missing one, so the relay no longer returns "different channel" or "no channel"; `may_delete_channel_event` (the author while still a member or the channel is open, a channel owner/admin, or the owning human of the author agent) is then checked in the `h` channel before the artifact-removal-marker error. No access restriction is removed and nothing becomes newly deletable. An authorized sender who names the wrong `h` channel for a `kind:9005` now gets the shared denial. Whether kind:5 content is full-text indexed depends on how the database was built. The desired-state schema (`schema/schema.sql`, which CI applies) and databases created before migration `0008` index it; an empty database initialized through `0008` gets a positive allowlist that doesn't. Where it is indexed, the per-event gate on search hydration is what keeps a private deletion out of other readers' search results. No current client is affected by the new rejection. `buzz-app`'s signer only allows `kind:5` for channel messages (with `h`) and managed-agent `30177` coordinates, and neither `buzz-app` nor this repo's desktop or CLI publishes deletions for `30300`, `30350` or `30179` today. NIP-ER already requires `k=30300` on reminder deletions. Coverage: - `crates/buzz-test-client/tests/e2e_author_only_deletion.rs` (Backend Integration job): a non-author gets nothing from WS REQ, WS COUNT, HTTP `/query` and `/count` (by author, mixed kinds, and id), search, or live delivery, while the author gets the deletion. Search runs WS and HTTP against a private deletion and a public control deletion sharing a reason word; with `BUZZ_TEST_SEARCH_INDEXES_DELETIONS=1` (set by the CI step) the author must get exactly both and another reader exactly the control; ingest rejects a missing, wrong, non-canonical, or public-target marker, and an untagged deletion of an already soft-deleted target. - `e2e_author_only_deletion.rs` also sends one non-owner's deletions over HTTP and WS for a missing, live, soft-deleted, private-channel and global target, plus the `kind:9005` equivalents including a target in another channel and one with no channel, and requires the identical rejection text and HTTP status in every case; another member's deletion of a private event never names its kind. - `e2e_author_only_deletion.rs` `removed_author_deletion_does_not_reveal_target`: an author removed from a private channel gets the missing-target rejection over HTTP and WS for `kind:5` and `kind:9005` (with `h` set to a channel they can write to) against their retained message; the channel owner can still delete the removed author's message, and the author can still delete in a channel they can write to. - `e2e_author_only_deletion.rs` `archived_target_deletion_does_not_reveal_target`: over HTTP and WS, `kind:5` against a message, the same message with a wrong `k`, and an artifact in an archived channel, and `kind:9005` against the archived-channel message with `h` set to an active channel, all match the missing-target rejection; a `kind:9005` for an artifact-removal marker in the archived channel matches a missing target with the same `h`; in an active channel the owner still gets the `k`-mismatch and artifact errors. - `e2e_author_only_deletion.rs` `deletion_of_private_deletion_rejected`: over HTTP and WS, the author's deletion of their private deletion, with no `k` and with `k=5`, gets the rejection and stores nothing; a non-author's gets the missing-target rejection; a `kind:9005` against it gets the missing-target rejection; the private deletion stays hidden from another reader in WS REQ and COUNT, HTTP `/query` and `/count`, and live delivery; a deletion of a public deletion is still accepted. - `e2e_author_only_deletion.rs` `soft_deleted_target_keeps_its_channel`: an author removed from a private channel, and an author in an archived channel, re-deleting their own soft-deleted message get the missing-target rejection; a distinct successful re-deletion is readable by a channel member and not by an outsider, by id and by `#h`, and is delivered live to the member's `#h` subscription but not to the outsider's. - `e2e_author_only_deletion.rs` `multi_target_deletion_error_ignores_targets`: deletions with two `e` tags, or an `e` and an `a` tag, get the identical single-target error whether the first target is missing, live, in another private channel, a private reminder or an artifact. - `handlers::ingest::postgres_tests::scoped_token_deletion_rejection_does_not_reveal_target`: a non-owner and the target's author, each with a token scoped to another channel, get the same rejection for a missing, live, soft-deleted and global target, and the author's `kind:9005` naming the token's channel gets it for a missing target, a live target in another channel and a global target; a token scoped to the target's channel still deletes, and its re-deletion of the soft-deleted target is stored with that target's channel. - `thread_window::postgres_tests`: stored private deletions never reach a non-signer through the thread-window or channel-window aux closures, or by id once their target is gone. - `api::admin::postgres_tests`: ban and stranded-ban recovery on a reported private deletion reach the stored author while report detail hides it; `admin_moderation` tests treat non-canonical markers as public. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
2b3f5023fe |
chore(ci): run Codex security review on gpt-6.1-sol (#8228)
🤖 Switches the Codex security review from `gpt-5.6-sol` to `gpt-6.1-sol`. Reasoning effort stays at `high`. - `CODEX_MODEL` changes in both the review job and the job that posts the comment, so the comment's `Model:` line stays accurate. - `codex-version` goes from `0.150.1` to `0.161.0`. That's the first Codex CLI release with GPT-6.1 Sol in its bundled model catalog ([openai/codex#49318](https://github.com/openai/codex/pull/49318)). Both `@openai/codex` and `@openai/codex-responses-api-proxy` are published at that version. - `gpt-6.1-sol` is the model ID listed on the [OpenAI models page](https://developers.openai.com/api/docs/models/gpt-6.1-sol). It supports `high` effort through the Responses API. The workflow runs on `pull_request_target`, so it uses the copy on the base branch. This PR's own review still runs the old model, and the new one takes effect on the first review after merge. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> |
||
|
|
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> |
||
|
|
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> |
||
|
|
d742fc5f30 | Use Silo for bundled object storage (#8023) | ||
|
|
f0eb5575ff |
fix(mobile): polish photo carousels and video viewer controls (#8079)
## Summary - Keep neighboring carousel photos visible, add page-change haptics, and reuse appearance-style dots with native iOS glass and grouped actions. - Refine full-screen video controls with fading transport, cycling speeds, pinch-to-zoom, and double-tap reset before swipe-to-dismiss. ### Related issue N/A. Checked recent open PRs; repository issue search was unavailable. ### Testing - Mobile analysis/format checks, file-size guards, and full Flutter suite passed (2,680 tests; 4 skipped). - Built, installed, and manually verified on the connected iPhone. - `just ci` was attempted but blocked fetching a Rust dependency because this Mac could not connect to GitHub. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: klopez4212 <klopez4212@gmail.com> |
||
|
|
16839a0773 |
feat(nip-fi): admin disconnect/deny API with in-memory deny-until-TTL map (S4) (#7977)
This adds the NIP-FI S4 admin command surface. An authorized issuer principal can deny an `(issuer, pubkey)` pair until a TTL expires. When that happens, the relay records the entry in a bounded in-memory deny map, closes matching live sockets on every pod, and refuses new admissions while the entry is live. Supersedes #7265 ## Breaking In `BUZZ_NIP_FI_MODE=enforce`, every entry in `BUZZ_NIP_FI_ISSUERS` now has to carry `maximum_command_age_seconds` (1–60) and a non-empty `authorized_principals` list, and startup fails if any issuer is missing either one. That means an S3-only enforce config that boots on `main` won't boot with this change. No deployment sets `BUZZ_NIP_FI_*` yet. The per-issuer field table and a working example are in the `nip_fi_config.rs` module docs. ## What changes - **`buzz-auth`**: - `nip_fi/command.rs` verifies signed admin commands. It checks issuer policy, command age, the authorized principal (exact, case-sensitive `sub` match), and a `jti` of at most 512 bytes. - `nip_fi/deny_map.rs` is a per-issuer, capacity-bounded deny-until-TTL map on top of `DashMap`. Expired entries and `jti` reservations are only swept when a write would otherwise hit capacity. The sweep is O(n) and holds the lock every admission check takes, so this keeps it off the admission path. Reads compare against `now`, so an expired entry that hasn't been swept yet never counts as live. - **Cross-pod command replay fence**: `nip_fi/command_replay.rs` adds a `CommandReplayGuard` trait, and `buzz-pubsub` implements it in Redis (`RedisCommandReplayGuard`), following the NIP-98 replay guard. Once a command is authenticated, the verifier takes an atomic `SET buzz:nip-fi:command:{sha256(len(iss)‖iss‖len(jti)‖jti)} 1 NX EX <ttl>` claim, with the TTL set to the command's remaining validity rounded up to whole seconds plus skew, clamped like NIP-98, so the claim outlives acceptance by a verifier whose clock trails by the full skew. Before the claim, the verifier reserves a pending `jti` and deny-entry slot in this pod's deny map under the issuer lock, so a full deny set returns `503 deny set full` without touching Redis and the same signed command can be retried. The claim is taken only after the signature and claims verify, so a forged token cannot burn a real `jti`. A `jti` already claimed on any pod returns `403`. A Redis error fails closed with `503 authorization unavailable`: no deny entry, no session close, no publish. A replayed claim, a Redis error, or a cancelled request drops the reservation. After a successful claim the reservation commits to the `jti` record and deny entry, which cannot fail on capacity. A claim can still remain with no deny entry if Redis applied `SET` but the reply was lost, or the pod stops between claim and commit; that fails closed, and a retry gets `403` until the TTL expires. - **`api/nip_fi.rs`**: the admin disconnect/deny endpoint. It verifies the command, inserts the deny entry, disconnects locally, and publishes over `buzz-pubsub` `conn_control` so every other pod runs the same disconnect. A receiving pod clamps `until` to its own ceiling instead of rejecting it. Otherwise a pod whose clock trails the origin would drop a command the origin already answered with 200, and the target's sockets on that pod would stay open. Malformed pubkeys, unknown issuers, and unrepresentable timestamps are still rejected. - **Admission and live enforcement**: - WS upgrade (`router.rs`) and HTTP (`nip_fi_http.rs`) refuse keys that have a live entry with `403 authorization_denied`. - Root NIP-42 auth and audio auth register the proven identity (pubkey + admitting issuer) first, and only then check the deny map. With that order, a concurrent disconnect either finds the socket in its close scan or the check finds the entry. `OK(true)` isn't sent if a concurrent disconnect already cancelled the session. - `ConnectionManager::disconnect_nip_fi` and `CommunityConnectionRegistry::disconnect_nip_fi` are scoped to the issuer. - **One wire form for `authorization_denied`** (FI-TRACE-DENIAL-ORACLE): every root and audio WebSocket `authorization_denied` denial now goes through one first-writer-wins transition on `CommunityConnectionControl`. That covers the S4 deny-set hit and admin disconnect, and also the existing S3 key-mismatch, lease-expiry, and membership refusals. The transition sets the `AuthorizationDenied` reason only if nothing else has set a reason, and only the winner enqueues the denial frame. So every one of these denials is the class's denial frame followed by `Close(1008, "authorization denied")`, and a concurrent registry scan can't queue a second one. Before this, S3 key mismatch and expiry closed with `Close(None)`, which made them distinguishable from a deny-set hit on the wire. `evidence_rejected` and `authorization_unavailable` keep the bare close. - **Config**: see Breaking. `deny_set_capacity` is optional and defaults to 50000. Setting any command field without a command age fails closed. - **Boot** (`main.rs`): installs the command verifier and deny map. It subscribes to cross-pod disconnects before the Redis subscriber starts, so disconnects relayed during boot are buffered instead of dropped. --------- 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> |
||
|
|
0ee2ad7fbf |
Fix rounded macOS icon on older Macs (#8018)
Older macOS versions display Buzz’s opaque icon as an oversized square. Bake rounded corners and transparent padding into the macOS `.icns`, preserving the original bee artwork and leaving other platforms unchanged. Generation uses Hermit-pinned uv, Python 3.14.3, and Pillow 12.2.0. `scripts/macos-icon.sh --check` regenerates temporarily and compares all decoded representations; macOS CI runs it before packaging. Pixel reproducibility is verified independently of Apple’s ICNS container encoding. Validated native AppKit preview, all 10 representations, fresh-cache generation, one-pixel/missing-representation drift rejection, workflow lint, and pre-push checks. Full local `just ci` is running. Packaged Dock checks on older/current macOS remain outstanding. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
0ee6093790 |
fix(ci): gate security review on repo write access (#7986)
🤖 The Codex Security Review gate trusted GitHub's `author_association` (`MEMBER`/`OWNER`), but GitHub only reports `MEMBER` to viewers who can see the membership. The workflow `GITHUB_TOKEN` cannot see private `block` org membership, which is GitHub's default, so most Block employees read as `CONTRIBUTOR` and their PRs never reviewed automatically. The same applied to the comment path, where private members could not authorize a review with `@buzz-security-review <sha>`. Trust is now repo write access, read from `GET /repos/{owner}/{repo}/collaborators/{user}/permission` via `getCollaboratorPermissionLevel`, which is independent of membership visibility and works with the authorize job's existing `contents: read` and `pull-requests: read` permissions. A user is trusted only when `permission` is `write` (which includes `maintain`) or `admin`. A successful lookup is not enough: on this public repository, an account with no relationship to it returns `200` with `read`, not 404. Outside contributors can only push to forks and come back `read`, so they still need an explicit command from a trusted user. The check covers the PR author on `pull_request_target`, the commenter on `issue_comment` (the job-level `author_association` filter is removed so the trusted script makes that decision), and the stale-marking path on PR updates. Transient API errors are retried. Authorization fails closed: any lookup error other than 404 fails the job instead of authorizing. Stale-marking is conservative: an existing review is always marked stale without consulting permissions, and a failed lookup counts as untrusted, so a previous range's review can never appear current for a new head. A commenter without write access is logged and ignored instead of failing the run. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
95b018c871 |
feat(acp): wrap workers at the subprocess launch boundary (#7985)
## Summary Replacing `BUZZ_ACP_AGENT_COMMAND` with a sandbox launcher hides the real adapter from Buzz: for example, Goose loses its default `acp` argument. Add optional `BUZZ_ACP_LAUNCH_PREFIX`, a JSON argument array applied at the shared subprocess boundary, after normal worker configuration. On Unix, every spawn runs `prefix... worker args...` without shell interpolation. Worker identity, environment setup, stdio and existing process cleanup are preserved. Invalid or unavailable prefixes fail launch without falling back to the worker; unset keeps direct launch on every platform. Configured prefixes fail closed on non-Unix because per-worker process-tree cleanup is unavailable. Desktop reserves the key against saved user-environment overrides, and spawn failures name the executable. ### Related issue Prerequisite for [Buzz-App #415](https://github.com/block/buzz-app/pull/415). One commit directly on `main`, independent of #7942–#7944. No duplicate launch-prefix PR found. Policy enforcement, verified launcher staging, protected paths and the supporting runtime pin remain Buzz-App/plugin work. ### Testing - Original head `9ae9d82f8`: complete ACP suite (990 passed, three existing ignored), shipped local-task test, real-Goose probe, and full repository-wide `just ci` passed. - Review fixes: five launch tests and 44 Desktop environment-filter tests pass locally, covering the real production platform gate, save rejection, both merge layers, case variants, and executable error context. - Subprocess tests cover direct/wrapped argument defaults, Pi skills, Hermes/Codex environment, adapter identity, repeated spawns and invalid/missing prefix refusal. - Shipped `buzz-acp run` entrypoint completes a task through the wrapper and deterministic ACP peer. - Real Goose 1.52.0: direct and wrapped `auth-methods` return identical results; the wrapper receives `goose acp` with initially empty worker args. - Independent agent review: no blocking findings. - Required push checks passed on the updated head. The existing native Windows CI job now explicitly runs the platform-contract tests. Its result is pending; the non-Unix refusal branch was not exercised on this macOS host. Repeated-spawn coverage is not a full lazy-pool wake/crash test. Protected Goose in the Buzz-App UI and human acceptance remain pending. To try the hook after building `buzz-acp`, use an installed Goose path: ```sh BUZZ_ACP_LAUNCH_PREFIX='["/usr/bin/env"]' target/debug/buzz-acp auth-methods \ --agent-command /absolute/path/to/goose --agent-args '' --json ``` Expect the same authentication methods as without the prefix. Changing the prefix to `["/missing-launcher"]` must fail. Draft pending human testing. Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> |
||
|
|
b37e477211 |
Make relay readiness process-local (#7341)
## Summary Make Kubernetes readiness depend only on the relay process lifecycle, so a shared Postgres or Redis slowdown cannot withdraw every pod at once. Move dependency checks to one fixed-cadence loop per pod. The loop permits only one evaluation at a time, emits metrics after each sample, and caches a timestamped report. `/_status` reads that cache without calling Postgres, Redis, or the deletion catalog. It reports `not_yet_sampled`, `fresh`, or `stale`. Each completed sample also writes a Unix timestamp gauge, so Datadog can compute sample age at query time. Fail closed when the community lifecycle lookup errors. An inconclusive tenant-state lookup no longer admits a socket to AUTH or REQ handling. ### Related issue None found. This addresses the elevated relay HTTP 500 and reconnect incident investigated on 2026-09-03. ### Tradeoff The readiness gauge now reports the latest private-probe observation, not the shutdown transition itself. After shutdown starts, a scrape can still see `1` until the next readiness probe refreshes the gauge to `0`. Kubernetes routing is unaffected because it uses the readiness response from the authoritative process state. A terminal `0` was never guaranteed: even a transition-owned gauge could update in process and then exit before Prometheus scraped it. If no scrape occurs before exit, both designs miss the terminal sample. The new design adds at most the interval until the next readiness probe when a scrape occurs during shutdown. Dependency status is now sampled every 30 seconds rather than on demand. A slow evaluation delays the next sample instead of stacking more work. The status payload exposes the cached report age. The completion timestamp stays unchanged when sampling stops, so `time() - timestamp` crosses the stale threshold without a server aging loop. Outcome counters and duration histograms remain cumulative and cannot provide no-data detection. ### Testing Gimli exercised exact SHA `edff1ec4a5362e132e367bf7449d2b0a68f1e9a9` on an isolated Blox stack. Repeated `/_status` bursts caused no dependency calls, metrics advanced without status traffic, stale status remained visible, and lifecycle lookup failures admitted no AUTH or REQ handling. Follow-up verification at `fdbb394ed598c94cde85ed01cb11e9de0ee8f00d` confirmed sampler-owned outcome and duration emission without status traffic and bound the real startup seam. The final timestamp-gauge change is covered by targeted red/green tests and the same boot regression at `c0f36e22b527c2fd6a63a548d036d006ffa17171`. ### Complexity Readiness remains one process-local sample per probe. Dependency I/O now has one owner and one execution path; request volume cannot increase dependency-check load. One completion timestamp replaces the prior server-side age update path; Datadog computes age without another loop. Community admission also returns to one fail-closed rule for inactive and unknown tenant state. Generated with [Claude Code](https://claude.ai/code) --------- 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> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
02c6309f53 |
fix(ci): select runtime suites from PR changes only (#7843)
A documentation-only update to #7809 selected desktop builds and E2E tests because path detection compared an old PR base SHA with GitHub's newer synthetic merge commit. The [failing run's path-detection log](https://github.com/block/buzz/actions/runs/35789957773/job/106955810630) includes four unrelated desktop files from `main`; `VISION_MOBILE.md` did not match a runtime filter. Use GitHub's PR file list for pull requests so unrelated changes in the synthetic merge commit cannot select runtime suites. The existing directory filters are unchanged: root and docs/ Markdown do not select runtime suites, while Markdown under runtime directories still selects its affected suites. Mixed code/documentation changes retain normal coverage. Always-on security, policy, and source-contract checks and full push-to-main coverage are unchanged. Added regression coverage runs the pinned paths-filter action against real fixture repositories, including a synthetic merge containing unrelated upstream desktop code. All 35 selection scenarios and 26 required-check scenarios pass; the new regressions failed before the fix. Existing required-context isolation, file-size policy, and security-review contract checks pass, as do script lint and workflow syntax validation. Full workflow lint reports the same two pre-existing shell-quoting findings in the untouched dead-token guard. A desktop E2E build was run to diagnose the unrelated required smoke failure. Related: #7809 (incident, unchanged) and #5756 (shared-input path coverage, separate scope). No matching issue found. PRs with at least 3,000 changed files now select every runtime suite, avoiding GitHub's PR-file-list ceiling. Boundary tests cover 2,999, 3,000, and 3,001 files, including a runtime file omitted beyond the API cap; disabling the safeguard makes the latter two regressions fail. Required checks now run and fail when path selection fails, is cancelled, or is skipped. Regression coverage exercises the workflow conditions and shell checks for all 13 required wrappers, plus an API-denial case against the pinned action. Mutating either the scheduling guard or the result check makes all 13 failure regressions fail. The required mention-settings smoke test expected a pin after explicitly disabling automatic mentions. Waiting for its old avatar to exit reproduced the CI failure 3/3; the test now checks that subsequent mentions remain manual and retains outgoing recipient-tag assertions. Corrected browser regression: 20/20 repeated runs; related picker unit tests: 18/18. No desktop production behavior changed. The always() requirement is now pinned in required-wrapper regression tests; deleting it fails all 26 gate cases. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
797012ff01 |
fix(ci): consume the published MinIO image (#7870)
Switch every CI job that starts MinIO to the public `ghcr.io/block/buzz-minio:latest` image published by #7869. Integration jobs pull the image; image builds stay in the separate, path-filtered publisher workflow. - Apply the CI Compose override to both desktop integration shards, backend integration, relay E2E, and mesh lifecycle, including script-mediated Compose calls. Both the server and bucket initializer use the Buzz image. - Override both MinIO images in Helm's quickstart CI values, covering the server, bucket initializer, and relay's wait-for-bucket init container in the gated kind installation. - Select integration and mesh checks when either Compose file changes, with regression coverage using the real pinned `dorny/paths-filter` action. ### Publication verified #7869 is merged, and [the first main-branch publication](https://github.com/block/buzz/actions/runs/36021389137/job/107706874954) succeeded. An anonymous pull using an empty Docker configuration returned the publisher's digest: `sha256:b8470bbeafbf57b20c86cf63804682b714bdcfdbb517f3770247e321e623f48f`. This PR is rebased onto main and ready for consumer integration checks. ### Validation - Ran the repository's MinIO smoke script against the actual published AMD64 image: healthy startup, bucket initialization, object upload/read/delete, and rejection of anonymous reads all passed. Used the rendered CI Compose services with isolated local ports and resource names. - Verified `mc` runs and writes its configuration as Helm's non-root UID/GID 65532 with dropped capabilities and no privilege escalation. - Helm 3.16.4 lint and quickstart rendering passed; all three rendered MinIO container references use the Buzz image. - All 14 CI selection cases and the required-context isolation check passed. Mesh workflow passes actionlint; shared workflows pass with shellcheck disabled for pre-existing findings. - Fresh repository-wide review traced workflow and script consumers and found no remaining CI image omissions. Full application integration checks and a real kind installation have not been run locally. --------- Signed-off-by: Tyler Longwell <tlongwell@squareup.com> |
||
|
|
99c2acf90c |
fix(ci): bootstrap the reusable MinIO image in GHCR (#7869)
Upstream MinIO image pulls are blocking CI (#7867). Add a separate publisher for `ghcr.io/block/buzz-minio:latest`, so the reusable image can be bootstrapped before CI adopts it in #7870. - Build the checksummed official MinIO/mc release binaries from #7868 on a digest-pinned Alpine base. - Build only when image/workflow/Compose inputs change or on manual dispatch. PRs build and smoke-test; only `block/buzz` main can publish. - Run the real Compose healthcheck and initializer, object upload/read/delete, and anonymous-access denial before publishing `latest` plus a unique `sha-<commit>-run-<id>-<attempt>` tag. - Manual dispatch disables Docker's layer cache to refresh Alpine packages. Normal image builds retain caching; each maintenance rebuild keeps its own tag. ### Rollout This PR contains only the publisher and opt-in Compose override. **It does not switch ordinary CI consumers**, so its checks do not require the unpublished image. This is a separate PR from adoption, as required by Buzz's squash-only merge rules. 1. Merge this PR; its main-branch workflow builds, tests, and publishes the image. 2. Make the `buzz-minio` GHCR package public and verify an anonymous pull of `ghcr.io/block/buzz-minio:latest`. New GHCR packages default to private. 3. Rebase draft #7870 onto current main, retarget it to main, run its integration checks, and merge it to switch all MinIO-backed CI jobs to the published image. ### Validation - Updated hosted [MinIO image build and smoke test](https://github.com/block/buzz/actions/runs/36018177877) passed at `700ab6d5346b37a64c9defde0016dc65c98aa790`; publication correctly skipped on the PR. - Hosted [ordinary CI](https://github.com/block/buzz/actions/runs/36018178123) passed at the same head. Its actual Git comparison detected only the five publisher files, all application path filters were false, and the MinIO-dependent integration checks were correctly skipped. Synced current main before this run to eliminate an earlier stale-base comparison that included unrelated ACP changes. - Actual five changed paths were passed through the repo's pinned `dorny/paths-filter`: all application domains were false. All 12 existing path-selection cases and the required-context isolation contract also passed. - Publisher actionlint, smoke-script shellcheck, and commit/push hooks passed. - Fresh local rebuild of `700ab6d5346b37a64c9defde0016dc65c98aa790` passed with `docker buildx build --no-cache --platform linux/amd64 --load`. Logs confirm new Alpine index fetches and package installation rather than a cached install layer. The committed smoke script passed under local AMD64 emulation using the current merged Compose services with isolated resource names and no published ports: healthcheck, initializer/private bucket, object upload/read/delete, and anonymous HTTP 403. Temporary containers, network, and volume were removed. Both pinned binary checksums match the official release SHA-256 files. - Full `just ci` was previously attempted: workspace formatting/Clippy, desktop checks, and Tauri formatting passed; the run was stopped during Tauri Clippy. No full local application-suite pass is claimed; hosted application suites correctly skip this publisher-only change. --------- Signed-off-by: Tyler Longwell <tlongwell@squareup.com> |
||
|
|
4544ba1713 |
chore(mesh): upgrade MeshLLM to 0.76.2 (#7559)
## Summary Upgrade Buzz from MeshLLM `v0.76.0-rc9` to the latest stable [v0.76.2 release](https://github.com/Mesh-LLM/mesh-llm/releases/tag/v0.76.2) (via v0.76.1). - Update all eight Mesh git dependencies across desktop and relay smoke examples; both lockfiles resolve all 47 Mesh packages to `a0c1e66b0ac037dd56544b9e2d94969ea43d694f` (tag `v0.76.2`). All non-Mesh lock records are unchanged by the 0.76.1 → 0.76.2 bump. - Match Mesh's `ed25519-dalek = 3.0.0` and `rusqlite = 0.40` dependencies. - Explicitly enable rusqlite's `fallible_uint` feature, preserving Buzz's existing `u64` SQL conversions when **mesh is disabled**. Mesh-enabled builds previously supplied this transitively, so testing both configurations matters. - Temporarily pin **only** the optional `sqlx-sqlite` package to the merged upstream compatibility fix [transact-rs/sqlx#4402](https://github.com/transact-rs/sqlx/pull/4402), revision `94aafe3a68884d923b0798a767c8d7f6cfda89d2`. SQLx 0.9.0 caps `libsqlite3-sys` below 0.38, while Mesh 0.76 requires 0.38. Cargo resolves the conflict even with SQLite disabled. The fix raises that upper bound; remove the patch once released. Buzz's active PostgreSQL driver stays on crates.io SQLx 0.9.0. - Preserve the explicit Linux keyring 3 `crypto-rust` feature so Secret Service sessions retain DH encryption independently of Mesh keyring 4. - Preserve the dedicated examples-only `buzz-mesh-smoke` crate and lifecycle workflow wiring, keeping Mesh/native-runtime dev dependencies out of relay CI builds. - v0.76.1 → v0.76.2 upstream delta: self-updater fix for composed product-v2 bundles (Mesh-LLM/mesh-llm#1844) plus release plumbing — no API changes affecting Buzz. - Merged `origin/main` (`7c789dee0`) without rewriting shared PR history. ### Validation at `ae4adba15` (local macOS ARM64, Hermit toolchain) - **Passed:** `cargo test --locked -p buzz-mesh-smoke --all-targets` (all six example harnesses compile; no live harness execution). - **Passed:** desktop Tauri workspace tests, both default and `--features mesh-llm` graphs (3,296 tests with mesh enabled, 0 failures). - **Passed:** `just desktop-tauri-clippy` (both feature graphs, `-D warnings`), root workspace clippy `-D warnings`, `cargo fmt --all --check`. - **Passed:** `--locked` metadata resolution for both workspaces; lockfile diff audited to touch only Mesh package records. - **Passed:** full pre-push hook suite on the final push (branch-skew, rust-tests, desktop-tauri-checks, file-size-check) — no hooks bypassed. - **Not run:** live Mesh inference/admission smoke or installed-app UI exercise. CI lifecycle smoke coverage applies. Originating Buzz conversation: buzz://message?channel=dffbc117-845a-4c02-bc3f-eaeeefc4deaa&id=670f9c262e1ec0a4df5600b3173a0c5de2abe17e08ba9ccfdd8eff109fadf1a8 --------- Signed-off-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz> Signed-off-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Thinker <75d8a808fa21bb8d1812e080cf471db601c6e8e6a62dcd516d37531e83a7bb77@meshllm.communities.buzz.xyz> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Galadriel <b81c841ed20a8e17caf4e6fac2f6f3bc61d304c84b8394720f6d00f37538c47e@meshllm.communities.buzz.xyz> Co-authored-by: Gu <gu@buzz> |
||
|
|
5621006bcf |
refactor: move agent Git bootstrap into ACP harness (#7819)
discovered in https://github.com/block/buzz/issues/7742#issuecomment-5783512985 ## Summary Goose uses its native shell, so it bypassed the Git identity and signing setup inside `buzz-dev-mcp`. Agent commits could use the host Git identity, while MCP shells had a separate config. This moves Git setup into `buzz-acp`, where both paths receive the same agent identity, Nostr signer, and relay-scoped credential helper. Before: ```text Buzz Desktop └─ buzz-acp ← Desktop supplied relay Git authentication ├─ Goose → native shell ← no agent Git author or signing setup └─ Buzz Agent └─ buzz-dev-mcp └─ shell ← MCP created the keyfile and Git signing config ``` After: ```text Buzz Desktop or Sprig └─ buzz-acp ← owns the keyfile, identity, signing, and scoped credentials ├─ Goose → native shell ← inherits the complete Git environment └─ Buzz Agent └─ buzz-dev-mcp └─ shell ← receives the same Git environment via mcpServers[].env ``` The harness keeps the signing key in a private temporary file and removes it on normal exit, startup failure, and graceful termination. It composes inherited `GIT_CONFIG_*` entries, forwards the complete block to MCP servers, and bundles the helpers through the ACP multicall entrypoint. For standard `buzz-acp` launches, Desktop and Sprig no longer write Git config. A custom Desktop ACP command bypasses the harness, so Desktop preserves its prior relay-scoped credential helper for that supported override. This changes Git settings for every ACP runtime. The startup signal path also changes so a termination during adapter initialization removes the keyfile. The temporary file can remain after SIGKILL or a machine crash. ### Related issue Related: #6177 covers stricter commit identity enforcement and touches the same Git paths. That PR and this one need to be reconciled before either lands. No issue was found for this runtime bootstrap gap. ### Testing - Ran the ignored `git_runtime_tests` with real Goose 1.50.0 and Buzz Agent ACP sessions. `real_goose_native_git_shell` passed again on this PR branch with `BUZZ_TEST_BIN_DIR` pointing to built binaries. Goose made a signed commit and tag through its native shell; Buzz Agent did the same through its MCP shell. Both verified signatures, identity, credential scoping, and keyfile cleanup. - Built the Linux Sprig image and ran `scripts/test-sprig-image.sh buzz-sprig:acp-git-bootstrap`. The container made and verified a signed commit and tag, then confirmed keyfile removal after SIGTERM. - `just ci` passed on the original branch. After rebasing onto `main` and addressing review feedback, the ACP Git bootstrap integration tests, focused desktop smoke test, custom-command regression test, formatting, and ACP/Desktop Clippy passed. The pre-push hook hit two timing-sensitive ACP deadline tests under parallel load; both passed alone and the complete ACP package passed serially (964 library, 2 Git bootstrap, 9 pool lifecycle tests). The push used `--no-verify` after those checks; GitHub CI is rerunning on the new head. - Under injected `BUZZ_ACP_ALLOWED_RESPOND_TO`, `BUZZ_ACP_LAZY_POOL`, and `BUZZ_ACP_IDLE_POOL_SLEEP`, the clean-env serial package command in `crates/buzz-acp/TESTING.md` passed: 964 active library tests, 2 Git bootstrap tests, and 9 pool lifecycle tests. Both named Databricks OAuth tests passed locally; another reviewer reproduced their environment-specific failures on `main`. - No authenticated relay clone or push was run. The local probes used disposable repositories and did not contact a relay. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
0ef7a2222e |
fix(mobile-infra): render push grant lifetimes as decimal in chart 0.3.2 (#7820)
A valid 30-day push gateway grant lifetime could render in scientific notation (e.g. `2.592e+06`) in the chart, which the gateway binary does not accept. Always ormat the integer as decimal text, preserving the strict gateway parser. ### Testing Added regression coverage that feeds actual Helm-rendered environment values into the production config parser. Covers defaults, values files, command-line overrides, exact lifetime boundaries, and invalid input rejection. The default case failed before the fix and passes afterward. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
7e96f91229 |
feat(mobile-infra): support development App Attest (#7744)
Development-signed iOS apps cannot enroll with the current gateway because their Apple App Attest AAGUID is rejected. Add an explicit personal development build feature and environment setting so a development gateway can use a developer's own application identity and sandbox APNs credentials. Ordinary gateway builds remain production-only. Special builds also default to production, enforce exactly the selected signed AAGUID, and retain certificate-chain, pinned-root, nonce, app-ID, key, credential-ID, and counter verification. Document the matching mobile signing configuration and expose the feature through an optional Docker build argument. ### Validation The development image deployed successfully to a personal Railway stack; private readiness, enrollment challenges, authenticated relay access, and APNs sandbox TLS/HTTP2 connectivity passed. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
a61239f0d8 |
fix(mobile): avoid opening empty threads on message tap (#7756)
Tapping a mobile channel message with no replies currently opens an empty thread. Only enable message-row thread navigation when the existing thread summary reports replies, including replies known to the relay but not loaded locally. Long-press → Reply still starts a new thread. Added widget tests for zero replies, loaded replies, relay-only replies, and starting a thread through Reply. The zero-reply test failed before the fix and passes afterward. Validation: all 227 channel-page tests, the full mobile pre-push checks, and all PR CI checks passed. The full repository gate failed in unchanged Rust ACP timing tests (idle reset, keepalive, and steering deadlines); this PR changes only mobile Dart files. No duplicate open PR or issue found. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
5511b56fcf |
fix(relay): exclude ephemeral activity from message quota (#7736)
## Summary Stop charging ephemeral WebSocket activity against the per-minute `Messages` quota. Presence, typing, and observer traffic should not consume the allowance intended for persistable event attempts. - Use the existing `buzz_core::kind::is_ephemeral` predicate (kinds `20000..=29999`) at the second admission gate. All authenticated EVENT/REQ/COUNT traffic still consumes the shared `WsEvents` budget first. Non-ephemeral EVENT attempts remain charged, including attempts later rejected by authorization or validation. - Add bounded `bucket` labels (`messages`, `ws_operations`, `api_calls`) to `buzz_admission_rejections_total`. HTTP changes are metric labeling only. Wire rejection text remains byte-for-byte unchanged, preserving existing client parsers. - Add real-Redis admission regressions and select them in the existing relay integration CI job. No quota increases, identity/keying changes, client changes, new limiter, or deployment. Downstream authorization and fail-closed Redis behavior are unchanged. ### Related issue Duplicate search found overlapping work: open #4912 and closed #4902. They propose the same ephemeral-accounting correction alongside broader wire/client/config changes. The relay fix is a deliberately narrower four-file implementation on the current admission module, with metrics-only diagnostics and real-Redis regression coverage. A separate test-only CI repair is documented below. It does not close or claim to supersede those PRs. Related symptom: #7411. This PR does **not** resolve subscription bursts exhausting the shared WS budget; that budget is intentionally preserved. It also does not establish or fix the cause of HTTP quota exhaustion. ### Testing Validation ran against base `779af8886caae1317b4de962082429867ab61503` plus the exact patch now committed as `70ea0528594e25e6dcb40cd74620bd5ef0177592` (binary diff SHA-256 `5064e1cbb87ecb9eff10336bae14b912cac442e1edc247aa4f093de30e4f9717`). Normal commit/push hooks subsequently passed on the committed head, without bypass. Passed: - Independent review and rerun: 11 rejection tests (including real Redis) plus 4 admission tests. Covers ephemeral range boundaries, human/agent budgets, persistable events, ephemeral admission after message exhaustion, mixed EVENT/REQ/COUNT flood exhaustion, Redis outage, exact wire payloads, and bucket metrics. Three mutations were caught: reintroducing ephemeral billing, exempting stored events, and bypassing WS rejection. - `just test-unit`: all 17 runner groups; `just test-integration`: all 3 runner groups. Auth integration reported no tests; ignored database integration tests are not claimed as executed. - Relay all-target Clippy, formatting, workflow actionlint, pre-commit/signoff hooks, and all applicable pre-push lanes (`push-head-scope`, `branch-skew`, `file-size-check`, `rust-tests`, `desktop-tauri-checks`). - Release relay/CLI/admin build and isolated release-binary workflow: channel creation, signed root/reply sends, exact-ID readback, and thread retrieval. Debug and release WS smoke admitted three presence events with a Messages limit of two, admitted two persistable events, rejected the third, still admitted presence afterward, and rejected author mismatch. Debug Prometheus confirmed `bucket="messages"`. **Outstanding validation gap:** the additional `cargo test -p buzz-relay --no-fail-fast` run was not green: library 1,055 passed / 2 failed / 100 ignored; binary 13 passed / 1 ignored; boot integration 9 passed. Failures were `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` (504 instead of 200) and `telemetry::tests::trace_context_lookup_does_not_enable_callsites` (tracing assertion). Neither failure was diagnosed or repaired in this scope, and this PR does not claim a fully green package suite. `just ci` was not run locally; hosted CI remains to validate the PR. All runtime tests used isolated local services. Owned services have been stopped; nothing was deployed and no production data was touched. ### CI cancellation-test repair At the original relay head, hosted Rust unit tests failed on the pre-existing cancellation harness race tracked in #4945. With owner authorization, this PR now adapts the one-file repair from #5003 (original commit `cc8dc6124957acfd67fe2336396b58e0ff43ec60`), preserving author credit. #5003's old hosted workflows failed without starting jobs; their status is not used as validation evidence. Commit `55fe5f16618a51d03ad48d6f2f97b2f8b463607a` holds the fake provider response until a successful cancellation acknowledgement, then replays all pre-acknowledgement usage/prompt frames in order. It retains the cancellation and usage-before-response assertions and explicitly requires prompt completion. No production code changes in this follow-up. Validation on parent `70ea052859` plus this test patch, before the pre-commit-only assertion wrapping: full `cargo test -p buzz-agent --no-fail-fast` passed (715 tests, one ignored); 100 targeted repetitions passed with four concurrent workers; all-target buzz-agent Clippy passed. The unmodified parent also passed 50 local repetitions, so this is not claimed as a local reproduction of the intermittent CI failure. The synchronization defect was confirmed from the source, original CI log, and #4945. Hosted CI must validate the updated head. Independent source review cleared exact commit `55fe5f16618a51d03ad48d6f2f97b2f8b463607a`. Normal pre-commit/signoff and all applicable pre-push hooks passed without bypass. The original relay-package mesh-demo and telemetry failures remain disclosed above and were not modified by this test-only repair. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: kiranmagic7 <262980978+kiranmagic7@users.noreply.github.com> |
||
|
|
4e1770aa3e |
Add mobile VISION (#7710)
## Summary Add the Buzz Mobile vision covering intended experience, architecture, shared client behavior, verification, release cadence, and critical functionality. Link it from the mobile README; the existing agent instruction to read relevant `VISION_*.md` documents makes it discoverable during planning and review. ### Related issue No duplicate issue or PR found. ### Testing Documentation-only change. Diff checks and pre-push hooks passed. Repository-wide `just ci` is running locally; Rust formatting has passed. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
01b6174a1c |
fix(ci): don't run desktop tests for purely mobile client changes (#7709)
Mobile-only PRs currently trigger desktop CI because the desktop test filter's glob inadvertently matches every path outside Tauri. Fix that filter so that pure mobile changes skip desktop builds, tests, and their relay artifact producer. ### Verification - 12 regression tests exercise the exact pinned paths-filter action on mobile, desktop, Tauri, backend, mixed, workflow, and documentation changes. - Restoring the bad glob makes the mobile-only regression test fail. - The filter tests, existing required-context isolation contract, workflow syntax validation, and Biome pass locally. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
76f7e5ef0c |
feat(push): make gateway origin configurable (#7261)
Make the push gateway origin configurable across the relay, gateway, mobile client, and deployment chart. This is a fresh deployment. The gateway has only run on personal development infrastructure, and push has no existing enabled users. Legacy migration, configured-to-unconfigured mobile transitions, and rolling upgrades from pre-launch gateway binaries are not supported. Populated pre-launch databases are rejected with an informative error before migrations run; their data is untouched. Mobile builds may omit the gateway URL, leaving push unavailable. Once push is enabled, subsequent builds must retain that configuration. ### Validation - All applicable CI checks pass at `5f546cfaf403f0386f146d213adfb4392f4ec29c`, including Rust, mobile/Swift, desktop/integration, security, image builds, and Helm checks. An unrelated agent cancellation test passed on retry. - PostgreSQL CI passed 384 tests, including the production-entry-point regression for refusing populated pre-launch databases before migrations, fresh initialization, and subsequent initialized deployments. - Focused local validation passed all 10 gateway PostgreSQL tests under the CI discovery filter and all 3 unconfigured-mobile tests. Repository-wide PostgreSQL discovery validation and the complete pre-push hooks passed. - Physical-device APNs/App Attest behavior and deployment to a live cluster were not verified in this work. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
4d08194ea9 |
Deduplicate thread context by ACP session delivery (#7620)
## Summary Buzz can repeat a thread reply in later prompts after the provider already received that event as context. The old fix treated every message signed by the agent key as retained by the current provider session. That could hide an unseen heartbeat reply because independent sessions share the same signing key. This PR now removes only exact event IDs already delivered to the live ACP session. It uses bounded overfetch so removed events do not shrink the new-context window, and it preserves unseen same-key replies from other sessions. The context header reports `truncated=true` whenever session history was omitted. The harness calls `/count` only when the relay query finds a sentinel beyond its fetch window. This deliberately does not parse event IDs from shell output. A reply that the agent just published may appear once in the next context delta; after that event reaches the provider as context, later prompts omit it. --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
7c789dee09 |
Fix Apple Silicon iOS simulator builds (#7646)
Apple Silicon simulator builds fail because MLImage and MLKit contain device-labelled ARM64 objects. Enable the Flutter ML Kit package's upstream compatibility helper, which restores device labels for iPhone builds. This preserves the MLKit-based functionality (avatar background removal) and complements the notification-extension linker isolation in #7187. Adds a complete iOS simulator build to CI to catch this build regression. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
6146c4fd16 |
feat(git): add default-branch management to relay and CLI (#7562)
Authored by Brain and opened on behalf of Wes (`wesbillman`).
## Summary
Add `buzz repos default-branch get/set` and the relay operation it
needs. Git push changes branch tips but cannot select the server's
symbolic HEAD. This selects an existing branch without deleting
branches, moving refs, or changing packs.
- Reuse immutable Git manifests and captured-ETag CAS; concurrent
pushes/default changes conflict, including stale no-ops.
- Require request-specific NIP-98, body binding, fail-closed replay
protection, host tenancy, current channel membership and
repository-management authority. Push/project roles alone are
insufficient.
- Send the CLI mutation once, without redirects; ambiguous delivery
retains the observed digest and is non-retryable.
- Document the narrow HTTP exception to Nostr-first, delegation scope
and admission-time ACL revocation. Kind:30618 remains derived; the
manifest CAS is authoritative.
- Register the Postgres/Redis/MinIO route and fresh-clone tests in the
existing Backend Integration lane.
## Review fixes — `7457bda50afa6b52e36315572fa96928b2e21db6`
- Reject signed valueless `payload` tags when verifying a body; preserve
the shared verifier's intentional absent-payload compatibility. The real
settings route now proves 401, unchanged manifest pointer, and unchanged
kind:30618 event IDs for malformed and other invalid credentials.
- Require a nonempty returned branch and `head == refs/heads/{branch}`;
SET additionally requires the returned branch to equal the attempted
branch. Malformed/mismatched mutation responses remain non-retryable
`DeliveryUnknown`, retaining the attempted branch and original digest.
Invalid reads fail before any POST.
- Strengthen the later-push/fresh-clone regression to select
`release/v1` while `main` exists.
## Verification
At `7457bda50afa6b52e36315572fa96928b2e21db6`:
- Full pre-push passed (all 14 Rust test groups, serialized Tauri
tests/clippy, file-size and branch-skew gates; 522 seconds). No hooks
disabled.
- All four isolated Postgres/Redis/MinIO settings tests pass, including
real Smart HTTP `ls-remote --symref` and fresh clone on `release/v1`.
- All 15 focused NIP-98 verifier tests and three default-branch CLI
tests pass; malformed credentials and missing-branch responses were
reproduced failing before the fixes.
- Five targeted mutants are killed: restoring the valueless-payload
bypass, weakening canonical HEAD validation, accepting an empty branch,
dropping requested-branch confirmation, and omitting hydrated HEAD for
the selected non-main branch. Mutations ran in a separate worktree and
target directory; original patch bytes were verified restored.
- `cargo clippy -p buzz-auth -p buzz-cli -p buzz-relay --all-targets --
-D warnings`, formatting, diff and file-size checks pass on the fixed
tree.
Earlier validation at `bb195b140`: seven Git authorization database
regressions and 28 existing transport tests passed (three infrastructure
cases in that module not selected); four mutations killed
management/replay/CAS/redirect regressions. Full pre-push passed with
`RUST_TEST_THREADS=1` (all 14 Rust groups, Tauri clippy and 3,173 tests,
size/branch-skew gates). Serial execution avoids an unrelated
process-global Desktop discovery counter flake; no hooks disabled or
Desktop changes.
Carl and Jude's review findings are addressed in the new head; **fresh
reviewer and exact-head security confirmation remain required**. The
earlier source review is not approval of this revision. No claim of
green new-head CI.
## Rollout
Merge and **deploy the relay support first**, then build/use the updated
CLI and explicitly set/verify the desired default. No schema migration.
This PR does not deploy anything or change a live repository.
Originating conversation (channel
`8e2fda80-9142-4df8-8e14-da288bea6471`):
buzz://message?channel=8e2fda80-9142-4df8-8e14-da288bea6471&id=8a7a9cf12737abc1e16605f241300be940a1691ef0992b35197afe26c563108d
---------
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
|
||
|
|
ad9591c43b |
fix(desktop): order unnamed roster members by full canonical npub (#7503)
🤖 ## Summary - Channel members appear in the Members sidebar. A member who has never set a display name is listed under an abbreviated form of their public key (npub), and the sidebar previously sorted those unnamed members by that short label. Short labels are not unique — different keys can share one — so the order of unnamed members could look arbitrary or unstable. Unnamed members now sort by their full public key, so the order is deterministic. - When two members display the same name, the previous tiebreak was membership order (who joined first), which is not visible to a reader and can shift as roster data loads in. The tiebreak is now the full public key, so identical display names always land in the same order. - Nothing gets noisier on screen: the full key is used only for sorting, and the sidebar still shows the compact abbreviated form. Priorities are unchanged — authored (custom) names still outrank fallback labels, and role/current-user grouping still applies. - Scope is the desktop app's Members sidebar and member management: the two existing sort comparators. Mobile and other lists in the app are untouched. ### Related issue Based on #7495 (introduced the abbreviated npub labels this follows up on). The original five presentation PRs remain independently reviewable. No closer duplicate found. ### Testing - 6469 desktop unit tests, typecheck, and check pass. - The 3 existing consumer-seam E2E tests still pass; a new E2E test asserts the sidebar lists unnamed members in full-key order, with fixture members deliberately inserted in the opposite order so incoming membership order cannot mask the sort. - Negative check: reverting only this change makes the new ordering assertion fail, so it genuinely binds the new sort. - CI has not run on this PR yet. Buzz provenance: channel 1f0e4a3d-7e01-4efe-bb16-843b357f85c9 / task 340c3de9b27dbedb8453c0c7652220f9080d30fcc70a7c4f6e27fdd4fa378056 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
44316ff72f |
feat(mesh): upgrade to mesh-llm 0.76.0-rc8 and recommend Qwen3.8 27B (#6189)
Upgrades Buzz's mesh-llm dependencies through the released `v0.76.0-rc8` tag (`2040765d`), including the Qwen3.8 curated recommendation and rc8's scheduler/runtime improvements. **Scope note:** the earlier open-relay/unenforced-admission mode has been removed from this PR at Mic's direction — it is not a product mode we want. Mesh admission remains roster/allowlist driven, exactly as on `main`: on a relay with no NIP-43 membership snapshot the mesh runs self-only. No NIP-11 mode probing, no mode-transition restarts. A future perimeter/admission strategy for open relays will be designed separately. This PR also: - seeds `BUZZ_AGENT_LLM_TIMEOUT_SECS=660` for mesh agents, above MeshLLM's 600-second backend timeout; - makes `desktop-tauri-clippy` lint both default and `mesh-llm` cfg graphs; - runs the feature-enabled desktop test suite in CI; - recommends Qwen3.8 27B Q4_K_M for 64 GB-and-larger machines, then ladders down through Gemma 4 E4B and Qwen 9B for smaller machines; - keeps stored shared-compute `auto` translated to MeshLLM's supported wire model `mesh`. RC8 verification: - `just ci` passed locally at `92ecc7ec933bdd4df804cc9f28a2b51efa5313c5`. - Pre-push differential gates passed, including both desktop Tauri cfg graphs and package tests. - A prior isolated runtime smoke used the RC8 binary's OpenAI endpoint for a Buzz-shaped system/user/tool/tool-result/final-response loop; all assertions passed and the isolated process was shut down. Perf previously measured on M5 Metal, Qwen3.8-27B-Q4_K_M: TTFT 0.22–0.32 s, ~25 tok/s streaming; agent-shaped turns ~1 s to first token after the first (prefix cache). --------- Signed-off-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Signed-off-by: Alessandro Joabar <sandro@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Jimmy <1fe240cd1a8cf775f6f3060f115e5a303181f3abf28ad4cb0c2515f4a02b36a8@meshllm.communities.buzz.xyz> Co-authored-by: Mic Neale <305999590+micspiral@users.noreply.github.com> Co-authored-by: Alessandro Joabar <sandro@squareup.com> |
||
|
|
cd02b693aa |
refactor(relay): extract NIP-29 membership authorization (#7285)
## Why NIP-29 membership authorization for kinds 9000, 9001, and 9022 was embedded in a large async handler, requiring Postgres and Redis to test and repeating the last-owner rule across five call sites. This addresses [TheSentinel454/buzz#24](https://github.com/TheSentinel454/buzz/issues/24) without adding the broader state/repository abstraction suggested there because HTTP and WebSocket writes already share ingestion. ## What - Extract pure, typed membership authorization decisions while leaving database reads and mutations in `validate_admin_event`. - Preserve existing client-visible errors and independent database last-owner safeguards. - Collapse five last-owner policy restatements into one predicate and share the identical self-departure policy. - Include the relay decision modules in `just test-unit`, so these tests execute in CI. ## Risk Assessment Medium-low. This touches production relay authorization, but intentionally preserves wire behavior and database defense in depth; exhaustive decision tables and relay-backed tests cover the affected paths. ## Simplification This removes repeated policy from the orchestration path and makes the rule set directly testable without introducing a repository trait or second transport path. ## Verification Verified at `15255a090797f85874921120003c645962fefaed`: - `just test-unit` — all 10 package summaries passed. An initial run hit two unrelated timing-sensitive `buzz-acp` failures; the complete retry passed 905/905 in that package. - `cargo fmt --all -- --check` - `cargo clippy --workspace --all-targets -- -D warnings` - `just file-size-check` — 10/10 policy tests passed. - `just security-review-check` — 13/13 tests passed. - Push preflight — `push-head-scope`, `branch-skew`, `file-size-check`, `rust-tests`, and `desktop-tauri-checks` passed. - Blox Postgres relay lane — 84/84 at the byte-identical source patch; full live `e2e_relay` behavior matched the clean base (45 pass and the same pre-existing kind:9002 failure on each). Generated with Claude Code --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
0e878664b0 |
ci: split CI into reusable workflows (#7168)
## Why The monolithic CI workflow is a frequent merge-conflict hotspot. Splitting cohesive domains into same-repository reusable workflows keeps one centrally filtered entry point while letting Rust, desktop, relay/PostgreSQL, client, and security CI evolve independently. ## What - Keep `ci.yml` as the only push/pull-request orchestrator with unchanged concurrency and path detection. - Move 18 execution jobs into five `workflow_call`-only domain workflows without changing their runners, steps, matrices, caches, artifacts, permissions, or timeouts. - Keep the relay artifact producer with desktop integration, the complete PostgreSQL lane, and relay E2E consumers. - Preserve all 12 existing required GitHub Actions contexts through lightweight top-level compatibility gates, so the repository ruleset does not need to change. - Update the Rust-cache contract to follow Unit Tests into `_ci-rust.yml`. ## Risk Assessment CI-only change with moderate workflow-orchestration risk. The main risks are reusable-workflow output propagation, skip behavior, and visible check naming; the old required names remain explicit top-level jobs, and the draft will stay open until an exact-head GitHub Actions run and independent review are complete. Generated with Codex --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Signed-off-by: tornquist <tornquist@squareup.com> |
||
|
|
5aed49b505 |
feat(buzz-agent): add DatabricksAuthCoordinator single-flight OAuth (#5545)
## What
Consolidates all Databricks OAuth acquisition behind one coordinator on
`PkceOAuthTokenSource`. Every entry point — the four `TokenSource`
methods (`bearer`, `bearer_no_browser`, `refresh_now`,
`interactive_login`) plus the public `acquire_with_intent` — routes
through a single `acquire()`/`acquire_locked()` core that owns browser
and cooldown policy.
Before this, acquisition logic was scattered across those methods with
no coordination: concurrent callers (Desktop discovery, the saved-agent
model picker, managed-runtime inference) could each pop their own
browser, and a just-denied attempt would immediately re-prompt on the
next passive read.
## How
- **Intent policy.** `AuthIntent::{Auto, UserInitiated, Headless}`
decides whether a caller may open a browser and whether it honors the
cooldown. `Headless` never browses; `Auto` browses but honors an
unexpired cooldown; `UserInitiated` browses and bypasses+clears the
cooldown.
- **Two-layer single-flight per cache key.** An in-process registry
(`INFLIGHT`) coalesces same-key, same-intent callers onto one leader's
attempt before the file lock. The slot key is `(lock_path, AuthIntent)`,
so a `UserInitiated` sign-in never inherits an `Auto` leader's result. A
joined result is revalidated against the waiter's own contract. Across
processes, callers serialize on a `flock`-based advisory lock and share
success through the on-disk cache. RAII `Drop` releases both lock and
leader slot.
- **Joiner credential-state reconciliation.** `SlotPublish` carries the
full `CachedToken` on success. Each joiner reconciles its own
independent `state` cell under `state.lock().await` before returning:
adopt when absent, expired, or matching the joiner's rejected
credential; preserve any distinct newer usable credential. On a matching
shared failure, neutralize the joiner's in-memory rejected entry under
lock via `expire_rejected_memory` — durable disk mutation is reserved
for `acquire_locked` under the cross-process file lock. Without this, a
joining source's state remains stale or empty and subsequent plain
`bearer()` calls resurface the rejected or absent credential.
- **Validate-before-persist boundary.** `finish()` is the
candidate-token persistence boundary for refresh and browser results.
Before a token is written to cache or the cooldown is cleared, a bearer
equal to the caller's rejected bytes yields a typed failure.
- **Token neutralization.** When `acquire_locked` enters with `rejected
= Some(bytes)`, it calls `expire_rejected()` under the state lock before
any cache check.
- **Cross-process failure single-flight.** An `AttemptRecord` sidecar
records a monotonically-increasing generation, intent, result code, and
SHA-256 digest of the completing caller's rejected token. Adoption is
temporal (pre-queue snapshot predates current generation) and
digest-matched.
- **Typed outcomes.** `AuthError` with stable `code()`/`from_code()`
replaces display-text matching.
- **Durable cooldown sidecar.** Every failed browser attempt is recorded
next to the cache key. 5-minute expiry.
- **Windows disk persistence disabled.** On non-Unix platforms,
`persist()` is a no-op. Lock, cooldown, and attempt sidecars are active
on all platforms. Tests that seed or assert on the on-disk token cache
are `#[cfg(unix)]`-gated.
- **Injected browser opener** invoked while the localhost callback
listener is live.
## Tests
- `crates/buzz-agent/tests/databricks_auth_coordinator.rs`:
browser/cooldown/classification acceptance matrix with a scripted
`BrowserOpener` and stub OIDC provider. P1 regressions exercise the full
`finish()` → `acquire_locked()` → `acquire_leader()` →
`LeaderGuard::complete()` → joiner wiring:
- `test_inprocess_joiner_reconciles_stale_state_after_shared_success`
(Unix): two real sources both loaded locally-fresh-but-rejected X; after
shared success Y, subsequent plain `bearer()` on both returns Y, not X.
-
`test_inprocess_joiner_neutralizes_rejected_on_matching_shared_failure`
(Unix): B's matching rejected X is force-expired in memory after shared
`RefreshRejected`; subsequent read cannot return X.
- `test_inprocess_joiner_populates_empty_state_no_second_acquisition`
(non-Unix): empty A/B join a browser success; B's subsequent headless
read returns Y without a second browser (no disk fallback on non-Unix
exposes the regression).
- `test_crossprocess_userinitiated_waiter_adopts_predecessor_denial`
(snapshot-marker barrier replacing an earlier sleep for deterministic
generation ordering).
- `auth.rs` in-crate tests: lock-primitive edges, disk-recheck on shared
failure (`#[cfg(unix)]`), and:
- `test_joiner_reconciliation_blocked_until_state_lock_released`:
deterministic direct-poll proof that awaited reconciliation requires
`state.lock().await`. The test task holds B's state mutex and manually
polls a pinned real `acquire()` future — Poll 2 (slot published, mutex
still held) must return `Pending` because `lock().await` blocks; with
`try_lock` instead, Poll 2 returns `Ready`, failing the assertion.
- `test_joiner_preserve_distinct_newer_credential`: deterministic direct
polling parks B at `slot.wait()`, then writes Z directly into B's state
in the same task, then publishes Y and awaits completion. B must return
Y but leave state == Z. Mutation check: unconditional adoption
overwrites Z with Y, failing the state assertion.
- `test_joiner_shared_failure_recovers_disk_replacement` (Unix): the
matching-failure joiner enters the recovery branch — after
`expire_rejected_memory` (in-memory, empty state no-op) it reads a
sibling-written disk replacement via `usable_from_disk` and returns it.
Mutation check: removing the `usable_from_disk` recovery branch returns
`Err(RefreshRejected)`.
- `test_joiner_failure_does_not_write_disk` (Unix): byte-for-byte
disk-invariance regression — a matching-failure joiner calls
`expire_rejected_memory` and must not touch the on-disk cache. An
independent process C may write a valid replacement between A's failure
and B's reconciliation; this guard ensures B's unfenced in-memory
neutralization cannot overwrite C's concurrent disk write. Mutation
check: reverting to `expire_rejected` rewrites the file (`expires_at =
0`), changing the bytes and failing the assertion.
- `test_lock_timeout_leaves_cooldown_sidecar_byte_for_byte_untouched`: a
waiter past its deadline returns `LockTimeout` before entering
`acquire_locked`; the cooldown sidecar bytes are unchanged. Documents a
pre-lock limitation: `LockTimeout` callers do not neutralize state or
sidecars.
## Scope / follow-ups
- **Runtime 401 handling is deferred.** This PR owns acquisition
single-flight and policy.
- **Desktop wiring is Phase 2** (not in this PR's boundary). Confined to
`crates/buzz-agent/`.
- **Windows DACL** is a follow-up once the `windows-sys` binding is
available.
## Stack
Built on [#5534](https://github.com/block/buzz/pull/5534)
(`hayt/databricks-oauth-cache-hardening`), now merged. Retargeted to
`main`.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
b270437a62 |
fix(mobile): isolate extension linker flags; complete iOS build in CI (#7187)
### What changed? Clear inherited linker flags on the NotificationService target in Debug, Release, and Profile. The extension continues to link BuzzPushKit through its target framework phase, while Runner retains its CocoaPods plugin flags. Extend the macOS mobile CI lane to install Flutter dependencies and build the complete unsigned iOS Release app. This exercises the production Flutter, CocoaPods, Runner, BuzzPushKit, and NotificationService build graph on every mobile change. ### Why? The iOS 0.16 RC archive exposed that NotificationService inherits Runner-only CocoaPods linker flags through the shared Flutter xcconfig. That made the extension link Flutter plugins without the Flutter engine and fail on unresolved Flutter symbols. The previous CI coverage built only the standalone BuzzPushKit package, so it could not detect an app or extension linker regression. ### How is it tested? - A complete unsigned Release build of the Runner scheme succeeded with Xcode and linked, embedded, and validated `NotificationService.appex`. - The new GitHub Actions lane builds the full unsigned iOS Release app with `flutter build ios --release --no-codesign --no-pub`. - Pre-commit and pre-push hooks passed. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
4365883151 |
chore(ci): lower Codex security review effort (#7179)
Changes `CODEX_REASONING_EFFORT` from `max` to `high` in the `security-review` job. `max` increases both normal review latency and exposure to the upstream PTY-shutdown hang ([openai/codex-action#169](https://github.com/openai/codex-action/issues/169)) where Codex finishes writing its output file but holds stdio open. The composite `uses:` action ignores the 30-minute step timeout and continues running until the 40-minute job timeout fires; at that point GitHub cancels the entire job and the `always()` salvage step never gets to run. `high` retains a high reasoning setting while trading some depth for speed, reducing typical completion time and shrinking the window during which a hung process blocks the salvage path. No other behavior changes. Action pin, CLI version, model, output schema, prompt, and timeout values are unchanged. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
e17a0d44c3 |
fix(dev): keep the canonical profile when launching from desktop/ (#7143)
## Summary Carl, an automated agent, implementing on Wes's request. Fix ordinary Desktop dev checkouts being misclassified as linked worktrees when launched through `just production`, `staging`, or other recipes that source `instance-env.sh` from `desktop/`. From the repository root, Git reports both directories as `.git`. From `desktop/`, it can report an absolute `--git-dir` and `../.git` for `--git-common-dir`. The textual comparison treats them as different and chooses a branch-suffixed app profile (for example `.dev.main`) instead of the canonical `.dev` profile populated by the agent-adoption script. - Request `--path-format=absolute` for both paths. Actual linked worktrees still have distinct Git/common directories and retain their existing isolated app identities. - Add eight isolated fixture cases executing the real environment script with real Git: ordinary root/subdirectory on main and feature branches, linked root/subdirectory, detached linked subdirectory, and symlinked ordinary subdirectory. Paths contain spaces. Only native icon generation is stubbed; no user profile, keychain, app, or relay is accessed. - Run the regression in the existing always-run CI contract job. No agent migration, global-settings copy, keychain changes, profile merging, branch-label redesign, or packaged-release behavior changes. Existing Git `--path-format` support is required (Git 2.31+). ### Related issue Searched open PRs for `instance-env` and worktree titles, and open issues for dev-profile/adoption reports. No matching path-normalization fix found. - #7142 touches `instance-env.sh` for descriptive labels. This fix is independent; it uses a distinct test filename to avoid colliding with that PR's new `test-instance-env.sh`. - #6915 proposes a separate release-profile launch recipe; this fix preserves the current dev-profile contract instead. ### Testing At exact head `0a09a2f3716b0378546b66be2302b750ed4330b9`: - `scripts/test-desktop-instance-detection.sh`: **8/8 passed**. - Before the production change, the same regression failed on the ordinary checkout's `desktop/` case: actual `xyz.block.buzz.app.dev.main`, expected `xyz.block.buzz.app.dev`. - `bash -n scripts/instance-env.sh scripts/test-desktop-instance-detection.sh`: passed. - `git diff --check`: passed before commit; pre-commit and commit-msg hooks completed. - Applicable pre-push gates passed: branch-skew, push-head-scope, and file-size policy (6 tests plus all three entrypoints). Rust/Desktop/Mobile package lanes were correctly inapplicable to this shell/CI-only diff. No native app launch or first-boot key import was performed. The validated boundary is the generated launch environment/profile identity, not successful agent startup. Hosted CI has not been checked or represented as green. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> |
||
|
|
bd73490418 |
ci: run PostgreSQL tests in isolated lane (#6730)
## Why PR #6660 introduced four PostgreSQL-backed persistence tests and a focused CI selector, but an exact test-name list does not automatically cover future database tests. Broad ignored-test execution also exposed shared-schema races and ambiguity between desired-state and migration-applied schema expectations. ## What - Establish a discoverable convention: PostgreSQL unit modules use `postgres_tests`, PostgreSQL integration binaries use a `postgres_` prefix, and unrelated external-infrastructure tests use an `external_infra_` prefix. - Add a dedicated nextest PostgreSQL profile and archive-backed CI job covering the relevant crates without enumerating test names. - Create a run-scoped desired-state source database and a unique PostgreSQL database per test process for parallel-safe isolation, with cleanup on success, failure, or interruption. - Route destructive migration and migration-parity tests to clean `template0` databases while desired-state tests clone the desired-state database. - Document discovery, schema modes, required role privileges, and a bounded portable local runner. - Keep the existing infrastructure-free unit-test jobs unchanged. This is intentionally limited to test and CI harness behavior. It does not move production database code, change persistence semantics, or implement issue #20's broader shared test-utility refactor. ## Risk Low production risk because all changes are confined to tests, CI configuration, documentation, and test harness scripts. Remaining operational risks: - Cleanup retries `dropdb --force` five times across roughly four seconds. Exhaustion warns with the database name but deliberately does not mask the test result; individual PostgreSQL diagnostics are suppressed. - The seven-package boundary is duplicated between the runner and CI archive and must remain synchronized if PostgreSQL tests move to a new crate. - Portable SHA fallback branches were exercised on Linux; no macOS Blox workstation was needed for this Linux CI artifact. - GitHub's unchanged generic Unit Tests job currently fails while cold-linking `buzz-voice` because `sherpa-onnx-c-api` is absent. The branch changes no voice/build/toolchain inputs; the exact parent previously passed that job, and Blox passes the same infrastructure-free suite (1,450/1,450). A prior-head one-job retry and the final-head run both reproduced the hosted-runner failure. ## Verification Author Blox workstation `2020088`: - `cargo fmt --all -- --check` - `just clippy` - `just test-unit`: 1,450 passed; PostgreSQL tests remain skipped in fast jobs - PR #6660 focused tests: 4/4 passed through the final runner - Full lane: 282/282 passed in 10.087s with a non-superuser role limited to `CREATEDB`, `CREATEROLE`, and `pg_read_all_stats` - 272 desired-state tests - 10 migration-applied tests - 6 explicitly filtered external-infrastructure tests - Cleanup fault injection: a deliberately underprivileged 282-test run produced the expected 277 passes and 5 failures, and the post-run catalog audit found zero lane databases after retry cleanup. The restored successful run also left zero lane databases. GitHub exact final head `92c231e48f299ea2af23817763f5ae80ac013d68`: - [PostgreSQL Tests](https://github.com/block/buzz/actions/runs/32789030203/job/97630827823): 282/282 passed across 10 binaries in 98.886s; 1,240 skipped, including 6 via the profile filter - The shared relay/PostgreSQL archive predecessor completed successfully - [Unit Tests](https://github.com/block/buzz/actions/runs/32789030203/job/97626930420): unrelated `sherpa-onnx-c-api` native-link failure described above - Independent exact-head review on separate Blox workstation `2022762`: no substantive findings after all initial findings were addressed. The reviewer independently verified cleanup retry behavior, exit-status preservation, shell portability, syntax, and a clean exact-head worktree. ## References - Stacked on #6660 at exact head `561de54be4d9c2b622b7c2aa5b61bc3068f47e2a` - https://github.com/TheSentinel454/buzz/issues/20 - https://github.com/TheSentinel454/buzz/issues/20#issuecomment-5398526792 Generated with Codex. ## Update — August 24, 2026 review follow-up - Centralized the repeated PostgreSQL test URL resolution in crate-local, test-only helpers without introducing the broader shared utility refactor from issue #20. - Restored descriptive hybrid/Redis test function names. Structural `external_infra_*_tests` modules now own exclusion, and the nextest filter only recognizes module path segments. - Added a three-second source guard that scans every Rust file and fails CI when an ignored PostgreSQL test would be omitted or an external-infrastructure test would be included. Fixture tests cover accepted modules/binaries and both failure modes. - Removed the two implementation-plan documents. Performance profiling on Blox workstation `2027352` (same prebuilt archive and cargo-nextest 0.9.143 for every comparison): - Current per-test database model, 8 workers: 10.15–10.40s across three runs. The 283 `createdb` calls and 284 `dropdb` calls consumed 28.85–29.46 aggregate seconds, about 39–40% of aggregate test-process duration. - Reusing one database per worker without cleanup: 6.55–6.84s, about 35% faster, but all three runs failed because global matcher-queue tests inherited a quiesced community from an earlier test; the failing test varied with scheduling. - Reusing workers with `TRUNCATE … RESTART IDENTITY CASCADE`: all 282 tests passed, but total time regressed to 10.47–10.67s. The 265 truncate calls consumed 24.34–25.45 aggregate seconds, so truncation merely replaced most clone/drop cost. - Raising concurrency to 16 workers reduced one isolated-database run to 8.79s, but 24 workers exposed a cluster-global `pg_stat_activity` race. The profile remains at the proven-safe 8 workers. Decision: retain unique per-test databases. Harness cleanup uses `dropdb --if-exists --force`; it drops the database rather than truncating tables or deleting rows. Verification at local branch head `60086c1f6ee34a193c342255a5e8e6293b60e988`: - Discovery inventory: exactly 282 intended tests; all 6 hybrid/Redis tests present and structurally excluded. - PR #6660 focused persistence tests: 4/4 passed. - Full PostgreSQL lane: 282/282 passed three times; 1,240 skipped each run; wall time 9.72–9.96s without profiling shims. - Non-ignored tests with a desired-state database: 1,234/1,234 passed; one mesh-demo timeout from the first run passed immediately in isolation. - `cargo fmt --all -- --check` and all-target/all-feature clippy for `buzz-db`, `buzz-deletion`, and `buzz-relay` passed. Final independent re-review at exact head `42097c0136ab3fa8f0efe5720ae829c04575b9b6` on separate Blox workstation `2028455`: no substantive residual findings. The reviewer independently exercised ordinary strings, zero/one/three-hash raw strings, line/block comments, and string-contained lookalikes, then reran the full 353-file scan, shell syntax, Python AST parsing, and diff check. Independent review found that the source guard's initial regular expression could miss raw-string ignore reasons and treat a commented attribute as real. Final head `42097c0136ab3fa8f0efe5720ae829c04575b9b6` parses valid ordinary/raw Rust string literals only at attributes found in comment-sanitized source; regression fixtures cover both cases. The fixture suite, Python compilation, shell syntax, diff check, and full 353-file repository scan pass on Blox. ## Update — August 25, 2026 restack PR #6660 merged, so this follow-up was rebased from its former exact parent onto current `origin/main` (`8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8`). The final candidate head is `05dcc2ab28948e3ab79bb44839a69f2ba44648a2`; the PR no longer carries #6660's pre-squash history. Fresh exact-head verification on Blox workstation `2028572`: - Discovery guard passed across 353 Rust files; inventory remained exactly 282 intended PostgreSQL tests. - PR #6660 focused persistence tests: 4/4 passed. - Full PostgreSQL lane: 282/282 passed three times; 1,243 skipped each run; wall time 9.70–9.87s. - `cargo fmt --all -- --check` and all-target/all-feature clippy for `buzz-db`, `buzz-deletion`, and `buzz-relay` passed. ## Update — August 25, 2026 final hosted verification - [GitHub PostgreSQL Tests](https://github.com/block/buzz/actions/runs/32801679226/job/97667835080): 282/282 passed in 53.787s; 1,243 skipped. - The final guard scanned all 353 Rust files successfully before the hosted lane ran. - The shared relay/archive prerequisite and infrastructure-free Unit Tests job both passed at exact final head `42097c0136ab3fa8f0efe5720ae829c04575b9b6`. --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Signed-off-by: tornquist <tornquist@squareup.com> |
||
|
|
5673c3307e |
feat(desktop): add protected-build Bestie experiment (#6902)
## Summary Introduces a protected-build boundary for the default-off Bestie experiment without adding any Bestie product surface. - Official OSS builds select an empty protected-feature module and emit no Bestie/Chief metadata or implementation content. - Protected internal builds select a separate module graph containing the Bestie experiment definition. - Within an internal build, Bestie remains disabled until the user opts in under Settings → Experiments. - The production build runs an artifact matrix and fails if OSS output contains protected content or internal output lacks the Bestie manifest. ## Build contract | Build variant | User opt-in | Result | | --- | --- | --- | | Official OSS | Any/forged | Bestie absent from the compiled artifact | | Protected internal | Off | Bestie available but disabled | | Protected internal | On | Bestie enabled | The companion protected-release change is squareup/buzz-releases#91. It sets `VITE_BUZZ_BESTIE=1`, requires that exact value, forwards it into the signed macOS build, and asserts the contract in release validation. ## Why this is separate This gives later Bestie PRs one build-selected import seam. Protected implementations must be reachable only from the internal module so they never enter the official OSS module graph. ## Non-goals - No Bestie persona or provisioning - No sidebar, app-chrome, or message-toolbar UI - No entitlement or secrecy claim: the source is public; this boundary controls official Block artifacts ## Verification - Exact commit `523cf49ced03cba9be43836a54d6aa5d6923cc82` - Full `just ci`: 5,673 Desktop tests, 2,773 Tauri tests, 1,860 mobile tests, Rust/Tauri/web/mobile static checks and builds - OSS production artifact: scanner confirms no `Bestie`, `Chief of Staff`, or `builtin:bestie` content - Internal production artifact: scanner confirms the protected Bestie manifest is emitted - Both build orders verified; `dist` retains the requested variant for Vite/Tauri packaging --------- Signed-off-by: Arjun Mahanti <arjun@squareup.com> Signed-off-by: Fizz <fizz@buzz.local> Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Fizz <fizz@buzz.local> Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz> |
||
|
|
93237b4a74 |
fix(acp): wake agents from workflow messages (#6953)
> Pinky, an AI agent, is opening this PR on Wes's behalf.
## Summary
Workflow-generated messages can contain a valid agent mention but still
fail the ACP inbound author gate because the relay signs the event. This
keeps the existing wake policy and gives ACP a narrowly verified
effective author:
- preserve the workflow owner's existing `p` tag and all
rendered-mention `p` tags
- add explicit `["buzz:workflow-owner", <owner hex>]` provenance to
relay-generated workflow messages
- add `["buzz:workflow-mention", <agent hex>]` authority only for
mentions resolved from the stored, unrendered workflow step template
- accept that owner only for a verified kind-9 event signed by the
relay's current NIP-11 `self` key, with unique canonical workflow
metadata and an explicit workflow mention for the receiving agent
- route the verified owner through the existing author and in-flight
mode policies in both normal and setup listeners
- refresh relay identity after reconnects, retaining the last verified
key on transient fetch errors while treating a successful response
without `self` as definitive removal
Malformed, duplicate, forged, tampered, wrong-kind, and wrong-relay
attribution all fail closed to the raw event signer. `respond-to=nobody`
remains absolute. Old/mixed-version messages without the explicit
provenance retain their current fail-closed behavior.
## Trust boundary
The workflow owner means **“scheduled by,” not “authored every rendered
word.”** Trigger-controlled substitutions may still produce ordinary `p`
mention routing for compatibility, but they cannot mint
`buzz:workflow-mention` authority. Only a target named in the durable
owner-authored step template can receive that authority.
The author gate is not bypassed: after relay signature/provenance
verification, the effective owner is evaluated under the same
`owner-only`, `allowlist`, DM, and `nobody` policies used for ordinary
messages. Owner control commands continue to use the raw event signer.
## Why this PR
This is the focused immediate fix for waking an **online** agent from a
stored workflow mention. Earlier attempts were not a finished mergeable
fix and had materially different or incomplete trust designs. Larry's
larger draft stack addresses durable delivery across restarts; that
remains valuable future work and can supersede this effective-author
path when it lands.
## Validation
At exact clean commit `fe5b55619fe44176343eefb4cb7fe180df45a7d8`:
- `buzz-relay workflow_sink`: 25/25 passed, including all four ignored
PostgreSQL cases
- `buzz-acp --lib`: 845/845 passed
- `buzz-workflow --lib`: 169/169 passed (2 unrelated PostgreSQL tests
ignored)
- warnings-denied Clippy passed for the changed Rust packages
- `cargo fmt --all -- --check` passed
- `git diff --check` passed
- repository pre-push gates passed, including branch-scoped Rust tests
- CI now selects the ACP library tests and the relay's pure + PostgreSQL
workflow-sink tests so these guards cannot silently remain unexecuted
The production event-to-author gate is shared by normal and setup
listeners and has biting regression tests for accepted explicit
attribution, legacy owner-`p` rejection, and forged-attribution
rejection.
## Exact-head local relay + ACP proof
Following the release-binary/local-relay shape in `TESTING.md`, the
exact commit above passed a fresh isolated real-process matrix using:
- a freshly recreated Postgres database with migrations
- isolated Redis
- exact-head release `buzz-relay`, `buzz`, `buzz-admin`, and `buzz-acp`
binaries
- newly provisioned owner, channel, and bot member through the CLI
- workflow creation and triggering through the running relay
- a deterministic ACP protocol subprocess capturing actual
`session/prompt` dispatches
- a NIP-11 `self` value verified against the running relay signer
Cases:
1. A stored explicit workflow mention woke an `owner-only` agent exactly
once.
2. A workflow message without an agent mention did not wake it.
3. A non-relay signer forging every workflow authority tag did not wake
it.
4. Trigger-controlled `{{trigger.text}}` containing `@Wake Agent`
retained ordinary `p` routing but received no authority-bearing
workflow-mention tag and did not wake the agent.
5. `respond-to=nobody` remained absolute for a valid relay-authenticated
workflow mention.
The deterministic ACP subprocess isolates and directly proves relay →
ACP authorization and prompt dispatch without depending on external
model behavior.
## Deployment and residual risk
Relay and ACP changes must be deployed together for the new wake
behavior; mixed versions fail closed. Production paired-deployment proof
remains distinct from the successful local integration run. Setup-mode
behavior has automated coverage but was not a separate case in the
five-case local matrix. Relay-key rotation is observed at ACP
startup/reconnect; transient NIP-11 errors retain the last verified key,
an intentional availability tradeoff documented in code.
---------
Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: LioLionel <62820906+LioLionel@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
|
||
|
|
17ecf0b8c9 |
fix(ci): salvage Codex review output on PTY-shutdown hang (#7042)
Codex CLI can leave a PTY descendant holding the action's inherited
stdio after the turn completes. The `runCodexExec.ts` wrapper waits on a
`close` event that never fires, so the `Review pull request` step hangs
until the job timeout kills it — discarding the finished review the CLI
already wrote to disk.
The CLI writes the completed review to the `--output-last-message` file
(exposed as `output-file`) **before** the hang. This PR adds a salvage
step that recovers it, and sets the step and job timeouts to preserve
the full 30-minute Codex execution budget.
**Changes (`codex-security-review.yml`):**
- Add `output-file: ${{ runner.temp }}/codex-review.json` to the `Review
pull request` step so the CLI writes the result before the hang.
(`runner` context is valid in `steps.with`; not in `jobs.env`.)
- Add `timeout-minutes: 30` and `continue-on-error: true` to the Codex
step — a hang now costs ≤30 minutes instead of 40, and the salvage step
still runs.
- Set job `timeout-minutes: 40` to give setup, step cancellation, and
salvage sufficient headroom without colliding with the Codex execution
budget. The original 30-minute job timeout was too narrow: evidence from
run
[33114428326](https://github.com/block/buzz/actions/runs/33114428326/job/98665369165)
shows completed output appearing 28m46s after step start, meaning a
20-minute step timeout could kill a legitimate review before the salvage
file exists.
- Add a `Salvage review output` step with `if: always()`: prefers
`steps.run_codex.outputs.final-message` on a clean exit; falls back to
the output file when the step timed out. The output file path is set in
the step's own `env` block (`CODEX_OUTPUT_FILE: ${{ runner.temp
}}/codex-review.json`), where `runner` is valid. Validates shape
(non-empty JSON object, has `overall_risk`); fails the job hard if
neither source is present.
- Wire the job `outputs.review_json` to
`steps.salvage.outputs.review_json`.
**Changes (`Justfile`, `ci.yml`):**
- Add `actionlint .github/workflows/codex-security-review.yml` to
`security-review-check` so expression-validity errors are caught
locally.
- Provision `actionlint` via Hermit (pinned v1.7.12) rather than a
one-off `Install actionlint` curl step, so the same binary is used
locally and in CI.
**Security posture is unchanged:** the salvage step reads the action's
own output and a file written to `runner.temp` — neither is
PR-controlled. Credential-stripping env block on the Codex step is
untouched.
Note this is a temporary workaround until
https://github.com/openai/codex-action/issues/169 is addressed
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
3ed623bb21 |
feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229)
## Why A wedged relay boot pod holding a relation lock can park every other writer in the fleet behind it: DB load pins at pool capacity in `Lock:relation` waits while CPU stays flat, and nothing server-side releases the lock until the holder dies. We hit exactly this in production — ~1,400 sessions queued behind one crash-looping pod's boot transaction for ~20 minutes until kubelet killed the container. ## What Applies session-level Postgres timeouts to every **writer** connection inside the existing single `after_connect` hook in `buzz-db`, all env-tunable through the same `Config::from_env → DbConfig` path as the existing pool-size knobs: | Env var | GUC | Default | Effect | |---|---|---|---| | `BUZZ_DB_LOCK_TIMEOUT_MS` | `lock_timeout` | 5000 | statements waiting on any lock fail fast instead of parking behind a wedged holder | | `BUZZ_DB_IDLE_TXN_TIMEOUT_MS` | `idle_in_transaction_session_timeout` | 60000 | reaps wedged clients idling inside an open transaction while holding locks | | `BUZZ_DB_STATEMENT_TIMEOUT_MS` | `statement_timeout` | 0 (off) | opt-in runaway-statement cap; off by default because startup migrations/backfills legitimately run long statements | `0` disables a timeout (Postgres semantics) and deliberately passes through the env parsing — unlike the pool-size knobs where `0` falls back to the default. The reader pool is untouched: replica sessions never take contended locks and already fail acquire in 150 ms. Deployers tune these via plain env vars (`.env`, or `relay.extraEnv` in the Helm chart) — no code changes needed. ## Behavior change to note With the 5 s default `lock_timeout`, a boot-time migration or backfill that waits >5 s on a lock now errors (surfacing in logs / crash-looping the pod) instead of stalling silently. That is the intended visible-failure-over-fleet-stall tradeoff; deployers with slow contended migrations can set `BUZZ_DB_LOCK_TIMEOUT_MS=0`. ## Testing - `cargo test -p buzz-db -p buzz-relay` — buzz-db green; buzz-relay has 9 failures that also fail on clean `main` in this environment (api::admin/api::media/mesh_demo — unrelated, pre-existing). - New config test covers override / `0`-passthrough / invalid-fallback for all three env vars. - Extended the existing `writer_pool_safety_hook_is_single_and_composed` source-shape test so the timeouts can't drift out of the single `after_connect` hook (SQLx replaces hooks — a second hook would silently disarm the floor guard). - `cargo fmt --check` and `cargo clippy --all-targets` clean for the touched crates. Closest existing PR/issue: none found. --- **Update Aug 28, 17:06 EDT:** Rebased onto `main` at `a3730784fc` and addressed the latest correctness review. - Ported the timeout policy onto the refactored `buzz-db::runtime` pool constructor and kept the shared env overlay for relay, admin, deletion, and audit writers. - Migration/schema-destruction connections now disable `lock_timeout` and `statement_timeout` for their intentional long wait/DDL path. This supersedes the earlier “Behavior change to note”: contended boot migrations wait for the current migration owner rather than crash-looping after five seconds. - The audit worker now preserves and retries the same entry on PostgreSQL `55P03` lock timeouts, using exponential backoff capped at one second. Other database errors retain the existing terminal error behavior, and retries emit `buzz_audit_log_lock_retries_total`. - Added CI-backed PostgreSQL regressions for writer GUC installation/migration exemption, audit-pool lock timeouts, and worker recovery. The worker regression holds the real audit advisory lock past `lock_timeout`, observes a retry, releases the lock, and proves the original entry is appended exactly once. Current verification supersedes the earlier testing notes: workspace Rust clippy passed with warnings denied; all nine infrastructure-free backend unit-test lanes passed; all three focused PostgreSQL regressions passed against PostgreSQL 17; formatting, diff checks, file-size guards, and desktop frontend checks passed. The Linux Blox workstation could not run the unrelated Tauri native lane because `glib-2.0` is absent, so that platform check is left to PR CI. --- **Update Aug 31, 11:09 EDT:** Rebased onto current `main` at `c3132c3ee9` and reran the requested audit-lock contention scenario on Blox at head `896c3fe9ed`. - `git range-diff` reports both PR commits unchanged by the rebase; the branch remains two commits and the worktree is clean. - `cargo fmt --all -- --check` and clippy with warnings denied passed for `buzz-db`, `buzz-relay`, `buzz-admin`, and `buzz-deletion`. - All three focused PostgreSQL 17 regressions passed: writer session timeout/migration exemption, audit writer timeout bounds, and audit worker recovery of the original entry exactly once. - Live protocol verification used a head-built relay and CLI, native PostgreSQL 17/Redis, `BUZZ_DB_LOCK_TIMEOUT_MS=300`, and an eight-second hold on the community audit advisory lock. The real message was accepted and persisted once while the lock was held; its audit-row count remained zero during contention while retries accumulated. After release, exactly one `event_created` audit row appeared and remained exactly one after an additional two-second duplicate check. The run recorded nine lock-timeout retries, zero audit failures, and event ID `7f8c4ffae28e78555fcf2d56396d6e6c01b3712e5411288dc79e9a54af9d9444`. Generated with Codex --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> |
||
|
|
c432a111ca |
feat(mobile): push notifications MVP (#6269)
This PR implements MVP, iOS-only,
[NIP-PL](
|
||
|
|
86b9142a09 |
refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777)
Adds authenticated, role-based moderation to the relay admin API. On
`main` the admin API is read-only and gated only by `Host`/`Origin`
matching; this branch adds NIP-98 authentication, a two-tier
Operator/Moderator principal model, mutation and staffing endpoints, and
NIP-11 auto-discovery so clients never type the admin URL by hand.
## Authentication (`BUZZ_ADMIN_AUTH`)
`BUZZ_ADMIN_AUTH` accepts `nip98` or `disabled`. Leaving it unset
defaults to `nip98` (fail-secure). Configuration fails closed: any other
value aborts startup, while a lingering `BUZZ_ADMIN_TOKEN` is ignored
with a startup warning — token (bearer) authentication is not supported.
`Host`/`Origin` matching is retained in every mode as defense-in-depth.
- **`nip98`** (default) — per-request signed NIP-98 (kind 27235) events,
resolved to an Operator or Moderator principal with per-person
attribution and individual revocability. Read-write per resolved
principal.
- **`disabled`** — no credential; relies entirely on network-layer
controls (reverse proxy, VPN, firewall) and logs a `WARN` on every boot.
Always read-only: `authorize()` resolves no principal, so mutation and
staffing routes always `403`.
## Roles
Buzz has two independent authority axes after this change.
**Relay-level** roles (new here) are deployment-global: they act across
every community on the relay, through the admin API. **Community-level**
roles (pre-existing, unchanged by this PR) are tenant-scoped: they act
inside one community, through signed Nostr moderation commands.
### Relay level (new)
| Role | Description |
|---|---|
| **Operator** | Full control of the deployment's moderation surface:
read all reports, feedback, and attachments across every community;
resolve reports with enforcement (`delete`/`kick`/`ban`/`timeout`) or
decisions (`dismiss`/`escalate`); reopen and cancel; update feedback
status; and manage the Operator/Moderator roster via the staffing
endpoints. |
| **Moderator** | Day-to-day triage: everything an Operator can do
except staffing — cannot view or change the roster. |
How a pubkey acquires a relay role (resolution order; config always
outranks DB):
1. Listed in `RELAY_OPERATOR_PUBKEYS` → **Operator** (source `config`)
2. Equals `RELAY_OWNER_PUBKEY` while `RELAY_OPERATOR_PUBKEYS` is empty →
**Operator** (source `owner_fallback`, a break-glass grant for
self-hosters that deactivates once any operator is configured)
3. Row in the `relay_operators` table → **Operator** or **Moderator**
(source `db`, managed via the staffing endpoints)
4. No match → `403`
### Community level (pre-existing, unchanged)
| Role | Description |
|---|---|
| **Owner** (community) | Full authority within their community: every
moderation action (delete, kick, ban/unban, timeout/untimeout, resolve
reports, view queue) plus member, role, and invite management. No guard
rails. |
| **Admin** (community) | Same community-wide moderation capabilities as
owner, except an admin cannot ban or time out the owner or a fellow
admin — only the owner may action an admin. Manages members and invites;
only the owner grants the admin role. |
| **Member** (community) | Standard participant; no moderation
capability. |
| **Owner / Admin** (channel) | Channel-local authority only: delete
messages and kick users within their own channel. |
| **Member / Guest / Bot** (channel) | No moderation authority. |
There is no community-level Moderator tier in v1; relay-level Moderator
is the only role by that name.
## Escalation scoping
The operator report queue is an escalation backstop, not the community's
day-to-day triage surface (per `VISION_MODERATION`, the severe class is
the platform's to review rather than the community's). Two rules enforce
that:
- **Escalated-by-default listing.** `GET /reports` with no `status`
parameter returns only `escalated` reports. An explicit
`status=<open|resolved|dismissed|escalated>` filter is always honored as
given, and full visibility across every status stays available for
platform-safety and legal review via `scope=all` (which lists reports
regardless of status). `scope` accepts only `all` and is ignored when an
explicit `status` is present.
- **Auto-escalated `illegal` reports.** Member reports whose category is
`illegal` are ingested with `status=escalated` rather than `open`, so
the severe class reaches the operator backstop without waiting for a
community admin to forward it. Every other category still lands `open`.
Auto-escalation only sets the queue status — it records no moderator
decision and stamps no resolver, so an auto-escalated report is
indistinguishable downstream from an admin-escalated one: the reopen
route returns it to `open` on the same terms, keyed only on status,
never on how the report became escalated.
## Principal resolution and NIP-98 admission
`resolve_admin_principal()` returns `AdminPrincipal { pubkey, role,
source }` per the resolution order above; `None` never falls through as
a role. Admission is ordered so the replay guard is a privilege, not a
public surface: signature/URL/method/payload-hash verification first,
roster check second, and only then is the deployment-scoped replay id
atomically consumed — a validly-signing but unrostered key never
allocates a replay slot. Redis failure fails closed.
## Report resolution, recovery, and enforcement provenance
`POST /reports/{id}/resolve` is a crash-safe enforcement state machine:
decision-only outcomes (`dismiss`/`escalate`) are a single
CAS-plus-audit transaction; enforcement
(`delete`/`kick`/`ban`/`timeout`) claims the report
(`open`→`processing`), runs the durable mutation, then finalizes — a
re-drive resumes at the step marker and converges to exactly-one
enforcement, fenced by a lease and an outbox claim token.
Person-directed enforcement on an `event`-kind report derives its target
from the stored event's author (server-owned truth, never the reporter's
`p` tag) via a single `derive_enforcement_target` shared by the HTTP
driver and the recovery worker. If the reported event was purged before
its author could be read, person-directed actions are rejected pre-claim
and the report stays `open`; `delete` needs only the event id and is
exempt.
`GET /reports/{id}` and the resolve response carry an `activeAction`
field surfacing the enforcement that actually executed — a report
dismissed after a reopen still reports the ban that ran. `POST
/reports/{id}/reopen` returns a terminal report to `open` (idempotent on
`requestId`). `POST /reports/{id}/cancel` is the sole recovery path for
a pre-mutation `failed` action, attributed via
`relay_admin_actions.cancelled_by`.
## Feedback
`GET /feedback` and `/feedback/{id}` survive a tenant purge: provenance
columns are severed to `NULL` rather than cascade-deleted, and the
attachment path fails closed to `404` on a severed row. `PATCH
/feedback/{id}` updates lifecycle `status`
(`new`/`reviewed`/`archived`).
## Staffing and probe
`GET/PUT/DELETE /operators/{pubkey}` are Operator-only; mutating a
config-backed pubkey returns `409 Conflict`. `GET /operators` returns
the union of config and DB principals with per-entry `source`. `GET
/probe` reports auth mode, role, source, `canAct`, and `canStaff` for
the desktop console.
## NIP-11 auto-discovery
The NIP-11 relay-information document gains an optional `admin_api`
field carrying the canonical admin origin (`scheme://host[:port]`, no
path), present iff `BUZZ_ADMIN_HOST` is set and omitted otherwise. The
scheme follows the same loopback rule as NIP-98 `u`-tag verification via
a shared `scheme_for_host` helper, so the advertised origin and the
origin the relay verifies against can never diverge.
## Operator API origin decoupling
`RELAY_OPERATOR_API_ORIGIN` is no longer required at boot when
`RELAY_OPERATOR_PUBKEYS` is set — it is used only by the
community-provisioning endpoints, which fail closed at request time
(with a boot-time `WARN`) until it is set. The admin console needs no
origin.
## Admin-web adaptation
The standalone `admin-web` dashboard signs each request as a NIP-98
event via a NIP-07 browser extension, discovers the auth mode with a
single unauthenticated probe (`200` → `disabled`, anything else →
`nip98`, fail-secure), and carries no token entry surface. Playwright
coverage exercises the NIP-98 and CSP paths.
## Security hardening
Three findings from security review are folded in:
- **Append-only roster audit.** `PUT`/`DELETE /operators/{pubkey}`
mutate the deployment-wide root of trust, but the upsert overwrites
`role`/`added_by` in place and the delete removes the only row — so a
grant→revoke sequence left no trace of who was ever granted or by whom.
Each mutation now writes an `relay_operator_audit` row (actor, target,
`grant`/`revoke`, pre-image `prev_role`, `new_role`, timestamp) inside
the same transaction as the mutation. A per-target transaction-scoped
advisory lock serializes concurrent mutations of the same pubkey before
the pre-image read, so the recorded `prev_role` is always the true
predecessor even under a concurrent-grant race. Chronology is keyed on a
`BIGINT GENERATED ALWAYS AS IDENTITY` `seq` column, not the wall clock:
the serializing lock guarantees insertion order and `seq` captures it,
so ordered reads (`ORDER BY seq`) follow the true privilege chain even
across a backward NTP step that a `clock_timestamp()` ordering would
invert. `created_at` (`clock_timestamp()`) is retained as informational
occurrence time only. Append-only by construction — no `UPDATE`/`DELETE`
path and no API surface. A no-op delete writes nothing.
- **`expirationSecs` overflow.** The timeout path built `Utc::now() +
Duration::seconds(secs as i64)` from an attacker-controlled `u64`:
`i64::MAX` panicked the handler, and a wrapped-negative magnitude minted
a *past* expiry that still passed validation. `compute_timeout_until`
now rejects zero, rejects magnitudes above a documented
`MAX_TIMEOUT_SECS` (365 days), and uses checked
`try_seconds`/`checked_add_signed` so no input can panic or produce a
past expiry — over-cap, zero, `i64::MAX`, and wrapping-negative inputs
all return a clean `4xx`.
- **Uppercase-hex config-backed bypass.** Config pubkeys are lowercased
at parse, but the `409` immutability check raw-string-compared the path
param while `decode_hex_pubkey` accepted uppercase — so `PUT
/operators/{UPPERCASE}` skipped the guard and wrote a shadow row for the
same 32 bytes. The validated param is now canonicalized (lowercased)
before the `409` check, DB write, `DELETE`, and response body.
## Migrations
- `0035_relay_operators.sql` — `relay_operators` roster table
(deployment-global), `actor_authority` on `moderation_actions`,
`processing` status plus `active_action_id` on `moderation_reports`,
`status` on `product_feedback`.
- `0036_relay_admin_actions.sql` — enforcement-action table with a
`request_id` idempotency key, a `step_marker` for crash recovery, and a
`cancelled_by` attribution column.
- `0037_relay_admin_action_lease.sql` — lease fencing for the action
worker.
- `0038_relay_admin_outbox_claim_token.sql` — fenced claim token on the
outbox worker.
- `0039_relay_operator_audit.sql` — append-only `relay_operator_audit`
trail for roster mutations (see Security hardening).
`docs/admin/README.md` documents the full principal model, NIP-98 event
requirements, capabilities by role, the startup error matrix, and the
discovery field.
## Production blast radius
A relay without `BUZZ_ADMIN_HOST` is completely unaffected — the admin
surface stays disabled and `BUZZ_ADMIN_AUTH` is ignored; a lingering
`BUZZ_ADMIN_TOKEN` logs a startup warning and must be removed. Where
`BUZZ_ADMIN_HOST` **is** set, unset `BUZZ_ADMIN_AUTH` defaults to
`nip98` (per-person signed auth); `BUZZ_ADMIN_AUTH=disabled` reproduces
`main`'s prior `Host`/`Origin`-only gating but is read-only (mutation
routes `403`). The five migrations add tables and columns without
touching existing data.
---
Related: [block/buzz#4768](https://github.com/block/buzz/pull/4768)
(desktop admin console consuming the `admin_api` field),
[squareup/bb-public#339](https://github.com/squareup/bb-public/pull/339)
(Phase 4 rollout config)
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
80177e4c8e |
fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962)
Codex CLI 0.149.x can leave a PTY descendant holding
`openai/codex-action`'s inherited stdio after the review turn completes.
The action's runner waits on the child's stdio *streams closing* rather
than on process exit
([`runCodexExec.ts`](https://github.com/openai/codex-action/blob/86365089/src/runCodexExec.ts#L322)),
so the `Review pull request` step never returns — the job idles until
its 30-minute `timeout-minutes` kills it and the already-written review
result is discarded.
Every `Run Codex Security Review` job since the workflow merged has hung
this way: the final JSON result and `tokens used` count are the last log
lines, with no step-end marker. Heavy runs (`gpt-5.6-sol` at `max`
effort, ~295k tokens) sit firmly in the failing regime.
Upstream:
[openai/codex-action#150](https://github.com/openai/codex-action/issues/150),
fixed in Codex CLI 0.150.0 by
[openai/codex@bf3eb2e](
|