Skip to content

docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 - #7214

Merged
wpfleger96 merged 9 commits into
mainfrom
duncan/nip-fi-v2-stateless
Sep 1, 2026
Merged

docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2#7214
wpfleger96 merged 9 commits into
mainfrom
duncan/nip-fi-v2-stateless

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Sep 1, 2026

Copy link
Copy Markdown
Member

Rewrites docs/nips/NIP-FI.md as a stateless spec and deletes the five companion profiles (NIP-FI-CONF, NIP-FI-DELEG, NIP-FI-EDGE, NIP-FI-LIFECYCLE, NIP-FI-MODEL) that presuppose relay-side state.

What changes

docs/nips/NIP-FI.md — full rewrite

The relay-side authority engine (bindings, receipts, lifecycle, invalidation, leases, delegation, enrollment modes, TOFU) is removed. The spec settles exactly five things:

  1. Assertion contract: JWT claims (iss, sub, nostr_pubkey, aud (required), iat/exp, finite TTL bounds), npub binding semantics, required NIP-42 proof-of-key pairing. Evidence rules (assertion-key-mismatch → authorization_denied, freshness) preserved from v1.

  2. Verification: offline against configured per-issuer JWKS snapshots; multi-issuer; fail-closed on missing/expired/unverifiable. Cites crates/buzz-auth/src/nip_fi/ (PR 3 / 70895b355) as implementing the assertion-verification procedure. The require_attested_key flag in IssuerPolicy is the enforcement primitive for the unconditional nostr_pubkey requirement; conformance requires forcing it true for every issuer — follow-on code outside this PR.

  3. Session policy: required finite max_connection_lifetime_seconds knob with no permissive default; re-auth on every reconnect; no in-band renewal.

  4. Admin disconnect API: session-only normative text with a dedicated VerifyCommandJwt procedure — distinct typ: nip-fi-command+jwt, signed method/path binding, normative maximum_command_age bound (0 < value ≤ 60 s), atomic (iss, jti) reservation as the final admission step after all pure checks. Non-normative note documents cumulative residual access: max(0, min(exp, iat + maximum_assertion_age) - now) assuming issuance stops; states that previously issued assertions remain valid for reconnection until their authority expires; states that continued issuance extends access with no protocol-level bound. Non-normative note documents the session-only vs deny-until-TTL trade-off. This verifier and endpoint are follow-on code.

  5. Deletions: bindings, enrollment, lifecycle, receipts, leases, invalidation, delegation gone. "Out of scope" section names every issuer and deployment concern explicitly.

Companion profiles deleted

NIP-FI-CONF, NIP-FI-DELEG, NIP-FI-EDGE, NIP-FI-LIFECYCLE, NIP-FI-MODEL — all presuppose relay-side binding/lifecycle state.

What does NOT change

The entire crates/buzz-auth/src/nip_fi/ Rust crate (PR 3 / 70895b355) implements the assertion-verification procedure described in this spec. No Rust code changes in this PR. The require_attested_key enforcement integration and the VerifyCommandJwt / disconnect endpoint implementation are explicit follow-on code work.

Follow-on work (separate PRs after spec merges)

  • require_attested_key forced-true startup enforcement
  • VerifyCommandJwt implementation and disconnect endpoint
  • Down-migration dropping migrations 0041/0042 (0042 before 0041 per FK order)
  • schema.sql NIP-FI sections removed
  • buzz-db migration test blocks cleaned up

Remove the relay-side authority engine (bindings, receipts, lifecycle,
invalidation, leases, delegation, enrollment modes) entirely. The new
spec settles exactly five things: assertion contract (JWT claims, npub
binding, NIP-42 pairing), offline JWKS verification (multi-issuer,
fail-closed), session policy (required finite max-lifetime knob, no
in-band renewal), admin disconnect API (session-only model with an
explicit non-normative note documenting the deny-until-TTL alternative
for Will/Tyler to adjudicate), and the deletions surface.

The PR 3 / 70895b3 multi-issuer JWKS runtime (FederatedAssertionVerifier,
ProductionJwksSource, IssuerRegistry, DenialClass) is the complete
implementation surface for the new spec. No Rust code changes.

