Skip to content

BUZZ-511: Honor relay mode for community NIP-FI enforcement - #8988

Merged
bradseiler merged 27 commits into
mainfrom
seiler/BUZZ-511/relay-policy-enforcement
Nov 5, 2026
Merged

bradseiler merged 27 commits into
mainfrom
seiler/BUZZ-511/relay-policy-enforcement

Conversation

@bradseiler

@bradseiler bradseiler commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Apply relay mode consistently across protected HTTP, root/audio WebSocket, Git, media and V1 admission. Off requires no NIP-FI, including for stored required communities. Shadow enforces only stored community requirements and observes other communities. Enforce requires NIP-FI for every community. Durable requirements survive all mode changes.

Off retains ordinary tenant routing and authentication. It skips FI policy/dependency reads for admission, uses cached WebSocket tenant routing, and loads no FI binding snapshot at startup. Discovery reports requires_nip_fi=false and no FI capability in Off. Evaluating modes retain same-row WebSocket binding and immutable HTTP policy tenants, including host remaps, pending deletion and unarchive races. Global Enforce discovery retains its existing compatibility behavior.

Host-specific root NIP-11 and /info expose the additive requires_nip_fi field with no-store. In evaluating modes, an unavailable policy returns 503 and an unknown or non-serving host returns 404. Off uses ordinary routing; a discovery lookup failure returns 404. Stored requirements in Shadow require usable FI dependencies.

Admission does not establish activation. BUZZ-512 owns existing-session application, drain and mode-aware readback; its separate revision must report stored requirements as suspended in Off. Production baseline verification and human acceptance remain open.

Community status now shares the bounded host cache: 10,000 entries, one-second leases measured before checkout, coalesced fills, and no cached misses or errors. Git reuses one request policy. Root/audio retain the final primary binding check: two reads on a cold successful upgrade, one on a cache hit. Off creates no verifier or JWKS source and reads no FI policy. Proof metrics count stored-required Shadow enforcement.

Acceptance locks and validates the community row, takes the matching host advisory lock, and drains optional leases for 1.1 seconds before commit. Reads begun after commit see required across participating relay instances, even without Redis invalidation. The row lock closes single and double remap races. Admissions already in flight remain subject to BUZZ-512 activation fencing.

Related issue

BUZZ-511. BUZZ-510 / #8985 is merged; this PR now targets main.

The restack retains main’s #8974 denial telemetry and artifact capabilities. Related work includes #8526 tenant lookup errors and #8444 media authorization.

Rollout and containment

Keep BUZZ_NIP_FI_UPGRADES_ENABLED=false. The rollout plan starts in staging with zero required rows and a 30-minute before/after traffic comparison. Initial abort limits include any policy timeout/503, policy-read p99 above 50 ms, writer acquisition p99 above 20 ms or twice baseline, and a five-minute 503 increase above 0.1 percentage point. These are initial limits to validate against measured staging results, not production capacity claims.

Verify signed/unsigned free controls, real required root/audio upgrades, dependency failures, restart behavior and discovery before expansion. Also watch binding timeouts/errors and their current generic 404 responses on known serving hosts alongside writer-pool pressure; evaluating root/audio binding has a 500 ms budget; Off uses ordinary cached routing. In Shadow, a pre-511 rollback loses enforcement of accepted community requirements. Stop new upgrade calls, retain accepted policy and receipts, and repair with an enforcing binary or operator-approved containment. Never clear durable policy to restore access. Off deliberately suspends FI enforcement and must be an explicit operator mode choice. No production activation is claimed.

All acceptance paths must implement the lease protocol before upgrades are enabled. Keep upgrades disabled through a mixed-version rollout. For rollback, disable new acceptance before removing the protocol while caching pods remain. A cold policy fill uses a six-statement transaction and a 400 ms server timeout within the 500 ms lookup budget. A contended policy fill returns 503. During the drain, archive, deletion, ownership, issuer-binding and competing acceptance writers for that community may wait; ordinary serving reads and writes retain their existing behavior. Host-binding changes retain the existing one-second routing bound. Local and Redis invalidation accelerate refresh; safety does not depend on delivery. Deploy the verified Shadow revocation behavior from #8991 before accepting required communities in Shadow.

Testing

