mirror of
https://github.com/block/buzz.git
synced 2026-11-06 17:31:19 +00:00
## 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](https://linear.app/squareup/issue/BUZZ-511/relay-enforce-per-community-nip-fi-policy-and-publish-compatible). [BUZZ-510 / #8985](https://github.com/block/buzz/pull/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](docs/community-auth-upgrades.md#staging-and-containment) 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 --------- Signed-off-by: Brad Seiler <seiler@squareup.com> Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
396 lines
24 KiB
Markdown
396 lines
24 KiB
Markdown
# Community NIP-FI upgrade operator contract
|
|
|
|
The acceptance layer records durable policy. The per-request enforcement layer
|
|
adds community checks and discovery; the activation layer updates existing sessions.
|
|
Do not expose upgrade calls in a deployment until those releases and the serving
|
|
fleet baseline have been verified. This change alone cannot establish enforcement.
|
|
It never reports `active` and never substitutes a timer for activation evidence.
|
|
|
|
The operator HTTP plane is separate from tenant event ingest. These management
|
|
operations extend the existing provisioning API; community clients cannot invoke
|
|
them with ordinary membership credentials. Both routes require an allowlisted
|
|
operator's NIP-98 signature bound to the configured operator API origin, exact
|
|
method and URL, including the query string on GET, and the POST body hash.
|
|
Operator POST verification does not bind query strings. Each attempt needs a fresh
|
|
signature; operation idempotency does not bypass transport replay protection.
|
|
Responses use `Cache-Control: no-store`.
|
|
|
|
`BUZZ_NIP_FI_UPGRADES_ENABLED` defaults to `false`. While disabled, new
|
|
acceptances return 503 `upgrades_disabled` with no policy mutation or
|
|
receipt. Authorized GET and identical POST receipt recovery remain available;
|
|
principal checks and payload conflicts are unchanged. Enabling this deployment
|
|
switch is an explicit operator action after the enforcement and activation fleet
|
|
baseline is verified; caller release discipline alone cannot enable acceptance.
|
|
|
|
## Generation storage
|
|
|
|
`communities.policy_generation` is `BIGINT NOT NULL DEFAULT 0`, without a
|
|
nonnegative database CHECK. Supported writers retain the current generation
|
|
when the community already requires FI, or increment it by one on acceptance.
|
|
Starting from zero, those writes preserve the nonnegative invariant. Direct SQL
|
|
can insert a negative community generation and is outside that writer contract.
|
|
Positive-generation CHECKs on acceptance receipts remain in place; activation
|
|
and receipt matching still require the exact accepted generation.
|
|
|
|
The community CHECK is omitted to avoid validating existing rows while adding
|
|
the column. The current schema planner folds a column and its CHECK together,
|
|
and can rebuild and validate a CHECK when its validation metadata differs.
|
|
Migration, fresh desired-state creation and later reconciliation therefore all
|
|
omit this community CHECK. No bootstrap or schema-tooling workaround is required.
|
|
|
|
## Enable
|
|
|
|
`POST /operator/communities/nip-fi-upgrade`
|
|
|
|
```json
|
|
{
|
|
"operation_id": "c2ffcc20-9505-48fd-b6c7-446aa833bcd1",
|
|
"community_id": "c9acf9a6-37d5-4af1-8df9-92a165637b01",
|
|
"expected_host": "team.communities.example",
|
|
"actor_pubkey": "<owner's registered 64-character hex public key>",
|
|
"expected_owner_pubkey": "<expected current owner's hex public key>",
|
|
"expected_issuer": "https://identity.example/api/goose"
|
|
}
|
|
```
|
|
|
|
The trusted operator mediates the authenticated actor. Kgoose must resolve that
|
|
actor's usable registered key from its session; it must never accept a user-supplied
|
|
actor key as identity. The actor must equal the expected and current owner.
|
|
`actor_pubkey` records the mediator's claimed authenticated actor for audit and
|
|
idempotency; it is not an independently verified end-user credential. The relay
|
|
authenticates the operator and checks the stored current owner.
|
|
Host and keys normalize before comparison; issuer bytes match exactly. UUIDs
|
|
must be non-nil. Unknown JSON fields are rejected.
|
|
|
|
The deployment selects the expected issuer from the **single** authorized issuer
|
|
in `BUZZ_NIP_FI_PROVISION_BINDING`. An absent or ambiguous selection is unavailable.
|
|
The target must be bindable under that policy and the issuer must have a configured
|
|
`BUZZ_NIP_FI_ISSUERS` policy. The same issuer must already be bound to the community
|
|
in `community_nip_fi_issuers`. The call never adds or broadens trust.
|
|
|
|
A transaction serializes UUID reuse across targets, then community lifecycle,
|
|
host and owner checks against transfer/archive/delete. It locks the exact issuer
|
|
row against unbind. Only a first optional-to-required transition increments
|
|
`policy_generation`; concurrent distinct operations converge on that generation.
|
|
Acceptance and its receipt commit together. No timeout or retry clears the policy.
|
|
|
|
## Readback and response
|
|
|
|
`GET /operator/communities/nip-fi-upgrade?operation_id=<UUID>`
|
|
|
|
Only the original authenticated operator can recover the receipt. Readback
|
|
survives owner transfer, archive and deletion; the recorded owner is historical
|
|
acceptance evidence, not authority for a fresh enable.
|
|
|
|
Enable and readback return HTTP 200 for an accepted receipt:
|
|
|
|
```json
|
|
{
|
|
"operation_id": "c2ffcc20-9505-48fd-b6c7-446aa833bcd1",
|
|
"community_id": "c9acf9a6-37d5-4af1-8df9-92a165637b01",
|
|
"expected_host": "team.communities.example",
|
|
"actor_pubkey": "<owner at acceptance>",
|
|
"expected_owner_pubkey": "<expected owner at acceptance>",
|
|
"expected_issuer": "https://identity.example/api/goose",
|
|
"operator_pubkey": "<authenticated operator>",
|
|
"policy_generation": 1,
|
|
"requires_nip_fi": true,
|
|
"state": "pending",
|
|
"pending_reasons": ["registry_baseline_unverified"]
|
|
}
|
|
```
|
|
|
|
`requires_nip_fi: true` records durable acceptance. `state` reports availability
|
|
and activation separately. The acceptance layer returns `pending` while the target's policy,
|
|
lifecycle and issuer binding still match, and `unavailable` if they do not or
|
|
trusted issuer configuration is no longer applicable. A missing database response
|
|
is HTTP 503, never a fabricated receipt. A receipt is not proof of current service
|
|
health. No response in this version reports `active`.
|
|
|
|
`pending_reasons` applies only while `state` is `pending`; clients must ignore
|
|
those obligations when state is `unavailable`, which always takes precedence.
|
|
`pending_reasons` is extensible. Clients must preserve pending on unknown values.
|
|
`registry_baseline_unverified` prevents completion until the activation layer verifies fleet
|
|
membership. That layer may add `boot_application_pending`, `boot_drain_pending`
|
|
and `boot_unreachable`, and must bind activation evidence to the exact operation
|
|
and generation. Baseline invalidation must prevent `active`.
|
|
|
|
The UUID binds the normalized community/host/actor/owner/issuer/operator tuple.
|
|
Identical retries recover that tuple and generation before rechecking mutable
|
|
preconditions. Changed payload or principal conflicts. After a lost POST response,
|
|
read by UUID or retry the same payload with a fresh NIP-98 signature. Do not generate
|
|
a new UUID to hide an ambiguous response. No rollback endpoint exists.
|
|
|
|
| HTTP | `code` | Meaning |
|
|
|---|---|---|
|
|
| 400 | `invalid_request` | Invalid enable JSON, UUID, host or key |
|
|
| 401/403 | Existing operator auth errors | Missing/invalid credentials or operator not allowlisted |
|
|
| 404 | `community_not_found` | No target for a first acceptance |
|
|
| 404 | `operation_not_found` | No receipt owned by this operator |
|
|
| 409 | `idempotency_conflict` | UUID already bound to a different tuple/principal |
|
|
| 409 | `host_conflict` | ID does not map to expected host |
|
|
| 409 | `owner_conflict` | Actor/expected owner/current owner disagree |
|
|
| 409 | `lifecycle_conflict` | Archive/deletion or durable deletion intent |
|
|
| 409 | `issuer_conflict` | Expected issuer/host not approved, or exact stored binding absent |
|
|
| 503 | `upgrades_disabled` | New acceptances disabled; wait for operator enablement, preserving the same UUID |
|
|
| 503 | `authorization_unavailable` | Trusted config or authoritative DB unavailable; retry/read back same UUID |
|
|
|
|
Malformed GET queries use Axum's existing query-extractor rejection. Transport
|
|
signature replay uses the existing operator replay error independently of this table.
|
|
|
|
## Schema and rollout
|
|
|
|
Migration 0082 is additive: legacy rows keep `requires_nip_fi=false` and generation
|
|
zero. It adds no trigger, function, procedure, rule or foreign key and rewrites no
|
|
existing rows. Lock and statement budgets are 5 and 30 seconds. This migration
|
|
changes no data-plane gate. Relay mode determines whether stored requirements
|
|
are enforced, as described below.
|
|
Do not infer requirements from issuer rows, backfill free communities, or remove
|
|
the enterprise enforce floor as part of this migration.
|
|
|
|
Apply schema before new code. Desired-state and migration definitions match;
|
|
The registry entry is static lint metadata, so it is present in the migration
|
|
and `schema/seeds/`, not the pgschema reconciliation script (database.md Rule 3).
|
|
This follows the other operator-global registry entries. Receipts intentionally survive community purge:
|
|
the target ID/host/owner tuple prevents reusing a lost-response UUID for another
|
|
community, including after deletion. They are operator-only recovery evidence.
|
|
No tenant serving writer owns this table and no fence trigger is attached.
|
|
|
|
The active-community primary accessor is
|
|
`Db::community_auth_policy(CommunityId) -> Result<Option<CommunityAuthPolicy>>`.
|
|
`None` means no active target. Repair mode denies protected traffic first. In
|
|
`off`, no community requires FI, including a stored-required community; FI lookup
|
|
or dependency failures must not deny ordinary admission. In `shadow`, enforce
|
|
stored requirements and observe optional communities. In `enforce`, require FI
|
|
for every community. Evaluating policy lookups fail closed on errors. Mode
|
|
changes do not clear the stored requirement, generation, issuer trust or receipts.
|
|
|
|
## Mistaken acceptance and containment
|
|
|
|
Stop the initiating caller and set `BUZZ_NIP_FI_UPGRADES_ENABLED=false` to prevent
|
|
new acceptances. Keep authenticated status/readback available. Record the exact
|
|
operation, target and generation for operator-led incident assessment and recovery.
|
|
Preserve the durable requirement, issuer trust and receipts while restoring a
|
|
working FI path or containing the affected community through the established
|
|
archive/lifecycle controls. Coordinate any necessary issuer/configuration repair
|
|
through its existing operator procedure; never broaden trust to make a probe pass.
|
|
|
|
Disabling calls or rolling back code does not undo acceptance. In Shadow, never
|
|
restore a serving binary that ignores the durable requirement; retain effective enforcement
|
|
or contain the community while repairing the FI path. There is no
|
|
clear-policy API or routine SQL reset. A timeout or mistaken call does not
|
|
authorize clearing the requirement or discarding operation evidence.
|
|
|
|
An explicit operator change to `off` intentionally suspends FI enforcement for
|
|
all communities while preserving durable requirements and receipts. This is a
|
|
relay-wide mode choice, not an undo of acceptance or a binary rollback. Readback
|
|
must distinguish stored policy from effective enforcement and must not report
|
|
active enforcement in Off. Returning to `shadow` resumes enforcement for stored
|
|
required communities; `enforce` requires FI everywhere. Off suspends FI for
|
|
every community; it is not per-community containment. Choose relay-wide Off
|
|
suspension or archive/lifecycle containment explicitly; neither a fleet-wide
|
|
drain nor a deployment is an automatic fallback.
|
|
|
|
Last-issuer removal is refused once the community requires NIP-FI, including
|
|
when the operator binding path requests `allow_last`. Bind a replacement before
|
|
removing an issuer, or use archive/lifecycle controls for containment. Removing
|
|
the issuer recorded by a receipt while another remains makes that receipt
|
|
`unavailable`; it never clears the requirement or destroys the receipt.
|
|
Bulk backfill does not recreate missing bindings for required communities;
|
|
its unbound report still exposes them for explicit operator repair.
|
|
|
|
## Request enforcement and discovery (BUZZ-511)
|
|
|
|
Relay mode controls the effective requirement without changing durable storage:
|
|
`off` disables FI for all communities, including stored `requires_nip_fi=true`;
|
|
`shadow` enforces FI only for stored required communities and observes optional
|
|
communities; `enforce` requires FI everywhere. `deny_protected` remains an
|
|
unconditional repair refusal. Off ignores missing FI policy, verifier, deny map
|
|
and issuer dependencies; ordinary tenant routing and Nostr authorization remain.
|
|
Outside Off, protected requests resolve their normalized Host's active policy
|
|
through a bounded primary-read lease cache, with a 500 ms budget on misses.
|
|
Existing Nostr proof, key pairing, deny map, membership and resource checks remain.
|
|
Discovery, pairing, enrollment and operator exceptions keep their own authentication.
|
|
|
|
The router shares one policy snapshot with HTTP handlers for that request. Git
|
|
also scopes its extractor to one policy and tenant snapshot, retaining its
|
|
existing header, signed-URL and denial ordering. Routing and policy share one
|
|
host cache capped at 10,000 entries. A serving entry carries immutable tenant ID,
|
|
requirement, generation and original read time from the same row. It is usable
|
|
for strictly less than one second from before the read's connection checkout;
|
|
neither a slow read nor a cache hit renews its lifetime. Concurrent fills for a
|
|
host share a successful read. Misses, errors and timeouts are never cached. A late result
|
|
after invalidation is usable once by its in-flight request but never published,
|
|
and an older generation cannot replace a newer
|
|
observed generation for the same community. Off retains ordinary routing-only
|
|
cache population and does no FI-policy fills.
|
|
|
|
Cache fills take a shared transaction-scoped advisory lock keyed by normalized
|
|
host in a dedicated namespace. Lock acquisition and the serving-row SELECT are
|
|
separate statements in an explicit READ COMMITTED transaction. A contended fill
|
|
refuses immediately; a statement has a 400 ms server-side timeout inside the
|
|
500 ms overall budget. Waiters on the per-host fill share its successful result;
|
|
when no result was cached, each waiter makes at most one lease read of its own
|
|
inside that same budget, with no retry loop. Unmapped hosts remain 404.
|
|
Each cold policy fill uses six database statements: BEGIN, isolation setup,
|
|
statement-timeout setup, try-lock, row SELECT and COMMIT. Ordinary routing keeps
|
|
its single plain serving-row query and a routing-only snapshot in the same
|
|
bounded host cache. That snapshot cannot supply FI policy. A routing refresh
|
|
preserves a fresh policy for the same tenant without extending its lease.
|
|
|
|
Acceptance takes the shared deletion lock and locks the target community row
|
|
`FOR NO KEY UPDATE`. It checks the row's host against `expected_host`, then takes
|
|
the exclusive advisory lock derived from that locked host. An optional row waits
|
|
1.1 seconds before the remaining owner/issuer checks and commit. The row stays
|
|
locked throughout, preventing single or double host remaps during the drain.
|
|
This also expires leases issued under any former host before row-lock acquisition.
|
|
An initially wrong host precondition is refused before waiting.
|
|
|
|
This drains every optional one-second lease across all pods BEFORE commit,
|
|
without relying on Redis delivery or synchronized wall clocks. The extra 100 ms
|
|
is a clock-rate margin. Lease duration and acceptance wait are compile-time fleet
|
|
protocol constants; increasing the lease requires a coordinated writer rollout
|
|
first. Existing receipt recovery skips this wait. Already-required targets need
|
|
no lease drain. Fills take only a shared advisory host lock and never lock the
|
|
community row, preserving the deletion-shared → row → host-lock ordering.
|
|
|
|
Policy resolutions that start after acceptance commits therefore cannot use an
|
|
optional lease for that community, even on a pod that missed invalidation.
|
|
Admissions that resolved policy before commit can still be in flight, exactly
|
|
as with primary reads; their completion and cancellation belong to BUZZ-512's
|
|
generation-registration and drain fence. Cache expiry is not activation evidence.
|
|
The acceptance drain blocks new policy fills and other acceptances for the
|
|
host. Its community row lock also blocks archive/unarchive, deletion admission
|
|
and fences, ownership transfer/convergence and issuer-binding writers for that
|
|
community during the 1.1-second wait. These are operator actions; ordinary
|
|
serving reads and writes do not take a conflicting row lock. Foreign-key
|
|
`FOR KEY SHARE` locks do not conflict with `FOR NO KEY UPDATE`, and the shared
|
|
deletion lock remains compatible with ordinary tenant writers. Cold policy
|
|
requests during the transition may return 503; acceptance is rare and this
|
|
avoids allowing stale optional leases after commit.
|
|
|
|
Relay acceptance, provisioning and archive/unarchive writes drop the local host
|
|
snapshot and publish a best-effort Redis invalidation to other pods. An epoch
|
|
fences fills that began before a local or received drop. Direct administrative
|
|
database writes and missed notifications retain the one-second routing bound.
|
|
Host remap, archive and deletion do not drain leases: cached tenant binding can
|
|
lag those changes by up to one second, as ordinary host routing already does.
|
|
Policy and tenant data always use the same cached row, including NIP-11
|
|
descriptors and decorations. Issuer-binding and JWKS freshness keep their own
|
|
existing bounds and checks; cached status never substitutes for those checks.
|
|
|
|
`buzz_nip_fi_policy_cache_hits_total` and `buzz_nip_fi_policy_cache_misses_total`
|
|
measure cache use; `buzz_nip_fi_policy_reads_total` counts fill attempts and
|
|
`buzz_nip_fi_policy_read_seconds` measures completed source reads. A warm ordinary
|
|
protected request or discovery request performs no policy writer read. Root and
|
|
audio upgrades intentionally keep a final uncached authoritative tenant/policy
|
|
binding after the early evidence gate: one read on a cache hit, two on a cold
|
|
successful upgrade. Its optional-to-required recheck preserves the unarchive and
|
|
host-remap race fence, as well as early refusal and deadline ordering.
|
|
|
|
Issuer-binding/JWKS caches keep their existing bounds; unavailable dependencies
|
|
refuse required admission, rather than treating it as optional.
|
|
|
|
WebSocket upgrades also bind tenant identity and its policy from the same primary
|
|
row. An earlier missing or optional policy result cannot authorize a socket for a
|
|
required row that becomes serving during lookup (for example, on unarchive).
|
|
Before upgrade, enforce that bound row's requirement even when the early check
|
|
ran in Shadow. Off never promotes a stored requirement. This changes no discovery fields.
|
|
|
|
Database-sourced deployments load the FI binding snapshot and command components
|
|
only in evaluating modes. Off does not depend on that snapshot and cannot report
|
|
required-admission readiness. Configure finite
|
|
`BUZZ_NIP_FI_MAX_CONNECTION_LIFETIME_SECS`, issuer JWKS and command configuration
|
|
before enforcing any row in Shadow or Enforce. Missing required components return
|
|
503 only when FI is enabled. An optional row
|
|
still follows global off/shadow behavior, including signed and unsigned legacy
|
|
proofs where the endpoint permits them.
|
|
|
|
Both `GET /info` and root NIP-11 negotiation return `Cache-Control: no-store`.
|
|
An unknown or non-serving Host returns generic 404. Outside Off, policy lookup
|
|
failure returns 503. Off uses ordinary tenant routing and returns generic 404 on
|
|
a tenant lookup failure, without consulting FI policy. The top-level `requires_nip_fi` is a host-specific boolean. Existing
|
|
`limitation.federated_identity` and `federated_identity` descriptor fields retain
|
|
capability semantics: advertise when global mode evaluates OR the host requires
|
|
FI. Shadow optional hosts still advertise capability. Off hosts advertise neither
|
|
FI capability nor an effective requirement, even when stored policy is required. No issuer, audience or identity
|
|
provider address is added to discovery. Existing clients can keep reading their
|
|
current fields; new clients must use the requirement field to distinguish optional
|
|
capability from mandatory evidence.
|
|
|
|
This is admission enforcement only. Existing requests and sockets may predate the
|
|
new policy. Only BUZZ-512's application and drain evidence may establish completed
|
|
activation; the operator receipt remains pending until that evidence is recorded.
|
|
|
|
### Staging and containment
|
|
|
|
Outside Off, this changes every serving host, including free hosts: protected
|
|
routes and discovery require a valid primary policy lease. Off retains ordinary routing. Unmapped discovery previously could
|
|
return a generic document; it now returns 404. Policy failures return 503.
|
|
Database-source startup loads its FI binding snapshot only in evaluating modes;
|
|
Off does not load it. A failed load still fails startup in Shadow or Enforce. The local dependency gauge
|
|
`buzz_nip_fi_required_admission_ready` must be 1 before accepting any upgrade;
|
|
it covers verifier, deny map and finite lifetime, not fleet baseline verification.
|
|
Binding and JWKS readiness must also be verified through a signed request.
|
|
|
|
Keep `BUZZ_NIP_FI_UPGRADES_ENABLED=false` throughout a mixed-version rollout
|
|
until EVERY acceptance path implements the host-lock lease-drain protocol.
|
|
Uncached serving pods can coexist with caching pods, but an old acceptance
|
|
writer cannot safely coexist with cached optional leases. Before rolling an
|
|
acceptance writer back or removing this protocol, disable new acceptance on
|
|
every writer and verify that disablement; keep the lease-aware writers until
|
|
in-flight acceptance has finished. Disabling new calls does not undo existing
|
|
requirements. Existing required-community enforcement and BUZZ-512 activation
|
|
and revocation prerequisites still apply. Never enable an older acceptance path
|
|
while any caching pod serves optional communities.
|
|
Older pods may log an unknown `CommunityHost` cache-invalidation message during
|
|
the mixed-version rollout. They keep serving through their existing uncached
|
|
policy reads; missing that best-effort notification does not weaken the lease
|
|
protocol. The acceptance-writer gate above still applies.
|
|
|
|
Keep `BUZZ_NIP_FI_UPGRADES_ENABLED=false` during the initial staging rollout.
|
|
Start with zero required rows and record a 30-minute baseline using representative
|
|
free signed/unsigned HTTP, root/audio WS and discovery traffic. Compare identical
|
|
traffic before and after this change. Record request rate, policy-read p95/p99,
|
|
writer pool acquisition p99 (`buzz_db_pool_acquire_duration_seconds`,
|
|
`pool_role=writer`, `operation=authorization`), policy timeout/error count, HTTP
|
|
503 rate and startup/restart success. A local PostgreSQL 16 probe measured 100
|
|
serial reads in 13.36 ms; that is not a production capacity result.
|
|
|
|
Initial staging abort criteria (validate before increasing traffic): any policy
|
|
timeout, policy-induced 503, unexpected free-admission denial or startup failure;
|
|
policy-read p99 above 50 ms; writer acquisition p99 above 20 ms or more than twice
|
|
its pre-change baseline; total 503 rate more than 0.1 percentage point above
|
|
baseline over five minutes. These are conservative rollout limits, not measured
|
|
production guarantees. Do not expand until the full 30-minute run stays within
|
|
them and the operator has reviewed measured results. Verify unknown-host 404 and
|
|
dependency-failure 503 with no-store explicitly.
|
|
|
|
Then test a disposable required community with complete FI dependencies, in both
|
|
Shadow and Enforce: unsigned/missing evidence denies, signed valid FI succeeds,
|
|
root/audio upgrades agree, discovery agrees, and a pending deletion request does
|
|
not weaken the requirement. Before production acceptance, complete BUZZ-512's
|
|
session application/drain and the production baseline-verifier gate. No such
|
|
production verification is established by this PR.
|
|
|
|
Before any required row exists, an operator may stop the rollout and restore the
|
|
previous binary after verifying the primary contains zero required rows. Once a
|
|
requirement has been accepted, a pre-511 binary in Shadow is unsafe: it ignores
|
|
that row. Off explicitly suspends FI; global Enforce still requires all hosts.
|
|
Stop new upgrade calls, retain policy and receipt records, and keep an enforcing
|
|
binary while repairing dependencies. If admission cannot be restored, contain the
|
|
affected serving hosts with an operator-approved repair-mode/traffic action;
|
|
Off explicitly disables FI; use it only as an operator mode decision and never
|
|
clear accepted policy. Returning to Shadow restores stored requirements. No automatic fleet
|
|
drain or deployment is prescribed here. Existing receipt recovery remains usable
|
|
while new acceptances are disabled.
|
|
|
|
Required sessions admitted under global Shadow are real enforced FI sessions.
|
|
The existing global-Shadow admin disconnect path still only observes would-close;
|
|
BUZZ-512 must close these enforced sessions independently of global rollout mode,
|
|
while preserving the separate optional-shadow registry. Before enabling upgrade
|
|
acceptance for any serving community in Shadow, deploy and verify BUZZ-512's local
|
|
and cross-pod disconnect behavior, including real Redis propagation. Keep upgrade
|
|
acceptance disabled until that prerequisite is met; admission tests alone do not
|
|
establish it. Production also requires the baseline-verifier gate above.
|