Repository navigation
Sync upstream block/buzz main (97e1f6a0) into the Kiingo fork — DO NOT MERGE until main's go - #202
Conversation
## Summary Add an opt-in, newest-first thread window to the authenticated HTTP query path. A filter with `thread_window: true` returns: - reply rows ordered by `(created_at DESC, id ASC)` with an exact composite cursor; - optional bounded reactions, edits, deletions, and delete-of-aux closure; - one relay-signed `kind:39007` bounds event bound to the normalized host, authenticated reader, channel, root, request cursor, kinds, depth, limit, and aux choice. Filters without the flag keep the existing oldest-first thread behavior. Channel windows and `kind:39006` are unchanged. No client opts into this mode in this PR. ### Why extend NIP-CW NIP-CW already defines relay-computed, cursor-paged conversation views over extended NIP-01 filters. Thread windows use the same protocol ideas: ordinary signed rows, composite keyset pagination, optional aux closure, and relay-signed bounds. Adding a thread mode keeps those related wire contracts together instead of creating a second NIP with duplicated terminology and compatibility rules. Extending a draft, optional NIP with an additive mode is normal protocol evolution. Existing fields and meanings are not reinterpreted: - `top_level: true` remains channel mode; - `kind:39006` remains channel-window bounds; - `thread_window: true` selects the new mode; - `kind:39007` has a distinct identity and root/request binding; - extension-unaware clients never send the selector; - legacy thread queries remain unchanged. NIP-CW documents only the wire contract and relay semantics. Database plans, replica internals, and rollout procedures remain implementation concerns. ### Why thread windows use independent modules The implementation deliberately keeps `thread_window.rs` separate in `buzz-core`, `buzz-db`, and `buzz-relay` rather than adding branches throughout the existing channel-window path. The modes share pagination concepts, but their invariants differ: - channel mode selects top-level rows; thread mode requires one root and depths `1..=depth_limit`; - thread mode accepts only conversation row kinds and rejects unknown/conflicting fields; - thread bounds bind the root and authenticated reader because cross-channel aux visibility can differ by reader; - thread aux includes the root and requires complete two-hop closure; - thread mode refreshes writer authorization before selection and before signing; - historical cursor pages may use a proved replica transaction, but fallback must restart both aux hops on the writer; - legacy thread traversal is oldest-first and must not inherit descending cursor semantics. Forcing these rules into channel-window code would create a mode flag across parsing, SQL predicates, bounds construction, authorization, replica fallback, and tests. That abstraction would not remove complexity. It would distribute it. Separate modules keep each contract local and leave the established channel and legacy paths untouched. ### Technical design - Strict request parsing normalizes UUIDs, event IDs, kinds, defaults, and cursors before computing the response binding. - SQL applies community, root, channel, depth, kind, deletion, and cursor predicates before `limit + 1`. - Ordered metadata scanning plus exact event-key lookup avoids the stale-statistics plan that previously exceeded the four-second SQL budget on a 10k-reply thread. - Aux work is bounded by target batches, query count, raw candidate count, response bytes, SQL timeouts, and one shared HTTP deadline. - Fresh authorization-set comparison catches both grants and revocations during closure. The relay returns retryable `503` rather than signing an incomplete page. - Replica failure permanently degrades the request to the writer and restarts closure from its original targets without resetting budgets. - Migration `0049_thread_window_index.sql` adds the mixed-direction index used by newest-first scans. It validates a prebuilt index and bounds startup lock/build time. ### Tradeoffs - The stale-statistics-safe plan is intentionally less clever. Measured warm 100k deep-cursor execution increased from roughly 0.30 ms to 4.80 ms, plus about 8 ms planning, in exchange for avoiding a reproduced multi-second timeout after rapid 10k-thread insertion. - Bounds are authoritative only for the served page. There is no cross-page snapshot guarantee, and replica insertion coverage does not imply latest edit/deletion/aux visibility. - Strict parsing rejects unsupported constraints instead of silently serving a different row set. - Required aux closure fails closed. The relay does not omit aux and sign plausible-but-incomplete bounds. - This PR ships relay capability only. Client rendering, retry UX, live-history overlap, and compatibility fallback remain separate work. ### Related issue No existing issue found. This work follows the thread-loading design and review discussion. ### Testing - `cargo test -p buzz-core`: 258 unit tests and 2 doctests passed. - `cargo check --workspace --all-targets`: passed at the rebased implementation head. - `cargo test -p buzz-db`: 131 non-PostgreSQL tests passed; 275 infrastructure tests were ignored as declared. - Focused PostgreSQL migration/runtime regressions: 3/3 passed against an isolated PostgreSQL container, including both corrected migration-ledger tests. - `cargo fmt --all --check`: passed. - Pre-rebase feature validation passed 398/398 PostgreSQL tests, signed-wire multi-tenant/auth/pagination/aux/revocation probes, clean Compose boot/reboot, and full core tests. - Independent HA validation exercised three relay pods and a physical PostgreSQL standby on the same runtime logic before the final documentation-only cleanup. - Independent final-delta review confirmed exactly two changed files, byte-identical executable migration SQL after comments/diagnostic normalization, matching prebuild/catalog/recovery instructions, and `git diff --check`; no runtime receipts were relabeled to the new SHA. - The pre-push hook ran repository checks but its Rust unit lane exited 100 without exposing an individual failed test in the captured multiplexed output. The independently run affected-package checks above passed; current-head repository CI remains required. Known unrelated baseline behavior remains: the mesh-demo echo test can return 504, and same-second archive → unarchive can deduplicate its membership notification. Neither path is changed here. --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz> Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Fix Databricks V2 Unity Catalog Claude service routing without erasing verified model capabilities. - preserve exact provider-qualified record precedence for `data_workflow_tools.goose.goose-claude-opus-5-5`, including adaptive thinking, explicit `output_config.effort` (default medium), and signed native tool continuation - route other boundary-matched `claude-*` service components to Anthropic Messages while preserving the full FQN and advertising no unverified effort controls - keep GPT-5+ service components on OpenAI Responses and other uncurated FQNs on MLflow Chat Completions; catalog/schema text does not affect routing - keep Rust and TypeScript resolver precedence aligned and regenerate the 168-vector normative corpus Validation at `1204f1eacff17ebe05eb2bba06b5eee210e21c25`: - `cargo test -p buzz-agent` - `pnpm -C desktop test` — 6,628 passed - `pnpm -C desktop typecheck` - `cargo fmt --all -- --check` - `cargo clippy -p buzz-agent --all-targets -- -D warnings` - `just test-unit` — passed on isolated rerun - pre-push file-size, desktop check, desktop typecheck, desktop tests, and Tauri checks passed; the repository-wide Rust lane exited without identifying a failing test in two concurrent hook runs, while its isolated `just test-unit` rerun passed --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
…ammar (block#7844) New Databricks endpoints (Claude Opus 5.5, GPT-6 Astra/Sol, etc.) showed as raw FQNs in the model picker until someone added a curated corpus record for each. This adds a strict token-class grammar as a third label tier inside the Databricks label helpers (Rust `model_capabilities.rs` + TS `modelCapabilities.ts`): exact record → unique alias → **generated label** → raw id. In TS it fires only for an explicit `databricks_v2` provider; providerless ids stay raw. ## Grammar Takes the last component of a UC FQN (or strips one wrapper: `databricks-`, `goose-`, `kgoose-`, `builderbot-`; an id that is neither stays raw), splits on `-`, and classifies each token as version, letter+digit version, size, trailing date (dropped), or word. Labels are built only from the id's own text, and the grammar never reinterprets a version. When in doubt it returns raw: - **Known families only.** The family (leading letters of the first token) must be in the manifest's existing `label_family_tokens`. Custom endpoints such as `prod.ai.rag-agent-3` or `builderbot-pr-reviews-v2` stay raw. Supporting a new vendor means adding one entry to that list, not a record per model. - **Attached digits stay whole** (`gpt10` → `GPT10`). If the family already carries digits and the next token is a version (`goose-qwen3-5-122b-a10b`, `system.ai.llama3-1-8b`, `system.ai.claude3-5-sonnet`), the id stays raw; this is checked before any reorder. - A third consecutive number (`gemini-3-7-1-flash`) returns raw rather than splitting off a patch version. - Any non-ASCII input returns raw, checked before trimming or case folding. - Any unclassifiable token, or no version-bearing token (`builderbot-pr-reviews`), returns raw. `(latest)`/`Preview` never appear unless an exact record supplies them. Special cases, each justified by an unseen-id shape rather than the corpus score: - `meta-llama-` → `Llama`: a namespace convention. - Claude numeric-first reorder: Goose names Claude endpoints `claude-4-7-opus`. Only 1–2 version tokens followed by `opus`/`sonnet`/`haiku` reorder (`claude-5-6-opus` → `Claude Opus 5.6`); anything else keeps its order (`claude-5-6-thinking` → `Claude 5.6 Thinking`). Both current numeric-first ids also have exact records. - Qwen compact decimal: Qwen's own naming `qwen35` → `Qwen3.5` when the second digit is nonzero. ## Colliding labels in the picker The same model is often served from several sources, so humanized labels collide (four rows are all `GPT-5.5`). `disambiguateModelLabels` in `formatAgentModelLabel.ts` suffixes only colliding Databricks labels with their source: - UC FQN: its `catalog.schema`, e.g. `GPT-6 Astra (system.ai)` / `GPT-6 Astra (sandbox.public)`. - Workspace endpoint: its wrapper, e.g. `GPT-5.5 (databricks)` / `GPT-5.5 (goose)` / `GPT-5.5 (kgoose)`. - Still colliding after that (e.g. a date variant in the same schema): the full id, e.g. `Gemini 3.7 Flash (system.ai.gemini-3-7-flash-0925)`. Unique labels, raw-id rows, the default row, and other providers are unchanged. It runs in `getDiscoveredPersonaModelOptions` (covering `AgentDropdownSelect`, `PersonaModelCombobox`, `PersonaDropdownField`, whose closed triggers read the option label) and in `ModelPicker`. Model search in `AgentDropdownSelect` and `PersonaModelCombobox` also matches the raw id, so searching `system.ai` finds those rows. Rust `ModelEntry.name` is unchanged. ## What labels drive Picker values, persistence, routing, catalog dedup, `resolve()` and capabilities all stay keyed on the raw id; two ids sharing a label both remain selectable. Labels do drive picker search (alongside the raw id), the workspace catalog's equal-time sort, and `TeamIdentityCard`'s "Mixed models" summary, which compares lowercased base labels, so `goose-` and `system.ai` GPT-6 Astra read as one model there (correct, since they are the same model). ## Tests Shared fixtures in `scripts/databricks-label-fixtures.json` are replayed by both Rust and TS. Each row asserts its tier: `exact` rows match a record, `alias` rows resolve with generation off, `generated` rows return nothing with generation off (so a future alias or record cannot mask them), and `raw` rows return nothing either way. A curator guard requires that across all 57 exact records, the ids the grammar does not reproduce equal exactly the 10 curator-only ids. `disambiguateModelLabels` has unit tests for namespace, wrapper, and full-id-fallback collisions, untouched rows, other providers, and case-insensitive grouping. The defaults-picker e2e checks the suffixed rows and trigger, raw-id search, and that choosing the second uncurated FQN by its visible name persists its raw id and restores its label. An edit-agent e2e covers the existing-agent model dropdown (`#edit-agent-model`): both suffixed rows render, the `system.ai` row is chosen by name, and after save and reopen the agent stores the `system.ai` id and shows `GPT-6 Astra (system.ai)`. --------- 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>
## 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](transact-rs/sqlx#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>
) Mobile heart and warning reactions and message text render as monochrome symbols because bundled Inter supplies U+2764 and U+26A0. Remove only those two character mappings from both regular and italic Inter fonts so Flutter uses native font fallback. No production Dart or Markdown parsing changes are needed. The reproducible font-processing script verifies that every other mapping and font table is unchanged, apart from the required checksum. Glyphs, metrics, language coverage and symbols such as © ® ™ ↑ ↓ − ∕ are preserved; the bundled Inter license is retained. Validated on an actual iPhone 17 Pro simulator running iOS 26.5. Reaction hearts and warnings remain centered within 0.33 logical pixels of the pill center. Native screenshot checks cover reactions and plain, emoji, explicit text-style, bold, italic, and enlarged emoji-only message hearts and warnings, plus pixel-identical preserved text samples. **Fallback behavior:** on iOS 26.5, plain ❤ / ⚠ and explicit text-style ❤︎ / ⚠︎ also render as color emoji. Native font metrics add one logical pixel to each of the three mixed-message lines in this fixture. Android visual behavior has not been validated. ### Reactions | Before | After | | --- | --- | |  |  | ### Message text | Before | After | | --- | --- | |  |  | Images are cropped native simulator screenshots of production widgets with synthetic content. Before uses the original bundled fonts; after uses the processed fonts. Added tests: the existing mobile CI test lane now checks both bundled fonts against the verified generated artifacts, preventing restoration of the old mappings or unintended font changes. Validation: the two asset tests, font-table verification, byte-identical regeneration, native screenshot regression checks, and Flutter analysis pass. The screenshot test fails against the original rendering. Pre-push checks, including the full mobile suite, pass. Full repository validation also passes. The enlarged emoji-only screenshot check passes and rejects the old normal-sized fixture. GitHub CI, Codex review, and the security review pass for commit 538167a. Both prior Codex findings are addressed and resolved. --------- Signed-off-by: Tom Brow <tomb@block.xyz>
🤖 ## Summary Scripts, scheduled jobs, and other automation need to give an agent one prepared task and wait for a result without starting a long-running chat listener. This adds `buzz-acp run`: it reads a task from a local file or stdin, runs one fresh agent session with the configured tools and instructions, cleans up, and exits with a machine-readable status. ```sh buzz-acp run --task ./task.json cat task.json | buzz-acp run --task - ``` - The versioned JSON task specifies a task ID, intended agent public key, prepared prompt, and execution time limit. Invalid input or an identity mismatch is rejected before agent launch. Credentials, executable, tools, and permissions remain controlled by trusted launch settings, not the task document. - Each invocation uses the normal agent equipment: base/persona/team instructions, enabled memory, tools, model and effort settings, permissions, and Git identity, credentials, and signing. Both task and conversational modes obtain these capabilities from one shared runtime owner; new shared setup belongs there rather than in separate entry points. - Launch configuration reads have a separate ten-second bound and can be cancelled even when a prompt file blocks. A deadline covers adapter startup through the task turn. Interrupts and termination signals trigger bounded cleanup. Stdout contains one terminal JSON record; diagnostics stay on stderr. A normal turn ending is not proof that the task achieved its goal. - Normal invocation remains the conversational service. Task mode does not start chat subscriptions, presence, heartbeat turns, or a persistent agent pool. Tasks are trusted executable instructions, not sandboxed input. Reusing a task ID does not prevent duplicate execution, and uncertain outcomes must not be blindly retried. See [the task contract](crates/buzz-acp/TASKS.md) for the document format, exit codes, limits, and examples. URL retrieval, a task server, and a persistent execution ledger are outside this change. ### Related issue None found in the existing issue and PR search. ### Testing - At `4e86bbc97`: 964 unit tests and 22 integration tests passed, with three existing ignored tests. The package suite ran serially with inherited harness/Git options cleared. A final test-only shutdown-result assertion was then checked with the focused memory test and all-target Clippy. - All eight real-binary task tests independently passed on the exact candidate, including blocked configuration reads, file/stdin validation, session equipment, deadlines, cancellation, and cleanup. The memory test passed 20 repeated runs. Removing the oversized-input guard fails the new error assertion; restoring it passes. - Package all-target Clippy, formatting, file-size checks, and pre-push Rust/Tauri checks passed. All executed hosted checks passed on the updated head (54 successful, 28 skipped). The separate Codex review comment still requests authorization for this exact range. - Earlier revisions passed the release build and a real Goose 1.51.0 tool-use smoke test. No new release artifact or installed Janet runtime is claimed for this feedback revision. - Repository-wide `just ci` was previously attempted; workspace Clippy passed, then the desktop dependency check stopped on a local pnpm fetch failure. Hosted checks cover the applicable PR lanes. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
…lock#7850) Pre-push hook failures were hard to diagnose in two ways that cost agents repeated reruns. **nextest printed no failures.** lefthook runs the `rust-tests` lane under a pty, and with an agent shell's `TERM=dumb` nextest picks its progress-bar reporter, which then swallows every FAIL line, panic, and summary — the lane only says `error: test run failed`. An orphaned nextest (left behind when a push is killed) also spins at ~100% CPU polling the dead pty for keyboard input. `rust-tests` now sets `NEXTEST_SHOW_PROGRESS=counter` and `NEXTEST_NO_INPUT_HANDLER=1` via lefthook's per-command `env:`. **Stale `node_modules` looked like a code error.** After merging main moved dependencies, `desktop-typecheck` failed with `Cannot find module` and no hint to reinstall. `desktop-check`, `desktop-typecheck`, and `desktop-test` now set `pnpm_config_verify_deps_before_run: warn` (a shared YAML anchor), so pnpm prints `Your node_modules are out of sync with your lockfile …` ahead of the script output. It is scoped to these lanes and set to `warn` because the check also gates `pnpm exec`, and `just bump-desktop-version` leaves the workspace flagged until the next install, so `error` would block desktop release pushes. pnpm uses recorded workspace state and manifest timestamps and is not a complete lockfile-content check, so this is a hint rather than a guarantee. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
🤖 I’m Larry. ## Summary Managed-agent definitions can now choose which installed ACP transport will launch the agent. The choice sits beside **Agent harness** while creating or editing an agent, so it is made before first deployment and remains visible afterward. Buzz discovers executable commands named `buzz-*-acp`, keeps **Buzz ACP (default)** as the safe default, and preserves existing custom command values. Discovery and launch share the same resolver, so a command offered in the picker is the command Buzz will execute. ## UX walkthrough ### 1. The ACP command picker appears while creating the agent The default is explicit, and the helper text explains that this selection controls deployment.  ### 2. Installed wrapper commands are offered by name Here Buzz has discovered `buzz-janet-acp` alongside the stock transport.  ### 3. The wrapper is selected before deployment  ### 4. The deployed profile reports the effective command  ### 5. Editing the deployed agent reopens its definition with the choice preserved  ## Details - Definitions carry the selected ACP command through relay publication, snapshots, imports, team adoption, deployment, and later edits. - Existing definitions without this field continue to use `buzz-acp`. - Unknown persisted commands remain visible but unavailable rather than being silently replaced. - Discovery is read-only and never executes candidates. It filters non-files and non-executables, supports Windows `.exe`, `.cmd`, and `.bat` shims, deduplicates aliases, and returns a stable sorted list. - Command names remain portable; Buzz does not persist machine-specific absolute paths. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@nostr> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Upstream MinIO image pulls are blocking CI (block#7867). Add a separate publisher for `ghcr.io/block/buzz-minio:latest`, so the reusable image can be bootstrapped before CI adopts it in block#7870. - Build the checksummed official MinIO/mc release binaries from block#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 block#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>
…ead (block#7806) Mobile half of the sidebar stale-reader fix (desktop: block#7805). It makes the channel sections, sort, stars and mutes managers converge on the latest relay head. ## Changes - **Tie-break:** sections, stars and mutes compare `(created_at, id)` with `id < cursor`, which matches the relay's `ORDER BY created_at DESC, id ASC`. Sort already did this. - **Coherent cursor:** `_publish()` records the signed event ID through `SignedEventRelay.submit(onSigned:)` and advances `(_lastRemoteCreatedAt, _lastRemoteEventId)` together. It only advances when the pair is after the current cursor, so a newer head learned during the await is kept. This applies to sections, stars and mutes. - **Stars/mutes startup retry:** one serialized `_syncWithRelay()` (`_syncInFlight`/`_syncAgain`) with 2s→30s backoff. It retries until the history head is applied and the live subscription is settled. The stars/mutes managers no longer re-subscribe on CLOSED: `RelaySessionNotifier` owns retryable and rate-limited CLOSED, and a terminal CLOSED is never re-sent. Reconnect works through session replay and provider rebuild. Sections and sort keep their existing manager CLOSED retry. - **Stars/mutes durability:** storage `write()` returns the `setString()` result. A decode or apply error, or a persist that returns false or throws, fails the attempt. The accepted head only advances once it is durable, so the same head is retried. - **Stars/mutes pending fence:** each read snapshots `_localRevision` and `_publishPending`. If a local edit or a publish happened while the read was in flight, the result is discarded. Startup recovery retries it; a resume read does not. Per-entry arbitration is unchanged. - **Sections publish commits content with its cursor:** the submitted blob is committed together with its cursor, so a same-second loser applied during the OK wait can't strand the device. A local edit made during the wait is kept. - **Stars/mutes undecodable head:** an undecodable head is skipped without holding startup open. Only a persist failure keeps the head unapplied and retried. - **Resume re-read:** all four providers listen to `appLifecycleProvider`. On `resumed`, each manager runs one guarded history read through its existing fetch/apply path. It is one-shot: skipped when a local edit or publish is pending as it starts, not retried on failure, and it starts no timers or poll loop. The result is discarded if a mutation lands before it applies, and a disposed or superseded manager (sort generation fence) never persists it. ## Residuals - If the app stays in the foreground the whole time and a healthy socket drops an EVENT, the missed head is picked up on the next resume or reconnect. - Sections: a live event can still overwrite a pending local edit. This is pre-existing, and the resume read adds no new path to it. - Sort: a greater remote timestamp wins, by policy. - Stars/mutes: entries with equal `updatedAt` can still resolve differently on different clients, under the existing merge policy. - Stars/mutes: publish reads first and keeps the entry with the later `updatedAt`, so a peer whose clock runs fast can override a pending local entry. This rule already exists on main and matches desktop. - Sections and sort persistence failures are pre-existing and not redesigned here. ## Size Production: 10 files, +495/−162. Tests: 3 files, +718. The new `sidebar_sync_fixture.dart` runs over the real `RelaySessionNotifier` seam and is shared by `channel_reader_recovery_test.dart`. Plus a sections tie-break case. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Switch every CI job that starts MinIO to the public `ghcr.io/block/buzz-minio:latest` image published by block#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 block#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>
## Buzz Desktop release v0.5.25 - **Frozen main:** `797012ff01a6d499959b45ed2e56f7927c6a4d6b` - **Reviewed candidate:** `c8f73213089cbd5a0f1e675d3193558280d46e10` - **Previous desktop release:** `desktop-v0.5.24` - **Proposed immutable tag:** `desktop-v0.5.25` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Why Buzz server components need one typed feature-flag contract without forcing the open-source build to depend on LaunchDarkly. A staging or production relay must not be able to choose the wrong provider through runtime configuration. ## What - Adds typed boolean and signed `i64` flags with static, environment, and compile-gated LaunchDarkly evaluators. - Defines an executable relay artifact contract that compiles exactly one provider. - Rejects zero or multiple relay provider features at compile time. - Runs the LaunchDarkly feature tests and the provider compile matrix in the unit CI lane and fallback test script. ## How `buzz-feature-flags` stays provider-neutral. The future relay composition root will expose one build path per artifact feature: `static-feature-flags`, `environment-feature-flags`, or `launchdarkly-feature-flags`. Runtime configuration carries only settings for the selected provider. It never carries a provider name, fallback, or precedence rule. The crate still has no relay consumer. A locked compile fixture proves each singleton builds and every zero or multi-provider combination fails until the relay adopts this contract. This removes the proposed runtime `ProviderMode` and leaves one provider, one construction path, and one evaluator per artifact. ## Risk This PR still has no relay or app consumer, so it changes no user-visible behavior. Risk is limited to the new feature-flag crate, its compile contract, and its unit-test lane. ## Testing An external Rust consumer at `30cf968bfe85e82980fda7e63f5bebe649ca939f` exercised six environment-evaluator scenarios and 59 assertions. The compile-time correction adds no live consumer to exercise manually. ## Bigger picture When `buzz-relay` adopts this crate, its build manifests must map each distribution to one provider feature. That consumer PR must verify live Relay Proxy reachability, secret ingestion, lifecycle ownership, and user-visible behavior. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ock#7873) Follow-up to block#7806. - **A retired sections manager never writes the shared prefs cache.** `_persist()` now returns once the manager is disposed. That covers every path a torn-down manager could take after an account or relay switch, including a late OK restoring its submitted layout and the `dispose()` flush's read-before-write merging a head. Before this change, both could overwrite the successor's cache on the same pubkey key. - **Coverage:** - a retired late OK and a retired flush pre-read both leave the successor's cache untouched - an edit made while an OK is held still publishes after the OK is released (`_lastPublishedStore` is built from the submitted store) - a same-second losing head adopted after a local edit still converges UI, cache and relay once our OK wins. The adopted layout's pending debounce is deliberately left armed, because its republish is what re-converges this race. **Residual:** an edit made just before our own echo arrives can be replaced by that echo. This is existing behaviour on main and on desktop. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
…face (block#4768) Adds a relay admin console under Settings — the single surface for relay-level operator work — replacing the previously unreachable moderation section. Operators authenticate with their own Nostr key over NIP-98 — no browser extension, no bearer token — to view deployment-wide moderation reports and product feedback, resolve/dismiss/escalate/reopen reports, update feedback status, and manage the operator/moderator staffing roster. ## Nav reachability The old `moderation` section id was defined in `settingsSections` but never wired into any group in `settingsNavGroups`, so the sidebar never rendered it — unreachable since block#1617. This PR replaces it with a `relay-admin` section wired into the Communities nav group, pointing at the relay admin console, and drops the separate `admin-console` section id so a single surface owns relay-level trust & safety. The Relay admin nav entry is always visible. Auth gates the panel itself; the nav entry is a door, not a credential — hiding it behind discovery state would strand an operator whose relay transiently fails NIP-11 discovery with no path back to the connection settings. ## Discovery and connection On mount the console reads the connected relay's NIP-11 document for the optional `admin_api` field. When a valid origin is discovered **and its host matches the connected relay's host** (case-insensitive, host identity only), it is automatically saved and probed — the panel renders immediately with no Save step required. A cross-host advertisement is pre-fill only: it populates the manual origin field under Advanced and is never saved or probed without an explicit Save, so a relay cannot direct an unconsented NIP-98 signature at a third-party origin; residual exposure on the same-host path is bounded by NIP-98's URL/method/payload binding. Saving is only required for manual origin changes. The advertised origin is untrusted input: revalidated through `AdminOrigin::parse`, falling back to manual entry when absent or invalid. A saved manual origin always takes precedence over discovery. ## Auth and transport (Rust, `src-tauri/src/commands/admin/`) - `AdminOrigin` value object: validates scheme + host + optional port, rejects credentials/path/query/fragment; `http://` only for loopback. The relay side normalizes its configured admin host at config load and compares inbound `Host`/`Origin` hosts case-insensitively. - `AdminRoute` closed enum: no IPC surface accepts arbitrary URLs, so the NIP-98-signed URL is byte-identical to the fetched URL. - Dedicated no-redirect `reqwest` client as an SSRF guard: a relay `3xx` is an error and the NIP-98 header never crosses origins. - `admin_probe` signs `GET /probe` and returns a typed state (`nip98Authorized` / `nip98Denied` / `disabled` / `notAdminApi` / `networkOrIntercepted`). The response body is validated against the full relay contract — `nip98Authorized` only when the complete NIP-98 invariant holds, `disabled` only on a coherent disabled-mode body; any incoherent or unrelated JSON classifies as `notAdminApi`, so the console never fails open at the network boundary. Unknown fields are tolerated for forward compatibility. - NIP-98 signing via `AppState::signing_keys()`, one retry on `401` with a fresh signed event; response size bounded by a `Content-Length` preflight and a streaming byte counter; per-pubkey origin storage with atomic `0o600` writes. ## Relay admin console The console is gated behind the relay `OPERATOR`/`MODERATOR` roles from block#3777; every `/api/admin/v1/*` route returns `403` to anyone else. Community self-administration (`community-members` / Invites) and in-channel enforcement (the 9040–9043 signed commands) are a separate axis and are untouched. The previously-duplicate community report queue (`ModerationQueueCard`, `moderationQueue.ts`, and three dead hooks) was unreachable dead code and is deleted in its own commit; `useBanMemberMutation` and the rest of `features/moderation/hooks.ts` remain in use by the members sidebar and message menu. ## Console UI (TypeScript, `src/features/admin-console/`) - `AdminConsoleSettingsCard`: discovery + probe flow, honest copy for every probe state, manual origin under an `Advanced` disclosure at the bottom. The role (operator/moderator) is rendered in the connection status line as plain text ("Connected as operator") — no badge-shaped elements above the tab row. The origin provenance (relay config / database) appears inside the Advanced card as small muted text. Section title changed to `Admin`. - `AdminConsolePanel` with Reports / Feedback / Staffing tabs (Staffing gated on `role === "operator"`); reports and feedback grouped by community for cross-community triage. - **Reports**: always requests `scope=all` so the existing resolve/cancel/reopen controls can reach every status (`open`, `processing`, `resolved`, `dismissed`, `escalated`). The relay's omitted-scope default is `escalated`-only (the platform-safety backstop); `scope=all` is explicit and scoped to this console. Full action matrix per `target_kind` (event → delete/kick/ban/timeout/dismiss/escalate; pubkey → ban/timeout/dismiss/escalate; blob → dismiss/escalate); kick is suppressed when the report carries no `channelId`. A `processing` row stays navigable — enforcement state lives in the detail view. A `failed` action (always pre-mutation) offers a single **Cancel & reopen** via `POST /reports/{id}/cancel` with `{actionId}` fencing; there is no client-side retry. `pending`/`enforcing` actions belong to the relay recovery worker and offer no button; a `409` reloads detail. Lists refetch on back-navigation after a mutation. - **Report failure UX**: `reportErrorMessage()` strips 4xx/5xx status prefixes and surfaces the relay's own error reason in the toast (e.g. relay returns `"400: you cannot report this content"` → toast shows `"you cannot report this content"`). Self-report blocking UI removed — the relay has no self-report gate, and the failure was purely client-side error surfacing. - **Reason audience disclosure**: the Reason (optional) field shows exact copy under the input based on selected action — Delete discloses that reason is sent verbatim to the affected user and posted publicly in the room; Kick/Ban/Timeout disclose affected-user only; Dismiss/Escalate disclose reporter only. These mirror the actual relay notice paths. - **Enforcement history**: the report detail carries the governing action even after the report leaves `processing` — a report enforced then reopened is `open` yet still shows its `succeeded` action as executed history alongside the resolve form; a reopen does not un-happen a ban. The resolve form gates on `open` status alone. - **Reopen**: terminal reports (`resolved`/`dismissed`/`escalated`) can be returned to `open` for re-triage via `POST /reports/{id}/reopen`. Reopen never reverses applied enforcement, and the copy says so. - **Feedback**: list, detail, and status control (`new`/`reviewed`/`archived`) with a generation-fenced attachment viewer. A purged source community severs provenance to `null` rather than deleting the row; severed rows bucket under a "source community removed" heading. - **Staffing**: Add is create-only with display names resolved via `useUsersBatchQuery` and npub cross-fade on hover (`HoverStaffingIdentity`). An entered key that already appears in the authoritative roster (config, owner-fallback, or DB) is rejected locally with an inline duplicate-key message and no request is issued; a `409 Conflict` from add, role change, or remove surfaces the relay's parsed error body via `adminErrorMessage(e)` — distinguishing config-backed key conflicts from last-operator conflicts without HTTP-status inference. Roster with `config`/`owner_fallback`/`db` source labels; config-backed entries disable remove client-side. In-place role change via `<select>` calling `putAdminOperator`; the relay responds with the effective `OperatorEntry` (`pubkey`, `effectiveRole`, `sources`) in the same shape as the roster list. Read-only badge for config-managed entries. - **Timeout expiry**: `useTimeoutState` clears the store when derived state is `INACTIVE` but the store is still active; the 1s tick is scoped to entries with a known `expiresAtMs` and owned by `ComposerTimeoutBanner` (mounted only while a timeout is active), while `ChannelPane` subscribes to the boolean `useTimeoutActive()` — the pane re-renders when a timeout starts or ends, not every second. `useMembersSidebarModeration` ticks `nowMs` only while the sidebar is open, the viewer can moderate, and some member has a future `mutedUntil`; the tick after the last expiry stops the interval. - Staffing refreshes the principal after a self-demotion or self-removal: `AdminConsolePanel` passes `onSelfMutation` to `StaffingTab`, which re-probes the saved origin so the role badge and tab visibility update without a manual re-probe. - Staffing mutation failures, including both 409 variants (config-backed key vs. last operator), surface the relay's parsed error via `adminErrorMessage`, so the last-operator recovery guidance is shown. - Mutations confirm via `sonner` toasts; failures surface the relay's parsed error `message`, not raw JSON. The `disabled` auth mode renders read-only. ## Mutation idempotency Resolve and reopen carry a per-attempt `requestId`. On first submit the whole command (`{requestId, action, reason, expirationSecs}`) is frozen; after an ambiguous failure (409, 5xx, transport error, truncated response) the snapshot is retained, action/reason/duration controls are locked, and the retry sends the exact same payload byte-for-byte. A definitive pre-commit rejection (non-409 4xx with `bodyComplete: true`) discards the snapshot, unlocks controls, and the corrected submit uses a fresh payload. On success the toast derives from the authoritative `AdminReportResolution` (`activeAction.action` when present; otherwise `status` for decision-only outcomes) — never from the mutable form selection. The native mutation commands — including operator delete — reject with a typed `AdminMutationError` carrying the relay's HTTP status (`relayStatus`, `null` when no verdict exists) and a `bodyComplete` flag that is `true` only when the full response body was read. ## Restriction routes contract change `GET /members/restrictions` and `DELETE /members/{pubkey}/ban|timeout` now take `communityHost` instead of `communityId`; the relay resolves the tenant itself and returns `400 unknown_community_host` for an unmapped host. This is a breaking contract change with no compatibility shim: the relay must run this build before a desktop that uses it, scripts sending `communityId` must switch, and rolling the relay back breaks the desktop Restrictions UI. `docs/admin/README.md` documents the three routes in the route inventory and carries the migration note. Restriction calls also carry the relay the list was loaded from, and the native side rejects a mismatch before building any URL. ## Hardening - Saved admin origins are stored per pubkey and per relay host. The host slug parses the relay URL, keeps the port-drop for DNS names, and encodes IPv6 hosts canonically, so two IPv6 relays never share a file and no IPv6 slug can match a DNS slug. - NIP-11 admin discovery has its own 10s request deadline and reads `/info` success and error bodies through the bounded reader with a 64 KiB cap (`Content-Length` is only a pre-check). - `PUT /operators` builds its `OperatorEntry` response from the validated body (`effectiveRole = role`, `sources = ["db"]`) instead of re-reading the roster, so a concurrent change can't turn a committed write into a `403`. Related: block#3777 (relay `OPERATOR`/`MODERATOR` role model + NIP-98 auth + `admin_api` NIP-11 advertisement — provides the runtime and the discovery field this console consumes) Related: [builderbot-platform-core-infrastructure#140](squareup/builderbot-platform-core-infrastructure#140) (prod admin ingress allowlist and NIP-98 rollout) ## Screenshots Captured headless at `197f0ba0c` against the mock bridge (1280×720). ### Setup **Origin setup — Advanced disclosure open, idle state**  **Authorized console — all three tabs**  ### Reports **Reports queue — open, processing, resolved across two relay groups**  **Open report detail — reporter, target, message snapshot, resolve form**  **Processing report — PROCESSING/HARASSMENT, ban enforcing state**  **Resolve form — Delete selected, audience disclosure copy**  **Resolved report — enforcement history block**  ### Feedback **Feedback list — community grouping with status badges**  **Feedback detail — mutable status control (operator mode)**  **Feedback detail — passive status badge (read-only/disabled mode)**  ### Staffing **Staffing roster — config/owner\_fallback/db source labels**  **Add form — new pubkey filled, ready to submit**  **Duplicate rejection — inline error for existing principal**  --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
…on leases under row lock (block#7853) ## What Two fixes to `crates/buzz-db/src/store/relay_admin_actions.rs` and one cleanup in `crates/buzz-db/src/store/thread.rs`. ### 1. Thread counter atomicity The admin delete action now calls the canonical `soft_delete_event_and_update_thread_in_tx` body shared with NIP-29 and NIP-09 event-ID deletion. The delete, parent `reply_count` decrement, root `descendant_count` decrement, and step marker commit or roll back together in one transaction. A second action against an already-deleted target is a no-op and does not double-decrement. After a committed delete, `emit_live_thread_summary` refreshes the live thread count the same way `DELETE_EVENT` does. ### 2. Lock-first lease expiry fence (all five admin-action paths) On main, the final marker `UPDATE` in every admin-action path (`execute_ban_with_marker`, `execute_timeout_with_marker`, `execute_kick_with_marker`, `execute_delete_with_marker`, `record_failure`) checks `clock_timestamp()` in the `WHERE` clause. PostgreSQL can evaluate that predicate before waiting to lock the target row. If another transaction holds the action row and releases it unchanged after the lease expires, the update commits under the stale timestamp. On this branch, each path acquires the action row with `SELECT … FOR UPDATE` inside the same transaction before any domain write. The wall-clock expiry check in the marker `UPDATE` runs after that lock is held, not before. The pre-entry ownership `SELECT EXISTS` checks remain — they are early-rejection optimizations that are safe because the correct final fence rolls back any provisional work. `record_failure` on main uses a single-statement `execute(pool)`. On this branch it begins a transaction, acquires the row lock with `SELECT … FOR UPDATE` and an explicit expiry check, then writes `state = 'failed'` — matching the four mutation functions. **Lease validity is checked at the protected ownership check under the action-row lock, not at physical `COMMIT` completion.** | Function | Change | |---|---| | `execute_ban_with_marker` | `SELECT … FOR UPDATE` added after pre-entry check, before domain write | | `execute_timeout_with_marker` | same | | `execute_kick_with_marker` | same | | `execute_delete_with_marker` | same | | `record_failure` | wrapped in transaction; `SELECT … FOR UPDATE` + clock check before state update | ### 3. Unused standalone counter helpers removed `increment_reply_count`, `decrement_reply_count` (both in `thread.rs`), and `Db::decrement_reply_count` have no production callers outside their forwarding wrappers. Both perform unguarded autocommit writes that bypass the deletion-transition/atomicity invariant. Removed to prevent future misuse. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
…ck#7881) The staging recipe pointed at the retired `sprout-oss.stage.blox.sqprod.co` relay. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lock#7879) Adds a short paragraph to the Protocol section of `VISION.md` about `/buzz/v1` endpoints, which are backend-for-frontend (BFF) reads. - **Dividing line:** every write is a signed Nostr event. Nostr reads must cover all of that state. `/buzz/v1/...` can add reads shaped for specific use cases (thread catch-up, sync) over the same store. - We start with this rule and refine it as more exceptions come up.
## Why Data backfills need a durable contract outside schema migrations. The contract must survive crashes and support automatic or manual operation without adding version-selection machinery. ## What - Define one PostgreSQL-backed lifecycle for finite data repair - Treat the stable backfill ID as the only durable execution and readiness identity - Use eventually consistent, idempotent registration; compatible workers compete for one exclusive claim - Require bounded batches, atomic mutation and checkpoint commits, generation fencing, and validation before completion - Define all four migration and automatic-backfill configurations - Define the authorized admin API and honest client projection ## How A backfill captures one immutable upper bound, processes finite batches, and commits each target mutation with its checkpoint. PostgreSQL owns lifecycle state, claims, progress, and completion. Registration order and build metadata do not choose a definition. Compatible processes are interchangeable. A material behavior change requires a new stable ID. The orchestrator does not resolve different behavior registered under one active ID. Diagnostic validation on completed rows records only its bounded outcome and audit; it does not create an execution claim. The spec now starts with a six-step mental model and follows the operator flow. It removes repeated requirements and cuts the document from 614 lines and 4,835 words to 474 lines and 3,656 words. ## Risk Low. This PR changes documentation only. It commits Buzz to a design contract, but it does not add runtime, schema, configuration, or protocol behavior. ## Testing No manual runtime testing. A fresh full-spec review checked the lifecycle, safety rules, four configuration cells, admin contract, and production-seam conformance requirements. `git diff --check` and the repository file-size policy pass at `13681664674d4667b181a6211f012090fa40a2cf`. ## Bigger picture The design keeps the relay as orchestrator, a focused backfill crate as domain owner, and `buzz-db` as narrow transaction infrastructure. It removes deployment generations and definition precedence from the implementation plan. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…7897) Adds a "Before opening a PR" checklist and "Reviewing" guidance to `AGENTS.md`, and makes the Review-Proven Rules the shared reviewer checklist. PRs stay in draft until an agent review has run, an agent has exercised the change on the affected surface, and a human has tested it. The review-completed passphrase goes in the description only once that checklist holds for the current change, and comes out if later edits change behavior. Docs-only changes use the light path: content, link, and diff checks plus human confirmation. Reviews read `VISION.md` and the surface's `VISION_*.md` first, check the change against the Review-Proven Rules, recommend blocking only for concrete correctness, security, or contract defects with a realistic failure, label everything else optional, and put all findings in the first pass. Agents comment and never use Request Changes; humans decide which findings must be fixed. Related: [block/buzz-app#268](block/buzz-app#268) --------- Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lock#7900) `api::admin::postgres_tests::disabled_mode_allows_unauthenticated_requests_on_the_admin_host` falls through to a DB-backed read route but was not `#[ignore]`d. The `test-unit` exclusions meant to skip it still named `api::admin::tests::…`, which stopped matching when block#6730 moved the admin tests into `mod postgres_tests`. As a result the test ran in the infra-free unit job and only passed after waiting out the sqlx acquire timeout (~150s in hosted CI), while the ignored-only PostgreSQL lane never ran it against a real database. - The test is now `#[ignore = "requires PostgreSQL"]`, like its DB-backed peers, so the PostgreSQL lane runs it. - The two dead exclusions are removed, and the `api::admin` clause is just `test(/^api::admin::/)`. `nip98_mode_unrostered_signer_does_not_consume_a_replay_slot` was already ignored and runs in the PostgreSQL lane. - The `Justfile` comment now states the convention (DB-backed admin tests are ignored; non-ignored ones reject before touching the DB), drops the stale reference to a ci.yml step that no longer exists, and keeps the timeout-trap warning. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
…lock#7264) Implements NIP-FI HTTP ingress enforcement per the merged spec (PR block#7254). Every protected HTTP surface in enforce mode requires a valid `Nostr-Federated-Identity: Bearer <JWS>` assertion alongside its NIP-98 authorization, bound to the same key. The NIP-98 pubkey (proven actor) must equal the assertion's `nostr_pubkey`; absent, mismatched, or unverifiable assertions are denied fail-closed with byte-exact text/plain responses per the spec's rejection table. This is S5 of the `buzz-enterprise-identity` program. WS enforcement (S3) is separate. ## What changed **New `nip_fi_http` module:** `admit_nip_fi_http()` covers the full admission sequence — `Nostr-Federated-Identity` extraction, offline JWKS verification, NIP-98 key pairing [FI-INV-05], issuer-qualified deny-map check via `(iss, pubkey, now)` [FI-INV-14], and byte-exact denial for all four `DenialClass` variants. `admit_nip_fi_http_on_state()` is the single call-site wrapper for all surfaces [FI-TRACE-AUTHORITY-UNIFORM]. **Protected surfaces** (all gated via the uniform authority): - HTTP bridge: `POST /events`, `/query`, `/count` - GIF proxy: `POST /gifs/search`, `/gifs/share` - Workflow reads: `GET /workflows/{id}/runs`, `GET /workflows/{id}/runs/{id}/approvals` - Moderation reads: `GET /moderation/reports`, `/moderation/audit`, `/moderation/restricted` - Media/Blossom: `PUT /upload`, `/media/upload` - Git smart-HTTP: `info/refs`, `upload-pack`, `receive-pack` - Git settings control plane: `GET`/`POST /git/:owner/:repo/default-branch` (now routed through `admit_nip_fi_http_on_state` for full assertion key-pairing) - Invites: `POST /api/invites` (mint only; claim exempt as pre-membership enrollment door) **NIP-98 strictness in NIP-FI mode:** - When NIP-FI is not Off, bridge POST handlers force `require_auth_token = true` — the `X-Pubkey` dev-mode fallback cannot satisfy the pairing requirement [NIP-FI.md:547-567] - POST bodies on `/events`, `/query`, `/count` require a matching `payload` tag in enforce mode [NIP-FI.md:579-597] - A payload tag present with no content (one-element `["payload"]` or empty string) is rejected as a structurally invalid claim — NIP-98 requires the content to be a valid 64-char lowercase hex SHA-256 digest. Malformed payload diagnostics use a static error message (no attacker-controlled content in logs). - Off mode preserves pre-NIP-FI behavior per [FI-INV-15] **Off-mode compatibility changes:** Admission now runs before request validation on every protected surface, so Off mode differs from pre-NIP-FI behavior in these ways: - **GIF routes:** NIP-98 authentication happens before the upstream provider lookup. An unauthenticated request on a tenant with no GIF provider now returns 401 instead of 404. The 401 goes through the legacy `api_error()` path, so the body is JSON `{"error":"..."}` with no `WWW-Authenticate` header. - **Workflow runs** (`GET /workflows/{id}/runs`): the query string is parsed after admission. A request with a missing credential and a malformed query (for example `?limit=abc`) now returns 401 instead of 400. - **Authorized malformed query:** once admitted, a malformed query returns a JSON 400 `{"error":"invalid query: ..."}` from `parse_query_or_400` instead of axum's `text/plain` `Query` rejection. - **Moderation reads** (`/moderation/reports`, `/moderation/audit`): same extraction change as workflow runs. Admission comes first, then a JSON 400 for a malformed query. - **Malformed `payload` tag:** shared NIP-98 verification now rejects a malformed `payload` tag in every mode, including on bodyless requests. That means a one-element `["payload"]`, an empty digest, or anything other than 64 lowercase hex characters. Before this change, a bodyless request ignored the tag. **Auth header cardinality:** Enforce mode rejects requests with more than one `Authorization` header with 403 `evidence rejected\n`. DenyProtected returns 503 `authorization unavailable\n` before this check (and before NIP-98 or assertion verification) runs. The cardinality gate is enforced at the NIP-FI admission boundary (`admit_nip_fi_http`), not via legacy auth boolean flags, so it applies uniformly to all Enforce-mode surfaces including Git. Off mode passes through the first header value unchanged, preserving pre-NIP-FI behavior [FI-INV-15]. **Denial-class remapping:** In Enforce mode, NIP-98 extraction/verification failures from the closure are mapped to the appropriate `DenialClass`: `MissingEvidence` when no `Authorization` header is present, `EvidenceRejected` when the header is present but invalid. Off mode propagates legacy responses unchanged [FI-INV-15]; for example, the Git missing-Authorization and wrong-scheme 401s keep their legacy bytes with no `content-type` header. **JWKS refresh scheduling:** The refresh timer's `*last` checkpoint is set after the fetch completes, not from the pre-fetch timestamp, preventing interval drift under network latency. The loop is extracted to `nip_fi_jwks_refresh_loop` for unit-testability. **Log hygiene [NIP-FI.md:777-779]:** Issuer identity is never written to logs or error messages. All startup/refresh diagnostics use an ordinal index (`issuer_index = N`). Config parse errors for malformed issuers use classify()/line/column codes rather than raw config values. **Admin SPA document routes:** `/reports`, `/reports/{id}`, and `/feedback` are exempt from the NIP-FI assertion guard only on the admin host (host-qualified via `is_admin_host`). These paths are not added to the broad `NIP_FI_EXEMPT_PREFIXES` list — tenant hosts remain fully protected. **Deny-map seam (`HttpDenyMap` trait):** signature is `(issuer, pubkey, now)` matching `NipFiDenyMap::is_denied` from S4 (PR block#7265) so integration is a one-liner. Stub is `AlwaysAdmitStubDenyMap` — named to accurately describe stub-phase semantics (fail-open intentionally pending S4). **Route inventory:** every authenticated HTTP route is classified as Protected or Exempt with justification. Documented in the F4 seam-test comment in `bridge.rs`. **Outer-guard tests:** `#[ignore = "requires Postgres"]` tests in `bridge.rs` drive the full router (`build_router`) in Enforce mode with valid NIP-98 but no `Nostr-Federated-Identity` header. These exercise the **outer `nip_fi_assertion_guard` middleware** in `router.rs` — the layer that fires before any handler runs. They prove the guard's exact response bytes (401 `authentication required\n`, `text/plain; charset=utf-8`, `WWW-Authenticate: Nostr`) for each protected route. Per-handler `admit_nip_fi_http_on_state` wiring is proven separately by the cardinality test and the media/settings/git handler-level suites. ## Seams **S3 seam:** `nip_fi_config.rs` is duplicated (S3 not landed; session-deadline parts stripped). Removed when S5 rebases onto S3. **S4 seam:** `AlwaysAdmitStubDenyMap` until S4 provides the real map. Integration is one line at the `admit_nip_fi_http_on_state` wrapper. ## Evidence At head `ee338ea9c` (branch `hayt/nip-fi-http-enforcement`), hosted CI run [36167450853](https://github.com/block/buzz/actions/runs/36167450853): - **Rust / Unit Tests** ([job 108179338458](https://github.com/block/buzz/actions/runs/36167450853/job/108179338458)): passed. The `buzz-relay` `test-unit` step ran 171 tests with 171 passes. Its PASS lines include all 57 non-ignored tests this PR adds to the relay: `nip_fi_http::tests` (25), `nip_fi_config::tests` (8), `router::tests` (8), `api::parse_query_tests` (5), `api::git::transport::off_mode_precedence_tests` (5), `tests::jwks_refresh_*` (3), and `composition_tests` (3). The same job ran the `buzz-auth` suite, which includes `nip_fi::jwks::tests::key_set_serves_live_snapshot_under_reader_and_refresh_overlap`. - **Rust / Rust Lint**: passed. - **Relay and PostgreSQL / Backend Integration (relay e2e)**: passed. - **PostgreSQL Domain / PostgreSQL Tests** ([job 108183303436](https://github.com/block/buzz/actions/runs/36167450853/job/108183303436)): passed. 542 tests run, 542 passed. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> Signed-off-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary Fix the upload/edit smoke-test race observed on main at `6410e685a80d42db0645fadc9bbe710559ef911e`. - Hold the mock upload at the Tauri IPC boundary and release it explicitly after checking edit rejection, rather than assuming menu interaction finishes within one second. - Wait for the Radix menu to unmount: edit dispatch happens in `onCloseAutoFocus`, after the click. Without that wait, the negative assertion can pass before the edit callback actually runs. - Keep the existing attachment-preservation and subsequent successful-edit assertions. No production code or CI coverage is changed. ## Evidence and scope - [Main failure](https://github.com/block/buzz/actions/runs/36189213149/job/108250341388): `opening edit during an immediate photo upload preserves the draft`, including an upload-progress timeout on retry. - Investigated from block#7791, which changes only `docs/nips/NIP-AR.md`. Its PostgreSQL failures and video-menu timeout are separate; PostgreSQL passed on this main run, and the video spec passed locally. This PR does not claim to fix those failures or the separate pointer-interception failure seen in the main job. - block#7843 already addresses unrelated runtime suites being selected for docs-only PRs. ## Validation - Full desktop `pnpm test`: 6,676 Node tests + 92 jsdom tests passed. - Upload/edit regression repeated 5 times: passed. Holding the upload without waiting for menu teardown failed all 5 runs, demonstrating the second race. - Complete file-attachment and video-attachment Playwright specs: 30 passed. - Pre-push desktop lint, typecheck, full desktop tests, and file-size gate passed at `46e3d4392e5b56dcb74ea864aa19aa10287f27d3`. - `just ci` attempted: initial formatting issue corrected; second run exceeded the 5-minute local limit during Tauri clippy. Not claiming full repository CI green. ## Review / human verification Self-reviewed the diff against the test contract; no production behavior changes. Draft pending human verification and CI. To verify: activate Hermit, then `cd desktop && pnpm test:e2e:smoke file-attachment.spec.ts --grep "opening edit during" --repeat-each=5`. Expect all five runs to pass, retaining the upload in the draft and allowing edit only after upload release. Originating conversation: buzz://message?channel=aa7f2b48-d367-4ca1-be09-116a2eb45b99&id=1b68585ba4483a87f5a453fd7bbdd11d434e332dcb4aff8acfe1f5d2601b6561 Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
A documentation-only update to block#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: block#7809 (incident, unchanged) and block#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>
The relay can use a configurable HTTP(S) gateway delivery URL and signs the same URL it sends. The gateway verifies NIP-98 against the received request URL, including authority, port, path and query. App Attest, scoped grants, replay checks and delivery quotas are unchanged. Forwarding proxies must preserve Host and supply the original request scheme through X-Forwarded-Proto. The NIP relay-delivery wording now defines the configured HTTP(S) URL as the signed URL and labels the public URL as an example. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz>
## Summary Adds a short, proposal-only **NIP-AR: Channel Artifacts** in `docs/nips/NIP-AR.md`, as a simpler alternative to the closed block#7771. - Defines complete-snapshot artifact events with stable identity and atomic, `prev`-based revisions. - Uses channel access and posting permissions for reads, edits, moves, and writer-authorized deletion. - Keeps client-defined content opaque to the relay, with exact tag filtering over current accessible state and bounded queries. - Covers durable source removal on moves, terminal deletion, and safe client fallbacks without defining task/project schemas or changing application behavior. ### Remaining design work This is a compact draft, not a complete implementation-ready wire contract. Concrete query request/pagination/error/limit-discovery formats, removal/replay wire details, and numeric query limits remain follow-up work. Prefer extending shared WebSocket `REQ` / HTTP `/query` surfaces rather than adding plugin-specific endpoints. No database layout or scale guarantees are proposed. ### Related issue Closest prior proposal: block#7771 (closed). Searched existing PRs and issues for channel artifacts; no matching issue found. ### Testing - Verified the committed document is byte-for-byte identical to the reviewed local draft. - Parsed the example JSON and its embedded JSON content; checked unique two-element example tags. - Reviewed the entire one-file diff and ran `git diff --check`. - Commit and push hooks passed (including DCO sign-off and differential file-size checks). - Full application CI was not run locally: this changes only a Markdown proposal, with no implementation or UI changes. --------- Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
We are introducing a companion service to the relay that needs pubkey event mentions for pubkeys it manages. This PR introduces an outbox for pubkey mentions that will be sent to these "operator listener" services. ## Summary Deliver mentions of registered pubkeys to deployment-global operator listeners. - Configure listeners with `BUZZ_OPERATOR_LISTENERS`; listeners manage target pubkeys through NIP-98-authenticated `POST`/`DELETE /operator/listener/pubkeys` requests. - During supported event ingestion, match `p` tags to registrations and insert one outbox row per listener in the event transaction. - A background worker claims up to 10 rows, sends NIP-98-signed notifications, and retries transient failures with exponential backoff for up to 9 attempts (idle polling backs off from 250 ms to 2 s). - Outbox rows are ineligible for delivery after 15 minutes and are reaped every 5 minutes. Registered pubkeys live for 30 days; re-registering renews them, and a daily reaper removes expired registrations. - Keep persistence app-managed: no database triggers, functions, foreign keys, or separate match queue. ## Changed behavior Listener outbox rows are enqueued inside the shared event-insert and thread-metadata transactions; enqueue failure rolls back the event. `event_mentions` behavior is unchanged: ordinary event/reaction mention indexing remains post-commit best effort with warning-only failures. ## Testing - `cargo fmt --all` - `cargo test -p buzz-db --lib` - `cargo test -p buzz-relay --lib config::tests::operator_listener_routes` - `cargo check -p buzz-db -p buzz-relay`
## Summary - Keep non-listener-mention event writes on the plain insert path without opening a transaction. - Preserve the transaction around listener mention event insertion and outbox enqueue. ## Validation - `cargo fmt --all -- --check` (Hermit) - Tests not run. Follow-up to Baxen’s review of block#7793, point 1. Signed-off-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz> Co-authored-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz>
) Addresses point 3 of [previous comment](block#7793 (review)) "3. Fail fast on a bad BUZZ_OPERATOR_LISTENERS (config.rs:857)" ## Summary - Propagate malformed `BUZZ_OPERATOR_LISTENERS` values from `Config::from_env` as `ConfigError`, including invalid UTF-8. - Update startup config tests to assert returned errors and restore the original admin warning capture helper, removing the shared helper introduced only for listener log assertions. - Cover valid and absent listener values through `Config::from_env`. ## Context Follow-up to the review of block#7793 specifically [reverting some config logging parts](https://github.com/block/buzz/pull/7793/changes#diff-eaa0eeb209ff1ad6212ffa56d3a983bb732858647c9fb227ef7483a3363b5a71). Invalid listener configuration currently logs an error and silently disables listener delivery. ## Verification - `cargo fmt --all -- --check` passed with Hermit. Signed-off-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz> Co-authored-by: Implementor <691cca7a870db1dad6990d5938d1b4a2a2ca9647ca81290b536c17b06b2e473f@buzz.block.builderlab.xyz>
…#7912) This removes implementation references and implementation status from `docs/nips/NIP-FI.md` so the spec holds normative text only. It drops the Blossom compliance note, the known-gap notes, crate/file paths and PR numbers, roadmap "supersedes" sentences, doc-history lines, and the design-rationale note on session-only vs deny-until-TTL (its restart re-push SHOULD remains stated normatively elsewhere). Two wording changes carry normative weight: `PUT /media/upload` is listed as a plain kind-24242 upload route instead of a temporary alias, and the JWKS fetcher's SSRF controls become a MUST instead of a description. It doesn't depend on any enforcement PR and can merge at any time. Related: block#7265 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
- `upstreamSnapshot` is now 97e1f6a, the exact upstream commit this branch merges. - The migrationLedger maps upstream 0041-0065 to fork 0046-0070, with each file's SHA-384. - Paths that upstream now ships are dropped and recorded per delta as `upstreamPresentAtSync`: D0, D8's harness-management spec, D12's respondToAllowlist/RespondToField, D13's push.rs. - New re-port paths are classified, and a `kiingo-migration-ledger` delta is added. - Exact budget amendment `upstream-sync-97e1f6a0`. Modified upstream files go from 262 to 277 and production-source files from 153 to 156. Changed lines (10588 to 9577) and hunks (1167 to 1163) ratchet down. The forbidden business-logic budget stays zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
Brings in mobile agent guest-access management (#197) and the TestFlight hosted-agent build flag (#200). - The Settings guest-access section hook is re-ported onto upstream's restructured settings page, next to the hosted-agents row. - lucide_icons_flutter is gone upstream (block#8109), so the new guest-access settings pages use BuzzIcons (Tabler). Four glyphs that BuzzIcons does not name (ban, calendar, calendar-month, history) are added to the fork-owned GuestAccessIcons. keyRound maps to BuzzIcons.key. - The inventory takes main's new guest-access settings delta. Budgets are unchanged (277 / 156 / 9577 / 1163). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
…go/buzz Kiingo/buzz runs no GitHub-hosted jobs (Spot-only policy); this upstream job ran on a hosted runner in the sync PR's CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
… limits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
RossHartmann
left a comment
There was a problem hiding this comment.
Review from C (push pipeline owner) of 0333f34: re-port of APNs token auth onto the single-application config, plus the relay guest-kind push path. The re-port looks correct. There are two must-dos outside this PR, listed under "Cutover items in Kiingo/kiingo" below.
I read the source and rendered the chart. I ran no tests.
Gateway: matches the #189 design
ApplicationConfig.apns_authreplaces the certificate path.parse_authkeeps the certificate default and requires all three token variables. Key and team ids must be ten uppercase characters.main.rsbuilds the token or certificate transport.apns.rson the branch still has the provider-token signer, the bearer header, and theExpiredProviderToken→ retry path. Every other 403 stays a configuration fault.- The
BUZZ_PUSH_APNS_{AUTH,CERT_PATH,KEY_PATH,KEY_ID,TEAM_ID}names fit upstream, which already usedBUZZ_PUSH_APNS_CERT_PATH. 👍 - Dropping the two fork fixes is right:
- The template now renders
maxGrantLifetimeSeconds | int64 | quote. - The gateway no longer takes a public delivery URL, so the host relaxation has nothing to apply to. The audiences stay the fixed
push.buzz.xyzconstants.
- The template now renders
- App Attest defaults to production when
BUZZ_PUSH_APP_ATTEST_ENVIRONMENTis unset, which is what the TestFlight build uses.
Chart: rendered with helm 4.3.1 against Kiingo's draft values
- The schema's
if/then/elserequiresapnsTokenin token mode andapnsCertotherwise. - Token mode renders
BUZZ_PUSH_APNS_AUTH=token,KEY_PATH=/run/buzz/apns/auth-key.p8, the quoted key and team ids, and mounts onlyauth-key.p8(0400). It renders no certificate env or mount. requiredalso catches the empty-string defaults ofkeyIdandteamId.- Minor, optional:
apnsToken.keyIdandteamIdallow""in the schema. The template catches that, but the schema could make them^[A-Z0-9]{10}$under thethenbranch sohelm lintreports it earlier.
Relay: guest-kind push looks right
PUSH_KINDS(descriptor) andbuzz_db::PUSH_MATCH_KINDS(app-code enqueue) both list 9/40002/45001/45003/46040/46042.push_match_kinds_match_advertised_push_kindspins them equal, now that the fork's 0045 SQL trigger is retired.- The enqueue runs from
after_admitted_insert.Db::insert_eventsets the producer, andchannel_idisNonethere, so global, h-less 46040/46042 events are staged. handlers/event.rs: the live fan-out filtersRESULT_GATED_KINDS, which includes 46040–46042, throughreader_authorized_for_event. Only the p-tagged owner gets them, even through a kindlessids:[…]subscription. That matches the pull paths.push_runtimehas the owner-only wake test,guest_approval_request_wakes_only_the_addressed_owner.- The relay keeps
BUZZ_PUSH_GATEWAY_DELIVERY_URL, soprod-valuesstays valid.
Cutover items in Kiingo/kiingo
push-gateway-values.yamlmust switch toapplication.*in the same cutover.- The sync chart's schema has
profiles: false, so the current values fail withat '/profiles': false schema. - My draft is ready and not pushed: Kiingo/kiingo
e7f1ac2e8a09on branchbuzz-push-gateway-single-application-20261016. It moves toapplication.{appAttestAppId, apnsTopic, apnsEnvironment, apnsAuth: token, apnsToken}and dropspublicDeliveryUrl.
- The sync chart's schema has
buzz-production.ymlalso needs the change in that draft.- "Require the push gateway sources" greps the chart for
BUZZ_PUSH_DOGFOOD_APNS_KEY_PATH. With this branch it would fail the deploy. The draft accepts either name. - The host-consistency check required
publicDeliveryUrl: https://push.chat.kiingo.com/...in the gateway values. It now reads the relay'sBUZZ_PUSH_GATEWAY_DELIVERY_URLinprod-values.yaml.
- "Require the push gateway sources" greps the chart for
- Leases and clients.
- Upstream 0060 refuses to migrate while push leases are active. Relay push is enabled in prod now.
- TestFlight build 1 speaks
nip-pl, so keep enrollment to Ross and testers. Expect either to clear leases before the cutover or to cut over before anyone enrolls. - Phones need a build from this branch (
buzz-push-v1) after the cutover.
…am-60de1db0 Conflict resolutions: - iOS Xcode 27 picks (#199) and the block#7745 backport (#208) are upstream commits already in the 97e1f6a snapshot: take the sync side for every #199 path and drop both drop/upstream-present deltas. - deny.toml: keep upstream's ignores and main's RUSTSEC-2026-0367/0368/0369; the sync tree pulls zip 8.6.0 via mesh-llm v0.76.2, re-audited: plugin manager and autoupdate still never call extract() and write plain files. - Message Details (#205): desktop composes the host ref with the right-click root ref; mobile adds Details to upstream's grouped action sheet (the popover was removed upstream in block#8280) with a Tabler info-circle icon; retired-notice filtering passes agentPubkeys at upstream's new formatTimeline call sites. - Hosted agents (#201): Lucide icons -> BuzzIcons (lucide removed upstream). - Inventory: sync inventory plus main's new deltas (remote signer, guest alert Review, Message Details) and the merged hosted-agents delta paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the #209 media cache delta and records the merged budgets (modified upstream files 282, production-source files 158, lines 9674, hunks 1183) as amendment sync-merge-main-20261017, pending approval above Ross's 2026-10-16 limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kiingo's Spot runner admission only runs a job whose workflow file matches main (public_workflow_not_current), so the upstream sync PR (#202), which replaces ci.yml with upstream's split _ci-*.yml, cannot run its own CI. Land the workflow files and their CI-support scripts, hermit pins and nextest config first, unchanged from the sync branch: Kiingo's Spot-only, no-test policy is re-applied and upstream-only GitHub-hosted jobs are skipped on Kiingo/buzz. Inventory: a temporary drop/upstream-present delta and a four-file budget amendment, both removed by the sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
|
Taken over by C (2026-10-17), from the mobile-parity agent, which has moved to the Assistant reliability fixes. This is still a draft. Don't merge until main gives the go. Refreshed against Kiingo/buzz main f5e365e (#199, #201, #203 and #205–#209):
|
- buzz-acp relay.rs: keep upstream's RestClient::relay_self() and add #210's cached relay_self_key()/read_relay_self_key() beside it. - ingest.rs: #210's kind:9007 dm refusal precedes upstream's visibility/labels parsing. - side_effects.rs: #210's DM-visibility guard on upstream's get_channel_for_event_write read. - channel_members.rs: #210's DM add guard calls get_active_role_tx with tx.conn() (upstream's AdmittedTx). - Inventory: #210's channel_members delta and Ross's 2026-10-17 ceilings (600/400/40000/4000); the pending sync-merge amendment is dropped. cargo check (acp, relay, db, all targets) passes; no tests run locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kiingo's Spot runner admission only runs a job whose workflow file matches main (public_workflow_not_current), so the upstream sync PR (#202), which replaces ci.yml with upstream's split _ci-*.yml, cannot run its own CI. Land the workflow files and their CI-support scripts, hermit pins and nextest config first, unchanged from the sync branch: Kiingo's Spot-only, no-test policy is re-applied and upstream-only GitHub-hosted jobs are skipped on Kiingo/buzz. Inventory: a temporary drop/upstream-present delta and a four-file budget amendment, both removed by the sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ross Hartmann <1750494+RossHartmann@users.noreply.github.com>
|
Merged main 765e8e8 (#210) in, now at 192627b. Resolutions:
|
Lands #202's workflow files, CI-support scripts, hermit pins and nextest config on main, so the sync PR's workflows match main and Kiingo's Spot runner admission can run its CI. Content is upstream's from the sync branch, with Kiingo's Spot-only, no-test policy and hosted-job skips. Its own changed workflows could not run here by design (admission public_workflow_not_current at Set up runner); unchanged workflows (CodeQL, Desktop Release Candidate) passed. Inventory: temporary drop/upstream-present delta upstream-ci-prelanding, which #202 removes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Workflow files already match; the temporary upstream-ci-prelanding delta is dropped because the sync moves upstreamSnapshot past those files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CodeQL triage for the upstream sync (C, 2026-10-17, approved by main). Alerts were checked mechanically against upstream block/buzz
The next upstream sync should re-check that these are still upstream-only. Dismissed (52)
Reviewed exceptions:
Still open (17), routed to main
|
…bus advisories - Workflows now match main again (M-4108 read-only token defaults). - Inventory: classify docs/implementation-plans/M-4108-workflow-token-permissions.md with the other fork implementation docs (#211 did not add it). - deny.toml: ignore RUSTSEC-2026-0370/0371/0372 (zvariant, zbus_names, zvariant_derive unmaintained; merged into zbus 6), transitive via zbus 5.19 for the desktop Linux keyring with no compatible upgrade yet. These were published today and failed #202's Dependency policy step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors the temporary Windows Rust guard (M-4109); workflows stay byte-identical to main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merging with Desktop Windows Build treated as non-blocking, per main's decision on 2026-10-18. Every other required check is green at ff08b24. That includes Rust Lint, the cross-compiles, Web, Mobile, Desktop JS and Tauri core, Security, the helm and push-gateway charts, Docker, Desktop RC, and CodeQL. Desktop Windows Build has been queued since 01:28Z because the Windows Spot pool hit its failed-purchase budget: 2 of 2 in a 6-hour window, after two cold VMs were dropped. One went when #212 removed the Rust demand; the other went in the 01:05Z controller deploy. No Windows VM can be bought until 06:42:51Z. This is not an admission regression. Ross has no Windows users and has paused Windows work. Tracked in M-4109. |
Caution
Do not merge. This is a planned merge: Ross approved the sync to land only after phone push is verified end to end on the current fork, and main will give the go. Draft until then. Cutover follows the separate runbook (summary at the end).
Summary
This PR merges upstream
block/buzzmain intoKiingo/buzz, as real merges with no rebase and no force-push.b743da9a(#196), then main merged in again twice:599d88ec(#198) andefc7fb7d(#197, #200)60de1db0(main sync, 394 commits), then97e1f6a0(2 follow-up commits; neither adds a migration)eed74bdeupstreamSnapshot97e1f6a0828d97d0031e2a31402554b679bf1146, the live upstream tip at the final fetchThe branch name keeps the first pinned SHA (
60de1db0).Commits, in first-parent order:
a38968f6merges upstream60de1db0, with every conflict resolved and every Kiingo delta re-ported. Push-gateway files take upstream's version in this commit.0333f34cre-ports APNs.p8token auth onto upstream's single-application gateway. This commit is for C's review.c886e4bamerges upstream97e1f6a0. fix(db): validate channel roster snapshots in the app writer block/buzz#8316's roster guard runs in the single fenced insert path.d371fab8merges fork main feat(mobile): create and manage hosted agents from the phone #198 (mobile hosted agents), re-ported onto upstream's new settings page and Tabler icons.008f86c4re-baselines the inventory: new snapshot, migration ledger, reclassification, and an exact budget amendment.f37f21ebmerges fork main feat(mobile): manage agent guest access from Settings #197 and chore(ios): build TestFlight with hosted-agent creation enabled #200. The guest-access Settings section is re-ported onto upstream's settings page, and its Lucide icons are moved to BuzzIcons/GuestAccessIcons. Budgets are unchanged.Every commit is signed off (DCO) and carries
Co-Authored-By. Upstream history and authorship are preserved.Migrations (decided: renumber upstream, never the fork)
Upstream
0041–0065are renamed byte for byte to0046–0070withgit mv. sqlx's SHA-384 checksums are unchanged.The fork's production-applied
0041–0045keep their numbers and checksums.Production validates 1–45 and applies 46–70. Fresh databases apply the same order.
The per-file map lives in
docs/kiingo-fork-inventory.jsonundermigrationLedger, withlastProductionVersion: 45.New static guard:
scripts/check-kiingo-migration-ledger.mjsruns fromcheck-kiingo-fork-boundary.mjsin the existing "Kiingo fork replay, footprint and migration-ledger boundary" CI step. It needs no database and no tests. It fails on any of:upstreamSnapshot:<upstreamPath>;include_str!of a missing migration.Negative cases were verified locally: a duplicate
0041and a one-byte edit to0050both fail.Upstream test literals naming upstream versions are shifted by +5. That covers
migration.rs(65 to 70,migrations[N],run_to,version ==),follow_up_retirement_tests.rs,thread_window_postgres_tests.rs,runtime/tests.rs,deletion.rs, and twoinclude_str!paths.docs/thread-window-deployment.mdanddocs/push-gateway-deployment.mduse the fork numbers.The policy is documented in
docs/kiingo-fork-maintenance.md(Migration ledger): next free number, and the ledger is the source of truth.Migration mapping (upstream name → fork name, SHA-384 prefix)
0041_managed_publication_fences.sqldf79c95f3ad3b3c10042_agent_status_thread_counters.sql0a3b105e82542f890043_user_stop_events.sql417047e6d862c0df0044_channel_event_sequence.sql8ab6ad125a9528950045_push_guest_notification_kinds.sql3ddca2a5c7cabd420046_nip_fi_identity_foundation.sql0041_nip_fi_identity_foundation.sqlad6359d4b86fc85d0047_nip_fi_authorization_foundation.sql0042_nip_fi_authorization_foundation.sql702326df04517e530048_push_gateway_dogfood_profile.sql0043_push_gateway_dogfood_profile.sqled55cbda5e3de7e50049_drop_nip_fi_ledger.sql0044_drop_nip_fi_ledger.sqla852862896862d0c0050_retain_push_revocation_tombstones.sql0045_retain_push_revocation_tombstones.sql7c8c13d6e6e7dec20051_storage_accounting_snapshots.sql0046_storage_accounting_snapshots.sqle02ae303449874410052_relay_admin_action_target.sql0047_relay_admin_action_target.sql1e676435f20133640053_audit_hash_version.sql0048_audit_hash_version.sql2b040688d14216cd0054_thread_window_index.sql0049_thread_window_index.sql35b707ad2a8ad28b0055_operator_listener_mentions.sql0050_operator_listener_mentions.sql08c1b91010b969db0056_owner_community_deletion_admission.sql0051_owner_community_deletion_admission.sqlfe6d3068a65125870057_channel_artifacts.sql0052_channel_artifacts.sqldcf66d35143fbd830058_owner_deletion_auto_approval.sql0053_owner_deletion_auto_approval.sql9a1914fcb2df16220059_owner_deletion_quota_reservation.sql0054_owner_deletion_quota_reservation.sqlef0f783e356c61a90060_relay_admin_direct_actions.sql0055_relay_admin_direct_actions.sql4a6dff9da91abd690061_personal_read_state.sql0056_personal_read_state.sqlf9b6824b395e1a120062_personal_read_started_at.sql0057_personal_read_started_at.sql9d5038ffc8e9cbc20063_personal_read_through_message.sql0058_personal_read_through_message.sql32b6817d8e7731020064_push_rollback_gate.sql0059_push_rollback_gate.sqlce3afd86480ded9a0065_push_single_application.sql0060_push_single_application.sql10149f8eeda128930066_channel_labels.sql0061_channel_labels.sqld584af4ed17070c20067_system_channel_type.sql0062_system_channel_type.sqlf154368d05b209590068_retire_push_match_trigger.sql0063_retire_push_match_trigger.sqla4156e039f95b5bc0069_profile_name_fields.sql0064_profile_name_fields.sql5a618efc012b8fca0070_retire_channel_ttl_trigger.sql0065_retire_channel_ttl_trigger.sql2440f69b41932348Conflict-resolution log
The rehearsal found 74 content conflicts; #196 added 4 more mobile ones. All 78 are resolved. The policy was to keep upstream behaviour unless an inventory delta requires otherwise. Re-ports land in fork-added files behind small hooks where possible.
Relay / DB / pubsub (deltas 1, 2, 5, 11, 12, 13, 20)
handlers/event.rs(D13, privacy). Upstream fix(relay): gate owner-only kinds in shared fan-out access filter block/buzz#8006 moved owner-only live fan-out intofilter_fanout_by_access, hard-coded to two kinds. Taking upstream's location as-is would have let 46040–46042 reach non-owners. The predicate there is nowRESULT_GATED_KINDS.contains(&kind)(event.rs:183). That one check covers local delivery, Redis cross-pod delivery and kindlessidssubscriptions. Hunk 2 keeps upstream's NIP-43 leave exception, with the fork's invocation-route scope check inside it.handlers/req.rs. Upstream imports and upstream's system-channel exclusion (feat(relay): leave system channels out of reads that do not name them block/buzz#8181/feat(relay): let channel-state filters opt in to system channels block/buzz#8182). The fork'sRESULT_GATED_KINDSp-gated read check is kept (req.rs:1663).Push (D13). Migrations 0064 and 0068 (upstream 0059 and 0063) retire the trigger that carried 46040/46042.
event_follow_up::PUSH_MATCH_KINDSis now[9, 40002, 45001, 45003, 46040, 46042]. This file had no conflict.htag still go throughrecord_push_event, and the matcher skips the membership check when there is no channel.push_lease.rskeeps the fork's 6-kindPUSH_KINDS. The test that pinned the 0045 trigger is replaced bypush_match_kinds_match_advertised_push_kinds, a set-equality check.push.rsis upstream's version. fix(push): stop backfilling messages on lease activation block/buzz#8178 removed the activation backfill.push_kindsstill advertises 46040/46042 underbuzz-push-v1.store/event.rs(D11, publication fence). Upstream's AdmittedTx structure is taken (refactor(db): type-level admitted event-write transactions (AdmittedTx) block/buzz#8130/BUZZ-175: Route serving event writes through the tenant lock block/buzz#7828/buzz-db: admit every fenced-table writer in app code block/buzz#8219). The re-introducedinsert_event_onis removed again, andinsert_event_in_transactiondelegates toinsert_event_with_thread_metadata_tx. Every insert path, including the canvas head (insert_canvas_head_checked), therefore passesmanaged_publication::apply_event_tx; the fence cannot be bypassed.reject_unfenced_managed_event)._in_txpath and keeps the 40098 exclusion.deletion.rs.EXPECTED_SCOPED_TABLESandPURGE_SCOPED_TABLEStake the union of fork and upstream tables. Upstream's purge loop still iterates them.dm.rs(D12).seal_dm_if_opener_inactivenow runs insidebegin_community_write_transaction. No conflict marker.api/bridge.rs(D1/D2/D11).KIND_AGENT_STATUSstays in the channel-window aux kinds, with superseded-status evidence on the window hop.user_stopandchannel_event_sequencedispatch is re-inserted after upstream's thread_window, read_state_snapshot and artifact handling. It follows the named-system-channel rule (feat(relay): leave system channels out of reads that do not name them block/buzz#8181). Unreadable and missing channels get the same 403 (fix(relay): reject unreadable write targets as missing block/buzz#8249).ingest.rs. Upstream'starget_readable_in_channel, then the fork's "operational status is immutable" rejection. The fork's drift-check placement is kept.router.rs/state.rs/main.rs/ pubsub (D20).transport_continuitymoves to/_statusand is never in readiness.restriction_cacheandserving_active_cacheare added to requalification and to the continuity bypass. The newobserver_owner_cachereaders are gated.set_presence_many/clear_presence_many, and the NIP-FI replaySET NX.buzz-adminstorage-snapshot (D5). It usesMediaStorage::from_runtime_env, so Azure metrics keep working.schema/. The fork schema delta is re-ported into upstream's per-object layout (refactor(schema): fan desired-state schema out to one file per table block/buzz#8131):user_stop_events,channel_event_heads;events.channel_sequencecolumn, CHECK and index;The fork's
NEW.kind IN (… 46040, 46042)desired-schema assertion is replaced by one that 0045 keeps its history and 0068 drops the trigger. The reconcile script's partition trigger drops merged cleanly.ACP / CLI / SDK (deltas 2, 3, 12, 14)
Trust lane. It is now part of upstream's per-thread key: a new
SessionScope::Guest { channel_id, requester, root_event_id }, applied bySessionScope::in_laneright after scope derivation. Owner and guest lanes, and any two guests, never share a provider session, queue partition, in-flight slot, steer target or typing entry. The owner lane is unchanged upstream behaviour.Per-requester conversations. These are a refinement inside one scope (
queue/conversation.rs):take_conversation,flush_cancelled_only,joins_in_flight_turnbefore upstream's steer/interrupt decision.lib.rsre-port ontoInboundAuthorGate. The trust tier comes fromlistener_trust::admit. Also re-ported:pool.rskeeps upstream context targeting and thread hydration, plus fork invocation-only prompts, the guest context filter,[your owner]marking and prompt authors.base_prompt.md. The fork's "Loyalty and Confidentiality" section sits above upstream's "Incoming Turn Contract".CLI
messages.rs. Upstream NIP-30 emoji resolution plus the fork's provenance/handoff tags, through the newbuzz_sdk::build_message_with_emoji_and_extra_tags.build_message_with_extra_tagskeeps its signature.First
ttag. Fork DM detection (dm_mentions.rs,dms.rs) uses the firstttag, matching feat(relay): channel labels block/buzz#8175.Upstream-compat wrappers.
session_cancel(test-only) andcancel_with_cleanup_gracedelegate to the fork's*_with_originvariants.Desktop (deltas 6, 8, 10, 12, 14, 15, 21)
commands/agents.rs(D6). Fork structure is kept (provider_create/provider_delete). Upstream fix(desktop): remove communities locally and refuse their agent pairs block/buzz#7997's assignment-cleanup crash recovery is ported intoprovider_delete.rs; that file had no marker. perf(desktop): publish mention sends before waking agents block/buzz#7154's replay floor is threaded through the fork'sdeploy_to_providersignature.AgentDefinitionDialog.tsx/AgentDialog.tsx. These now use upstream'screateRunSection(effortOptions)render prop andAgentIdentityFields. ForkcreateRunSectionFirst, readiness gating, preferred-destination draft andproviderOwnsExecutionProfileare kept.discovery.rs(D10). Upstream'scatalog.rs, with#[cfg(unix)]re-applied to everyskill_dir.MachineOnboardingFlow.tsx(D8). Upstream Refresh desktop onboarding flow block/buzz#7528 flow; only the surviving identity routes tocontinueWithExistingIdentity.useInboxThreadContext.ts(D12). Fork state model with upstream fix(desktop): bound startup request bursts and recover quota refusals block/buzz#7790 settled-event readiness.rosterFreshness.ts/hooks.ts. The fork helper now calls upstream'snoteChannelMembershipChange(fix(desktop): read channel metadata and member lists from writer after writes block/buzz#7999). The fork's direct members invalidation is dropped as superseded.useAgentManagement.ts,usePersonaActions.ts,personas/update.rs,agent_providers.rs,backend.rsandprovider_access.rstake fork + upstream. The rest are unions.RespondToField.tsxandrespondToAllowlist.*(fix(desktop): npub identity controls across profile, agents, and workflows block/buzz#7489 ships the same feature). Upstream's deletion of the F8 onboarding e2e is accepted.desktop/src-tauri/Cargo.lock. Upstream's lock, with cargo adding the fork-only packages. rustls stays at 0.23.46.Mobile (deltas 11, 16, 23, 24, 25)
push_bridge.dart,BuzzPushNavigationTarget.swift,AppDelegate.swift. feat(mobile): open guest-access pushes in the owner review; Kiingo Buzz name and icon #196's channel-less guest target (46040/46042 to the owner review) is merged with upstream'sthreadRootIDreply routing. The payload accepts either a channel target with an optionalthread_root_idor a guest target withguest_kind, never both.dev_push_lease.dart. Auto-merged onto upstream'sbuzz-push-v1descriptor with tolerant NIP-11 parsing (fix(mobile): tolerate unrelated NIP-11 relay metadata block/buzz#8160/refactor(push): one app profile per gateway block/buzz#8206). It keeps the fork'spushKindsopt-in for 46040/46042.thread_detail_page/message_list.dart. Upstream's restructured list with the fork'sAgentStatusRowbranch re-inserted.activity_page.dart. Import union.dev_push_lease_test.dartis also a union.lucide_icons_flutter. The Kiingo guest-approval sheets and feat(mobile): create and manage hosted agents from the phone #198's hosted-agent pages now useBuzzIcons(Tabler). Glyphs thatBuzzIconsdoesn't name live in fork-ownedguest_access_icons.dartandhosted_agent_icons.dart, with codepoints verified against the bundledTablerIcons.ttfglyph names.CI, hooks, misc (deltas 0, 7, 9, 19)
ci.yml. Upstream split CI into_ci-{rust,desktop,desktop-macos,relay,clients,security}.yml(ci: split CI into reusable workflows block/buzz#7168). The Kiingo Spot-only, test-free policy is re-applied inci.yml,_ci-rust.yml,_ci-desktop.yml,_ci-clients.ymland_ci-security.yml._ci-relay.ymland_ci-desktop-macos.ymlare left untouched; their domains are skipped on Kiingo/buzz. The re-applied policy:runs-on, plus Windows Spot with the custody step;minio-image.yml. This new upstream workflow runs on a hosted runner and smoke-tests. It is skipped on Kiingo/buzz.docker.yml. Forkruns-onplus upstream'sneeds: [push-gateway-build, qualify].lefthook.yml. The fork's test-free static hooks, wrapped in upstream'swithout-git-env.sh, with upstream's extended globs.Justfile. Upstream's create-if-missing sidecar stub with the fork's${EXE}.docker-compose.yml. Upstream Silo (Use Silo for bundled object storage block/buzz#8023); D0 is dropped as upstream-present.Cargo.lock. Upstream versions win for shared packages: ed25519-dalek 3.0.0, iroh 1.0.3. Fork-only crates are kept.buzz-core,buzz-acpandbuzz-dbgo from=3.0.0-rc.0to=3.0.0, matching upstream chore(mesh): upgrade MeshLLM to 0.76.2 block/buzz#7559 (main's decision). The rc.0 → 3.0.0 API is verified only bycargo check/clippy --all-targetsover the fork's signing, verifying and key-parsing call sites.Needs C review (push gateway, chart, docs)
These are best-effort resolutions. The tree builds, but C should finalize them. They are mostly in commit
0333f34c, which can be reviewed on its own. C's #199 (Xcode 27 upstream picksb270437a,46159fcf,73962435) becomes upstream-present after this sync; once main merges it, main gets merged into this branch again.crates/buzz-push-gateway/src/config.rs. Upstream's singleApplicationConfig, withapns_auth: ApnsAuthConfigreplacingapns_cert_path.parse_applicationcallsapns_token::parse_auth. The fork'sBUZZ_PUSH_PUBLIC_DELIVERY_URLhost relaxation is dropped, because feat(push): support configurable HTTP(S) delivery URLs block/buzz#7877 moved the delivery URL to the relay (BUZZ_PUSH_GATEWAY_DELIVERY_URL) and the gateway no longer reads it. The fork's delivery-URL tests went with it.crates/buzz-push-gateway/src/main.rs. The certificate/token transport match is re-applied on upstream's application runtime, next toAppAttestVerifier::with_environment.crates/buzz-push-gateway/src/apns_token.rs(no conflict). The env vars are renamed fromBUZZ_PUSH_DOGFOOD_APNS_{AUTH,CERT_PATH,KEY_PATH,KEY_ID,TEAM_ID}toBUZZ_PUSH_APNS_*.deploy/charts/buzz-push-gateway/templates/deployment.yaml. The token branch is now underapplication.apnsAuth/application.apnsToken: env vars, plus theauth-key.p8mount on upstream'sapnsvolume. The fork'sint64render fix is dropped as a duplicate of fix(mobile-infra): render push grant lifetimes as decimal in chart 0.3.2 block/buzz#7820.values.yaml/values.schema.json.application.apnsAuth: certificateby default.apnsTokenhaskeyId/teamIdmatching^([A-Z0-9]{10})?$. The schema uses if/then/else, so certificate requiresapnsCertand token requiresapnsToken.publicDeliveryUrlis gone, as upstream removed it.deploy/charts/buzz-push-gateway/tests/render.sh. Upstream's script, plus the fork's token assertions under the new names. Not run: there is no helm here, and it is a test.docs/push-gateway-deployment.md. Upstream's env table with the token rows, and the token section withapplication.*chart keys. Relay migration numbers are shifted to the fork numbering.handlers/push_lease.rsPUSH_KINDSand the new parity test;store/event_follow_up.rsPUSH_MATCH_KINDS;store/push.rs, now upstream's version;handlers/event.rs;dev_push_lease_test.dartunion.infrastructure/buzz/kubernetes/push-gateway-values.yamlstill usesprofiles.dogfood.*andpublicDeliveryUrl. The upstream chart ignores those and falls back to the defaultTEAMID.com.example.buzzapp id, so every enrollment would fail attestation. Move them toapplication.*before the cutover deploy. The relay needsBUZZ_PUSH_GATEWAY_DELIVERY_URLwhen push is enabled.Risk summary
This is adapted from the read-only risk analysis, with an addendum for this branch.
Per migration (46–70 apply in production; 41–45 already applied)
app_profile <> 'buzz-ios-dogfood', then narrows the CHECKpush_gateway_installationsrelay_admin_actionsaudit_log.hash_version NOT NULL DEFAULT 1 CHECKaudit_logscan with no lock_timeout; seconds at this sizeidx_thread_metadata_window, withlock_timeout 1sandstatement_timeout 5sCONCURRENTLY(runbook)lock_timeout 5s; tiny tablesPUSH_MATCH_KINDScarries thempush_leasesrow or relay-DB gateway installation exists; dropsapp_profilepush_leases(small); retire leases firstchannels.labelswith CHECKchannelsscan with no lock_timeout; sub-secondchannel_typegainssystemeventsand every partition,lock_timeout 5s; catalog-only holdprofile_name_fields()Production runs migrations as a separate step. Kiingo prod has
migrate.autoMigrate: false.deploy.shruns thebuzz-database-migrateJob (buzz-admin migrate, new image) before the gateway and relay rollouts. Usedeploy-production, neverdeploy-relay, which runs no migrations.Relay behaviour changes for existing clients
These assume NIP-FI stays off and
BUZZ_V1_ENABLED/BUZZ_OPERATOR_LISTENERSstay unset.OK falsefor EVENT,CLOSEDfor REQ/COUNT. Ephemeral kinds no longer count toward the message quota. Query deadlines are terminalCLOSED. Tag pushdown matches by position.#hmembership is checked in one query, with the cap raised to 2048. All three clients already handleCLOSED.invalid:. Deletion denial text changed; no client parses it.ttype first, then labels andPcreator. Desktop and mobile read the firstt. Labels andsystemchannels are new. System channels are excluded from every read that doesn't name them, and 39002#p:[me]doesn't list them. Identity rotation would therefore skip system-channel memberships once those exist./_readinessis process-local and the dependency report is on/_status. The fork's transport continuity moved there. Azure probes lose the shared-dependency signal, by design (Make relay readiness process-local block/buzz#7341).buzz-push-v1(nonip-pl, noapp_profiles) and always emitsartifacts. The current TestFlight build stops registering for push against this relay. Chat is unaffected.Desktop compatibility
0.5.22-kiingo.x stays compatible, and no desktop release is needed for the cutover. It does degrade in three places:
Rebuild the harness and hosted images soon after the cutover. The old
buzz-acpand CLI read the lastttag, which misreads labeled channels.NIP-PL / push / auth / membership
buzz-push-v1(refactor(push): one app profile per gateway block/buzz#8206). Every enrollment made before the sync is discarded, and the gateway needs a fresh database: its bootstrap guard refuses a populated pre-v5 DB. Before 0065 can run, push must be disabled on the relay and every activepush_leasesrow retired.RESULT_GATED_KINDS), and push-eligible throughPUSH_MATCH_KINDS. They are untested at runtime and belong on Spot/E2E.payloadtag, when present, must be 64 lowercase hex characters; all clients already comply. NIP-OA time bounds use the signed timestamp.Decisions / open questions (conservative choices, keeping Kiingo behaviour)
Migration renumbering follows main's decision (renumber upstream). The future rule is next-free-number, documented in the maintenance doc.
Budget amendment
upstream-sync-97e1f6a0.The business-logic budget stays zero. This needs Ross's explicit policy approval with the PR; the maintenance doc requires it.
Transport continuity is reported in
/_status, not readiness. Putting a shared Redis dependency back into readiness would contradict Make relay readiness process-local block/buzz#7341.Owner
!cancelalso stops guest lanes in the same thread or conversation, keeping the fork rule that owner authority wins. A member's!cancelonly stops turns triggered by that member's own events.ACP terminal failures, including model-not-found, are reported as agent-status events (40098), not chat. This replaces upstream's chat notice, and upstream's five notice tests were rewritten.
D8 onboarding. Only the surviving identity routes to
continueWithExistingIdentity; any other selected key follows upstream's flow. The desktop owner should confirm. The F8 onboarding e2e coverage needs a new spec.Upstream Postgres tests that expect cache hits for the restriction and serving caches (
ephemeral_gates_*, relay_admin restriction cache) will fail on Spot while transport continuity is false in fixtures. This is the same class of failure as the fork's existing membership-cache bypass. They need a buzz-pubsub test hook; they are test-only.Upstream thread windows (39007) don't include agent status (40098). Left as upstream.
buzz-deletion's drain still builds S3-onlyMediaStorage::new, as before. The storage-accounting cronjob in the Kiingo overlay needs the Azure storage env vars and workload identity.Branch name keeps the first pinned upstream SHA (
60de1db0). The merged and snapshotted upstream is97e1f6a0.Checks (static only; no tests were executed on the Mac)
All heavy commands were serialized one at a time, with a load gate.
cargo fmt --all -- --check; desktopcargo fmt -- --checkcargo check --workspace --all-targets;cargo clippy --workspace --all-targets -- -D warningscargo check/clippy -D warnings --all-targets(--manifest-path desktop/src-tauri/Cargo.toml)pnpm install --frozen-lockfile,tsc --noEmit,biome check .flutter pub get,dart format --set-exit-if-changed .,flutter analyzeswift build --build-testsfor BuzzPushKit (compile only)actionlintonci.yml,_ci-*.yml,docker.yml,minio-image.ymlnode scripts/check-kiingo-fork-boundary.mjs, including the migration ledgerCHECK_FILE_SIZES_BASE=$(git rev-parse origin/main) node {desktop,web,mobile}/scripts/check-file-sizes.mjsNot run, by policy: every test suite, including
render.sh, Postgres-backed migration tests and E2E. These run on Spot after deployment. Specifically recommended:e2e_guest_routing;CI status on Spot
The Kiingo public runner admission (
gha-runner-controllerpublic-buzz.cjs) refuses any job whose workflow file differs frommain's (public_workflow_not_current). This PR changes these workflows:ci.yml, plus the_ci-*.ymlfiles, which come from upstream;docker.yml,push-gateway-helm-chart.yml,sprig.yml,codex-security-review.yml,mesh-lifecycle.ymlandminio-image.yml.Their Spot jobs fail admission by design. Examples:
Docker image / Build (linux/amd64)andpush gateway helm chart / validate. That is not a code failure. Workflows that are unchanged against main do run:helm chart,CodeQL,Desktop Release Candidate, and the personal-dev gateway build all passed or are queued.Full CI signal on this tree will only exist once the workflow changes are on
main, or once main or Ross runs an admitted workflow at this revision. The static checks listed above were run locally instead.Cutover
~/Projects/buzz-upstream-sync-20261016-reports/cutover-runbook.mdis local to the operator. In order:T0andpg_dumponpsql-buzz-prod-wjymjvi3lf6gy.CONCURRENTLY.buzz_push_gatewayDB.deploy-production: migrate Job, then gateway, then relay rolling 2 pods one at a time.🤖 Generated with Claude Code