mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
b8b1ab5b3cac3692f4231a32679de8d2e7113b70
37 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fcdabcf774 |
docs(db): database target architecture and schema-change rules (#8273)
## Summary Adds `docs/database.md`, which covers two things: - **The target architecture.** `schema/tables/public/` is the declarative source of truth, written within the constraints that out-of-band, online schema tools can apply. Deployments at scale apply schema changes out of band; auto-migration stays supported for smaller and self-hosted deployments, and `migrations/` stays. The database has no triggers, functions, or foreign keys; that behavior lives in the owning application transaction or in durable application work. Data repair runs through the backfill framework in `docs/data-backfills.md`, not through migrations. - **Rules for schema changes made now:** 1. Add no triggers, functions, procedures, rules, or foreign keys, with no exceptions. New tenant-owned tables are fenced by community admission in the application. Startup and deletion validation already tolerate missing fences, but desired-state bootstraps still attach a fence trigger to every `community_id` table, so a new tenant-owned table is blocked until that loop stops (part of the fence-retirement work). The doc includes checklists for adding a tenant-owned table and a provenance-only table. 2. Migrations change schema only. They never mutate existing rows. Until the backfill framework ships, a repair may be a reviewed, idempotent, batched one-off script in `scripts/maintenance/` that takes the community deletion lock and touches only active communities. A repair that writes historical channel events must keep replica routing closed until its WAL has replayed. 3. Update every schema location: `migrations/`, `schema/tables/public/`, the manifest and sibling directories, the pgschema reconcile script where it applies, and both homes of the existing `push_gateway_*` tables (the target is for the gateway to own its tables alone). Never edit a merged migration. Add catalog-level parity tests for what you touch. 4. Every migration must be safe for the relay that is currently deployed, under both supported modes: auto-migration at startup and out-of-band application. Either way the old relay runs against the new schema for a while. Because auto-migration can apply expand and contract in one boot, a contract migration must fail closed when its backfill has not run, and a contract that depends on a writer release (such as dropping a trigger whose work moved into the app) must name that release as a required stop in the release notes, because no row check can prove it. The order is expand, deploy, backfill, then contract. The doc includes a table of changes that break the old relay and the safe form of each. 5. Bound every lock, build `migrations/` indexes concurrently (the declarative schema uses plain `CREATE INDEX`), and keep changes within what out-of-band apply can make online. AGENTS.md (Product Contract), ARCHITECTURE.md (buzz-db), and new path-local `migrations/AGENTS.md`, `schema/AGENTS.md` (a pointer), and `crates/buzz-push-gateway/migrations/AGENTS.md` point to the doc. The path-local files mean agents editing those directories get the rules automatically. AGENTS.md also had a stale claim that migrations are "auto-applied on relay startup". This PR corrects it: `BUZZ_AUTO_MIGRATE` defaults off in the binary and on in the Helm chart, and both modes are supported. ## What got simpler The rules were scattered across `schema/schema.sql` comments, AGENTS.md Common Gotcha #7, the backfill spec, `docs/thread-window-deployment.md`, and test names. They now live in one document, and the rest link to it. No code changes. ## Testing This PR changes documentation only. I checked links and diffs, and every claim in the doc points to code at the base commit: the layout and parity tests in `crates/buzz-db/src/runtime/migration.rs`, `EXPECTED_SCOPED_TABLES` and the serving-catalog check in `crates/buzz-db/src/store/deletion.rs`, and `BUZZ_AUTO_MIGRATE` in `crates/buzz-relay/src/main.rs`. The new `AGENTS.md` files aren't `.sql`, so neither the sqlx migrator nor the schema layout test picks them up; CI's layout, migration-count, and deletion-parity tests pass. At an earlier head, a real relay against isolated PostgreSQL 17 showed that a new unfenced `community_id` table serves normally and makes community deletion report `unknown=<table>`. Since then, the fence requirement at startup was removed on main; the doc now cites `migration_schema_fence_objects_are_optional_for_serving_and_deletion` for that. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
2745a419a8 |
buzz-db: tolerate absent universal write-fence objects (expand) (#8322)
## What This is the expand step for retiring the 32 universal `community_write_fence_*` triggers and their SQL functions (BUZZ-315). Relay pods auto-migrate at boot (`BUZZ_AUTO_MIGRATE`). The drop migration therefore runs while the previous release is still serving, and that release must not depend on the objects it drops. Today the app depends on them in four places, and this PR removes all four from its own code paths. Seven in-SQL `community_write_allowed` predicates in fleet sweeps and push (`push_enqueue.rs`, `channel.rs`, `event.rs`, `push.rs`, `relay_invite.rs`, `workflow.rs`) are #8318's scope. The release is independent of the fence functions only with both this PR and #8318. **It changes no schema.** The objects stay and keep working until the follow-up drop PR. | Old dependency | Now | |---|---| | Startup and readiness (`validate_serving_catalog_on`) require `community_write_allowed`, `assert_community_write_allowed`, `enforce_community_write_fence` and all 32 triggers | No longer required. Still required: the tenant tables, `community_deletion_lock_key` and the enabled community tombstone trigger. | | Serving-lease admission runs `SELECT assert_community_write_allowed($1)` | Uses `guard_community_write`, the same shared lock and state check as ordinary serving writes, in the same transaction as the lease `INSERT`. Fenced or missing communities still map to `AccessDenied`. | | Live scoped-table discovery calls `community_write_fence_excluded_table()` | Uses a Rust `EXCLUDED_SCOPED_TABLES` list. A unit test pins it equal to the SQL list while both exist. | | The deletion executor compares the live `fenced_tables` against the frozen inventory | Only tenant-table equality is compared. Inventories **still record** live fences, so an older executor that validates an inventory frozen by this release still matches. The digest still covers the full frozen manifest. | `lock_community_deletion_shared` now refuses transactions that are not READ COMMITTED, so every lock-then-read admission gets the check: `guard_community_write`, serving-lease finalization (`guard_transaction_with_serving_lease`), lease renewal/verification, owner preparation and owner-mutation admission. The old SQL assertion did this check for the lease path, and the trigger backed up the rest. The isolation level comes back from the existing `pg_advisory_xact_lock_shared` statement, so the hot path makes no extra round trip. The refusal comes before any lifecycle-state read. ## Mixed versions | Combination | Why it is safe | |---|---| | Previous release, this DB | No schema change, so all old checks still see the 32 fences. | | This release, DB with fence objects | Extra objects are tolerated, and the trigger backstop still applies. | | This release **plus #8318**, DB without fence objects | Serving, readiness, lease admission and deletion proven by `migration_schema_fence_objects_are_optional_for_serving_and_deletion`, which drops them inline. Without #8318, serving and deletion still work, but the push matcher, exhausted-job reaper, invite retention, ephemeral reaper and startup d_tag backfill fail with `42883 community_write_allowed(uuid) does not exist` (seen in deploy-stack E2E). | | Older executor, inventory frozen by this release | `fenced_tables` is still populated from the live catalog (32). | This PR does not depend on #8318 to merge and can ship alongside it, but it is not drop-safe alone. The drop PR (#8323) needs #8219, #8318 and this PR deployed on every pod, and every rollback image after the drop must include #8318. ## Tests - `store::deletion::postgres_tests::migration_schema_fence_objects_are_optional_for_serving_and_deletion` migrates to 0060. It then runs the following with the fence objects present, then again after dropping them inline: - serving startup - readiness - the deletion catalog check - `inventory_schema` - lease acquire/release - `AccessDenied` for quiescing, fenced, tombstoned and missing communities - `store::deletion::postgres_tests::deletion_lock_admission_rejects_pinned_snapshot_isolation` checks that REPEATABLE READ and SERIALIZABLE are refused by both `guard_community_write` and `guard_transaction_with_serving_lease`. The lease case uses a never-acquired lease, so if the isolation check is skipped it fails as stale instead. - `buzz-deletion` `postgres_tests::approved_legacy_fence_inventory_accepts_current_catalog` checks that a legacy digest with 32 fences validates against a live catalog with none, and that an unknown tenant table is still rejected. - `store::deletion::tests::scoped_table_exclusions_match_sql_during_transition` checks exclusion-list parity. - `store::deletion::postgres_tests::serving_lease_acquire_waits_for_fence_lock_then_refuses` holds the exclusive deletion lock and checks that lease acquisition stays pending. It then commits `fenced` and requires `AccessDenied` with no lease row persisted. Removing the lease path's `guard_community_write` call fails this test, while the other four lease/admission tests above still pass. - `buzz-deletion` `serving_guard_heartbeats_long_operation_through_quiescing` now requires `AccessDenied` for a lease acquired during quiescing, rather than any error. Each test was confirmed to fail when its fix is removed (rc 100, 0/1): the isolation refusal, the three validators restored to the base version, the fence-list comparison restored, and one name dropped from the Rust list. ## What got simpler The lease path loses its special SQL assertion and its error-code translation and reuses the ordinary app guard. The serving and deletion catalog checks each lose one requirement. No new abstraction. The `fenced_tables` reader stays only for mixed-version executors; the drop PR deletes it. **Identified debt (pre-existing, outside this diff):** `runtime::postgres_tests::writer_pool_rejects_non_read_committed_database_default` hangs on clean `177551dda` too. ## Verification (Blox, tree identical to this commit) | Gate | Result | |---|---| | `cargo fmt --all -- --check` | pass | | `cargo clippy --workspace --all-targets -- -D warnings` | pass | | `cargo nextest run -p buzz-db --lib` | 167/167 | | `scripts/test-postgres-test-discovery.sh` | pass | | `scripts/postgres-test-run.sh -p` buzz-admin / audit / deletion / push-gateway / relay / search / workflow | all rc 0 (4, 7, 12, 11, 402, 20, 2) | | `scripts/postgres-test-run.sh -p buzz-db` | 510 pass, 1 fail: the pre-existing hang above, interrupted | | Round 2 (this head): `-p buzz-db` / `-p buzz-deletion` | 511 pass + the same pre-existing hang interrupted / 12/12 | The new PG tests are selected by the `postgres-ci` filter `(test(/postgres_tests::/) or binary(/^postgres_/)) and not test(/(^|::)external_infra[^:]*::/)`. buzz-review-completed --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
2d0f78c6e1 |
feat(auth): support explicit mesh JWKS transport (#8382)
## Summary Allow an issuer to opt into mesh HTTP for its JSON Web Key Set (JWKS). Public HTTPS remains the default. This supports deployments whose service mesh authenticates and protects the key endpoint. The sealed source contract selects transport and contributes to policy identity. Mesh mode requires a DNS hostname and checks every resolved address against an explicit allow-list before pinning one address. Both modes disable proxies, reject redirects, cap responses, and enforce a whole-fetch deadline. The relay cannot verify mesh security: operators must enforce mutual TLS, endpoint authorization, and egress restrictions. ### Related issue No duplicate mesh-JWKS issue or PR found. Existing NIP-FI relay enforcement: #7224. No deployment change is included. ### Testing - At `eccb8cb19420abda44cf6e5e62b1361e2843bd1d`: full auth package and auth/relay all-target clippy passed. Full relay package passed: 1,457 library tests, 37 binary tests, and 9 boot tests; 446 tests were ignored. - At prior head `113939838cd81d5e8d0b0f8783c42ffaae44a4d3`: the full relay library suite failed the demo echo test (1,455 passed, 1 failed, 440 ignored). Independent checks reproduced that timeout at base `d61fe2071d2b3d8e56dd931947f16bfe0d6173fe`; the receive-cancellation defect is outside this change. A later pass does not establish that defect is fixed. - Three falsifying mutations at `eccb8cb194` were caught: mixed DNS answers, proxy bypass, and whole-fetch deadline. The full locked auth package passed after restoring the exact source. - At `eccb8cb194`, `cargo build --locked --release -p buzz-relay -p buzz-cli -p buzz-admin` passed with command-local `CARGO_PROFILE_RELEASE_STRIP=none`. This avoids a macOS loader error without a source, lockfile, or project-profile change. - Earlier hooks failed the ACP signal test and Tauri login-shell PATH test. The ACP failure also reproduced on unmodified main `93ffc37c`. Main merged without conflict. The full Tauri package then passed with `RUST_TEST_THREADS=1`: 3,422 library tests and 3 integration tests, 19 ignored. All pre-push hooks passed with that same command-local setting; no hook or test was skipped. - At `eccb8cb194`, isolated `just test` passed all 107 runner steps. The monolithic `just ci` run timed out after 1,120 seconds; all recipes passed across that run and separate `just desktop-tauri-test`, `just web-build`, and `just mobile-test` invocations. No monolithic pass is claimed. - Exact-head release live-local checks passed: loopback HTTP mesh JWKS warm; valid assertion 101; expired/wrong issuer/wrong audience 403; valid signed disconnect 200; wrong command audience 403. Redirect-only JWKS returned assertion 503 without a request to its target. Both omitted/default and explicit public transport rejected HTTP at startup. - A separate NIP-FI-off release CLI smoke test created an open channel, sent an accepted message, read it back, and fetched its thread. The assertion probe closes after upgrade; it does not prove a complete NIP-42 session. Local loopback tests do not prove deployment mesh authentication, endpoint authorization, egress controls, or real `240/4` routing. Generated with Codex ## Human Testing valid assertion / configured community audience: HTTP 101 (expected 101) expired assertion: HTTP 403 (expected 403) wrong issuer: HTTP 403 (expected 403) wrong community audience: HTTP 403 (expected 403) valid command JWT / configured command audience: HTTP 200, body=b'{"disconnected": true}' (expected 200) valid command signature / wrong audience: HTTP 403 (expected 403) --------- Signed-off-by: seiler <seiler@squareup.com> Signed-off-by: Local Builder <1c476cb1127ea270cb900bdc3891060140bdd1173f91498030ee9b4db01c2f20@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Local Builder <1c476cb1127ea270cb900bdc3891060140bdd1173f91498030ee9b4db01c2f20@buzz.block.builderlab.xyz> |
||
|
|
71d7aa0436 |
feat(relay): add the system channel type (#8180)
🤖 ## Summary Buzz uses channel membership to decide who can read what, and every channel a person belongs to shows up in their client as a chat. So there is no way to make a group that exists only for access control, for example "only this agent and its owner can read the agent's config". Such a channel would appear in both people's sidebars, search results and unread counts. This stack adds a **system channel**: an ordinary channel that queries leave out unless they ask for it. This first PR adds the type itself. - `kind:9007` (create channel) with `["channel_type", "system"]` creates a system channel. - It is private unless the creator sends `["visibility", "open"]`. - While it is private, its channel metadata event (kind:39000) carries the NIP-29 `hidden` marker, which means "hide this group's metadata from non-members". Until now the relay set `hidden` only for DMs. On its own this PR does not yet hide system channels from queries. Part 2 does that. Part 1 of 3, stacked on #8176 (the channel labels and creator lookup stack, which this feature uses). ### Related issue None found. ### Details - Migration 0062 adds `system` to the `channel_type` Postgres enum. No new columns. - `buzz_core::channel::ChannelType::System`, and `system` is now a reserved label name (labels cannot equal a channel type name). - `buzz-admin` reconcile writes `hidden` with the same rule as the relay. Usage metrics count system channels. - **Rollback.** A relay older than this change cannot read `channel_type = 'system'`. Do not roll back past it while system channels exist. - **Spec.** NIP-CL gets a "System channels" section. `ARCHITECTURE.md` lists the `System` type. ### Testing - Relay test: a system channel is private and `hidden` by default; opening it removes `hidden`; its first `t` tag is `system`. - Migration embed and usage-metrics tests. ### Stack These are two stacks. Merge them in this order. Client support (creating, labeling and finding these channels) will be in block/buzz-app, not the buzz SDK and CLI. **Stack 1: channel labels and creator lookup (#8174–#8176)** 1. #8174 channel type and creator tags, and the channel creation time 2. #8175 channel labels 3. #8176 database-side label and creator match, and the rule for choosing one channel **Stack 2: system channels (#8180–#8182)**, stacked on stack 1 because it uses labels and the creator tag 4. #8180 the system channel type ← this PR 5. #8181 leave system channels out of reads that do not name them 6. #8182 channel-state filters opt in to system channels --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
d25384a4f3 |
buzz-db: admit every fenced-table writer in app code (#8219)
## Summary This makes every serving writer of a community-write-fenced table take community admission in application code. That's the "Dual enforcement" step of the Supported Writer Contract in `ARCHITECTURE.md`. Admission means the shared community deletion lock plus the active/not-deleted check from `deletion::guard_community_write`. It's taken before the transaction's first tenant write or row lock and held until commit. The 32 `community_write_fence_*` triggers, the six fence SQL functions and `validate_serving_catalog` are unchanged. They stay authoritative. Removing them is a separate migration that will be checked table by table. ## Changes - **New admission entry point.** `begin_community_write_transaction` (`runtime/mod.rs`) builds an `AdmittedTx` for non-event writers. It sits next to the existing event-write entry points and shares their implementation. - **Writers moved onto it:** - `api_tokens`, `archived_identities`, `channels`, `channel_members` (including DM and admin paths), `community_bans`, `git_repo_names` - `moderation_actions` and `moderation_reports`, `personal_read_accounts` and `personal_read_frontiers`, `pubkey_allowlist` - `push_leases`, `push_match_queue`, `push_wake_outbox` - `relay_invites`, `relay_members` (including the new-community owner bootstrap and the startup `pubkey_allowlist` backfill), `join_policy_acceptances` - `thread_metadata`, `users`, `workflows`, `workflow_runs`, `workflow_approvals`, `scheduled_workflow_fires` - Personal-read helpers now require `&mut AdmittedTx`. - **`buzz-audit`.** An append now passes admission first, then takes a transaction-scoped audit-chain lock (`pg_advisory_xact_lock`). This replaces the session lock and the `catch_unwind` unlock cleanup. The relay audit worker retries a `lock_timeout` on either lock and keeps the entry; an admission refusal is terminal, as the trigger's refusal was. - **Fleet-wide statements.** These keep `community_write_allowed(...)` in the same SQL statement, because there's no single community to admit up front: - scheduled-fire prune - invite expiry - push matcher claim and exhausted-job cleanup - startup `d_tag` backfill - channel TTL cleanup - **Relay-member removal.** `remove_relay_member` and `remove_relay_member_if_role` run `delete_member_with_acceptances` inside their admitted transaction. The helper no longer opens its own transaction, so the member lock, the `join_policy_acceptances` delete and the member delete all hold admission until commit. `delete_workflow` and `delete_workflow_for_owner` are gone on `main`, and the coordinate deletion path they used to back was already admitted. - **Unchanged exceptions.** The deletion-executor generation exception and the serving-lease path work as before. - **Tables with no production writer.** `delivery_log` and `subscriptions` are only written by a historical cutover script and a test fixture. Their triggers stay. - **No cross-community moves.** No production `community_id` move exists on a fenced table. The only `SET community_id = NULL` is the deletion-executor purge. So no dual-community lock ordering was needed. ## What got simpler - Per-store ad-hoc writer helpers are gone: `begin_event_write_transaction` and `acquire_event_write_connection` in `channel.rs`, and `begin_operation_transaction` in `push.rs`. - Audit append drops the session advisory unlock, the `catch_unwind`/`resume_unwind` panic handling, `log_inner` and the `futures-util` dependency. ## Notes for reviewers - **New crate dependency.** `buzz-audit` now depends on `buzz-db` only to call the free function `buzz_db::deletion::guard_community_write`. `ARCHITECTURE.md` names this as the one sanctioned subsystem edge, and its audit section is updated to match. - **Hot paths.** `ensure_user` first checks whether the user exists and returns without admission when it does (no write happens). Only a real insert takes admission. `touch_api_token` has no production caller today. - **No-op writers stay read-only.** The startup allowlist backfill returns early on plain reads when the legacy table is gone, it has no rows for the community, or the community already has members, so a quiescing deployment community still starts. It re-checks under admission before inserting. - **Owner mutations.** When admission refuses a transfer or a provisioning and a deletion request exists, both report `DeletionPending`, as before. For a transfer, the operator API keeps its coded `deletion_lifecycle_conflict` 409. For provisioning, it keeps the existing uncoded 409, `owner_conflict: community deletion is pending`. - **Per-community worker loops.** `claim_due_push_wakes` and similar now return `AccessDenied` for a community that goes quiescing between the active-host scan and the claim. The worker logs it at `debug` and the next scan skips the community. - **Kick side effects.** `membership_removal_fence` admits before the membership lock to keep the lock order of `add_member`. A community that leaves `active` between the kick and the fence skips live subscription eviction; the membership cache is already invalidated and fan-out re-checks membership. This is documented on the function. ## Testing - **New refusal tests.** 19 tests: the 18 in `store::admission_postgres_tests`, plus `audit_append_requires_application_admission_with_triggers_bypassed`. Each runs with ordinary triggers disabled (`session_replication_role = replica`), checks that a raw fenced insert would succeed, and asserts that the writer is refused for both `quiescing` and `fenced` communities. Active writes still succeed. - **The tests catch a missing check.** Measured in review at `6fc073c3f`: with the guard in `AdmittedTx::admit` bypassed, 25 tests fail. That includes 16 of the 18 database admission tests. The audit test stays green there, because audit calls `guard_community_write` directly. - **Affected PG suites, with triggers on:** 183/183 passed across 16 store suites, push 18/18 run isolated, audit 7/7. The existing artifact, reaction and replaceable admission tests pass. - **Review fixes, each checked to fail without its fix:** - `allowlist_backfill_requires_application_admission` and `owner_mutations_report_pending_deletion_when_admission_refuses` fail with the old `relay_members.rs`. - `empty_allowlist_backfill_skips_admission` fails (`AccessDenied … write-fenced (quiescing)`) without the allowlist-rows early return. - `audit_worker_retries_admission_lock_timeout_until_original_entry_is_appended_once` holds the community deletion lock exclusive and fails without the admission retry arm. - The existing audit-chain retry test now looks for the transaction-scoped lock and passes. - The two owner-revoke mini-schema tests (`bridge.rs`, `ingest.rs`) now include `communities` and `community_deletion_lock_key`, which admission reads; both failed without that. - **Static checks:** `cargo fmt --check` and workspace `cargo clippy --all-targets -D warnings` pass. - **Not run locally:** the `mobile-check` step of `just ci` (the build machine couldn't reach pub.dev, and clean base fails the same way) and the full `just test` lane. CI on this PR covers both. `mesh_demo::demo_join_forwarded_arm_round_trips_echo` fails on the build machine at base `394d4a434` as well; it uses no database. Refs BUZZ-314. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
1d9f2c81a5 |
feat(relay): accept an owner's presence for its agents (kind 20003) (#8261)
## Summary App-hosted agents (buzz-app agents2) run inside the desktop app without a socket per agent, so they can't send their own kind 20001 presence and always read as offline — including to their owner on another device. This adds **kind 20003, an owner's presence for its agents**: - Owner-signed; `content` is the status (`online` / `away` / `offline`); each `p` tag names an agent (1–256 distinct). - WebSocket only (rejected on HTTP ingest, like 20001). - The relay keeps only the agents the signer owns (`users.agent_owner_pubkey`) — cached `true` from `observer_owner_cache`, the rest resolved with one `list_agents_for_owner` query. Only positive links are cached, so a just-linked agent isn't hidden for the cache TTL and the observer path isn't poisoned. - Sets (180s TTL) or clears each agent's presence in one Redis round trip, the same keys kind 20001 writes, so the presence snapshot reports them with no client change. - Not fanned out: live presence is matched by author, and the author here is the owner. A new kind rather than `p` tags on 20001, so a relay without this ignores the batch instead of storing it as the owner's own presence. ## Testing - `agent_presence_request_names_distinct_agents_with_a_known_status` (unit). - `presence_storage_postgres_tests::agent_presence_stores_only_owned_agents` (Postgres + Redis): only the owner's agent goes online, another owner's agent doesn't, a pre-cached `false` doesn't hide a linked agent, and `offline` clears it. All 5 presence Postgres tests pass via `scripts/postgres-test-run.sh -p buzz-relay --lib -E 'test(/presence/)'`. - `buzz-pubsub` ignored Redis test `test_presence_many_sets_with_ttl_and_clears` passes. - clippy clean. Plain `cargo test` failures locally were environmental only (buzz-media "role buzz does not exist", mesh_demo 504). - Not yet exercised end to end against a local relay + app. App side: block/buzz-app#862. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6c388bde62 |
feat(relay): accept typing indicators over HTTP POST /events (#8207)
## Summary Agents hosted inside the Buzz app sign their own events but don't hold a WebSocket, so they couldn't show "typing…" in a channel. `POST /events` rejected kind 20002 (typing indicator) with `restricted: unknown event kind`. Now, when `POST /events` receives a kind 20002 event, the bridge passes it to the same channel broadcast the WebSocket path uses. Live subscribers in the channel get the pulse. Nothing is written to Postgres. Before broadcasting, the relay checks, in order: - the event has an `h` tag naming the channel - the serialized event is at most 2 KiB (real typing events are ~600 bytes). Measuring the whole JSON, not just content and tag values, means piling on empty tags can't get around the cap - the signature and id are valid - bans and timeouts - the community write fence (over HTTP, a quiescing community's host already fails to bind with a 404, so this catches quiescing that starts after the tenant is bound) - the sender is a channel member, or the channel is open (same rule as WebSocket) - the signer is the pubkey that authenticated the request (checked first; a mismatch returns 403) The existing HTTP admission rate limit and relay-membership gate still run before any of this. Other ephemeral kinds are still rejected over HTTP. `publish_channel_ephemeral` is the old WebSocket channel-broadcast code moved into a shared helper, so both paths use the same membership check and fan-out. Unlike the WebSocket path, HTTP requires the `h` tag. Over WebSocket, a 20002 with no channel goes to every subscriber. Points left for review: - HTTP typing pulses count against the same per-pubkey HTTP quota as message posts (default 300/min). One pulse every 3 s is 20/min per active channel. - A failed Redis publish is logged and the POST still returns `accepted: true`, same as the WebSocket path. A pulse is resent every 3 s and expires after 8 s, so a retry would only resend a stale pulse. The `sadscan:disable` comment on an existing test URL in `ingest.rs` is there because the secret scanner reads every file a commit touches. It uses the same form as the other local test fixtures in the repo. ### Related issue Needed for Agents2 typing indicators in buzz-app. The client PR depends on this one and will follow as a draft. ### Testing - 9 new Postgres-backed tests in `api/typing_postgres_tests.rs` cover these cases: a member's pulse is published and not stored; a non-member gets a 400; a timed-out member gets a 403; a signer mismatch gets a 403; a missing `h` tag gets a 400; a tampered event gets a 400; kind 20001 is still rejected; oversized events get a 400 (2 KiB of content, one 2 KiB tag value, or `h` + 10k empty tags); a community that starts quiescing after the tenant is bound gets a 400 `community writes are fenced`, and nothing is published. Two mutation checks: with the fence removed, the fence test fails; with the old content-plus-tag-values cap, the empty-tags case is accepted and the test fails. - The full buzz-relay Postgres lane on |
||
|
|
a8fe2b612c |
feat(db): advance an empty partition catch-all under bounded locks (#7993)
## Summary Adds bounded, serialized advancement of an empty right-edge partition catch-all for `events` and `delivery_log`. The repaired catalogs currently end in an empty catch-all from 2027-01-01, and the merged manager (#6515) only audits them. Without this change, January writes land in the catch-all, after which only an operator can move them. When `BUZZ_PARTITION_MANAGER_ADVANCE_ENABLED=true`, each relay keeps six dedicated monthlies after the current month. It does this at startup and on every audit interval by replacing the empty catch-all with canonical monthlies and a new canonical `<table>_p_future` after the horizon. From today's catalogs the first run creates January–March 2027 and moves the catch-all to April 2027. ## Safety design - **Refuse before locking.** The following return `operator_required` without taking any table lock: - a populated catch-all, or one whose occupancy is unknown; - a catch-all with child-only triggers, which the replacements would not inherit; - a misaligned bound; - a `DEFAULT`, anomalous or pending-detach child; - a name collision. - **One winner.** A schema-scoped `pg_try_advisory_xact_lock` is taken before any DDL. Losers report `skipped_locked` and never issue DDL. The winner re-plans from a fresh audit before taking any table lock and checks for collisions. A runner that lost a race therefore sees the committed layout as a no-op, not as colliding names. - **Writer lock order.** First `ONLY` the parent is locked `ACCESS EXCLUSIVE` under a 2 s `lock_timeout`. Then each foreign-key counterpart (today only `communities`) is locked in OID order `SHARE ROW EXCLUSIVE NOWAIT`, then the catch-all `NOWAIT`. - The counterparts are read again once the parent is locked, and that set is the one locked. Any FK DDL on or to the parent conflicts with the parent lock, so the set can't change after that. An FK committed while maintenance waited is therefore locked `NOWAIT` too, so attaching never waits on it under the parent lock. The read before the lock remains as a cheap early refusal. - This is the order an insert takes locks (parent, then the FK check on `communities`), so maintenance cannot deadlock with ingest. The internal manual runbook used to lock `communities` first; it now uses this order too. - `SHARE ROW EXCLUSIVE` is the strongest lock the DDL takes on `communities`; measured on PG 16 and 17, the catch-all `DROP` takes none, as long as the catch-all's only foreign keys are those inherited from the parent. A catch-all with its own foreign key, or one that another table's foreign key references, is refused: once before any lock, and again after the catch-all lock, where the check is authoritative. Readers and other writers' `FOR KEY SHARE` checks proceed. A concurrent `communities` writer makes the run fail fast with `lock_timeout`. - A `40P01` deadlock victim is also reported as `lock_timeout`. - Parents that are referenced by a foreign key, or that reference an inherited table, are refused, including when such a key commits while maintenance waits on the parent. - **Re-plan under lock.** The catalog is re-audited under those locks. The transaction proceeds only if the plan is unchanged. - **Bounded.** `statement_timeout` 5 s, `idle_in_transaction_session_timeout` 5 s, and a 10 s client deadline per table. A deadline-abandoned connection is closed, never returned to the pool, and the server rolls back anything uncommitted. If the deadline fires after `COMMIT` was sent, the change may have landed; the next audit reports the truth. The orphaned backend keeps its locks until its in-flight statement ends, at most `statement_timeout`. - **Verify before commit.** The changed catalog is re-audited inside the transaction for bounds, canonical kinds, trigger parity and catch-all emptiness, and serving safety must not regress. It also checks that every new child has valid, ready clones of each parent index and validated clones of each PK/unique/FK constraint. - **Audits tolerate a concurrent advance.** Catalog rows come from the statement snapshot, but `pg_get_expr` renders from current catalog state. So an audit that overlaps another relay's `DROP` of a catch-all can see a partition that no longer exists. The read-only audit detects this (a NULL bound, or `42P01` from the emptiness probe) and retries up to twice with a fresh snapshot. - **Fail fast on audit probes.** If another relay's audit is probing the catch-all's emptiness, the catch-all `NOWAIT` lock fails and the run reports `lock_timeout`. The parent lock is then released immediately, and the next runner or interval completes the advance. Uncovered-month creation (`BUZZ_PARTITION_MANAGER_CREATE_ENABLED`, default `true`) now runs through the same locked transaction, replacing the unlocked per-month `CREATE`. That flag remains the kill switch for all automatic partition DDL: with it off, advancement is disabled too. With advancement disabled (the default) the periodic loop stays read-only and startup behaves as before, apart from the locking. A runner that planned DDL always re-audits before returning, even when a peer did the work, so it never reports the pre-maintenance layout. A serialized re-plan that finds only disabled work reports `skipped_disabled`. ## Behavior changes - The relay audit horizon and the `buzz-admin partition-audit` default are now 6 months (`PARTITION_MANAGER_MONTHS_AHEAD`), up from 3. Until advancement is enabled, months already covered by the catch-all make `degraded` true. That correctly reports a runway under six months. - New metric `buzz_partition_maintenance_runs_total{table,outcome}` with outcomes `noop | skipped_disabled | created | advanced | skipped_locked | operator_required | lock_timeout | deadline | error`. `buzz_partition_create_attempts_total` keeps its per-month `created | skipped_covered | error` labels. `error` there still means a DDL or collision failure; `lock_timeout` and `deadline` runs add nothing to it, and only `buzz_partition_maintenance_runs_total` records them. - `ensure_future_partitions` (free function and `Db` method) is removed; `maintain_partitions` is the one entry point. - New advisory-lock label `lock_type="partition_maintenance"`. ## Rollout Enable `BUZZ_PARTITION_MANAGER_ADVANCE_ENABLED` per environment in BPCI: `bb-block-staging` first, then production. The staging canary proves a real advance because the six-month horizon already extends past January. Fallback: rerun the internal manual runbook, which uses the same parent-first lock order as of 2026-10-05. It keeps `ACCESS EXCLUSIVE` on the counterparts, because its detach/attach can drop FK triggers on them. ## Testing - `cargo test -p buzz-db --lib partition`: 17 unit tests. The 10 new ones cover: - plans: repaired layout, policy gating, no-op, refusal states including child-only triggers, December→January; - the create kill switch also stopping advancement; - the month cap at exactly 120; - per-month metric outcomes for transient failures versus DDL failures; - the mid-audit-drop retry classifier. - `cargo test -p buzz-db --lib partition -- --ignored` (PostgreSQL 17): 48/48 on 9 consecutive runs, including 17 new tests: - advance to the canonical layout, with routing and idempotent rerun; - replacing a canonically named catch-all in one transaction; - populated catch-all refused in under 1 s while the parent is locked; - a `communities` writer making the run fail fast and roll back; - a `communities` reader not blocking the advance; - an in-flight insert's FK check, and its write to `communities`, behind queued maintenance completing without a deadlock; - a foreign key committed while maintenance waits on the parent, with a writer on its new counterpart, failing fast as `lock_timeout` on that table instead of waiting under the parent lock; - a foreign key referencing `events`, committed while maintenance waits on the parent, being refused under the parent lock; - a catch-all's own foreign key, or a foreign key referencing it, being refused before the parent lock with the key intact, and a catch-all key committed while maintenance waits on the parent being refused after the catch-all lock; - a `40P01` deadlock victim being classified as `lock_timeout`; - parent contention timing out without blocking the other table; - held advisory lock skipping with no DDL; - concurrent runners converging, with at most a transient `lock_timeout`; - a stale audit after a concurrent advance being a no-op, and the losing runner returning the committed layout; - a re-plan that finds only disabled work reporting `skipped_disabled`; - an audit retrying when a listed partition is dropped mid-audit; - a catch-all with a child-only trigger being refused; - client deadline abandoning the attempt and closing the session (scoped to the test's own blocked backend); - catch-all name collision; - incoming foreign key refused. - Mutation checks: - restoring the old counterpart-first `ACCESS EXCLUSIVE` order fails the reader test (`lock_timeout`) and the writer test (a ~1 s deadlock wait); - dropping `NOWAIT` from the counterpart lock, or allowing a populated catch-all, fails the matching tests; - removing the parent-locked counterpart re-read fails the committed-FK tests (the parent is held 2.06 s waiting on the new counterpart), and ignoring its refusal fails the referencing-FK test; - locking the counterparts `SHARE ROW EXCLUSIVE` before the parent fails the writer test; - removing either catch-all foreign-key check fails its test. - Full PostgreSQL lane: 716/718. Both failures are timing-sensitive tests outside partition code, and each passes 3/3 when run alone: - `hanging_redis_peer_times_out_before_the_health_listener_binds`; - `readiness_check_cancellation_balances_waiter_and_inflight_connection`. - `cargo clippy -p buzz-db -p buzz-relay -p buzz-admin --all-targets -D warnings` and `cargo fmt --check` are clean. ## Related Refs #2396. This PR conflicts textually with #7994 (partition audit observability follow-ups) in `crates/buzz-db/src/store/partition.rs` and `crates/buzz-relay/src/main.rs`. Whichever PR merges second will be rebased onto the other. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Ian Oberst <ioberst@block.xyz> Co-authored-by: Ian Oberst <ioberst@block.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d742fc5f30 | Use Silo for bundled object storage (#8023) | ||
|
|
fe9e2409a0 |
BUZZ-175: Add application-owned writer lock foundations (#7706)
## Summary Buzz is moving database invariants from triggers and foreign keys into application-owned transaction protocols. This PR adds the first transition foundations while preserving all current database backstops. - Add a shared community-write lock for single-community writer transactions, and make deletion lifecycle transitions take the matching exclusive lock. A supported serving transaction never spans communities. - Add shared event-writer and exclusive replica-probe lock ordering around the existing heartbeat publication model. - Document the supported writer contract, reviewed backfill path, and transition gates. - Keep one replica-floor publication concept and remove an unrelated reconciliation-script edit found during review. This narrows the change rather than adding parallel protocols. The blast radius is limited to new opt-in writer helpers plus lock acquisition in deletion and replica probing. Existing trigger and GUC enforcement remains authoritative, so callers can migrate in later pull requests. ### Related issue [BUZZ-175](https://linear.app/squareup/issue/BUZZ-175). This is the first implementation slice from the OSC migration plan. ### Testing No manual testing. CI covers the Rust checks and database tests added here. Generated with [Codex](https://openai.com/codex) --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
53a12100b3 |
fix(deletion): require sole owner at admission (#7966)
Follow-up to #7830: reject owner-origin deletion admission for a legacy co-owned community before creating a request. Under the existing community-row lock, admission locks all current owner memberships in pubkey order and requires exactly one matching asserted owner. Existing-request idempotent replay remains unchanged; completion retains its independent post-lock revalidation. **New optional operator field: `community_id`** (`ab0cd713`, `766eb07f`). Owner-origin `POST /operator/communities/delete` now accepts an optional `community_id` that binds the request to the host's community: - **Fresh submission:** checked after sole-owner authority is proven, so a non-owner still gets `404 community_not_found`. A mismatch returns `409 community_id_mismatch` and writes no request row. - **Replay:** checked against the stored request, and only when the stored request is for the same host. A mismatch returns `409 community_id_mismatch`, and the stored request is unchanged. A known UUID sent with a different host goes through the existing convergence check and returns `409 deletion_request_conflict`. - **Omitted:** behaves as before. `admit_owner_request` takes `expected_community_id: Option<Uuid>` rather than a second public entry point, and existing callers pass `None`. - `docs/operator-community-deletion.md` documents the ordering. Also folds in the non-blocking review nits from #7969 (wpfleger96, at `e21151f4`): - **`limit_reached` has a stable `code`.** Create (`provision_community`) and transfer-in now return `409` with `code: "limit_reached"`, matching the other coded lifecycle errors. The `error` message keeps its `limit_reached:` prefix, so clients that match on the message still work. The provision limit test now asserts the code, and a new PostgreSQL test covers a transfer to an owner at the limit (coded 409, no membership change). - **Docs: an ack-version mismatch is a 400, not a 409.** `docs/operator-community-deletion.md` and the `delete_community` handler doc now say a changed host or owner under a known UUID is `409 deletion_request_conflict`, and an unsupported acknowledgement version is rejected first with `400 unsupported_acknowledgement_version`. - **Docs: active-cap overrides above the lifetime cap can't be reached.** The `max_communities_per_owner` doc and the quota section now say `MAX_LIFETIME_COMMUNITIES_PER_OWNER` (20) counts live ownership, so `BUZZ_MAX_COMMUNITIES_PER_OWNER` above 20 can't be reached. - **Legolas P2s:** - The UUID-conflict docs now list every 409 case, including a stored ack version that doesn't match and a UUID held by an operator-origin request. - The runbook now says a legacy co-owned community is rejected as `404 community_not_found` and should be converged with a transfer first. - The preparation drift test now pins `DeletionSafety` plus "sole-owner authority drifted". - **Guard wording:** the `delete_community` handler doc now says "sole current owner", matching the admission check. What got simpler: admission and automatic preparation now enforce the same sole-owner authority, so no accepted-but-unexecutable co-owned request needs a new recovery path. `deletion_api_error` is renamed to `coded_api_error`, since it now carries non-deletion codes too. There's one helper for coded operator errors instead of a second one. Rebased onto main after #7969 merged (`d7a35afa`). The sole-owner commit applied cleanly. Earlier focused Blox verification (at the pre-rebase head `161e9857`): the new admission regression was red before the change and green after it. Five temporary, restored mutants each failed its matching production-seam regression. At the current head `766eb07f`, the relay `api::operator::` suite passes 23/0 on Blox. Each new `community_id` guard, removed one at a time, fails its own regression test. CI owns the full suites. Remaining debt: the separate P2-A purge/completion membership/community lock-order cycle is unchanged. Generated with Codex --------- Signed-off-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
bba75a71eb |
Automate owner deletion preparation (#7830)
## Why Owner deletion admission stops at `submitted`. Manual inventory and approval still stand between owner intent and the existing deletion executor, preventing self-serve deletion. ## What The existing `buzz-admin deletions drain` process prepares and automatically approves operator-attested owner requests. Operator-origin requests retain manual approval. ## How The drain prioritizes approved work, then claims one owner submission with the existing generation lease. It freezes inventory and records digest-bound `owner_automatic` approval in one transaction before entering the unchanged executor. Before approval, the transaction rechecks archived state and current ownership. It locks the deletion advisory fence, community, owner memberships, and request in that order. A regression test pins privileged abort during a live preparation lease. What got simpler: one drain still owns preparation and execution. There is no second worker, queue, lifecycle, or retry authority. The rebased branch contains only this feature and its review fixes. ## Risk Automatic approval permits irreversible deletion without a human grace period. Operator attestation is not a cryptographic owner signature. Provenance, locked authority checks, generation fences, and digest-bound approval constrain the operation. This remains work in progress. Broad test failures and incomplete mobile checks are disclosed below; publication is not a merge-ready verdict. ## Testing Published head: `e059ae9056b447ca3b5d0c2893da86550ef16f5c`. All seven commit trees match tested source `5605f27eaece1e809f746cc1839b55c7ceef8d80`; only attribution metadata changed. Source-SHA results below are not new-head execution receipts. At source commit `5605f27eaece1e809f746cc1839b55c7ceef8d80`, isolated PostgreSQL tests showed stale archive and ownership fixtures fail closed after the fix. Before the fix both reached automatic approval. Removing abort's generation increment or stage transition made the new live-lease regression fail; restored production passed. `just test` failed in two ACP native-git fixtures and one agent body-timeout test. The failing source files match the pinned base, but there is no exact-base execution comparison proving those failures unrelated. `just test-unit` also failed in the ACP fixtures, leaving 18 tests unrun. `just ci` stopped in Flutter tooling package resolution; separately run non-mobile lanes do not constitute a full `just ci` pass. No live tenant deletion or human testing was performed. A dedicated populated `0052` → `0053` approval-backfill fixture remains a coverage gap. Final-head hosted CI and security review are required. ## Bigger picture Rebased directly onto main `12670bd0f037c66a682272bb81c46c3f254fad74`; prerequisite #7818/#7827 are already merged. Main owns `0052_channel_artifacts`, so this feature's automatic-approval migration is now `0053`. Client/KGoose and quota work are separate PRs and are not part of this diff. Generated with Codex --------- Signed-off-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
6918dd3a83 |
Schedule the deletion drain safely (#7827)
## Why The deletion engine already exposes a one-shot `buzz-admin deletions drain` command, but operators have no safe way to schedule it. A generic workload wrapper could expose arbitrary commands or inherit relay credentials, which would add a second control plane and widen the blast radius. ## What This PR adds an opt-in Kubernetes CronJob for the typed deletion drain command. It is stacked on #7818, which adds owner-request admission. ## How The chart keeps the job disabled by default. The CronJob uses `Forbid`, zero Kubernetes retries, bounded runtime and history, and only the database, Redis, and object-store settings required by the executor. PostgreSQL leases, checkpoints, and retry timing remain the sole durable retry authority. The closed `_operator-jobs.tpl` helper centralizes safe CronJob mechanics without creating an arbitrary command or environment registry. This leaves the touched system simpler than a general job framework would. ## Risk The deployment blast radius is low because the job is disabled by default. When enabled, it can run destructive deletion work that the database has already approved, so the chart limits concurrency, retries, credentials, and execution time. ## Testing Rendered every `deploy/charts/buzz/ci/*.yaml` fixture with `helm template`; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`. Ran the full independent Desktop, Tauri, and web lanes; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`. Flutter package resolution made no progress within three minutes, so the mobile lane is not claimed green. ## Bigger picture A later stack layer must inventory and approve admitted owner requests. Until then, this scheduler cannot make a new owner request destructive. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Signed-off-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
15d44dccee |
Add owner deletion admission control plane (#7818)
## Summary Adds the relay and database admission boundary for owner-requested deletion of archived hosted communities. The operator endpoint records accepted owner intent with stable UUID idempotency and immutable owner/operator provenance. Admission serializes with unarchive and owner transfer, and accepted requests disappear from actionable owner listings. This keeps destructive execution out of the request path. The existing durable deletion engine remains the only executor, so this change adds no lifecycle state or second deletion mechanism. Complexity stays bounded: one durable request is both the idempotency record and the mutation fence. ### Related issue None found. ### Testing - `REDIS_URL=redis://127.0.0.1:6379 BUZZ_POSTGRES_ADMIN_URL=postgres://buzz:buzz_dev@localhost:5432/postgres PGHOST=localhost PGPORT=5432 PGUSER=buzz PGPASSWORD=buzz_dev PG_BIN_DIR=/tmp/buzz-pg-bin ./scripts/postgres-test-run.sh -p buzz-db -p buzz-relay --lib --tests` — 385 passed. - `cargo test -p buzz-db` — passed. - `cargo fmt --all -- --check` and `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` — passed. - `just desktop-test desktop-build desktop-tauri-check desktop-tauri-test web-build` — passed; Desktop ran 6,589 tests. - Full `just test` could not start uncached Keycloak dependencies because `quay.io` reset connections. Full `just ci` reached Flutter dependency resolution, then `pub.dev` reset connections. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Signed-off-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
b37e477211 |
Make relay readiness process-local (#7341)
## Summary Make Kubernetes readiness depend only on the relay process lifecycle, so a shared Postgres or Redis slowdown cannot withdraw every pod at once. Move dependency checks to one fixed-cadence loop per pod. The loop permits only one evaluation at a time, emits metrics after each sample, and caches a timestamped report. `/_status` reads that cache without calling Postgres, Redis, or the deletion catalog. It reports `not_yet_sampled`, `fresh`, or `stale`. Each completed sample also writes a Unix timestamp gauge, so Datadog can compute sample age at query time. Fail closed when the community lifecycle lookup errors. An inconclusive tenant-state lookup no longer admits a socket to AUTH or REQ handling. ### Related issue None found. This addresses the elevated relay HTTP 500 and reconnect incident investigated on 2026-09-03. ### Tradeoff The readiness gauge now reports the latest private-probe observation, not the shutdown transition itself. After shutdown starts, a scrape can still see `1` until the next readiness probe refreshes the gauge to `0`. Kubernetes routing is unaffected because it uses the readiness response from the authoritative process state. A terminal `0` was never guaranteed: even a transition-owned gauge could update in process and then exit before Prometheus scraped it. If no scrape occurs before exit, both designs miss the terminal sample. The new design adds at most the interval until the next readiness probe when a scrape occurs during shutdown. Dependency status is now sampled every 30 seconds rather than on demand. A slow evaluation delays the next sample instead of stacking more work. The status payload exposes the cached report age. The completion timestamp stays unchanged when sampling stops, so `time() - timestamp` crosses the stale threshold without a server aging loop. Outcome counters and duration histograms remain cumulative and cannot provide no-data detection. ### Testing Gimli exercised exact SHA `edff1ec4a5362e132e367bf7449d2b0a68f1e9a9` on an isolated Blox stack. Repeated `/_status` bursts caused no dependency calls, metrics advanced without status traffic, stale status remained visible, and lifecycle lookup failures admitted no AUTH or REQ handling. Follow-up verification at `fdbb394ed598c94cde85ed01cb11e9de0ee8f00d` confirmed sampler-owned outcome and duration emission without status traffic and bound the real startup seam. The final timestamp-gauge change is covered by targeted red/green tests and the same boot regression at `c0f36e22b527c2fd6a63a548d036d006ffa17171`. ### Complexity Readiness remains one process-local sample per probe. Dependency I/O now has one owner and one execution path; request volume cannot increase dependency-check load. One completion timestamp replaces the prior server-side age update path; Datadog computes age without another loop. Community admission also returns to one fail-closed rule for inactive and unknown tenant state. Generated with [Claude Code](https://claude.ai/code) --------- Signed-off-by: tornquist <tornquist@squareup.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
5079c770fe |
fix(workflows): make deletion persistent and retryable (#7735)
## Summary Fix workflow deletion leaving its definition queryable, and make public deletion acceptance agree atomically with executable/definition state. - Delete the executable workflow and soft-delete its definition under the existing coordinate lock, in the same transaction as kind-5 acceptance and mention indexing. Failed deletion leaves no public accepted event; identical retries can succeed. Legacy already-stored failed requests still repair. - Choose one committed dispatch/audit owner for concurrent identical requests. Completed duplicates remain silent. Preserve owner/community isolation, newer revisions, orphan cleanup, and intentional backdated recreation. - Align Desktop confirmation and architecture documentation with recreation semantics. Add deterministic PostgreSQL, live WebSocket, and Desktop regressions. No schema migration or new retry/delivery subsystem. ## Current validation Head: `40d86ddab0853ffb292316653b22c3fd4f983f0c`. - **Exact clean head:** live relay workflow-deletion E2E **4/4 passed**. All applicable normal pre-push hooks passed: Rust unit suites, full Desktop unit suite, Desktop lint/typecheck, Tauri clippy/tests, and file-size/branch checks. No hook bypass. - **Pre-commit implementation tree, then committed at this head:** full discovered DB/relay PostgreSQL lane **351/351 passed**; built Desktop deletion Playwright smoke **3/3 passed** (success, failure preserving editor/draft, corrected confirmation copy). - **Deterministic mutation evidence:** unchanged previous relay `659b79282` fails both new regressions: concurrent identical requests produce two normal/dispatch-owner responses, and rejected deletion leaves one public event. Both controls pass. Fixed relay passes all four. - **Independent source review:** transaction/authorization/lock-order and regression/cleanup lanes clear. Reviewers did not run tests independently. ## Honest limits / merge gates The broad pre-commit DB/relay/test-client selection passed **1,215/1,216**; the untouched mesh-demo echo test returned HTTP 504 instead of 200. It remains outside this repair and is not counted green. Initial hook attempts also exposed an untouched Desktop inventory ordering assertion and unidentified ACP/agent test failures; standalone package reruns and the final complete hooks passed without changing those packages. Causes are not established. The previous Desktop CI shard failed before tests on a Hermit `just` download (HTTP 500). New-head CI/security review and reviewer disposition remain required. No deployment, fresh interactive Desktop session, full local `just ci`, or multi-replica runtime validation claimed. Postgres/Redis validation used task-owned local containers, fresh identities, and disposable fixtures. ## Compatibility No automatic cleanup of existing workflows: repeating deletion repairs definitions left by the old path. Canonical UUID coordinates and legacy name fallback (`LIMIT 1` for duplicate names) are unchanged. Backdated recreation remains intentional per [Tyler’s decision](https://github.com/block/buzz/pull/7735#issuecomment-5729881295), not an irreversible tombstone. Mention-index failure now rolls back the complete deletion rather than best-effort accepting partial work. Detailed reviewer disposition and evidence: https://github.com/block/buzz/pull/7735#issuecomment-5737367825 --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz> |
||
|
|
213092074c |
Instrument database pool roles (#7356)
## What
Name the relay's four Postgres pool roles—writer, reader, audit, and
search—and export one fixed-cardinality utilization metric contract for
all of them. Existing writer and reader gauges remain unchanged.
## Why
Audit and search already use separate pools, but they are missing from
pool pressure telemetry. Operators cannot see all process-level
connection demand before setting a deployment-wide connection budget.
## How
A closed `DbPoolRole` vocabulary now labels
`buzz_db_pool_connections{pool_role,state}` and
`buzz_db_pool_configured{pool_role}`. The relay retains cheap pool
clones for statistics only; ownership, capacities, timeouts, query
routing, and failure behavior do not change. The docs also replace the
stale async search-index description with the generated-column behavior
used today.
This simplifies four ad hoc observability states into one bounded model
without adding a pool manager or changing service boundaries.
## Risk
Low to moderate. This changes relay metrics and pool construction
plumbing, but not SQL execution or routing. Legacy metric names remain
available for current dashboards.
## Testing
- Push gate at `3f5468a0463bdfc5879bcdd3878fa06b42ca84c2`: all 14 Rust
test lanes passed; desktop Tauri checks passed.
- Blox: `cargo test --workspace --all-targets` with repository-declared
Postgres services.
- Blox: `cargo clippy --workspace --all-targets -- -D warnings`.
- Blox: `cargo fmt --all -- --check`.
## Bigger picture
Aggregate deployment budget enforcement remains deferred. This PR
exposes the per-role facts that a later deployment-policy change can
consume.
Generated with Claude Code
---------
Signed-off-by: tornquist <tornquist@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f3940ff21e |
fix(ci): pull pinned MinIO images from Quay (#7599)
## Summary CI is failing in `main` before tests start because Docker Hub denies pulls for `minio/minio` and `minio/mc`. Retrying the failed jobs produced the same errors. Use MinIO’s Quay images in the development, harness, deployment Compose, and Helm configurations. Pin each image to its multi-platform digest and retain the release tags already used by deployment templates. Update the architecture reference to match. ### Related issue No duplicate issue or PR found. Addresses the image-pull failures in [main CI attempt 2](https://github.com/block/buzz/actions/runs/34645306855/attempts/2). ### Testing - Pulled both pinned images for Linux ARM64 and AMD64. - Started an isolated Compose stack derived from the updated MinIO service definitions, once per architecture (AMD64 under local emulation). - Verified the existing health check passed and the bucket initializer exited with code 0. - Uploaded an object with `mc cp`, read it with `mc cat`, compared its content, and deleted it with `mc rm` on both architectures. - Confirmed the initialized bucket was private and removed the temporary stack and volume. Generated with Codex Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
70895b355f |
feat(buzz-auth): add production NIP-FI federated assertion runtime (#7109)
Depends on #6994 (merged). Stack: PR 2 #6994 (merged) → this PR (#7109) → PR 4 #7148 → PR 5 ## What Production assertion runtime for NIP-FI Phase A: JWKS caching layer, SSRF-hardened HTTP fetcher, startup validation gate, NIP-11 discovery serialization, federated assertion verifier with sealed key-source authority, and supporting invariant tests. ## Changes ### `crates/buzz-auth/src/nip_fi/jwks/` `ProductionJwksSource<F>` implements the sealed `IssuerKeySource` trait: - `HttpJwksFetcher`: reqwest-backed fetch with SSRF protection — URI validation (HTTPS, no credentials, no fragment, no host matched by the shared enumerated deny policy), per-fetch DNS resolution rejecting any resolved address matched by the shared enumerated deny policy, address pinning to prevent DNS rebinding TOCTOU, redirect denial, incremental body streaming capped at 512 KiB before any parse - IPv6 host extraction via typed `Url::host()` accessor (strips brackets before SSRF check and provides the correct bare input form for reqwest `resolve()` pinning; the bracketed form from `host_str()` fails `IpAddr::parse` and does not match the URL authority key) - Complete-operation deadline via `tokio::time::timeout` covering DNS resolution through body streaming - Bounded periodic refresh with configurable interval and hard snapshot deadline - Cancellation-safe RAII refresh permit: dropped on future cancellation so the next caller can re-fetch - Content-digest-gated generation counter: identical re-fetches preserve generation; key rotations advance it - Injectable clock (`now_fn: Arc<dyn Fn() -> DateTime<Utc> + Send + Sync>`): production uses `Arc::new(Utc::now)`; all four deadline creation and expiry checks use `(self.now_fn)()`, enabling controlled-time testing without wall-clock sleep `JwksSourceContract`: a closed value type that is the single source of truth for the three deployment fields whose change alters which keys the runtime trusts and how long it trusts them: - `jwks_uri` — selects the authenticated key source; validated at construction (HTTPS, no credentials/fragment, no bare private-IP host); stored as the `Url`-normalized form so that equivalent spellings (uppercase host, explicit default port `:443`, dot-segment paths like `/.well-known/./jwks.json`) converge to the same `AssertionPolicyId` - `refresh_interval_seconds` — defines bounded refresh behavior; positive, ≤ 1 year, strictly < `key_snapshot_hard_deadline_seconds` - `key_snapshot_hard_deadline_seconds` — defines the source's accepted time rule; every `VerifiedAssertion.revalidation_dependencies` deadline derives from this `JwksSourceContract` is a required `IssuerPolicy` input and is included in `derive_assertion_policy_id` after a domain separator. `IssuerJwksConfig` embeds the contract instead of independently restating these fields — startup validation rejects any contract mismatch (`NipFiStartupError::JwksContractMismatch`). ### `crates/buzz-auth/src/nip_fi/verifier.rs` - `FederatedAssertionVerifier<S>`: provider-neutral verifier over a closed multi-issuer registry and sealed `IssuerKeySource` - `Arc<S>: IssuerKeySource` forwarding impl (blanket seal for `Arc<S>` in the sealed module) — one `Arc<ProductionJwksSource>` can be shared across multiple verifiers; all observe JWKS refreshes through the shared cache without rebuilding the verifier - `AssertionKeySet`: crate-private constructor seals issuer binding — no external crate can relabel issuer B's JWKS as issuer A - Sealed `IssuerKeySource` trait closes the authority-construction seam at both ends ### `crates/buzz-core/src/network.rs` Renamed `is_private_ip` to `is_not_global_unicast` (compat alias retained) and restored the complete IANA deny/exception table from this branch's own history (`272dacadb`). The predicate is an enumerated deny/explicit exception policy: addresses covered by a named deny rule are rejected; addresses not covered by any explicit deny rule (e.g. `fe00::1`) pass through. Deny rules are derived from the IANA Special-Purpose Address Space registries (last updated 2025-10-09), with globally-reachable exceptions carved out explicitly (e.g. PCP/TURN anycast inside 2001::/23). Blocked IPv4 classes: loopback (127/8), private RFC 1918 (10/8, 172.16/12, 192.168/16), link-local (169.254/16), unspecified (0/8), broadcast, CGNAT/RFC 6598 (100.64/10), benchmarking/RFC 2544 (198.18/15), IETF Protocol Assignments (192.0.0.0/24, globally reachable exceptions: 192.0.0.9 PCP anycast RFC 7723 and 192.0.0.10 TURN anycast RFC 8155), documentation/RFC 5737 (192.0.2/24, 198.51.100/24, 203.0.113/24), deprecated 6to4 relay anycast (192.88.99.0/24, RFC 7526, global=None → conservative deny), multicast/RFC 5771 (224/4), reserved class-E (240/4). Blocked IPv6 classes: loopback (::1), unspecified (::), ULA (fc00::/7), link-local (fe80::/10), deprecated site-local (fec0::/10, RFC 3879), multicast (ff00::/8), IETF Protocol Assignments envelope (2001::/23, globally reachable exceptions: 2001:1::1–::3 PCP/TURN/DNS-SD anycast, 2001:3::/32 AMT RFC 7450, 2001:4:112::/48 AS112-v6 RFC 7535, 2001:20::/28 ORCHIDv2 RFC 7343, 2001:30::/28 DETs RFC 9374), documentation (2001:db8::/32 RFC 3849, 3fff::/20 RFC 9637), 6to4 (2002::/16, RFC 3056), Discard-Only (100::/64, RFC 6666), Dummy IPv6 Prefix (100:0:0:1::/64, RFC 9780), SRv6 SIDs (5f00::/16, RFC 9252), NAT64 local-use (64:ff9b:1::/48, RFC 8215). IPv4 embedded in mapped, compatible, NAT64 well-known (64:ff9b::/96), and SIIT IPv4-translated (::ffff:0:0:0/96) forms is checked recursively. All three callers (JWKS boundary, webhook SSRF, link-preview SSRF) inherit the complete predicate through the inline `is_private_ip` compatibility alias. ### Invariant coverage **Canonical URI convergence.** `jwks_contract_uri_canonicalization_convergence_and_divergence` asserts that uppercase host, explicit `:443`, and dot-segment path (`/.well-known/./jwks.json`) each produce the same `AssertionPolicyId` as the canonical form; a genuinely different host or path diverges. Mutation: storing raw input bytes instead of `parsed.to_string()` turns the three convergence assertions red. **Resolved-target and pin-input seam.** `resolved_target_and_pin_key_seam_public_ipv6_and_fec0_rejection` carries a public `2606:4700::1` URI through all three stages of `fetch_jwks_inner`: `extract_url_host_and_port` yields the bare host (no brackets), `resolve_and_check_ssrf` takes the IP-literal fast path and returns the accepted `IpAddr`, and the extracted host string equals the URL authority form (verifying the correct bare input to reqwest's `resolve()` pin call). `fec0::1` traverses the same extraction and SSRF stages and is rejected as `InvalidUri`. Network-free: both addresses are IP literals with no DNS lookup. Mutation: restoring `host_str()` brackets the address, `IpAddr::parse` fails, the SSRF fast path is unreachable, and all three assertions flip red. **Controlled original-deadline rotation.** `shared_arc_source_verifier_rejects_expired_a1_accepts_a2` uses an `AtomicI64`-backed injectable clock to advance past A1's original absolute deadline without wall-clock sleep. A1's deadline is computed at T0 and never mutated. The clock advances to T0 + HARD_DEADLINE_SECS + 1; `get_snapshot` fires a re-fetch and installs A2. One unchanged `FederatedAssertionVerifier` then rejects A1-signed tokens (deadline enforced by the `key_set` read path) and accepts A2-signed tokens, proves A2's generation is strictly greater, and confirms A2's deadline is later than A1's original. Mutation oracle: replace the shared `Arc` with an independently constructed source built from the same configs and sharing the same controlled clock, warmed with a separate A1 fetch before advancement. Post-advancement, `key_set()` on the verifier's independent source filters the expired A1 snapshot (`filter(|c| now < c.hard_deadline)`) and returns no keys — the verifier never re-fetches and never observes A2. A1-reject stays green (the independent cache is also expired, so no A1 keys are served), but A2-accept flips red, because the verifier never observes A2. A2 acceptance is the reliable shared-source oracle. **Complete SSRF classifier boundary.** JWKS-boundary tests cover every newly restored class through `validate_jwks_uri` (URI-validation path): `192.0.0.1` (IETF Protocol Assignments interior), `192.0.0.9`/`.10` (PCP/TURN anycast global exceptions), `192.88.99.1` (deprecated 6to4 anycast), `2001:2::1` (2001::/23 interior), `2001:1::1` (2001::/23 global exception), `100::1` (Discard-Only), `3fff::1` (documentation), and `5f00::1` (SRv6 SIDs). URI validation and resolved-target enforcement share the same `is_not_global_unicast` predicate, so these URI-path tests exercise the complete classifier table. All pass mutation: removing any deny branch makes the rejection assertion red; removing any exception branch makes the acceptance assertion red. The resolved-target enforcement path is covered separately by `resolved_target_and_pin_key_seam_public_ipv6_and_fec0_rejection` for `::1` (loopback), public `2606:4700::1`, and `fec0::1`. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com> |
||
|
|
4952f58102 |
docs: add review-proven failure-path & async-state rules to AGENTS.md (#7061)
Mining the last 25 PRs' review threads (45 substantive findings, 11 reviewed PRs, avg **4.8 review rounds** each) shows **53% of findings are repeats** of five clusters: swallowed failures, stale-async-state races, tests that don't bind the production seam, unbounded resources/retry loops, and non-atomic multi-step persistence. PR #6956 alone burned 4 rounds converging on one of these classes. A second, independent mining pass over **71 agent-review rooms (303 findings, Aug 18–29)** confirmed the same clusters and added outcome data — how often authors actually fix each finding class once flagged: test-seam binding and unbounded-resource findings **100%**, swallowed errors **90%**, stale-state races **70%**. It also surfaced two clusters the GitHub-thread pass under-sampled: **assistive-semantics defects** (44 findings, second-largest cluster) and **input-modality divergence** (27 findings), now rules 7–8. This PR distills those clusters into eight imperative rules in AGENTS.md so agents apply them **before writing code**, adds one client-consumption invariant to ARCHITECTURE.md §5, and places the test-quality rule in TESTING.md (per the team decision that testing docs are the canonical guide for review standards), cross-referenced from AGENTS.md. Each rule cites the PRs where it was litigated. Raw mining data: `reviews.jsonl` / `comments.jsonl` + `backfill/buzz-review-findings.jsonl` (review-mining artifacts, not committed). No code changes. CLAUDE.md is a symlink to AGENTS.md and picks this up automatically. 🤖 Drafted by Jude's agent from automated mining of this repo's last 25 PRs' review threads and 71 agent-review rooms; every rule cites the PRs where it was litigated. Jude reviews and owns the result. Mining method + raw cluster data available on request. --------- Signed-off-by: Jude Edwards <judeedwards@squareup.com> |
||
|
|
909a3b2c31 |
docs: fix stale kind count, quick-start numbering, and empty Further Reading (#2613)
## Problem Three small documentation defects, each verified against the code at |
||
|
|
bf139e8d0b |
perf(presence): reduce heartbeat frequency (#3783)
## Summary - send desktop presence heartbeats every 60 seconds instead of every 30 seconds - extend presence TTL from 90 to 180 seconds to preserve the existing three-heartbeat expiry window - add mutation-sensitive tests that pin the one-minute / three-window timing contract - update presence documentation to match This halves steady-state **desktop** presence `SET` + `PUBLISH` traffic while retaining tolerance for two missed heartbeats. Mobile already uses a 60-second heartbeat, so the fleet-wide reduction depends on desktop's share of connected clients. ## Rollout order Deploy the relay TTL increase before shipping the desktop heartbeat change. Old desktop + new relay is safe; new desktop + old relay leaves only a 90-second TTL on a 60-second cadence and can flap after one missed heartbeat. ## Verification At initial live-test commit `00816e233b187bc5ba12c667d675ed050a8cc1c9`: - isolated clean-room relay built from the exact SHA against fresh Postgres, Redis, and MinIO - live Redis `MONITOR` observed kind-20001 writes as `SET ... EX 180`, global `PUBLISH`, and clean-disconnect / explicit-offline `DEL` - normal workflows passed: channel create/update/archive/unarchive; message send/get/reply/thread/search; archived-channel write rejection and resumed write after unarchive At follow-up commit `bf38a8c5c96f196ff8ee46e48d4141ee7811f186`: - `pnpm -C desktop test` — 3829 passed - `pnpm -C desktop typecheck` - `cargo test -p buzz-pubsub` — 24 passed, 11 Redis-dependent tests ignored - mutation probes fail when the server TTL changes to `999999` or the desktop heartbeat changes back to 30 seconds - `git diff --check` The pre-push suite's relevant checks passed, but its unrelated Tauri clippy step fails on current `origin/main`: `desktop/src-tauri/src/linux_media.rs` has three dead-code warnings on macOS. This PR does not modify that file, so the branch was pushed after independently running the suites above. ## Buzz context Originating channel: `buzz-redis-cluster-mode` (`f4e36d32-afdb-447f-8c87-ab003e069d18`) --------- Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> |
||
|
|
14fba21e57 |
Multi-tenant Buzz relay: community_id as a server-resolved key (comprehensive rewrite) (#1321)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Quinn <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: Sami <sami@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> |
||
|
|
2ecdcce7bd |
Multi-tenant relay: spec + mechanized formal proof (S1–S8) (#1285)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> |
||
|
|
0cee0435f7 |
feat(relay): add buzz-admin member management CLI with NIP-43 roster publish (#1265)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> |
||
|
|
1bb8b8d547 |
docs: clean up remaining Buzz references (#977)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> |
||
|
|
79bcee55cb |
docs: finish Buzz rename cleanup (#974)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> |
||
|
|
f1c672fea5 | chore: deprecate sprout-mcp — fill CLI gaps, remove crate and all references (#850) | ||
|
|
86f4c5d9a9 |
docs: fix stale claims, remove counts, purge LiveKit references (#742)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> |
||
|
|
0dcfa86f84 |
chore: remove dead API token references from GUI, docs, and config (#498)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
0ac9649a84 | docs: ARCHITECTURE.md accuracy pass — verified against every line of code (#236) | ||
|
|
6093fc6066 |
feat: migrate from MySQL to Postgres with pgschema (#114)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Tyler Longwell <tlongwell@squareup.com> |
||
|
|
124047ef88 | feat: NIP-29 native compatibility — standard nostr clients can chat on Sprout (#63) | ||
|
|
6a5fcbb0c2 | feat: reliability and security hardening (#53) | ||
|
|
975b7be4a8 | feat(proxy): NIP-28 translation proxy for third-party Nostr clients (#43) | ||
|
|
cd3b45f948 | Initial backend revisions, workflow expansion (#5) | ||
|
|
058c4b92a9 | Initial release — Sprout Nostr relay with enterprise extensions (#2) |