## Summary Operators can enable, disable and inspect a community's stored NIP-FI requirement through signed POST/GET `/operator/communities/nip-fi-policy` and `buzz-admin nip-fi-policy`. This replaces the old enable-only endpoint and drops the `nip-fi-upgrade` command added earlier in this PR, along with obsolete compatibility code. Each saved intent binds its desired state and observed generation/state. Every accepted new intent advances the generation, including same-state changes. Committed retries recover history without reapplying policy; stale requests are refused. Current status exposes one pair of reusable `expected_*` preconditions. Historical receipts remain readable after policy changes or target deletion, including old rows with null preconditions. Their UUIDs cannot be reused for new requests. Migration 0085 stays additive; main’s default-channel recovery migration retains version 0084. It preserves old receipt rows and INSERT compatibility. Operator instructions cover use, recovery and client migration: [usage guide](33a1c0bb87/docs/operator-nip-fi-upgrade.md), [client contract](33a1c0bb87/docs/operator-nip-fi-policy-client.md). ### Rollout and containment The old `/operator/communities/nip-fi-upgrade` route is removed; GET and POST return 404 without its old auth handler. A pending sponsorship client still targets that route. Migrate that client to the supported API and deploy it with mutation dispatch paused before this relay version reaches its fleet. Its complete CAS preconditions must be durably saved before first dispatch and reused unchanged after a crash or lost response. This is a pending integration dependency, not a claim that the caller is deployed. Close `BUZZ_NIP_FI_UPGRADES_ENABLED` on every old relay. Keep `BUZZ_NIP_FI_CHANGES=false` on new relays. Apply schema first and finish the whole-fleet rollout before opening the new gate and resuming migrated clients. Before opening the new gate, verify the deployed artifact SHA on every pod and probe each pod directly: the old route must return 404, and a fresh, well-formed, signed policy request with a new UUID must return 503 `upgrades_disabled`. A committed retry is not a gate probe. Old pods with the old gate open can still accept unfenced enables. Do not roll back to those writers after a policy operation. The old variable is no longer read by this version; the new gate defaults off and controls both directions. Read accepted intent in `receipt`, current preconditions/effective policy through status, and effective discovery through `/info`. Enable stays `pending` because receipts cannot certify fleet-wide socket closure. Under global `enforce`, stored disable leaves FI effectively required. Closing the gate preserves receipt recovery; use a fresh CAS disable or existing archive controls for containment. Preserve uncertain requests and their UUIDs. This PR performs no live state change or deployment. ### Related issue N/A ### Testing Published repair head: `33a1c0bb87a6fa10212eb4ec60788e24189627e2`, incorporating main `39d5159caf`. The Git policy guards now apply only to matched paths. Unknown GET/POST paths on relays without a web directory return 404 instead of inheriting the internal endpoint’s 403. The matched internal endpoint retains its localhost restriction and 1 MB body limit, including its method fallback. The signed removed-route 404 assertion is unchanged. Main’s migration 0084 is unchanged; the additive policy migration is now 0085 with identical SQL. Its populated-receipt upgrade test starts at schema 84. Formatting and commit hooks pass; independent source review has no findings. At this exact head, formatting, all-target Clippy (`-D warnings`, relay/db/admin/pubsub), and test compilation (relay/db/admin, `--lib --tests --no-run`) pass with exit 0. The full native PostgreSQL lane passes: 1,373/1,373 tests, exit 0, including the unchanged signed removed-route 404 assertion, fresh-database migration and populated receipt upgrade from main schema 84 to 85. The first full package run used an uninitialized private database and stopped with two missing-table errors; that failed attempt is retained. After schema initialization and verification, the corrected full package rerun passes: 2,041/2,041 tests, exit 0 (1,396 skipped by unit selection). The separate PostgreSQL lane skips 2,064 tests by its selection. Both full runs use the exact published repair head. Hosted CI remains pending. The new router test reproduced the old failure with the exact localhost-only response body; the new production-router regression also passes in the corrected package run. Four individual guard mutations are running serially; their assertion-failure receipts and the restored final green run remain pending. Brad authorized one `git push --no-verify` of this repair conditional on passing lint and compilation. Those gates passed; the push exited 0 and both the remote branch and GitHub PR SHA were verified as `33a1c0bb87a6fa10212eb4ec60788e24189627e2`. The normal publication attempt was stopped before completion and its log is retained. This exception leaves remaining tests and acceptance requirements outstanding. Historical evidence below applies to the earlier published head, not the published repair. Published head: `d31793ccbcdfd748361bcd559358915862b5ed57`. This integrates main's missing `ChannelEvent.received_at` fixture correction and corrects the newly integrated initial-policy test to use `nip_fi_changes`, with matching documentation. Independent source review has no open findings. At this exact head, all-target Clippy with `-D warnings` passes for buzz-relay, buzz-db, buzz-admin and buzz-pubsub; independent Clippy and formatting checks also passed. The normal push hooks failed (push exit 1): - Rust: `buzz-acp` recovery test hit its 15-second test deadline. Nextest completed 844/1101 tests: 843 passed, 1 failed, 3 skipped; 257 were not run. - Tauri: `buzz-terminal` login-shell test received no child output within its 10-second test deadline; 26 passed, 1 failed. - Mobile: gallery test `2 trailing images match carousel at 208.0, scale 1.0` expected no loading placeholders and found two; 4,524 passed, 60 skipped, 1 failed. These are two test-level deadline failures and one assertion failure, not compiler errors. The failing source paths are unchanged from main `6c3ffd160b`; no baseline reproduction or flaky classification is claimed. Desktop JS checks/tests/typecheck, branch-skew and file-size checks passed. Full hooks are not reported as passing. The first hosted PostgreSQL run atd31793ccfailed the removed-route assertion (expected 404, got 403); the router repair above addresses that failure. That hosted run’s Rust unit, Tauri and mobile jobs passed. Historicald31793ccpublication exception: Brad explicitly authorized a single `git push --no-verify` for this head on 2026-11-06 after the failed normal-hook run. That push exited 0 and its remote SHA was verified. Failed-hook evidence is retained. The earlier `cf2c9ba599` publication also used an explicit exception. Neither exception waives outstanding validation, review or human acceptance before merge. Historical evidence at `5b2eb6f09366b8a8f480a5fc61e8e5368f62302d`: 2,006 affected-package tests and 32 disposable-relay checks passed. These predate the latest main merge and are not final-head evidence. Earlier native PostgreSQL and separate `just ci` / `just test` attempts were incomplete, interrupted or failed and are not passing results. No staging run or human runtime confirmation is claimed. This PR is not a draft and is ready for review; readiness does not attest human runtime confirmation. Generated with Codex --------- Signed-off-by: Brad Seiler <seiler@squareup.com> Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
4.0 KiB
Manage a community's NIP-FI requirement
Use buzz-admin nip-fi-policy to enable, disable or inspect a community's
stored requirement through the deployment operator API. The command needs the
configured RELAY_OPERATOR_API_ORIGIN and an allowlisted operator signing key
in a protected file. It sends one signed request and never retries automatically.
Before rolling, close BUZZ_NIP_FI_UPGRADES_ENABLED on every old relay and
keep BUZZ_NIP_FI_CHANGES=false on new relays. Pause callers and upgrade them
for the supported API before deploying this
relay version. Apply the schema and roll compatible management writers across
the whole fleet. Then explicitly
enable that gate, which controls both enable and disable. Enable additionally
requires the deployment-selected issuer to be trusted and already bound to the
community. The active community must have exactly one owner; the authenticated
actor mediated by the operator must be that owner. See the
policy contract and binding guide.
Read current preconditions first:
buzz-admin nip-fi-policy --origin https://operator.example.org --key-file operator.key \
status --community-id c9acf9a6-37d5-4af1-8df9-92a165637b01 --host team.communities.example.org
Save a new request file with a fresh operation UUID and the exact
expected_policy_generation and expected_requires_nip_fi from that response:
{
"operation_id": "c2ffcc20-9505-48fd-b6c7-446aa833bcd1",
"community_id": "c9acf9a6-37d5-4af1-8df9-92a165637b01",
"expected_host": "team.communities.example.org",
"actor_pubkey": "<owner's registered lowercase hex public key>",
"expected_owner_pubkey": "<same current owner's public key>",
"expected_issuer": "https://identity.example.org",
"requires_nip_fi": true,
"expected_policy_generation": 0,
"expected_requires_nip_fi": false
}
buzz-admin nip-fi-policy --origin https://operator.example.org --key-file operator.key \
enable --request policy.json
buzz-admin nip-fi-policy --origin https://operator.example.org --key-file operator.key \
status --operation-id c2ffcc20-9505-48fd-b6c7-446aa833bcd1
To disable, save a new file/UUID with requires_nip_fi:false, omit
expected_issuer, and copy freshly read preconditions. Run disable --request
with that file. The subcommand checks the saved action before reading the key
or sending anything. A same-state operation also advances the generation,
creating a barrier against earlier uncommitted requests.
After policy_conflict, re-read current policy and use a new UUID for the revised
intent. After a timeout or lost response, keep the complete original file and UUID:
read its status or retry that exact file. A committed replay never reapplies policy.
| Exit | Result |
|---|---|
| 0 | Current-state read, or applied stored disable |
| 10 | Enable accepted; connection closure remains unverified (pending) |
| 11 | Historical acceptance superseded by a later generation |
| 12 | Receipt target or required trust unavailable |
| 5 | Invalid input, API refusal or uncertain transport/response |
Inspect mode and effective_required separately from the stored flag.
enforce keeps FI required after disable; off suspends FI after enable.
shadow follows the stored flag. An enable receipt remains pending even after
local sweeps, since it cannot certify fleet-wide connection closure. Required
host caches can over-restrict for their remaining one-second lease after disable.
A racing sweep may close one last non-FI socket; it can reconnect under optional
policy. FI-admitted sockets retain their finite deadline. Re-enable repeats the
optional-lease drain and existing periodic revalidation.
The old nip-fi-upgrade command and endpoint are removed. Requests to that
route now return 404. Recover historical receipts with nip-fi-policy status --operation-id; their original null CAS preconditions remain visible. Never
reuse a historical operation UUID for a new intent.