Companion docs (NIP-FI-CONF, DELEG, EDGE, LIFECYCLE, MODEL) and
migrations 0041/0042 are flagged for deletion in separate work.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
NIP-FI-CONF, NIP-FI-DELEG, NIP-FI-EDGE, NIP-FI-LIFECYCLE, and
NIP-FI-MODEL all presuppose relay-side binding/lifecycle state that the
v2 stateless spec removes. A deferred doc that assumes removed state is
worse than absence. Edge transport and delegation can be re-proposed as
separate profiles if they become relevant under the stateless design.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
CRITICAL: replace unsigned disconnect body with signed command JWT.
The disconnect endpoint now requires a command JWT verified against the
same per-issuer JWKS surface as client assertions. The JWT carries iss,
sub, aud, iat, exp, jti (replay guard), cmd='disconnect', and
target_pubkey. The relay verifies the signature, asserts cmd and
target_pubkey match the request body, checks jti replay, and confirms
the caller is an authorized adapter principal — all fail-closed.

IMPORTANT (time bounds): separate required JWT claims (iat, exp) from
policy knobs (maximum_assertion_age required positive finite; skew
non-negative finite with default 0; nbf optional).

IMPORTANT (key binding honesty): document that require_attested_key in
IssuerPolicy is the enforcement primitive and that NIP-FI v2 conformance
requires startup validation forcing it true — a follow-on code change.
Remove the 'complete implementation surface' claim from the spec text.

IMPORTANT (at+jwt subject contract): restore closed v1 rule — issuer
policy names one authenticated marker claim and two non-empty disjoint
value sets; absent/unknown/ambiguous classification denies; client-subject
acceptance requires recorded non-collision posture.

IMPORTANT (residual-window formula): distinguish upstream-revocation
(no disconnect call) from session-only disconnect. Upstream revocation:
max(existing session remaining lifetime, assertion TTL). Disconnect:
two sequential sub-windows — live session bounded by
max_connection_lifetime_seconds, then reconnect bounded by
min(remaining assertion TTL, max_connection_lifetime_seconds). Both
places in the doc now use the same scenario-specific formula.

MINOR: fix broken relative link ../crates -> ../../crates/buzz-auth/...
MINOR: 'enrolled Nostr key' -> 'assertion-named Nostr key'
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
CRITICAL: replace VerifyAssertion with dedicated VerifyCommandJwt for
the disconnect command. The command JWT carries 'typ: nip-fi-command+jwt'
(domain-separated from assertion types); signed 'method' and 'path'
claims bind it to exactly POST /api/nip-fi/disconnect; a dedicated
'maximum_command_age' policy knob governs TTL; and jti replay prevention
uses an atomic (iss, jti) reservation held until the command's effective
expiry. The procedure reuses IssuerRegistry, JWKS, signature, audience,
and time-bound primitives but produces a closed CommandResult — not a
VerifiedAssertion. VerifyAssertion is not called.

IMPORTANT (disconnect window): single correct formula in both the
non-normative note and Security considerations. After a successful
disconnect, old-session remainder is zero; the only residual is a
reconnect bounded by min(remaining assertion authority,
max_connection_lifetime_seconds, remaining key-snapshot hard deadline).
Contradictory two-part sequential formulation removed.

IMPORTANT (aud optionality): 'aud' promoted to unconditional required
claim in the required-claims table, matching the existing verifier
contract (IssuerPolicy requires non-empty audience; VerifyAssertion
always validates it).

MINOR: NIP-FI-MODEL added to the removed companion profiles list.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
IMPORTANT (reconnect window): distinguish individual-session bound from
cumulative residual access. After a successful synchronous disconnect,
cumulative access (assuming issuance stops) is bounded by
max(0, min(exp, iat+max_assertion_age, key_snapshot_deadline) - now).
max_connection_lifetime_seconds only partitions that interval into
sessions; it does not shorten the total window. State explicitly that
continued issuance after disconnect extends access without a
protocol-level bound. Same formula used in both the non-normative note
and Security considerations.

IMPORTANT (jti admission ordering): reorder VerifyCommandJwt steps so
AtomicReserveJti is the final admission step immediately before side
effects — after signature/claim validation, principal authorization, and
signed-target/body agreement. A failed-authorization or mismatched-body
request no longer burns the signed command identity. Procedure now
accepts request_body_pubkey as a parameter; body agreement is a pure
check inside the procedure. Make maximum_command_age upper bound
normative: 0 < maximum_command_age <= 60 seconds.

MINOR: narrow Request prose to reference VerifyCommandJwt directly
rather than re-describing steps now inside the procedure.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tions