Published review-fix head: 39dfe79b1fc04f10ee3eb94d6ea42cf686bafe1e, tree 1e2e8d3abd39ee1d6821defe8c8d34de73726e08. McKeon cleared this cache delta. Full affected auth (291), DB (192), pubsub (31) and relay (1,682) package tests passed, including integration binaries. Two previously documented hardcoded-Redis audit tests remained excluded. All-target clippy, all 1,297 native PostgreSQL tests and the full pubsub Redis suite passed. The full ACP package and just ci passed after rebuilding cached test executables that embedded a removed checkout path. No ACP source changed. Tauri's generated lockfile delta was archived and restored; it is excluded from this candidate.

Both positive lease regressions passed. Four deliberately broken variants failed at the intended assertions: removing the drain left an optional lease alive after 202.935667 ms; skipping the fill lock failed contention refusal; moving the timestamp after the read failed the anchor check; taking the host lock before the row failed the remap-blocking check. Exact source was restored after each mutation. These are local validation results, not a deployed-fleet acceptance claim. All applicable normal push hooks passed. GitHub reports the exact tested head and tree; the published code has no delta from the reviewed candidate.

Earlier mode-correction evidence follows for context.

Current mode-correction head: aa9abf07284b56a88eb78a2014e590f39d705288, on main af54258f19ac32667c027f889652bdeb479c8e13. Its code equals reviewed f1e7da5fc5, plus the independently reviewed operator documentation follow-up; all 19 earlier remote patches are preserved. McKeon verified the correction and its mapping to the tested candidate.

The mode-correction candidate passed full auth (291) and relay (1,679) package tests, clippy, and four explicit native PostgreSQL policy workflows. There were 623 ignored relay tests and two excluded Redis tests. Real PostgreSQL checks cover an ordinary unsigned caller on a stored-required host in Off with no FI token or dependencies, discovery false, durable policy retained, root/audio upgrades, host remaps and the unarchive race in evaluating modes. The STORED_OFF mutation fails the enum truth table only; OFF_PRIMARY fails cached upgrade routing. Restored source hashes match. Earlier failed compile/fixture attempts remain recorded.

The first normal push hook attempt passed Tauri checks but failed the gateway startup verifier test (82/83 gateway tests passed). The remote remained unchanged. The retry rebuilt gateway artifacts with separate root and Tauri caches, then passed every normal push hook and published this head. No gateway source changes or hook bypasses were needed. Current-head hosted CI is tracked separately. Human acceptance and the staging binding-timeout/404 workflow remain open. The following retained validation and controls are historical and describe behavior before the mode correction; their Off-enforcement statements are superseded by the Summary above.

Published head: 9a717fa89725f9430ff4a8bb78cd600affb8836d, on main 1055f79d048ef22c878d02a7afb04f49c753564f. CI, three schema checks, 2,136 ordinary tests, all 291 auth tests, 12 Redis tests, all 1,300 native PostgreSQL tests and discovery passed. The required wrapper passed units, then exited 1 at unavailable Docker bootstrap. BUZZ-510 is complete at 95c8d31c835d6f7ff6e432fb1ed248771d4f06dd: CI, three schema checks, 2,127 ordinary tests, 12 Redis tests, all 1,295 native Postgres tests and discovery passed; its wrapper passed units and failed at unavailable Docker bootstrap.

The first native run at historical BUZZ-510 5380cb72 passed 1,291 tests and failed two event-follow-up queue assertions and the scale-sync case. All three passed serially, then the full 1,294-test run passed with normal concurrency. The original cause remains unresolved; failed runs and resource samples are retained.

Standalone BUZZ-511 checks exposed access-slot callers, a Clippy warning and early HTTP 503 attribution that had remained in the child. All are now corrected in the parent. The unchanged real-router refusal regression passed before the latest commit. BUZZ-512's final tree is unchanged by moving these parent prerequisites. The remaining 46-file child delta was audited; no additional parent prerequisite was found. Earlier compile and native failures remain archived.

The following earlier-head results and controls are historical; the current-head validation is reported above.

