Commit Graph

229 Commits

Author SHA1 Message Date
Tom Brow
5730c83f7d fix(release): keep mobile candidates pinned as branches advance (#8454)
Mobile candidate publication currently fails if the source branch
advances after selection, and the operator can report failure even after
the correct immutable tag was created. It also prevents releasing an
explicitly selected historical baseline.

Add `--commit FULL_SHA` and independently verify in the operator and
publisher that the selected commit belongs to the selected source
branch's history. Compare immutable SHAs, keep the candidate pinned
through publication, and verify the resulting annotated tag instead of
requiring the branch to remain stationary. Stable-branch review/check
protection and immutable-tag permissions remain enforced. Without
`--commit`, selection still defaults to the current source tip.

### Validation

- Updated operator and publisher contracts pass: historical main/stable
selection, branch advancement during publication, unrelated or missing
commits, ancestry lookup failure, and unchanged tag/sequence checks.
- Removing ancestry enforcement causes the unrelated-history regression
to fail.
- Live read-only ancestry verification accepts the original 0.24
baseline. Local publication stops at the existing App-permission check;
no tag or build was created.
- Full local `just ci` passed.

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-19 16:21:11 +00:00
Tom Brow
30ca980b49 feat(release): support version-matched mobile stable branches (#8430)
Mobile candidates currently require the current `main` commit, so a
stable release branch cannot publish through the existing protected tag
workflow.

Add optional `--source-branch mobile-vMAJOR.MINOR` selection while
preserving `main` as the default. Both the command and App-backed
publisher reject a stable branch whose major/minor differs from the
requested version. Candidates retain exact-tip checks and immutable
annotated tags; the private build pipeline is unchanged.

Document the temporary stable-only distribution policy and explicit
per-fix backport decisions. No candidate, build, or upload is triggered
by this change. No duplicate implementation was found among open release
PRs.

Stable publication now checks active review and status-check protection
in both the operator and publisher. The process permits approved
backport PRs and stable-only corrections for regressions caused by
earlier backports. Administrator activation and verification of the
no-bypass ruleset remain outstanding; non-admin API responses hide the
bypass actor list.

### Validation

- Full local `just ci` passed during preparation; the final changes
affect shell release tooling, its workflow inputs, CI path detection,
and documentation.
- Publisher contract passed, including version-matched stable sources
and mismatched branch rejection.
- Operator contract covers default main, a divergent stable commit,
missing or mismatched sources, immutable sequencing, and publication
races. The final operator contract run passed.


- Updated publisher and operator contracts pass, including 17
weakened-policy cases and rejection of unavailable protection. The live,
currently unprotected stable branch is rejected.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-18 18:04:04 -07:00
Tom Brow
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>
2026-10-16 14:06:53 -07:00
klopez4212
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>
2026-10-15 13:22:12 -04:00
Wes
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>
2026-10-14 20:57:47 -06:00
Tom Brow
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>
2026-10-14 18:42:39 -07:00
Will Pfleger
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>
2026-10-14 17:18:38 -04:00
Logan Johnson
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>
2026-10-14 15:31:05 -04:00
Luke Tornquist
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>
2026-10-14 14:36:00 -04:00
Luke Tornquist
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>
2026-10-13 15:47:53 -04:00
Will Pfleger
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>
2026-10-13 14:45:59 -04:00
Will Pfleger
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>
2026-10-09 16:58:00 -04:00
klopez4212
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>
2026-10-08 14:32:55 -04:00
Luke Tornquist
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>
2026-10-08 11:33:37 -04:00
Luke Tornquist
d742fc5f30 Use Silo for bundled object storage (#8023) 2026-10-07 18:02:27 -04:00
klopez4212
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>
2026-10-04 17:17:46 +01:00
Will Pfleger
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>
2026-10-01 19:40:09 +00:00
klopez4212
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>
2026-10-01 17:00:57 +01:00
Will Pfleger
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>
2026-09-30 12:19:56 -04:00
Alex Rosenzweig
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>
2026-09-30 10:52:29 -05:00
Luke Tornquist
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>
2026-09-28 14:02:12 -04:00
Tom Brow
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>
2026-09-25 23:19:32 +00:00
Tyler
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>
2026-09-24 16:28:17 +00:00
Tyler
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>
2026-09-24 11:36:10 -04:00
Mic Neale
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>
2026-09-24 07:32:07 -06:00
Salman Mohammed
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>
2026-09-23 14:41:00 -04:00
Tom Brow
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>
2026-09-23 08:47:21 -07:00
Tom Brow
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>
2026-09-22 22:53:46 +00:00
Tom Brow
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>
2026-09-21 08:51:05 -07:00
Wes
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>
2026-09-19 02:39:37 -06:00
Tom Brow
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>
2026-09-18 19:17:10 +00:00
Tom Brow
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>
2026-09-17 11:27:55 -07:00
Tom Brow
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>
2026-09-15 14:56:45 -07:00
Salman Mohammed
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>
2026-09-15 10:24:27 -04:00
Tom Brow
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>
2026-09-14 22:14:06 +00:00
Wes
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>
2026-09-11 09:10:43 -06:00
Logan Johnson
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>
2026-09-09 13:32:10 -07:00
Mic Neale
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>
2026-09-08 07:30:45 -07:00
Luke Tornquist
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>
2026-09-04 10:46:55 -04:00
Luke Tornquist
0e878664b0 ci: split CI into reusable workflows (#7168)
## Why

The monolithic CI workflow is a frequent merge-conflict hotspot.
Splitting cohesive domains into same-repository reusable workflows keeps
one centrally filtered entry point while letting Rust, desktop,
relay/PostgreSQL, client, and security CI evolve independently.

## What

- Keep `ci.yml` as the only push/pull-request orchestrator with
unchanged concurrency and path detection.
- Move 18 execution jobs into five `workflow_call`-only domain workflows
without changing their runners, steps, matrices, caches, artifacts,
permissions, or timeouts.
- Keep the relay artifact producer with desktop integration, the
complete PostgreSQL lane, and relay E2E consumers.
- Preserve all 12 existing required GitHub Actions contexts through
lightweight top-level compatibility gates, so the repository ruleset
does not need to change.
- Update the Rust-cache contract to follow Unit Tests into
`_ci-rust.yml`.

## Risk Assessment

CI-only change with moderate workflow-orchestration risk. The main risks
are reusable-workflow output propagation, skip behavior, and visible
check naming; the old required names remain explicit top-level jobs, and
the draft will stay open until an exact-head GitHub Actions run and
independent review are complete.

Generated with Codex

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Signed-off-by: tornquist <tornquist@squareup.com>
2026-09-02 12:23:16 -04:00
Will Pfleger
5aed49b505 feat(buzz-agent): add DatabricksAuthCoordinator single-flight OAuth (#5545)
## What

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

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

## How

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

## Tests

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

## Scope / follow-ups

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

## Stack

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-01 15:59:21 -04:00
Tom Brow
b270437a62 fix(mobile): isolate extension linker flags; complete iOS build in CI (#7187)
### What changed?

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

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

### Why?

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

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

### How is it tested?

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

---------

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

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

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

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-01 14:20:36 -04:00
Wes
e17a0d44c3 fix(dev): keep the canonical profile when launching from desktop/ (#7143)
## Summary

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

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

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

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

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

### Related issue

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

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

### Testing

At exact head `0a09a2f3716b0378546b66be2302b750ed4330b9`:

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

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

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-01 11:55:42 -06:00
Luke Tornquist
bd73490418 ci: run PostgreSQL tests in isolated lane (#6730)
## Why

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

## What

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

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

## Risk

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

Remaining operational risks:

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

## Verification

Author Blox workstation `2020088`:

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

GitHub exact final head `92c231e48f299ea2af23817763f5ae80ac013d68`:

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

## References

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

Generated with Codex.

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

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

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

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

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

Verification at local branch head
`60086c1f6ee34a193c342255a5e8e6293b60e988`:

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

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

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

## Update — August 25, 2026 restack

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

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

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

## Update — August 25, 2026 final hosted verification

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

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Signed-off-by: tornquist <tornquist@squareup.com>
2026-09-01 12:07:28 -04:00
Arjun Mahanti
5673c3307e feat(desktop): add protected-build Bestie experiment (#6902)
## Summary

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

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

## Build contract

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

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

## Why this is separate

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

## Non-goals

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

## Verification

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

---------

Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Signed-off-by: Fizz <fizz@buzz.local>
Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Fizz <fizz@buzz.local>
Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
2026-08-31 15:03:35 -06:00
Wes
93237b4a74 fix(acp): wake agents from workflow messages (#6953)
> Pinky, an AI agent, is opening this PR on Wes's behalf.

## Summary

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

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

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

## Trust boundary

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

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

## Why this PR

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

## Validation

At exact clean commit `fe5b55619fe44176343eefb4cb7fe180df45a7d8`:

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

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

## Exact-head local relay + ACP proof

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

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

Cases:

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

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

## Deployment and residual risk

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

---------

Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: LioLionel <62820906+LioLionel@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-31 16:24:09 -04:00
Will Pfleger
17ecf0b8c9 fix(ci): salvage Codex review output on PTY-shutdown hang (#7042)
Codex CLI can leave a PTY descendant holding the action's inherited
stdio after the turn completes. The `runCodexExec.ts` wrapper waits on a
`close` event that never fires, so the `Review pull request` step hangs
until the job timeout kills it — discarding the finished review the CLI
already wrote to disk.

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

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

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

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

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

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


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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-31 13:31:22 -04:00
Luke Tornquist
3ed623bb21 feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229)
## Why

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

## What

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

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

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

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

## Behavior change to note

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

## Testing

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

Closest existing PR/issue: none found.

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

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

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


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

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

Generated with Codex

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
2026-08-31 11:59:13 -04:00
Tom Brow
c432a111ca feat(mobile): push notifications MVP (#6269)
This PR implements MVP, iOS-only,
[NIP-PL](8d2d0ff5ad/docs/nips/NIP-PL.md)-compliant
push notifications.

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

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

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

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

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

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

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

## Push-time flow

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

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

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

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

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

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

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

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

## Known limitations

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

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

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

## Validation

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

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

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

## Independent pre-reviews

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

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Jordan Mecom <jm@squareup.com>
2026-08-28 15:15:09 -06:00