The 'no reconnect can succeed' claim was framed as a consequence of
max_connection_lifetime_seconds, which does not govern reconnect
availability. Restate accurately: after the live session closes
naturally, reconnect requires a fresh assertion; if the adapter has
stopped issuing, none can be obtained; if the adapter continues issuing,
access continues. Removes the last session-bound confation from the
revocation analysis.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 deployed to codex-review September 1, 2026 22:41 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is beb76406c12ab8a7af9b2fcf7547c353e3369c34...dc5f99360d16f720d498aa18e121d7a3828c2c23.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review dc5f99360d16f720d498aa18e121d7a3828c2c23 to authorize a new review.
Any previous review applies only to its recorded range.

…not fixed

The key_snapshot_hard_deadline is computed as now + configured seconds on
every JWKS refresh (jwks/mod.rs), so it renews even on unchanged content
and does not cap cumulative access across serial reconnects. Remove it
from both residual-window formulas; the correct cumulative bound (assuming
issuance stops at disconnect) is:

  max(0, min(exp, iat + maximum_assertion_age) - now)

Add a note that snapshot failure, hard-deadline expiry, or key removal
can terminate access earlier but are not reliable protocol-level bounds.

Also fix the upstream-revocation paragraph: a still-valid compact JWS can
reconnect without new issuance, so 'requires a fresh assertion' was wrong.
Corrected to 'requires an assertion that remains valid when reverified'.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 1, 2026
@wpfleger96
wpfleger96 deployed to codex-review September 1, 2026 22:52 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 1, 2026
Replace all 'adapter' references with 'assertion issuer' / 'issuer'
throughout. The spec's trust anchor is the configured issuer registry;
the entity that authenticates users and mints assertions is the assertion
issuer, which is the term now used everywhere.

- 'adapter' Terms entry replaced with 'assertion issuer' definition
- Okta example removed; 'an OIDC identity provider integration' used instead
- 'employees' -> 'users'; 'employee number' -> 'opaque user ID'
- 'SCIM, HR system integration, employee offboarding automation' ->
  'directory integration and account-offboarding automation'
- AssertAuthorizedAdapterPrincipal -> AssertAuthorizedIssuerPrincipal
- 'adapter->relay disconnect API' -> 'issuer->relay disconnect API'
- 'adapter-initiated disconnect' -> 'issuer-initiated disconnect'
- 'adapter principal' -> 'issuer principal' in claim table and prose
- All remaining adapter/adapter-side/adapter's prose occurrences renamed
- PR body updated: 'adapter concern' -> 'issuer and deployment concern';
  residual-window formula corrected to drop key_snapshot_hard_deadline

No normative semantic changes.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 1, 2026
…e heading

IMPORTANT: the upstream-revocation paragraph incorrectly stated that
stopping issuance means 'no valid assertion can be obtained and no
reconnect can succeed.' Previously issued assertions that have not yet
expired remain valid for reconnection until min(exp, iat +
maximum_assertion_age). Stopping issuance prevents minting new
assertions that extend the window; it does not invalidate already-issued
ones. Replaced the false conclusion with the two correct cases.

MINOR: genericize non-normative note heading from
'open product question for Will/Tyler' to
'open product question (session-only vs deny-until-TTL)'.
PR body: remove Slack/parent-channel and Will/Tyler process framing.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 merged commit d4420eb into main Sep 1, 2026
31 of 34 checks passed
@wpfleger96
wpfleger96 deleted the duncan/nip-fi-v2-stateless branch September 1, 2026 23:05
johnmatthewtennant added a commit that referenced this pull request Sep 2, 2026
…e-read-model