At 48388834f99b8d045e243b619904cb3e27b47146, based on BUZZ-510 130f14a675edd7cbe6a69d1c9d7818bd1a51f46c and reconciled main 152441ae0a56b204b42b6faa56a36ad2b98218b1:

  • just ci: passed.
  • Four-package ordinary suite: 2,069 passed; full buzz-auth package: 291 passed.
  • Native Redis: 12 passed. Native PostgreSQL: 1,261 passed. PostgreSQL discovery checks passed.
  • Required just test: units passed, then exited 1 because Docker service bootstrap was unavailable. Native service results above are separate.
  • Real-router remap checks reject an A-only member under B policy, avoid A data after B admission, and advertise B in discovery despite a stale A cache. Removing each tenant pin separately fails its intended witness; restored source passes. An unmapped host cannot reuse a cached tenant.
  • Deadline witnesses advance their clock during real HTTP signature verification and retain socket primary-lookup barriers. Removing either final enforced or shadow HTTP deadline check separately fails its intended witness; restored source passes both deadlines and the single-resolution control.
  • Telemetry regression: unsigned dev proofs now expect the earlier NIP-98 missing denial; signed mismatched-key proofs retain pairing denial and signer attribution. Removing unsigned-proof rejection fails the exact two-counter assertion. Restored full relay package: 1,654 passed.
  • Preserved failed runs: two prior bottom-layer desktop gates failed an immediate inventory assertion, repaired with a bounded wait for the same observable arrays and verified with delayed-notification and frozen-provider controls. An earlier BUZZ-511 native run passed 1,242 tests and failed two deadline fixtures that relied on the tenant lookup removed by the policy pin; the test-only adaptation above replaces that seam. These failures remain archived. The first native run on bottom-layer 130f14a6 passed 1,255 tests and failed partition contention; its focused original test and fresh full 1,256-test run passed without source changes. The cause remains unresolved. The first CI run at d8d88433 failed the old telemetry expectation; the test-only correction above preserves exact denial counts and attribution.

Earlier evidence at dddf05d8be8195d567db0f989f70825b1246a6c1 (historical head):

  • cargo nextest run -p buzz-auth -p buzz-db -p buzz-relay --lib --tests: 2,139/2,139 passed.
  • scripts/postgres-test-run.sh -p buzz-db -p buzz-relay --lib --tests --success-output final: 1,236/1,236 passed with native PostgreSQL 16 and Redis. Docker's legacy just test wrapper was unavailable.
  • just ci: passed, including repository checks, tests and client builds.
  • Pre-correction behavior (superseded): Real ES256 assertions through TCP root/audio upgrades: valid evidence upgrades; missing evidence denies in Off/Shadow, including submitted/inventoried/approved deletion stages. Restoring the old query makes the regression fail on an assertion-less Off/submitted/root HTTP 101.
  • Pre-correction behavior (superseded): Deterministic unarchive race: Notify barriers pause an actual archived-host policy miss; the same required row becomes serving before binding. Both root/audio deny missing assertions and admit valid ES256 assertions in Off/Shadow. Ignoring bound-row policy makes this test fail on an assertion-less HTTP 101.
  • The full rerun exposed an existing cancellation fixture whose synthetic five-second replica fence expired between probes. Refreshing that test fence per operation preserves its timeout assertions; the full suite now passes.
  • Local probe: 100 serial uncached primary policy reads took 12.33 ms. Production latency is unmeasured.

No packaged-app, human or deployed-fleet acceptance run. Live activation awaits BUZZ-512 and the staging verification tickets.

Published with the three-layer stack in one atomic push through normal hooks. The published stack includes main 1055f79d048ef22c878d02a7afb04f49c753564f, including access logging, storage reporting and mobile read-state changes. Access-slot integration and early HTTP refusal attribution belong to BUZZ-511; activation callback refusal close owners belong to BUZZ-512. Every changed layer requires review at its published head and new-head CI. Fresh trunk, remote membership, queue and immutable-lease guards passed before publication. All nine normal pre-push hook lanes passed; no hooks were bypassed. Remote heads, chained bases, membership, ready/draft states and native tracking were verified after publication.

Trunk’s merged 0081 is unchanged. The parent upgrade expansion is 0082 and activation is 0083, with contiguous migrator versions, updated documentation and a populated-upgrade fixture starting at 81. Trunk’s storage-test repair supersedes our earlier local repair.

Before publication, main advanced three commits from 1055f79d048ef22c878d02a7afb04f49c753564f to ba7d7825ea (desktop deep-link settling and mobile pairing/sign-in). The normal branch-skew guard passed. Those later trunk commits are outside this validated published head; current-trunk CI and delta review remain required before merge.

Generated with Codex

