Manage community NIP-FI policy with enable, disable and status (#9176)

## 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 at d31793cc failed 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.

Historical d31793cc publication 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>
This commit is contained in:
Brad Seiler
2026-11-06 17:07:31 -05:00
committed by GitHub
parent 39d5159caf
commit 2cd40badb8
27 changed files with 2338 additions and 1144 deletions

View File

@@ -165,6 +165,9 @@ run_unit_tests() {
run_test_step "buzz-admin NIP-FI binding command tests" \
cargo test -p buzz-admin nip_fi::tests -- --nocapture
run_test_step "buzz-admin NIP-FI policy command tests" \
cargo test -p buzz-admin nip_fi_policy::tests -- --nocapture
run_test_step "buzz-admin user-deletion intake command tests" \
cargo test -p buzz-admin user_deletions::tests -- --nocapture