* origin/main:
  fix(desktop): retain automatic mentions only in threads (#7144)
  feat: add databricks fable 5.1 model capabilities (#7213)
  docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
  feat(relay): add detailed readiness metrics (#7149)
  feat(desktop): add Pi agent preset (#7208)

Signed-off-by: John Tennant <jtennant@squareup.com>
johnmatthewtennant added a commit that referenced this pull request Sep 2, 2026
…l' into jtennant/project-state-mutations

* origin/jtennant/project-state-read-model:
  Fix historical migration catalog tests
  fix(desktop): retain automatic mentions only in threads (#7144)
  feat: add databricks fable 5.1 model capabilities (#7213)
  docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
  feat(relay): add detailed readiness metrics (#7149)
  feat(desktop): add Pi agent preset (#7208)

Signed-off-by: John Tennant <jtennant@squareup.com>
johnmatthewtennant added a commit that referenced this pull request Sep 2, 2026
…' into jtennant/project-related-channels-desktop

* origin/jtennant/project-state-mutations:
  Fix Project channel preservation test
  Fix historical migration catalog tests
  fix(desktop): retain automatic mentions only in threads (#7144)
  feat: add databricks fable 5.1 model capabilities (#7213)
  docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
  feat(relay): add detailed readiness metrics (#7149)
  feat(desktop): add Pi agent preset (#7208)

Signed-off-by: John Tennant <jtennant@squareup.com>
wpfleger96 pushed a commit that referenced this pull request Sep 2, 2026
…-history

* origin/main:
  fix(acp): replace real user name in base prompt mention example (#7250)
  ci: split CI into reusable workflows (#7168)
  fix(desktop): retain automatic mentions only in threads (#7144)
  feat: add databricks fable 5.1 model capabilities (#7213)
  docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
  feat(relay): add detailed readiness metrics (#7149)
  feat(desktop): add Pi agent preset (#7208)
  feat(buzz-agent): add DatabricksAuthCoordinator single-flight OAuth (#5545)
  fix(desktop): preserve keyring identity during recovery (#7203)
  feat(mobile): prepare `buzz-push-gateway` for deployment (#7158)
  ci: relax file-size ceilings by surface (#6485)
  fix(mobile): isolate extension linker flags; complete iOS build in CI (#7187)
  chore(ci): lower Codex security review effort (#7179)
  fix(dev-mcp): extend shell timeout cap to 20 minutes and align outer budgets (#7185)
  fix(dev): keep the canonical profile when launching from desktop/ (#7143)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 added a commit that referenced this pull request Sep 2, 2026
Removes the dead relay-side authority ledger introduced by migrations
0041 and 0042, and the dead `require_attested_key` verifier knob from
`buzz-auth`. Both are unreachable by design under NIP-FI spec v2 (#7214,
squash `d4420eb47`), which makes OSS Buzz stateless for identity: the
relay neither stores nor verifies an authority chain.

## What changes

**`migrations/0044_drop_nip_fi_ledger.sql`**
Drops all fifteen NIP-FI ledger tables and their trigger functions using
`CASCADE` to resolve the circular deferred FK between
`identity_bindings` and `identity_lifecycle_history`. Drops proceed in
FK dependency order: selectors → history/bindings →
enrollment_policies/receipts → parallel drop of auth tables. Restores
`community_write_fence_excluded_table` to its pre-0041 body (removes
NIP-FI table names from the exclusion array).

**`schema/schema.sql`**
Removes the NIP-FI section (~1885 lines of tables, functions, and
triggers) and updates `community_write_fence_excluded_table` to match.

**`crates/buzz-db/src/runtime/migration.rs`**
- Updates the `embedded_migrator_contains_consolidated_initial_schema`
sanity check: count 43→44, adds 0044 assertion block (verifies `DROP
TABLE` statements and absence of NIP-FI names from `schema.sql`).
- Removes ~2580 lines of NIP-FI Postgres integration tests (all
`#[tokio::test] #[ignore = "requires Postgres"]` from the 0041/0042
behavioral coverage).
- Removes the `extract_excluded_table_array` drift check (0042 body no
longer matches `schema.sql` by design).
- Adds `migration_0044_drops_populated_nip_fi_ledger_cleanly`: runs
migrations to 0042, seeds rows in `authorization_operation_receipts` and
`authorization_invalidation_domains`, then runs to 0044 and verifies all
fifteen NIP-FI tables are absent.

**`crates/buzz-auth/src/nip_fi/config.rs`**
Removes `require_attested_key: bool` from `IssuerPolicy` — field,
constructor parameter, accessor, and its contribution to
`derive_assertion_policy_id`.

**`crates/buzz-auth/src/nip_fi/verifier.rs`**
`parse_nostr_pubkey_claim` no longer takes a `policy` parameter. The
`None` (absent claim) arm now returns
`Err(VerifierError::ClaimRejected)` unconditionally instead of
conditionally on `policy.require_attested_key()`.

**`crates/buzz-auth/src/nip_fi/verifier/tests.rs`**
- Removes `missing_nostr_pubkey_denies_under_attested_key_policy` (the
sole `require_attested_key: true` call site).
- Removes `false,` from all eleven `IssuerPolicy::new` call sites.
- Injects `nostr_pubkey` by default in `mint_signed_by` (spec v2
requires it unconditionally).
- Updates `valid_access_token_verifies` to assert
`asserted_key().is_some()`.

**`crates/buzz-auth/src/nip_fi/startup/tests.rs` + `jwks/tests.rs`**
Removes `false,` from all `IssuerPolicy::new` call sites and adds
`nostr_pubkey` to all token-minting helpers.

## Verification

- Fresh-DB migration run to head: all migrations apply cleanly in
sequence.
- Populated-0041/0042-DB migration through 0044: seeds rows in live
NIP-FI tables, verifies all fifteen are dropped without error.

Closes the dead-code inventory item from the spec-v2 cleanup plan
(channel `48374f48`). Follows #7214.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 added a commit that referenced this pull request Sep 2, 2026
The OSS artifact must not reference deployment-specific concepts.
Replace the three remaining 'adapter' occurrences with 'issuer'
to match the genericization applied in #7214.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96 pushed a commit that referenced this pull request Sep 2, 2026
* origin/main:
  feat(agents): harness-agnostic effort write path and spawn bridge (#4625)
  chore(db): drop Phase-A NIP-FI relay-side authority ledger (#7221)
  fix(acp): replace real user name in base prompt mention example (#7250)
  ci: split CI into reusable workflows (#7168)
  fix(desktop): retain automatic mentions only in threads (#7144)
  feat: add databricks fable 5.1 model capabilities (#7213)
  docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
  feat(relay): add detailed readiness metrics (#7149)
  feat(desktop): add Pi agent preset (#7208)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

# Conflicts:
#	desktop/src-tauri/src/commands/agent_models_update.rs
#	desktop/src-tauri/src/commands/agents_deploy.rs
#	desktop/src-tauri/src/managed_agents/types/requests.rs
#	desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
#	desktop/src/shared/api/types.ts
wpfleger96 added a commit that referenced this pull request Sep 2, 2026
…ress (#7254)

Spec revision following two decisions: Option B admin deny (2026-09-02)
and the HTTP ingress ruling (2026-09-02). Revises `docs/nips/NIP-FI.md`
only. Follows #7214 (merged spec v2).

## What changed

### Admin disconnect: session-only → deny-until-TTL

The disconnect operation proceeds in two steps, in order:

1. Insert a memory-resident deny entry keyed by `(iss, target_pubkey)`
with absolute expiry `until` — atomically combined with the `(iss, jti)`
replay-identity reservation as one all-or-nothing mutation. If the deny
set is at capacity (per-issuer bound), the relay rejects `503`; neither
the jti nor the deny entry is recorded, and the caller may safely retry
the same signed command.
2. Close all live WebSocket connections for the target pubkey,
synchronously.

The single atomic admission mutation (jti reservation + deny-entry
insertion) lives inside `VerifyCommandJwt` step 7, after all pure
authorization checks. The endpoint only closes sessions on success. This
ordering ensures a capacity failure leaves no state behind and makes the
retry-safe 503 contract implementable.

The deny set is RAM-cache only — no durable storage, no schema changes.
The same operational posture as the JWKS snapshot. A relay restart MAY
forget active entries; the issuer SHOULD re-push still-active deny
entries on observed restart (same publish/cache pattern as JWKS). If the
issuer stops issuing assertions and re-push completes before any
expired-entry reconnection attempt, residual exposure after restart is
bounded by `max(0, min(exp, iat + maximum_assertion_age) - now)`. If the
issuer continues issuing or re-push does not complete in time, that
formula does not apply and access may continue beyond it.

**`until` claim:** Required on the disconnect command JWT. Because an
assertion accepted at the future-skew boundary (`iat <= now + skew`)
remains valid until `iat + maximum_assertion_age`, the latest possible
authority deadline is `now + skew + maximum_assertion_age`. The relay
enforces `until <= now + skew + maximum_assertion_age`. A value above
this ceiling rejects `400`; a past `until` still closes live sessions —
absent an active same-key entry it creates no future denial, while an
active entry remains unchanged under the merge rule.

**Capacity and eviction (per-issuer):** The relay MUST bound the deny
set size **per issuer**. Capacity exhaustion under one issuer MUST NOT
cause rejection of another issuer's commands; the `503` capacity check
is evaluated against the command's own issuer bound. Implementations
MUST evict only expired entries; when an issuer's partition is at
capacity and all entries are still active, the relay MUST reject the new
command `503` without removing any existing entry. There is no LRU
eviction of active denies.

**Issuer-global deny:** The deny entry applies to admission across all
communities served by the relay under that issuer. Identity-level
revocation is intentionally not community-partial.

**Cross-replica propagation:** In a deployment with multiple relay
processes, the deployment MUST propagate both the session-close and the
deny entry to every process serving admissions for the issuer's
communities. The mechanism is deployment-defined (e.g. the existing
inter-process message bus, same posture as JWKS convergence).
Propagation is asynchronous with no protocol-level completion bound. The
issuer re-push duty is the recovery path for lost propagation, exactly
as for relay restart.

**Response shape:** A successful disconnect responds `{"disconnected":
true}` regardless of how many sessions were closed. No session count is
returned; a count would aggregate activity across communities and
constitute an information leak.

**Admission procedure:** Step 5 registers the session's proven `k`
before the deny-set check (new step 6) — ensuring any connection that
straddles a concurrent disconnect is caught by one side or the other.
`FI-TRACE-DENY-SET` oracle covers the per-issuer capacity rule and the
straddling termination requirement.

### HTTP ingress enforcement

Without explicit enforcement, a protected HTTP surface (bridge, invites,
media, git) with NIP-98-only authorization allows a principal holding an
active key to mint fresh NIP-98 events indefinitely — NIP-98 proves key
possession only, not identity. Without assertion verification there is
no expiry bound; the key remains valid for as long as it is accepted.

**Pairing rule:** in enforce mode, a protected HTTP request MUST carry
both:

```
Authorization: Nostr <base64-NIP-98-event>
Nostr-Federated-Identity: Bearer <compact-JWS>
```

The NIP-98 pubkey MUST equal the assertion `nostr_pubkey` claim.
Missing, mismatched, or invalid evidence of either kind denies, fail
closed.

**Verification:** reuses `VerifyAssertion` unchanged — offline, same
JWKS, same claim requirements, same denial classes.

**Per-request:** HTTP is sessionless; every request re-verifies. No
session lifetime, no cached admission. The cumulative residual bound
applies per request.

**Deny-set applicability:** the deny-until-TTL entry introduced above is
consulted per HTTP request identically to WebSocket admission.

**Protected surface:** deployment-configured set of routes, fail-closed
default (unclassifiable routes treated as protected). No normative route
names in the spec.

`FI-TRACE-HTTP-INGRESS` oracle added. Security considerations updated
with HTTP ingress bypass analysis. NIP-98 source reference added.

### Other changes

- `authorization_denied` rejection table row updated to "active deny-set
entry for pubkey".
- Discovery: `maximum_residual_upstream_revocation_seconds` remains
`null` — the deny-until-TTL model is best-effort RAM state and provides
no unconditional finite revocation bound.
- Rejection and privacy: explicit sentence for HTTP denial path.
- Client-attached transport: opening sentence generalized to cover both
WebSocket and HTTP.

## Scope

Single file: `docs/nips/NIP-FI.md`. No code changes.

References #7214. Channel: buzz-enterprise-identity-spec-v2
(#a6fe0b1c-987a-43c5-a974-71ee36678d78).

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 pushed a commit that referenced this pull request Sep 2, 2026
…agent-edit

* origin/main:
  feat(desktop): add persistent Bestie experience (#7223)
  fix(desktop): harden profile batch and thread-reply fetches against relay slowness (#7188)
  docs(nip-fi): adopt deny-until-TTL and extend enforcement to HTTP ingress (#7254)
  fix(composer): align wrapped inline chip fragments (#7242)
  Add operation-aware database pool acquisition metrics (#7195)
  fix(desktop): keep explicit agent profiles bound to their exact key (#7131)
  fix(desktop): discover authenticated owned relay agents (#7122)
  feat(agents): harness-agnostic effort write path and spawn bridge (#4625)
  chore(db): drop Phase-A NIP-FI relay-side authority ledger (#7221)
  fix(acp): replace real user name in base prompt mention example (#7250)
  ci: split CI into reusable workflows (#7168)
  fix(desktop): retain automatic mentions only in threads (#7144)
  feat: add databricks fable 5.1 model capabilities (#7213)
  docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
  feat(relay): add detailed readiness metrics (#7149)
  feat(desktop): add Pi agent preset (#7208)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

# Conflicts:
#	desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
#	desktop/src/features/agents/ui/agentInstanceEditPinning.test.mjs
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.

1 participant