@bradseiler
bradseiler added this pull request to stack #8989 November 3, 2026 07:26
@bradseiler
bradseiler force-pushed the seiler/BUZZ-511/relay-policy-enforcement branch from dddf05d to 9a717fa Compare November 4, 2026 04:35
@bradseiler
bradseiler force-pushed the seiler/BUZZ-511/relay-policy-enforcement branch from 9a717fa to 7075b9e Compare November 4, 2026 16:27
@bradseiler
bradseiler force-pushed the seiler/BUZZ-511/relay-policy-enforcement branch from 7075b9e to b340f04 Compare November 4, 2026 19:31
Base automatically changed from seiler/BUZZ-510/relay-auth-upgrade to main November 4, 2026 19:56
bradseiler added a commit that referenced this pull request Nov 4, 2026
## Summary

Record that a community requires NIP-FI and provide an idempotent
operator API to enable it and recover the original receipt. Acceptance
checks the current owner, host, lifecycle and existing issuer trust in
one transaction. It never creates trust. Receipts survive community
deletion so an operation UUID cannot be reused.

New acceptances default to disabled through
`BUZZ_NIP_FI_UPGRADES_ENABLED`. Disabled calls return HTTP 503
`upgrades_disabled`; identical retries and authenticated readback still
recover existing receipts. Policy is permanent: stopping new calls or
rolling back code does not clear it. Last-issuer removal is refused for
required communities, and bulk backfill leaves missing bindings for
explicit operator repair.

Apply additive migration 0082 before code. Existing communities remain
optional. This layer records acceptance and reports only `pending` or
`unavailable`; deploy and verify [per-request
enforcement](#8988) and [session
activation](#8991) before enabling
acceptance. The global enforce floor remains. See [the operator
contract](https://github.com/block/buzz/blob/seiler/BUZZ-510/relay-auth-upgrade/docs/community-auth-upgrades.md)
for signing, recovery and containment.

### Related issue


[BUZZ-510](https://linear.app/squareup/issue/BUZZ-510/relay-persist-community-nip-fi-requirements-and-expose-idempotent).
Base of native stack `8989`; retains trunk's enforcement telemetry from
[#8974](#8974). Includes a separate
commit that repairs an observer timing flake in a desktop test required
by the repository gate.

### Testing

Controlled local regressions detect the old disabled error code and
removal of the last issuer with `allow_last=true`; intended source is
restored. Full PostgreSQL validation and the updated live relay/admin
exercise remain pending. Human acceptance remains open. No deployment or
production activation.

Generated with Codex

---------

Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
@bradseiler
bradseiler force-pushed the seiler/BUZZ-511/relay-policy-enforcement branch 2 times, most recently from b2bbd15 to 08f81d0 Compare November 4, 2026 20:21
@bradseiler
bradseiler marked this pull request as ready for review November 4, 2026 20:21
@bradseiler
bradseiler requested a review from a team as a code owner November 4, 2026 20:21
@bradseiler
bradseiler marked this pull request as draft November 4, 2026 22:51
@bradseiler
bradseiler force-pushed the seiler/BUZZ-511/relay-policy-enforcement branch from 08f81d0 to 551610a Compare November 4, 2026 23:19
@bradseiler bradseiler changed the title BUZZ-511: Enforce per-community NIP-FI policy BUZZ-511: Honor relay mode for community NIP-FI enforcement Nov 4, 2026
bradseiler and others added 7 commits November 4, 2026 19:20
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Resolve serving hosts and their policy with one shared query. Exercise real root/audio upgrades and pending deletion in Off and Shadow.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
bradseiler and others added 9 commits November 4, 2026 19:20
HTTP now reuses its policy tenant, so it has no second database binding lookup. Advance the fixture clock after the handler's real signature verification, preserving the fresh guard and stale final admission checks in Enforce and Shadow. Keep the primary acquisition barrier for socket routes and require each armed seam to fire.

Use the seeded community ID for the fixture's policy tenant. Keep exact refusal bytes, shadow labels, and fresh controls unchanged.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Keep signed-key pairing attribution while asserting that unsigned dev proofs
are rejected at the earlier NIP-98 evidence check.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Adapt the strict proof rejection and direct admission fixtures to the
access logging contract without attributing an unsigned signer.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Keep the policy enforcement layer independently buildable after the access logging integration.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Carry the same request slot through the bypass-policy branch as the stored-policy branch.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Keep the original denial response while recording its access result, using the lint-compliant Result inspection method.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Record the shared request access result before early HTTP policy denials in the enforcement layer.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Disable FI enforcement and FI startup dependencies in Off while preserving stored community requirements for Shadow and global Enforce behavior.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
@bradseiler
bradseiler force-pushed the seiler/BUZZ-511/relay-policy-enforcement branch from aa9abf0 to cb97024 Compare November 5, 2026 00:20
@bradseiler
bradseiler marked this pull request as ready for review November 5, 2026 00:22

@baxen baxen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed with a focus on the efficiency of the per-request policy design. I didn't find correctness problems in the mode logic. The main thing: outside Off, admission now costs uncached writer reads on every protected HTTP request (1), root/audio WS upgrade (2), Git request (2) and NIP-11 GET (1, unauthenticated). The policy columns are already selected by the cached host lookup, so folding them into tenant_host_cache (1s max-age, coalesced misses) would bring most of these to zero writer reads. Because the requirement is monotonic, that costs at most 1s of staleness on a false→true flip, and BUZZ-512's drain has to handle that moment anyway. Inline comments have specifics.

Comment thread crates/buzz-relay/src/nip_fi_policy.rs Resolved
Comment thread crates/buzz-relay/src/router.rs Resolved
Comment thread crates/buzz-relay/src/audio/handler.rs Resolved
Comment thread crates/buzz-relay/src/state.rs Resolved

@baxen baxen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Two more items. Neither is on a changed line, so they're links instead of inline comments.

1. In Shadow, revocation doesn't close live sessions on required communities. The admin disconnect command and its cross-pod consumer still decide by the global mode:

In Shadow every disconnect records would_close and closes nothing. This PR makes Shadow the mode that enforces stored-required communities. So a revoked key on a required community is refused on new requests (the deny entry is kept), but its existing sockets stay open until they end on their own. I understand existing-session application belongs to BUZZ-512. Could that be called out explicitly as a precondition for accepting any requirement in Shadow? Alternatively, the close could key on whether the session was admitted with an enforced assertion rather than the global mode.

4. Enforcement metrics undercount required communities in Shadow. EnforcedProof::new (nip_fi_shadow.rs:536) is inert unless the global mode is Enforce. So buzz_nip_fi_enforce_strict_proof_total records nothing for requests that are actually enforced because their community is required under Shadow. Keying it on the request's effective mode (the per-request policy is already in scope inside the guard) would make the staging and rollout dashboards reflect real enforcement.

@baxen baxen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, but see the comments above which i think need to be addressed before mergin

bradseiler and others added 6 commits November 4, 2026 20:13
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Drain bounded optional leases before acceptance commits, independently of
notification delivery. Preserve authoritative upgrade binding rechecks.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Lock and validate the community row before deriving the host advisory key. Preserve that binding until commit so single and double host remaps cannot leave an optional lease alive after acceptance.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
@bradseiler
bradseiler enabled stacked auto-merge (squash) November 5, 2026 03:10
@bradseiler
bradseiler merged commit 64d12e2 into main Nov 5, 2026
108 of 109 checks passed
@bradseiler
bradseiler deleted the seiler/BUZZ-511/relay-policy-enforcement branch November 5, 2026 03:17
johnmatthewtennant added a commit to block/buzz-app that referenced this pull request Nov 5, 2026
…-11 (#2653)

Fixes #2449.

## Problem

Joining a self-hosted community (Helm chart, `buzz:0.2.1`) by address or
invite link says "No community is set up at this address", even though
the relay has a community and has just minted the invite.

The pre-join lookup (#1815) treats a Buzz NIP-11 document with none of
`read_state_snapshot`, `buzz_v1` or `icon` as an unmapped host. The
released relay,
[relay-v0.2.1](https://github.com/block/buzz/tree/6e5c462ac524de60d7edb46c66130fd779cc9006),
never serves the first two, and serves `icon` only after an admin sets
one. So every community on it without an icon is refused.

A current relay signals an unmapped host differently: NIP-11 answers 404
with `relay: no community is configured for this host`. The app shows
that as "This address isn't a Buzz community".

## Solution

`findCommunity` classifies `no-community` from the relay's unmapped-host
404, and accepts any other Buzz NIP-11 document. A relay that answers
200 for a host it doesn't map now gets past the lookup and fails at the
invite claim, since its NIP-11 can't tell the two apart. That covers
relay-v0.2.1 and relay `main` builds from
[6a86531ac](block/buzz@6a86531ac) (2026-09-21)
until [block/buzz#8988](block/buzz#8988)
(`64d12e297`, 2026-11-04). Staging already answers an unmapped host with
the 404.

## Screenshots

### Invite link to a relay-v0.2.1 community finds it

![Connect to your community for
localhost:3444](https://github.com/user-attachments/assets/66256184-6bd5-4d65-afb3-74fb66116f03)

### Joined through the invite

![Messages view after joining the 0.2.1
community](https://github.com/user-attachments/assets/b6fede89-277c-4474-9bb4-dadbf2f7376b)

## Validation

- `find-community.test.ts`: a relay-v0.2.1 NIP-11 document (no
`read_state_snapshot`, `buzz_v1` or `icon`) is found, and the relay's
unmapped-host 404 is `no-community`. `FindCommunity.test.tsx` shows "No
community is set up at this address" for that 404. All three fail before
the change and pass after.
- Native, at `ecea41e42`, against relay-v0.2.1 built from source behind
TLS (no `seed-local-community.sh`): an invite minted through `POST
/api/invites` and pasted under Other options reaches "Connect to your
community" for `localhost:3444`. Use an existing key, Join and a display
name land in the community's Messages view, and the relay records the
new member.

Signed-off-by: John Tennant <jtennant@block.xyz>
Co-authored-by: Martha <73923046810b68ae182000e74ef7c444383a907df20f81f2ba1008870a6f45a1@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
bradseiler added a commit that referenced this pull request Nov 6, 2026
## Summary

When a community starts requiring NIP-FI, the periodic community
revalidator now closes its existing legacy root and audio WebSockets
with Close 1008. It reads current policy from the writer by immutable
community ID, with a 500 ms budget per read. Failed reads retry on later
sweeps; late registrations are caught by the next pass.

Enforced FI admissions survive. Optional-shadow admissions remain
legacy. Off and repair mode retain their existing behavior. No client
request or Redis notification is needed.

The default sweep interval is 30 seconds, not a deadline during database
failure, overload or paused processes. Already-running requests and
descendant work may finish. Receipts prove policy acceptance and remain
`pending`. The `pending_reasons` value changes from
`registry_baseline_unverified` to `connection_closure_unverified`;
clients must tolerate unknown reasons. This change does not certify
fleet-wide completion.

### Related issue

[BUZZ-512](https://linear.app/squareup/issue/BUZZ-512), under the
revised eventual-closure scope. Builds on merged #8985 and #8988. The
replacement starts from main and adds no activation schema or migration.

### Rollout and limits

There is no new flag. Count already-required communities before
deployment: each relay closes their legacy sockets on its first
successful sweep. Closures within a community can cause simultaneous
reconnects. Keep new upgrades disabled until every serving relay runs
this version.

Observe `buzz_community_auth_policy_checks_total{outcome}` and
`buzz_community_auth_policy_disconnects_total` per relay, and verify
real idle root/audio Close 1008 probes plus FI reconnects. Counters
record checks and cancellation requests, not fleet completion. A binary
rollback requires no migration and restores the old pending reason, but
loses eventual closure; it does not undo durable policy. See the
[operator
contract](https://github.com/block/buzz/blob/1c74532516f697c2f06498646326f8c479574b14/docs/community-auth-upgrades.md).

### Testing

The real-socket journey runs a local relay against isolated PostgreSQL
and Redis. It covers idle root/audio closure after a durable change
without pubsub, actual read timeouts and recovery, late registration on
both routes, FI reconnects, unrelated sockets, and Off/repair/Enforce
controls.

With local PostgreSQL and Redis configured per the repository's test
guide, run:

```sh
CARGO_TARGET_DIR=target/fi-closure-review scripts/postgres-test-run.sh -p buzz-relay --lib -E 'test(sweep_closes_idle_root_audio_and_late_registrations_after_read_recovery)'
```

Expected: one passing journey, covering both late-registration routes
and all socket controls.

The journey passed at `1c74532516f697c2f06498646326f8c479574b14` in a
branch-specific build target (5.56 seconds). Independent source review
has no remaining P1/P2 findings. Earlier shared-target results are
unreliable and are not acceptance evidence. All mandatory push hooks
passed in the same isolated target, including Rust unit tests, both
Tauri Clippy modes, Tauri tests and file-size checks. Hosted CI passed
at this head. The full local `just ci` chain was not completed. Human
runtime acceptance remains open. No merge or deployment is included.

Generated with Codex

---------

Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>

This branch was successfully deployed

No deployments
codex-review — 39dfe79b Deployed Nov 5, 2026 by bradseiler via Run Codex Security Review #12756
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants