mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
relay/observer-frame-http-ingest
113 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5c50bf5975 |
fix(relay): accept observer frames over HTTP
Signed-off-by: Prem Pillai <prem@squareup.com> |
||
|
|
12423f2868 |
docs(nips): fix stray angle brackets in created_at clauses (#4486)
## Summary The `<` in a `created_at<` condition is the comparison operator, but in three places it had been paired with a closing `>` as if it opened a `<placeholder>` bracket: - **NIP-OA** clause list: `created_at<unix-timestamp>` → `created_at<unix-timestamp` - **NIP-OA** satisfaction rule: `created_at<t>` → `created_at<t` - **NIP-GS** "Conditions in Git Context": `created_at<t>` → `created_at<t` (line 437 of the same file already used the correct form) This matches the `created_at>...` forms used alongside them, which never carried a closing bracket. Also normalizes the NIP-OA headings to ATX style and pretty-prints the signed-event JSON example for readability. ## Verification Docs-only change. The NIP-OA test vectors are untouched and were re-verified by recomputing them: the preimage SHA-256, `tag-bytes-hex`, and the signed-event `id` all still match the documented values. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: zmeyer44 <zmmeyer44@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
45c288c179 |
docs: specify desktop-driven mobile push suppression (#7809)
## Summary Define when desktop activity suppresses otherwise eligible mobile message notifications: a ten-minute inactivity window, short-lived relay-owned suppression, and per-installation push preferences. Include a shared glossary and renewal timing diagram. The gateway remains unaware of suppression. This is a design draft ready for review, not an implementation-ready protocol. Wire format, synchronization, and several failure-handling details remain explicitly open. Please also review the tension between the agreed relay-confirmed preference changes and VISION_MOBILE.md's requirement that stopping notifications must not depend on a relay response. ### Related issue Related: #3234 covers per-channel notification settings and push filtering; this spec focuses on desktop-activity suppression. No duplicate spec PR found. ### Testing Documentation-only change. Local Markdown links and anchors resolve; whitespace validation and commit hooks passed. The full local CI gate could not complete: dependency compilation exhausted local disk space (OS error 28). GitHub checks currently report no failures. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Diem Nguyen <ngthuydiem@users.noreply.github.com> |
||
|
|
bba75a71eb |
Automate owner deletion preparation (#7830)
## Why Owner deletion admission stops at `submitted`. Manual inventory and approval still stand between owner intent and the existing deletion executor, preventing self-serve deletion. ## What The existing `buzz-admin deletions drain` process prepares and automatically approves operator-attested owner requests. Operator-origin requests retain manual approval. ## How The drain prioritizes approved work, then claims one owner submission with the existing generation lease. It freezes inventory and records digest-bound `owner_automatic` approval in one transaction before entering the unchanged executor. Before approval, the transaction rechecks archived state and current ownership. It locks the deletion advisory fence, community, owner memberships, and request in that order. A regression test pins privileged abort during a live preparation lease. What got simpler: one drain still owns preparation and execution. There is no second worker, queue, lifecycle, or retry authority. The rebased branch contains only this feature and its review fixes. ## Risk Automatic approval permits irreversible deletion without a human grace period. Operator attestation is not a cryptographic owner signature. Provenance, locked authority checks, generation fences, and digest-bound approval constrain the operation. This remains work in progress. Broad test failures and incomplete mobile checks are disclosed below; publication is not a merge-ready verdict. ## Testing Published head: `e059ae9056b447ca3b5d0c2893da86550ef16f5c`. All seven commit trees match tested source `5605f27eaece1e809f746cc1839b55c7ceef8d80`; only attribution metadata changed. Source-SHA results below are not new-head execution receipts. At source commit `5605f27eaece1e809f746cc1839b55c7ceef8d80`, isolated PostgreSQL tests showed stale archive and ownership fixtures fail closed after the fix. Before the fix both reached automatic approval. Removing abort's generation increment or stage transition made the new live-lease regression fail; restored production passed. `just test` failed in two ACP native-git fixtures and one agent body-timeout test. The failing source files match the pinned base, but there is no exact-base execution comparison proving those failures unrelated. `just test-unit` also failed in the ACP fixtures, leaving 18 tests unrun. `just ci` stopped in Flutter tooling package resolution; separately run non-mobile lanes do not constitute a full `just ci` pass. No live tenant deletion or human testing was performed. A dedicated populated `0052` → `0053` approval-backfill fixture remains a coverage gap. Final-head hosted CI and security review are required. ## Bigger picture Rebased directly onto main `12670bd0f037c66a682272bb81c46c3f254fad74`; prerequisite #7818/#7827 are already merged. Main owns `0052_channel_artifacts`, so this feature's automatic-approval migration is now `0053`. Client/KGoose and quota work are separate PRs and are not part of this diff. Generated with Codex --------- Signed-off-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
12670bd0f0 |
feat(relay): implement NIP-AR channel artifacts (#7919)
## Summary Implements NIP-AR channel artifacts (spec merged in #7791) on the relay, after a simplicity pass over the earlier two-agent implementation. An artifact is an editable record (kind 45010) with a stable identity (`d`), one home channel (`h`), and full-snapshot revisions chained by `prev`: - **Writes:** one transaction per write. It locks the artifact's head row, requires `prev` to name the current revision, records the revision in a ledger that survives retention, and advances the head. Two competing edits can't both win. A stale edit gets `conflict: artifact head changed`. Resubmitting an accepted event succeeds without applying it twice. - **Permissions:** writes pass the same gates as a kind 9 message in `h`. A move also passes them in the source channel. The move stores the destination snapshot and a relay-signed kind 45011 removal marker for the source in the same transaction, so both channels recover by replay. The marker never names the destination. - **Moderation:** redaction is the ordinary kind 9005 removal. The ledger keeps the ID so `prev` checks still work. Redacting the current revision retires the artifact. - **Reads:** HTTP `/query` and `/count` accept explicit `artifact: current|history` filters with exact multi-character tag matching (e.g. `#assignee`). Generic REQ and search return only current revisions. Kind 5 against artifacts is rejected. NIP-11 advertises the limits. - **Spec:** NIP-AR updated to match: kind 9 permission checks, 9005 redaction and retirement, no creator-only delete rule. A separate small commit routes `insert_channel_head_checked` through the metered writer pool (`acquire_writer`), like every other event write in that module. ### Related issue Follows #7791. No separate issue found. ### Testing - Clippy clean. - Unit lane passes. Failures seen along the way came from agent env leaking into `buzz-acp` tests (they pass in a clean env) and known timing flakes (`keepalive_resets_idle_past_deadline`, two `buzz-agent` databricks tests that passed on retry). This diff doesn't touch those crates. - Postgres lane passes, including new `artifact_postgres_tests` in `buzz-db` and `buzz-relay`. Three presence tests need `REDIS_URL` set. - Docker smoke against a fresh local relay, 29/29: create and idempotent resubmit, update and stale conflict, current, `#assignee` and history queries, generic REQ, kind 5 rejection, delete and restore, move with source marker, private-channel isolation, and 9005 retirement. - A pasteable curl walkthrough of the same flows was shared with the reviewer. Draft until the human hands-on check (AGENTS.md step 3) is confirmed. The pre-push hook was skipped because every lane above had already run on this tree. CI is the gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Signed-off-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Co-authored-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> |
||
|
|
6918dd3a83 |
Schedule the deletion drain safely (#7827)
## Why The deletion engine already exposes a one-shot `buzz-admin deletions drain` command, but operators have no safe way to schedule it. A generic workload wrapper could expose arbitrary commands or inherit relay credentials, which would add a second control plane and widen the blast radius. ## What This PR adds an opt-in Kubernetes CronJob for the typed deletion drain command. It is stacked on #7818, which adds owner-request admission. ## How The chart keeps the job disabled by default. The CronJob uses `Forbid`, zero Kubernetes retries, bounded runtime and history, and only the database, Redis, and object-store settings required by the executor. PostgreSQL leases, checkpoints, and retry timing remain the sole durable retry authority. The closed `_operator-jobs.tpl` helper centralizes safe CronJob mechanics without creating an arbitrary command or environment registry. This leaves the touched system simpler than a general job framework would. ## Risk The deployment blast radius is low because the job is disabled by default. When enabled, it can run destructive deletion work that the database has already approved, so the chart limits concurrency, retries, credentials, and execution time. ## Testing Rendered every `deploy/charts/buzz/ci/*.yaml` fixture with `helm template`; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`. Ran the full independent Desktop, Tauri, and web lanes; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`. Flutter package resolution made no progress within three minutes, so the mobile lane is not claimed green. ## Bigger picture A later stack layer must inventory and approve admitted owner requests. Until then, this scheduler cannot make a new owner request destructive. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Signed-off-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
b37e477211 |
Make relay readiness process-local (#7341)
## Summary Make Kubernetes readiness depend only on the relay process lifecycle, so a shared Postgres or Redis slowdown cannot withdraw every pod at once. Move dependency checks to one fixed-cadence loop per pod. The loop permits only one evaluation at a time, emits metrics after each sample, and caches a timestamped report. `/_status` reads that cache without calling Postgres, Redis, or the deletion catalog. It reports `not_yet_sampled`, `fresh`, or `stale`. Each completed sample also writes a Unix timestamp gauge, so Datadog can compute sample age at query time. Fail closed when the community lifecycle lookup errors. An inconclusive tenant-state lookup no longer admits a socket to AUTH or REQ handling. ### Related issue None found. This addresses the elevated relay HTTP 500 and reconnect incident investigated on 2026-09-03. ### Tradeoff The readiness gauge now reports the latest private-probe observation, not the shutdown transition itself. After shutdown starts, a scrape can still see `1` until the next readiness probe refreshes the gauge to `0`. Kubernetes routing is unaffected because it uses the readiness response from the authoritative process state. A terminal `0` was never guaranteed: even a transition-owned gauge could update in process and then exit before Prometheus scraped it. If no scrape occurs before exit, both designs miss the terminal sample. The new design adds at most the interval until the next readiness probe when a scrape occurs during shutdown. Dependency status is now sampled every 30 seconds rather than on demand. A slow evaluation delays the next sample instead of stacking more work. The status payload exposes the cached report age. The completion timestamp stays unchanged when sampling stops, so `time() - timestamp` crosses the stale threshold without a server aging loop. Outcome counters and duration histograms remain cumulative and cannot provide no-data detection. ### Testing Gimli exercised exact SHA `edff1ec4a5362e132e367bf7449d2b0a68f1e9a9` on an isolated Blox stack. Repeated `/_status` bursts caused no dependency calls, metrics advanced without status traffic, stale status remained visible, and lifecycle lookup failures admitted no AUTH or REQ handling. Follow-up verification at `fdbb394ed598c94cde85ed01cb11e9de0ee8f00d` confirmed sampler-owned outcome and duration emission without status traffic and bound the real startup seam. The final timestamp-gauge change is covered by targeted red/green tests and the same boot regression at `c0f36e22b527c2fd6a63a548d036d006ffa17171`. ### Complexity Readiness remains one process-local sample per probe. Dependency I/O now has one owner and one execution path; request volume cannot increase dependency-check load. One completion timestamp replaces the prior server-side age update path; Datadog computes age without another loop. Community admission also returns to one fail-closed rule for inactive and unknown tenant state. Generated with [Claude Code](https://claude.ai/code) --------- Signed-off-by: tornquist <tornquist@squareup.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
447951a19c |
🤖 docs(nip-fi): remove implementation references from the spec (#7912)
This removes implementation references and implementation status from `docs/nips/NIP-FI.md` so the spec holds normative text only. It drops the Blossom compliance note, the known-gap notes, crate/file paths and PR numbers, roadmap "supersedes" sentences, doc-history lines, and the design-rationale note on session-only vs deny-until-TTL (its restart re-push SHOULD remains stated normatively elsewhere). Two wording changes carry normative weight: `PUT /media/upload` is listed as a plain kind-24242 upload route instead of a temporary alias, and the JWKS fetcher's SSRF controls become a MUST instead of a description. It doesn't depend on any enforcement PR and can merge at any time. Related: #7265 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
b0d6fb8ad2 |
docs(protocol): propose simplified channel artifacts (#7791)
## Summary Adds a short, proposal-only **NIP-AR: Channel Artifacts** in `docs/nips/NIP-AR.md`, as a simpler alternative to the closed #7771. - Defines complete-snapshot artifact events with stable identity and atomic, `prev`-based revisions. - Uses channel access and posting permissions for reads, edits, moves, and writer-authorized deletion. - Keeps client-defined content opaque to the relay, with exact tag filtering over current accessible state and bounded queries. - Covers durable source removal on moves, terminal deletion, and safe client fallbacks without defining task/project schemas or changing application behavior. ### Remaining design work This is a compact draft, not a complete implementation-ready wire contract. Concrete query request/pagination/error/limit-discovery formats, removal/replay wire details, and numeric query limits remain follow-up work. Prefer extending shared WebSocket `REQ` / HTTP `/query` surfaces rather than adding plugin-specific endpoints. No database layout or scale guarantees are proposed. ### Related issue Closest prior proposal: #7771 (closed). Searched existing PRs and issues for channel artifacts; no matching issue found. ### Testing - Verified the committed document is byte-for-byte identical to the reviewed local draft. - Parsed the example JSON and its embedded JSON content; checked unique two-element example tags. - Reviewed the entire one-file diff and ran `git diff --check`. - Commit and push hooks passed (including DCO sign-off and differential file-size checks). - Full application CI was not run locally: this changes only a Markdown proposal, with no implementation or UI changes. --------- Signed-off-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Fizz <400e8babadcee6a7f420103f10a2849d84c4a9c71d5bd04f3948c814216648a3@buzz.block.builderlab.xyz> Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz> |
||
|
|
781d39510c |
feat(push): support configurable HTTP(S) delivery URLs (#7877)
The relay can use a configurable HTTP(S) gateway delivery URL and signs the same URL it sends. The gateway verifies NIP-98 against the received request URL, including authority, port, path and query. App Attest, scoped grants, replay checks and delivery quotas are unchanged. Forwarding proxies must preserve Host and supply the original request scheme through X-Forwarded-Proto. The NIP relay-delivery wording now defines the configured HTTP(S) URL as the signed URL and labels the public URL as an example. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
9263cda845 |
docs: specify durable data backfills (#7326)
## Why Data backfills need a durable contract outside schema migrations. The contract must survive crashes and support automatic or manual operation without adding version-selection machinery. ## What - Define one PostgreSQL-backed lifecycle for finite data repair - Treat the stable backfill ID as the only durable execution and readiness identity - Use eventually consistent, idempotent registration; compatible workers compete for one exclusive claim - Require bounded batches, atomic mutation and checkpoint commits, generation fencing, and validation before completion - Define all four migration and automatic-backfill configurations - Define the authorized admin API and honest client projection ## How A backfill captures one immutable upper bound, processes finite batches, and commits each target mutation with its checkpoint. PostgreSQL owns lifecycle state, claims, progress, and completion. Registration order and build metadata do not choose a definition. Compatible processes are interchangeable. A material behavior change requires a new stable ID. The orchestrator does not resolve different behavior registered under one active ID. Diagnostic validation on completed rows records only its bounded outcome and audit; it does not create an execution claim. The spec now starts with a six-step mental model and follows the operator flow. It removes repeated requirements and cuts the document from 614 lines and 4,835 words to 474 lines and 3,656 words. ## Risk Low. This PR changes documentation only. It commits Buzz to a design contract, but it does not add runtime, schema, configuration, or protocol behavior. ## Testing No manual runtime testing. A fresh full-spec review checked the lifecycle, safety rules, four configuration cells, admin contract, and production-seam conformance requirements. `git diff --check` and the repository file-size policy pass at `13681664674d4667b181a6211f012090fa40a2cf`. ## Bigger picture The design keeps the relay as orchestrator, a focused backfill crate as domain owner, and `buzz-db` as narrow transaction infrastructure. It removes deployment generations and definition precedence from the implementation plan. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8f8c4dfade |
feat(desktop): relay admin console for the /api/admin/v1 operator surface (#4768)
Adds a relay admin console under Settings — the single surface for relay-level operator work — replacing the previously unreachable moderation section. Operators authenticate with their own Nostr key over NIP-98 — no browser extension, no bearer token — to view deployment-wide moderation reports and product feedback, resolve/dismiss/escalate/reopen reports, update feedback status, and manage the operator/moderator staffing roster. ## Nav reachability The old `moderation` section id was defined in `settingsSections` but never wired into any group in `settingsNavGroups`, so the sidebar never rendered it — unreachable since #1617. This PR replaces it with a `relay-admin` section wired into the Communities nav group, pointing at the relay admin console, and drops the separate `admin-console` section id so a single surface owns relay-level trust & safety. The Relay admin nav entry is always visible. Auth gates the panel itself; the nav entry is a door, not a credential — hiding it behind discovery state would strand an operator whose relay transiently fails NIP-11 discovery with no path back to the connection settings. ## Discovery and connection On mount the console reads the connected relay's NIP-11 document for the optional `admin_api` field. When a valid origin is discovered **and its host matches the connected relay's host** (case-insensitive, host identity only), it is automatically saved and probed — the panel renders immediately with no Save step required. A cross-host advertisement is pre-fill only: it populates the manual origin field under Advanced and is never saved or probed without an explicit Save, so a relay cannot direct an unconsented NIP-98 signature at a third-party origin; residual exposure on the same-host path is bounded by NIP-98's URL/method/payload binding. Saving is only required for manual origin changes. The advertised origin is untrusted input: revalidated through `AdminOrigin::parse`, falling back to manual entry when absent or invalid. A saved manual origin always takes precedence over discovery. ## Auth and transport (Rust, `src-tauri/src/commands/admin/`) - `AdminOrigin` value object: validates scheme + host + optional port, rejects credentials/path/query/fragment; `http://` only for loopback. The relay side normalizes its configured admin host at config load and compares inbound `Host`/`Origin` hosts case-insensitively. - `AdminRoute` closed enum: no IPC surface accepts arbitrary URLs, so the NIP-98-signed URL is byte-identical to the fetched URL. - Dedicated no-redirect `reqwest` client as an SSRF guard: a relay `3xx` is an error and the NIP-98 header never crosses origins. - `admin_probe` signs `GET /probe` and returns a typed state (`nip98Authorized` / `nip98Denied` / `disabled` / `notAdminApi` / `networkOrIntercepted`). The response body is validated against the full relay contract — `nip98Authorized` only when the complete NIP-98 invariant holds, `disabled` only on a coherent disabled-mode body; any incoherent or unrelated JSON classifies as `notAdminApi`, so the console never fails open at the network boundary. Unknown fields are tolerated for forward compatibility. - NIP-98 signing via `AppState::signing_keys()`, one retry on `401` with a fresh signed event; response size bounded by a `Content-Length` preflight and a streaming byte counter; per-pubkey origin storage with atomic `0o600` writes. ## Relay admin console The console is gated behind the relay `OPERATOR`/`MODERATOR` roles from #3777; every `/api/admin/v1/*` route returns `403` to anyone else. Community self-administration (`community-members` / Invites) and in-channel enforcement (the 9040–9043 signed commands) are a separate axis and are untouched. The previously-duplicate community report queue (`ModerationQueueCard`, `moderationQueue.ts`, and three dead hooks) was unreachable dead code and is deleted in its own commit; `useBanMemberMutation` and the rest of `features/moderation/hooks.ts` remain in use by the members sidebar and message menu. ## Console UI (TypeScript, `src/features/admin-console/`) - `AdminConsoleSettingsCard`: discovery + probe flow, honest copy for every probe state, manual origin under an `Advanced` disclosure at the bottom. The role (operator/moderator) is rendered in the connection status line as plain text ("Connected as operator") — no badge-shaped elements above the tab row. The origin provenance (relay config / database) appears inside the Advanced card as small muted text. Section title changed to `Admin`. - `AdminConsolePanel` with Reports / Feedback / Staffing tabs (Staffing gated on `role === "operator"`); reports and feedback grouped by community for cross-community triage. - **Reports**: always requests `scope=all` so the existing resolve/cancel/reopen controls can reach every status (`open`, `processing`, `resolved`, `dismissed`, `escalated`). The relay's omitted-scope default is `escalated`-only (the platform-safety backstop); `scope=all` is explicit and scoped to this console. Full action matrix per `target_kind` (event → delete/kick/ban/timeout/dismiss/escalate; pubkey → ban/timeout/dismiss/escalate; blob → dismiss/escalate); kick is suppressed when the report carries no `channelId`. A `processing` row stays navigable — enforcement state lives in the detail view. A `failed` action (always pre-mutation) offers a single **Cancel & reopen** via `POST /reports/{id}/cancel` with `{actionId}` fencing; there is no client-side retry. `pending`/`enforcing` actions belong to the relay recovery worker and offer no button; a `409` reloads detail. Lists refetch on back-navigation after a mutation. - **Report failure UX**: `reportErrorMessage()` strips 4xx/5xx status prefixes and surfaces the relay's own error reason in the toast (e.g. relay returns `"400: you cannot report this content"` → toast shows `"you cannot report this content"`). Self-report blocking UI removed — the relay has no self-report gate, and the failure was purely client-side error surfacing. - **Reason audience disclosure**: the Reason (optional) field shows exact copy under the input based on selected action — Delete discloses that reason is sent verbatim to the affected user and posted publicly in the room; Kick/Ban/Timeout disclose affected-user only; Dismiss/Escalate disclose reporter only. These mirror the actual relay notice paths. - **Enforcement history**: the report detail carries the governing action even after the report leaves `processing` — a report enforced then reopened is `open` yet still shows its `succeeded` action as executed history alongside the resolve form; a reopen does not un-happen a ban. The resolve form gates on `open` status alone. - **Reopen**: terminal reports (`resolved`/`dismissed`/`escalated`) can be returned to `open` for re-triage via `POST /reports/{id}/reopen`. Reopen never reverses applied enforcement, and the copy says so. - **Feedback**: list, detail, and status control (`new`/`reviewed`/`archived`) with a generation-fenced attachment viewer. A purged source community severs provenance to `null` rather than deleting the row; severed rows bucket under a "source community removed" heading. - **Staffing**: Add is create-only with display names resolved via `useUsersBatchQuery` and npub cross-fade on hover (`HoverStaffingIdentity`). An entered key that already appears in the authoritative roster (config, owner-fallback, or DB) is rejected locally with an inline duplicate-key message and no request is issued; a `409 Conflict` from add, role change, or remove surfaces the relay's parsed error body via `adminErrorMessage(e)` — distinguishing config-backed key conflicts from last-operator conflicts without HTTP-status inference. Roster with `config`/`owner_fallback`/`db` source labels; config-backed entries disable remove client-side. In-place role change via `<select>` calling `putAdminOperator`; the relay responds with the effective `OperatorEntry` (`pubkey`, `effectiveRole`, `sources`) in the same shape as the roster list. Read-only badge for config-managed entries. - **Timeout expiry**: `useTimeoutState` clears the store when derived state is `INACTIVE` but the store is still active; the 1s tick is scoped to entries with a known `expiresAtMs` and owned by `ComposerTimeoutBanner` (mounted only while a timeout is active), while `ChannelPane` subscribes to the boolean `useTimeoutActive()` — the pane re-renders when a timeout starts or ends, not every second. `useMembersSidebarModeration` ticks `nowMs` only while the sidebar is open, the viewer can moderate, and some member has a future `mutedUntil`; the tick after the last expiry stops the interval. - Staffing refreshes the principal after a self-demotion or self-removal: `AdminConsolePanel` passes `onSelfMutation` to `StaffingTab`, which re-probes the saved origin so the role badge and tab visibility update without a manual re-probe. - Staffing mutation failures, including both 409 variants (config-backed key vs. last operator), surface the relay's parsed error via `adminErrorMessage`, so the last-operator recovery guidance is shown. - Mutations confirm via `sonner` toasts; failures surface the relay's parsed error `message`, not raw JSON. The `disabled` auth mode renders read-only. ## Mutation idempotency Resolve and reopen carry a per-attempt `requestId`. On first submit the whole command (`{requestId, action, reason, expirationSecs}`) is frozen; after an ambiguous failure (409, 5xx, transport error, truncated response) the snapshot is retained, action/reason/duration controls are locked, and the retry sends the exact same payload byte-for-byte. A definitive pre-commit rejection (non-409 4xx with `bodyComplete: true`) discards the snapshot, unlocks controls, and the corrected submit uses a fresh payload. On success the toast derives from the authoritative `AdminReportResolution` (`activeAction.action` when present; otherwise `status` for decision-only outcomes) — never from the mutable form selection. The native mutation commands — including operator delete — reject with a typed `AdminMutationError` carrying the relay's HTTP status (`relayStatus`, `null` when no verdict exists) and a `bodyComplete` flag that is `true` only when the full response body was read. ## Restriction routes contract change `GET /members/restrictions` and `DELETE /members/{pubkey}/ban|timeout` now take `communityHost` instead of `communityId`; the relay resolves the tenant itself and returns `400 unknown_community_host` for an unmapped host. This is a breaking contract change with no compatibility shim: the relay must run this build before a desktop that uses it, scripts sending `communityId` must switch, and rolling the relay back breaks the desktop Restrictions UI. `docs/admin/README.md` documents the three routes in the route inventory and carries the migration note. Restriction calls also carry the relay the list was loaded from, and the native side rejects a mismatch before building any URL. ## Hardening - Saved admin origins are stored per pubkey and per relay host. The host slug parses the relay URL, keeps the port-drop for DNS names, and encodes IPv6 hosts canonically, so two IPv6 relays never share a file and no IPv6 slug can match a DNS slug. - NIP-11 admin discovery has its own 10s request deadline and reads `/info` success and error bodies through the bounded reader with a 64 KiB cap (`Content-Length` is only a pre-check). - `PUT /operators` builds its `OperatorEntry` response from the validated body (`effectiveRole = role`, `sources = ["db"]`) instead of re-reading the roster, so a concurrent change can't turn a committed write into a `403`. Related: #3777 (relay `OPERATOR`/`MODERATOR` role model + NIP-98 auth + `admin_api` NIP-11 advertisement — provides the runtime and the discovery field this console consumes) Related: [builderbot-platform-core-infrastructure#140](https://github.com/squareup/builderbot-platform-core-infrastructure/pull/140) (prod admin ingress allowlist and NIP-98 rollout) ## Screenshots Captured headless at `197f0ba0c` against the mock bridge (1280×720). ### Setup **Origin setup — Advanced disclosure open, idle state**  **Authorized console — all three tabs**  ### Reports **Reports queue — open, processing, resolved across two relay groups**  **Open report detail — reporter, target, message snapshot, resolve form**  **Processing report — PROCESSING/HARASSMENT, ban enforcing state**  **Resolve form — Delete selected, audience disclosure copy**  **Resolved report — enforcement history block**  ### Feedback **Feedback list — community grouping with status badges**  **Feedback detail — mutable status control (operator mode)**  **Feedback detail — passive status badge (read-only/disabled mode)**  ### Staffing **Staffing roster — config/owner\_fallback/db source labels**  **Add form — new pubkey filled, ready to submit**  **Duplicate rejection — inline error for existing principal**  --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> |
||
|
|
dd1c2afc2a |
Discover alternate Buzz ACP commands (#6948)
🤖 I’m Larry. ## Summary Managed-agent definitions can now choose which installed ACP transport will launch the agent. The choice sits beside **Agent harness** while creating or editing an agent, so it is made before first deployment and remains visible afterward. Buzz discovers executable commands named `buzz-*-acp`, keeps **Buzz ACP (default)** as the safe default, and preserves existing custom command values. Discovery and launch share the same resolver, so a command offered in the picker is the command Buzz will execute. ## UX walkthrough ### 1. The ACP command picker appears while creating the agent The default is explicit, and the helper text explains that this selection controls deployment.  ### 2. Installed wrapper commands are offered by name Here Buzz has discovered `buzz-janet-acp` alongside the stock transport.  ### 3. The wrapper is selected before deployment  ### 4. The deployed profile reports the effective command  ### 5. Editing the deployed agent reopens its definition with the choice preserved  ## Details - Definitions carry the selected ACP command through relay publication, snapshots, imports, team adoption, deployment, and later edits. - Existing definitions without this field continue to use `buzz-acp`. - Unknown persisted commands remain visible but unavailable rather than being silently replaced. - Discovery is read-only and never executes candidates. It filters non-files and non-executables, supports Windows `.exe`, `.cmd`, and `.bat` shims, deduplicates aliases, and returns a stable sorted list. - Command names remain portable; Buzz does not persist machine-specific absolute paths. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@nostr> Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
5a349db91c |
feat(relay): add opt-in newest-first thread windows (#7823)
## Summary Add an opt-in, newest-first thread window to the authenticated HTTP query path. A filter with `thread_window: true` returns: - reply rows ordered by `(created_at DESC, id ASC)` with an exact composite cursor; - optional bounded reactions, edits, deletions, and delete-of-aux closure; - one relay-signed `kind:39007` bounds event bound to the normalized host, authenticated reader, channel, root, request cursor, kinds, depth, limit, and aux choice. Filters without the flag keep the existing oldest-first thread behavior. Channel windows and `kind:39006` are unchanged. No client opts into this mode in this PR. ### Why extend NIP-CW NIP-CW already defines relay-computed, cursor-paged conversation views over extended NIP-01 filters. Thread windows use the same protocol ideas: ordinary signed rows, composite keyset pagination, optional aux closure, and relay-signed bounds. Adding a thread mode keeps those related wire contracts together instead of creating a second NIP with duplicated terminology and compatibility rules. Extending a draft, optional NIP with an additive mode is normal protocol evolution. Existing fields and meanings are not reinterpreted: - `top_level: true` remains channel mode; - `kind:39006` remains channel-window bounds; - `thread_window: true` selects the new mode; - `kind:39007` has a distinct identity and root/request binding; - extension-unaware clients never send the selector; - legacy thread queries remain unchanged. NIP-CW documents only the wire contract and relay semantics. Database plans, replica internals, and rollout procedures remain implementation concerns. ### Why thread windows use independent modules The implementation deliberately keeps `thread_window.rs` separate in `buzz-core`, `buzz-db`, and `buzz-relay` rather than adding branches throughout the existing channel-window path. The modes share pagination concepts, but their invariants differ: - channel mode selects top-level rows; thread mode requires one root and depths `1..=depth_limit`; - thread mode accepts only conversation row kinds and rejects unknown/conflicting fields; - thread bounds bind the root and authenticated reader because cross-channel aux visibility can differ by reader; - thread aux includes the root and requires complete two-hop closure; - thread mode refreshes writer authorization before selection and before signing; - historical cursor pages may use a proved replica transaction, but fallback must restart both aux hops on the writer; - legacy thread traversal is oldest-first and must not inherit descending cursor semantics. Forcing these rules into channel-window code would create a mode flag across parsing, SQL predicates, bounds construction, authorization, replica fallback, and tests. That abstraction would not remove complexity. It would distribute it. Separate modules keep each contract local and leave the established channel and legacy paths untouched. ### Technical design - Strict request parsing normalizes UUIDs, event IDs, kinds, defaults, and cursors before computing the response binding. - SQL applies community, root, channel, depth, kind, deletion, and cursor predicates before `limit + 1`. - Ordered metadata scanning plus exact event-key lookup avoids the stale-statistics plan that previously exceeded the four-second SQL budget on a 10k-reply thread. - Aux work is bounded by target batches, query count, raw candidate count, response bytes, SQL timeouts, and one shared HTTP deadline. - Fresh authorization-set comparison catches both grants and revocations during closure. The relay returns retryable `503` rather than signing an incomplete page. - Replica failure permanently degrades the request to the writer and restarts closure from its original targets without resetting budgets. - Migration `0049_thread_window_index.sql` adds the mixed-direction index used by newest-first scans. It validates a prebuilt index and bounds startup lock/build time. ### Tradeoffs - The stale-statistics-safe plan is intentionally less clever. Measured warm 100k deep-cursor execution increased from roughly 0.30 ms to 4.80 ms, plus about 8 ms planning, in exchange for avoiding a reproduced multi-second timeout after rapid 10k-thread insertion. - Bounds are authoritative only for the served page. There is no cross-page snapshot guarantee, and replica insertion coverage does not imply latest edit/deletion/aux visibility. - Strict parsing rejects unsupported constraints instead of silently serving a different row set. - Required aux closure fails closed. The relay does not omit aux and sign plausible-but-incomplete bounds. - This PR ships relay capability only. Client rendering, retry UX, live-history overlap, and compatibility fallback remain separate work. ### Related issue No existing issue found. This work follows the thread-loading design and review discussion. ### Testing - `cargo test -p buzz-core`: 258 unit tests and 2 doctests passed. - `cargo check --workspace --all-targets`: passed at the rebased implementation head. - `cargo test -p buzz-db`: 131 non-PostgreSQL tests passed; 275 infrastructure tests were ignored as declared. - Focused PostgreSQL migration/runtime regressions: 3/3 passed against an isolated PostgreSQL container, including both corrected migration-ledger tests. - `cargo fmt --all --check`: passed. - Pre-rebase feature validation passed 398/398 PostgreSQL tests, signed-wire multi-tenant/auth/pagination/aux/revocation probes, clean Compose boot/reboot, and full core tests. - Independent HA validation exercised three relay pods and a physical PostgreSQL standby on the same runtime logic before the final documentation-only cleanup. - Independent final-delta review confirmed exactly two changed files, byte-identical executable migration SQL after comments/diagnostic normalization, matching prebuild/catalog/recovery instructions, and `git diff --check`; no runtime receipts were relabeled to the new SHA. - The pre-push hook ran repository checks but its Rust unit lane exited 100 without exposing an individual failed test in the captured multiplexed output. The independently run affected-package checks above passed; current-head repository CI remains required. Known unrelated baseline behavior remains: the mesh-demo echo test can return 504, and same-second archive → unarchive can deduplicate its membership notification. Neither path is changed here. --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz> Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: cid <d9f92a72922bf45c17379a47d64dae84b6020397c2d5a52b5317d512068cd9d3@buzz.block.builderlab.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
5621006bcf |
refactor: move agent Git bootstrap into ACP harness (#7819)
discovered in https://github.com/block/buzz/issues/7742#issuecomment-5783512985 ## Summary Goose uses its native shell, so it bypassed the Git identity and signing setup inside `buzz-dev-mcp`. Agent commits could use the host Git identity, while MCP shells had a separate config. This moves Git setup into `buzz-acp`, where both paths receive the same agent identity, Nostr signer, and relay-scoped credential helper. Before: ```text Buzz Desktop └─ buzz-acp ← Desktop supplied relay Git authentication ├─ Goose → native shell ← no agent Git author or signing setup └─ Buzz Agent └─ buzz-dev-mcp └─ shell ← MCP created the keyfile and Git signing config ``` After: ```text Buzz Desktop or Sprig └─ buzz-acp ← owns the keyfile, identity, signing, and scoped credentials ├─ Goose → native shell ← inherits the complete Git environment └─ Buzz Agent └─ buzz-dev-mcp └─ shell ← receives the same Git environment via mcpServers[].env ``` The harness keeps the signing key in a private temporary file and removes it on normal exit, startup failure, and graceful termination. It composes inherited `GIT_CONFIG_*` entries, forwards the complete block to MCP servers, and bundles the helpers through the ACP multicall entrypoint. For standard `buzz-acp` launches, Desktop and Sprig no longer write Git config. A custom Desktop ACP command bypasses the harness, so Desktop preserves its prior relay-scoped credential helper for that supported override. This changes Git settings for every ACP runtime. The startup signal path also changes so a termination during adapter initialization removes the keyfile. The temporary file can remain after SIGKILL or a machine crash. ### Related issue Related: #6177 covers stricter commit identity enforcement and touches the same Git paths. That PR and this one need to be reconciled before either lands. No issue was found for this runtime bootstrap gap. ### Testing - Ran the ignored `git_runtime_tests` with real Goose 1.50.0 and Buzz Agent ACP sessions. `real_goose_native_git_shell` passed again on this PR branch with `BUZZ_TEST_BIN_DIR` pointing to built binaries. Goose made a signed commit and tag through its native shell; Buzz Agent did the same through its MCP shell. Both verified signatures, identity, credential scoping, and keyfile cleanup. - Built the Linux Sprig image and ran `scripts/test-sprig-image.sh buzz-sprig:acp-git-bootstrap`. The container made and verified a signed commit and tag, then confirmed keyfile removal after SIGTERM. - `just ci` passed on the original branch. After rebasing onto `main` and addressing review feedback, the ACP Git bootstrap integration tests, focused desktop smoke test, custom-command regression test, formatting, and ACP/Desktop Clippy passed. The pre-push hook hit two timing-sensitive ACP deadline tests under parallel load; both passed alone and the complete ACP package passed serially (964 library, 2 Git bootstrap, 9 pool lifecycle tests). The push used `--no-verify` after those checks; GitHub CI is rerunning on the new head. - Under injected `BUZZ_ACP_ALLOWED_RESPOND_TO`, `BUZZ_ACP_LAZY_POOL`, and `BUZZ_ACP_IDLE_POOL_SLEEP`, the clean-env serial package command in `crates/buzz-acp/TESTING.md` passed: 964 active library tests, 2 Git bootstrap tests, and 9 pool lifecycle tests. Both named Databricks OAuth tests passed locally; another reviewer reproduced their environment-specific failures on `main`. - No authenticated relay clone or push was run. The local probes used disposable repositories and did not contact a relay. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
4142d2b84e |
Use worker snapshots for relay storage metrics (#7845)
## Summary Make the relay read completed storage-accounting snapshots from PostgreSQL by default. An environment with no snapshot emits no storage metrics. When a worker publishes its first result, the relay picks it up on the next successful leader usage tick, without a restart or configuration change. This changes how storage usage is measured and reported. **S3 remains the object store. PostgreSQL stores the completed counts, byte totals, community breakdown, and calculation metadata—not the objects themselves.** ### Why move relay storage accounting entirely to database reads? Today the relay supports two sources: its own S3 scan (`inline`, the default) and the worker's saved snapshot (`external`). That requires operators to deploy a worker and then coordinate a separate relay-mode change in each environment. The worker already owns the S3 listing and calculation. It runs in a separate process with its own object cap, memory limit, and deadline. Keeping a second scan inside the serving relay retains the resource pressure and failure modes that this isolation is meant to remove. PostgreSQL provides a durable handoff. The worker replaces one completed snapshot atomically after a successful calculation. The relay reads that row and exposes its values through the existing metrics endpoint. A relay restart or leadership change can load the last completed result without scanning the bucket again. There is deliberately no S3 fallback when a snapshot is absent or a read fails. Such a fallback would put the expensive scan back in the relay precisely when a worker is absent or unhealthy. It would also leave two calculation paths with different resource limits and freshness behavior. ### What happens if no worker exists? **A worker that has never published is a normal, successful inactive state.** The snapshot table must exist through the normal database schema, but it may contain zero rows. The reader returns success, emits no storage totals or failed-load gauge, and checks again on the next usage tick. It does not exit the relay, start a worker, or list S3. The relay checks for a completed database row; it does not inspect Kubernetes Jobs or CronJobs. Worker presence and snapshot presence are therefore different: | Situation | Storage-reader behavior | |---|---| | No worker and no snapshot row | Return success, emit no storage metrics, and query again next tick. | | Worker starts later and publishes its first row | Read and emit it on the next successful leader usage tick; no restart or mode change. | | Worker is removed or fails, but its last valid row remains | Continue reporting the last completed totals with their increasing age. | | Query succeeds but a previously present row has been removed | Clear the cache and stop refreshing storage series; existing exporter series expire through the idle timeout. | | Database read fails, times out, or returns an invalid snapshot | Log a warning, set `buzz_storage_snapshot_load_ok=0`, retain any last-good cached totals, and retry next tick. | | Snapshot table is missing | Treat this as a schema error, not normal worker absence; follow the failed-read behavior above. | | `BUZZ_STORAGE_METRICS=off` | Skip the snapshot query and all storage metric emission. | No row means “not measured,” not zero bytes. A valid completed snapshot of an empty bucket can report zero. If a read fails before any good snapshot has been cached, only failed-load health is emitted. These reads happen in the existing background metrics task, not the relay startup path. A storage-read failure does not request process exit or change readiness directly. A wider database outage can still affect the relay's existing database-dependent operations and readiness. ### Read, parse, and emit path ```text Worker: S3 listing -> BucketSnapshot -> JSON -> PostgreSQL Relay: PostgreSQL -> JSON -> BucketSnapshot -> existing metric gauges Collector: relay /metrics endpoint -> Datadog ``` 1. `main.rs::run_storage_sweep_tick` calls the reader from the existing leader-only usage task. The default interval remains 300 seconds. 2. `storage_sweep.rs::run_storage_metrics_tick` coordinates the read, cache update, failure health, and metric emission. 3. `refresh_persisted_snapshot` calls the existing `Db::load_storage_accounting_snapshot()` method. A five-second timeout covers pool acquisition and the query. It uses the relay's existing pool. 4. The existing query in `buzz-db/src/store/storage_accounting.rs` reads `snapshot`, `completed_at`, `duration_ms`, `max_objects`, and `code_sha` from the singleton row. 5. `serde_json::from_value(stored.snapshot)` decodes the JSON into the existing `buzz_media::BucketSnapshot` type. Rust infers that type from `cache_persisted_snapshot`'s argument. The generated `Deserialize` implementation handles all fields, including the UUID-keyed `per_community` map. 6. `emit_cached_storage_metrics` sets the existing physical/logical totals and community byte/object gauges. The existing Prometheus exporter exposes them for Datadog collection; this code does not send snapshot JSON to Datadog. The worker's `BucketSnapshot`/`CommunityStorage` structures, JSON field names, SQL publication method, and database schema are unchanged. The old external-mode JSON conversion moves out of `main.rs` into the reader helper. No new format or hand-written field parser is needed. Duration and object-cap metadata are validated before replacing the cache. The five-second relay read timeout is separate from the worker's 30-second startup acquisition budget introduced in #7770. This PR does not change the worker's pool or startup logic. ### Configuration and code changes | Configuration | Before | After | |---|---|---| | Unset, `inline`, or `on` | Relay-local S3 scan | Read completed database snapshots. `inline` logs one migration warning. | | `external` or `snapshot` | Read completed database snapshots | Continue reading snapshots. | | `off` | Disabled | Disabled. | | Unknown or empty value | Disabled with a configuration error | Same. | Existing `inline` settings intentionally become reader aliases. Merely changing the default would leave older deployments on their explicit `inline` setting and preserve the need for coordinated configuration PRs. Remove relay-local scan scheduling, in-flight scan state, scan configuration, and obsolete scan-attempt tests and health metrics. Preserve the physical/logical total and per-community metric names, leader-only emission, community scope filtering, and cleanup of old community labels. The shared relay-state field remains in place; its comment now describes a cached worker result. The Helm chart defaults to `relayMode: external` while leaving `storageAccounting.enabled: false`. Its rendering test verifies that the reader is configured even when no worker is created. The reader regression tests cover missing rows, later publication, updates, invalid data, timeout, removal, reactivation, and the explicit off switch. `docs/storage-accounting.md` describes this operating model. ### Freshness and rollout impact `buzz_storage_snapshot_age_seconds` uses the worker's original completion timestamp. Re-reading an old row never makes it fresh. `buzz_storage_snapshot_load_ok=1` means the row was read and decoded; it does not prove that the worker's latest attempt succeeded. Monitor snapshot age against the worker's schedule and allowed runtime, and monitor Job failures separately. The old relay scan-attempt health gauges are retired because the relay no longer runs those scans. Deploy this relay version through the usual release process. Once it reaches an environment, workers can be enabled independently. Already-released charts that still set `inline` work with the new binary; future chart releases default explicitly to `external`. An intentional `off` override still requires an operator to enable the reader. **Environments with no completed snapshot lose relay-calculated storage totals after this upgrade.** That is the intended tradeoff for removing scans from the serving process. Environments with a prior snapshot continue showing its last completed totals and age. This PR does not install workers in additional environments or deploy the new relay image. ### Related issue Related to #4601. Follows the worker startup fix in #7770. The separate fleet-usage collector work in #7176 overlaps this code and may need reconciliation when it merges. ### Testing Built and ran the relay locally against isolated PostgreSQL and Redis with the legacy `inline` setting, five-second usage ticks, and a 15-second metric idle timeout. The same process was exercised through these steps: 1. Start with an empty snapshot table: relay becomes ready and exposes no storage series. 2. Publish a snapshot containing 123 bytes: the community gauge becomes 123. 3. Replace it with 456 bytes: the gauge becomes 456. 4. Remove the row: storage series expire without fabricating a zero-byte measurement. 5. Publish 789 bytes: metrics reactivate without restarting the relay; readiness remains healthy. The local S3 endpoint recorded zero requests. The unrelated Git conformance startup probe was disabled for this isolated reader check; the test covers storage accounting, not every other use of S3 in the relay. No new relay image was deployed to staging or production during this test. The broad local integration run also hit an existing `p0_pool_acquisitions_use_typed_operation_pairs_without_other` failure in `buzz-db/tests/observability_source.rs`. The same source-only test fails on unmodified `main` at `0ef7a2222`; this PR changes neither the test nor its failing source. The full local `just ci` run stopped at mobile native-asset setup: the `objective_c` build hook received no SDK path from `xcrun`. Mobile tests did not run. Generated with Codex Signed-off-by: Ravneet Arora <rarora@squareup.com> |
||
|
|
0ef7a2222e |
fix(mobile-infra): render push grant lifetimes as decimal in chart 0.3.2 (#7820)
A valid 30-day push gateway grant lifetime could render in scientific notation (e.g. `2.592e+06`) in the chart, which the gateway binary does not accept. Always ormat the integer as decimal text, preserving the strict gateway parser. ### Testing Added regression coverage that feeds actual Helm-rendered environment values into the production config parser. Covers defaults, values files, command-line overrides, exact lifetime boundaries, and invalid input rejection. The default case failed before the fix and passes afterward. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
7e96f91229 |
feat(mobile-infra): support development App Attest (#7744)
Development-signed iOS apps cannot enroll with the current gateway because their Apple App Attest AAGUID is rejected. Add an explicit personal development build feature and environment setting so a development gateway can use a developer's own application identity and sandbox APNs credentials. Ordinary gateway builds remain production-only. Special builds also default to production, enforce exactly the selected signed AAGUID, and retain certificate-chain, pinned-root, nonce, app-ID, key, credential-ID, and counter verification. Document the matching mobile signing configuration and expose the feature through an optional Docker build argument. ### Validation The development image deployed successfully to a personal Railway stack; private readiness, enrollment challenges, authenticated relay access, and APNs sandbox TLS/HTTP2 connectivity passed. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
26ede6dfa2 |
docs(nip-fi): clarify federated identity amendments (#7803)
## Summary Aligns the NIP-FI wire contract with the post-sync enterprise identity decisions: - bind each community's canonical host URI as its RFC 8707 resource-indicator audience, resolve `Host` to an immutable community configuration before verification, and require both exact audience binding and explicit community-to-issuer authorization - require relays to ignore unverified claims and avoid logging or exposing them; recommend that issuers minimize relay-visible claims - make per-session (`sid`) revocation issuer-side while keeping the relay's revocation primitive pubkey-scoped and community-wide, keyed `(iss, target_pubkey)` - scope the disconnect close scan to connections registered under the command's issuer, including admission-in-progress, matching the deny-entry key; terminally cancel pending connections and close the cross-issuer session-termination gap - clarify proactive assertion re-exchange and reconnect before expiry without introducing in-band renewal Originating Buzz discussion: `buzz://message?channel=51940054-5a7a-4773-a337-8004496630fa&id=d551ee7c61882ef33843e752f63a837921e7c5df2671a88c68a84524490089cc` --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> Signed-off-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz> Co-authored-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> |
||
|
|
6a86531ac9 |
feat(relay): add atomic complete read-state snapshots (#7572)
Opened by Brain on behalf of Wes (GitHub: wesbillman). ## Summary Add an opt-in, host-scoped completeness primitive for NIP-RS read-state clients. Ordinary query arrays and WebSocket behavior remain unchanged. - Advertise a version-1 `read_state_snapshot` capability in NIP-11 only when the request resolves a community. - Accept exactly one self-author kind-30078 snapshot filter on authenticated `POST /query`, after the existing admission, replay and membership gates. - Read all retained, nondeleted self-author kind-30078 coordinates from the writer at one SQL-statement MVCC cut—including unrelated application coordinates and events without a read-state tag. - Return a complete versioned envelope bound to community, signer and a content-identity hash. Reject malformed contracts, reconstruction/signature/storage failures and whole-request overflow; never return a truncated complete response. - Bound snapshots at 4,096 events and 8 MiB of compact encoded event-array bytes, advertised as `max_event_array_bytes`; envelope metadata is additional. Retain the separate stored-payload preflight budget. ## Safety and scope This is point-in-time enumeration, **not** a live freshness guarantee, cursor, CAS token, cross-relay transaction, or unread total. NIP-RS now explicitly requires fresh complete enumeration from every write relay before each override action, canonical publication, carry-forward or deletion. It does not enable synchronized manual-unread overrides in any client. No schema migration or deployment is included. ## Validation Executed against the source committed as `1fd5a5e0c0d893c1259386885517c0c44ae7630f`: - Four real-router/PostgreSQL snapshot cases: beyond-page-cap enumeration and overflow; corruption/storage failure; production payload-bound NIP-98, Redis replay guard and membership enforcement; host discovery/scope/strict request shape. - Two database cases: replacement during preflight preserves a single statement cut; encoded-byte overflow and writer-not-replica routing. The cluster-wide probe uses the required `cluster_global_` serialization prefix. - Production-call-site mutations killed for dispatch, strict filter validation, discovery tenant, snapshot tenant, encoded-byte guard, membership enforcement and replay enforcement; isolated mutation tree restored. - Both affected crates pass all-target Clippy; formatting, differential file-size and repository PostgreSQL test-discovery checks pass. - Normal pre-push gates pass without bypass: branch skew, push-head scope, file size, Rust unit lanes and desktop Tauri checks. - Independent read-only review by Pinky found no remaining relay blocker at this commit. The execution/mutation results above were run by Brain, not duplicated by the reviewer. Hosted CI and maintainer review remain separate gates. No production traffic or deployment was used for these fixture tests. Originating Buzz channel: `a394ecdb-9c67-4c4e-a3c6-4a95f69e8cc8` (unread-but-good), thread `2413d0ba7fa5b2ac5c777df6b3754e2099b042ec8fb96ca64302a0919a0550b3`. ## Review repair (2026-09-21) Carl updated this existing branch to `2f9c9b11266c24947412cf28cefae4e96e7167b7`, merging main `5079c770fe30bb3d8204822ce6c2431eacac6d4b` without rewriting published history. - Resolved the DB export conflict, preserving both snapshot and storage-accounting exports. - Addressed the blocking review using its accepted alternative: rename `max_bytes` to `max_event_array_bytes` and explicitly document that success-envelope metadata is outside the array budget. No auth, query, runtime-limit, or client-behavior redesign. - Added a PostgreSQL/router regression proving discovery, exact 8,388,608-byte array success with a larger envelope, and one-byte-over rejection (413, no partial complete response). Exact-head validation: all seven snapshot PostgreSQL/router tests passed in an isolated local database; both affected crates passed all-target Clippy; mandatory pre-commit/pre-push gates passed without bypass (Rust unit lanes, Tauri default/mesh Clippy and tests, differential file size, branch-skew/ref checks). Reverting only the descriptor rename made the new regression fail before the final commit. Independent bounded source-delta review by Mordecai found no blockers; execution was Carl's. A broader `cargo test -p buzz-db -p buzz-relay --all-targets` run on the merged candidate passed 131 DB tests and 1,057 relay tests, then encountered the same unrelated `mesh_demo::demo_join_forwarded_arm_round_trips_echo` 504 recorded in the prior review. Full repository `just ci` / `just test` were not rerun locally; hosted CI remains a separate gate. No deployment or production traffic was exercised. --------- Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> |
||
|
|
a61239f0d8 |
fix(mobile): avoid opening empty threads on message tap (#7756)
Tapping a mobile channel message with no replies currently opens an empty thread. Only enable message-row thread navigation when the existing thread summary reports replies, including replies known to the relay but not loaded locally. Long-press → Reply still starts a new thread. Added widget tests for zero replies, loaded replies, relay-only replies, and starting a thread through Reply. The zero-reply test failed before the fix and passes afterward. Validation: all 227 channel-page tests, the full mobile pre-push checks, and all PR CI checks passed. The full repository gate failed in unchanged Rust ACP timing tests (idle reset, keepalive, and steering deadlines); this PR changes only mobile Dart files. No duplicate open PR or issue found. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
76f7e5ef0c |
feat(push): make gateway origin configurable (#7261)
Make the push gateway origin configurable across the relay, gateway, mobile client, and deployment chart. This is a fresh deployment. The gateway has only run on personal development infrastructure, and push has no existing enabled users. Legacy migration, configured-to-unconfigured mobile transitions, and rolling upgrades from pre-launch gateway binaries are not supported. Populated pre-launch databases are rejected with an informative error before migrations run; their data is untouched. Mobile builds may omit the gateway URL, leaving push unavailable. Once push is enabled, subsequent builds must retain that configuration. ### Validation - All applicable CI checks pass at `5f546cfaf403f0386f146d213adfb4392f4ec29c`, including Rust, mobile/Swift, desktop/integration, security, image builds, and Helm checks. An unrelated agent cancellation test passed on retry. - PostgreSQL CI passed 384 tests, including the production-entry-point regression for refusing populated pre-launch databases before migrations, fresh initialization, and subsequent initialized deployments. - Focused local validation passed all 10 gateway PostgreSQL tests under the CI discovery filter and all 3 unconfigured-mobile tests. Repository-wide PostgreSQL discovery validation and the complete pre-push hooks passed. - Physical-device APNs/App Attest behavior and deployment to a live cluster were not verified in this work. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
78618804ec |
Configure ACP session scope per agent (#7578)
## Summary PR #6732 taught the ACP harness to honor `BUZZ_ACP_SESSION_POLICY`, but the desktop exposed that policy as one global experiment. Turning it on changed every managed agent at once, even though conversation scope is part of how an individual agent should behave. Some agents need continuity across a channel, while agents such as validators may need clean context for each thread. This moves the choice onto the agent definition under Advanced as **Conversation context**, with **Entire channel** and **Each thread** options. **Entire channel** remains the default, so existing definitions keep their current behavior. The policy now travels through create and update IPC, returned agent summaries, persona events, snapshots, teams, and community catalog copies. Launches pass the effective definition value to the harness. Editing a running agent uses the same lifecycle as a model edit: it shows the restart-required state, leaves the deployed process untouched, and applies the new policy on restart. The global Thread Scoped ACP Sessions experiment and its desktop command/state have been removed because the setting no longer has a useful global meaning. There is intentionally no migration for the old preview override. Users who enabled it can choose **Each thread** for the agents that need it. Unknown or null policy values degrade to **Entire channel** across stored agents, persona events, and catalog projections. This keeps forward-version data from dropping complete agent records. ### Related issue No matching issue found. Builds on #6732. ### Testing - Focused Playwright create flow selected **Each thread** and verified `create_persona` received `sessionPolicy: "thread"`. - Focused Playwright catalog flow added a thread-scoped agent and verified the local copy stored `session_policy: "thread"`. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
6146c4fd16 |
feat(git): add default-branch management to relay and CLI (#7562)
Authored by Brain and opened on behalf of Wes (`wesbillman`).
## Summary
Add `buzz repos default-branch get/set` and the relay operation it
needs. Git push changes branch tips but cannot select the server's
symbolic HEAD. This selects an existing branch without deleting
branches, moving refs, or changing packs.
- Reuse immutable Git manifests and captured-ETag CAS; concurrent
pushes/default changes conflict, including stale no-ops.
- Require request-specific NIP-98, body binding, fail-closed replay
protection, host tenancy, current channel membership and
repository-management authority. Push/project roles alone are
insufficient.
- Send the CLI mutation once, without redirects; ambiguous delivery
retains the observed digest and is non-retryable.
- Document the narrow HTTP exception to Nostr-first, delegation scope
and admission-time ACL revocation. Kind:30618 remains derived; the
manifest CAS is authoritative.
- Register the Postgres/Redis/MinIO route and fresh-clone tests in the
existing Backend Integration lane.
## Review fixes — `7457bda50afa6b52e36315572fa96928b2e21db6`
- Reject signed valueless `payload` tags when verifying a body; preserve
the shared verifier's intentional absent-payload compatibility. The real
settings route now proves 401, unchanged manifest pointer, and unchanged
kind:30618 event IDs for malformed and other invalid credentials.
- Require a nonempty returned branch and `head == refs/heads/{branch}`;
SET additionally requires the returned branch to equal the attempted
branch. Malformed/mismatched mutation responses remain non-retryable
`DeliveryUnknown`, retaining the attempted branch and original digest.
Invalid reads fail before any POST.
- Strengthen the later-push/fresh-clone regression to select
`release/v1` while `main` exists.
## Verification
At `7457bda50afa6b52e36315572fa96928b2e21db6`:
- Full pre-push passed (all 14 Rust test groups, serialized Tauri
tests/clippy, file-size and branch-skew gates; 522 seconds). No hooks
disabled.
- All four isolated Postgres/Redis/MinIO settings tests pass, including
real Smart HTTP `ls-remote --symref` and fresh clone on `release/v1`.
- All 15 focused NIP-98 verifier tests and three default-branch CLI
tests pass; malformed credentials and missing-branch responses were
reproduced failing before the fixes.
- Five targeted mutants are killed: restoring the valueless-payload
bypass, weakening canonical HEAD validation, accepting an empty branch,
dropping requested-branch confirmation, and omitting hydrated HEAD for
the selected non-main branch. Mutations ran in a separate worktree and
target directory; original patch bytes were verified restored.
- `cargo clippy -p buzz-auth -p buzz-cli -p buzz-relay --all-targets --
-D warnings`, formatting, diff and file-size checks pass on the fixed
tree.
Earlier validation at `bb195b140`: seven Git authorization database
regressions and 28 existing transport tests passed (three infrastructure
cases in that module not selected); four mutations killed
management/replay/CAS/redirect regressions. Full pre-push passed with
`RUST_TEST_THREADS=1` (all 14 Rust groups, Tauri clippy and 3,173 tests,
size/branch-skew gates). Serial execution avoids an unrelated
process-global Desktop discovery counter flake; no hooks disabled or
Desktop changes.
Carl and Jude's review findings are addressed in the new head; **fresh
reviewer and exact-head security confirmation remain required**. The
earlier source review is not approval of this revision. No claim of
green new-head CI.
## Rollout
Merge and **deploy the relay support first**, then build/use the updated
CLI and explicitly set/verify the desired default. No schema migration.
This PR does not deploy anything or change a live repository.
Originating conversation (channel
`8e2fda80-9142-4df8-8e14-da288bea6471`):
buzz://message?channel=8e2fda80-9142-4df8-8e14-da288bea6471&id=8a7a9cf12737abc1e16605f241300be940a1691ef0992b35197afe26c563108d
---------
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
|
||
|
|
4d447b9c20 |
Add generic information-flow control core (#7293)
Adds a dependency-free `ifc-core` crate implementing reader-set confidentiality labels and monotonic per-computation flow state. - Defines flow ordering, join, and meet over caller-supplied principal universes. - Fails closed for unknown or cross-universe input and checks reader widening at egress. - Includes property tests for lattice laws and monotonic taint, plus the design paper that motivates the broker integration. The crate deliberately contains no Buzz, Nostr, channel, membership, or grant policy; those remain in the Buzz adapter. --------- Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
4afef8649a |
fix(desktop): restore mention chip identity icons (#7338)
## Summary Restore the missing **@ glyph for people and robot icon for agents** after #7133, and incorporate the requested compact public-key display. - Wrapping mention chips render their existing bounded icon-bearing leading fragment. - Readonly chips show bound keys using the same `8 leading…4 trailing` formatter as the channel member list: `Scout (150b20bd…15dc)`. - Full literal labels and exact keys remain authoritative in metadata, profile targets, title/accessible-name attributes, editor text, saved bodies and recipient tags. Display abbreviations are never used for recipient lookup. - Copy/paste restores the full literal label for a complete compact chip; partial selections remain plain text. Two keys sharing the same abbreviation still round-trip to their separate exact recipients. - No recipient-resolution, authorization, wire-format, composer, or CSS changes. Existing labels, icons, cloud markers and ordinary mentions remain intact. ## Verification Published candidate `9365ab9bc9d9c5d10802580cd576f8e378ff492f`, based on main `e09f715c9d0ee2cb7bf8a39061e601f3a502f588`: - **6,444 desktop unit tests passed** on this candidate's final source tree. - **44 mock-Chromium tests passed, zero retries in the final run** across mention recipients, clipboard and cloud provenance: exact recipient selection, ambiguity rejection, send/edit/reopen, forwarding, full/partial copy, mismatched-key rejection, matching-abbreviation collisions and 100%/150% narrow-window geometry. - TypeScript, desktop Biome/check guards, protected-feature production build and E2E build passed. - The compact-renderer regression fails with the formatting call removed. Original missing-icon and hidden-text accessibility regressions have red/green evidence. - Fresh self-review traced rendering, full-key metadata, copy classifier, paste normalization and identity trust. The same display formatter owns the accepted compact form on both clipboard sides. Iteration exposed an existing team-insertion separator flake (passed final full run) and two new fixture assumptions: non-member sends require invitation, and Chromium rich paste may retain an NBSP separator. Tests now exercise invitation and normalize only that separator when comparing the captured full body and exact tags; no product change was needed for either. Earlier local repository-wide `just ci` completed in two invocations because its initial call hit the ten-minute tool limit during Tauri compilation. Unchanged native/mobile/backend evidence is reused; the desktop delta received the full checks above and new remote CI. Native VoiceOver, real Tauri selection, dark theme and non-Chromium observation were not performed. Browser artifacts exercise real frontend with mocked Tauri/relay boundaries, not an installed release. ## Review and visual evidence Current-head CI and automated review must complete after this update; the old `2997bfb5` green results do not establish this new head. Required human review remains separate from agent approvals. No merge/install/restart authorization. Before/after icon evidence: https://github.com/block/buzz/pull/7338#issuecomment-5544575866 Updated compact-key screenshots are posted below. The editor intentionally retains the full literal address; only readonly chip display is abbreviated. Origin: buzz://message?channel=3355d33a-b72a-423a-b064-a58275f9a8af&id=38b3a27e689f5a9604e273d45f4e3122fceba76081e7bcd3bbdbf439524a5a18 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
e7e29937a1 |
feat(desktop): invite owned agents from standalone forums (#7125)
## Summary In Desktop's standalone Forums, selecting an owned agent from another device could leave a post or reply unsendable if the agent had not joined the forum. This adds **Invite / Cancel** to the send flow so you can resolve membership without leaving your draft. - **Invite** checks response policy and your permission to add members, adds the agent to the forum, waits for refreshed membership, then rechecks authorization before posting to the original destination. Membership is forum-wide, not limited to one post. - **Cancel / Escape** keeps the text, attachments and selected recipients for retry. Unlike chat's **Do nothing / Send anyway**, this dialog has no reference-only send choice. Invite is disabled while pending; Cancel remains available. - Leaving the source post/reply cancels its pending invitation, even if you return. Errors remain visible, focus returns to the initiating editor when appropriate, and late completion cannot resume a cancelled post or interfere with a newer attempt. - A rejected send restores text, uploaded media and exact selected recipients to the source draft only if no newer edit, deletion, upload intent or send supersedes it. Clipboard verification settles before recipient capture, with stale edits/navigation fenced out. ### Related issue Targets `main` after #7124 merged. This PR reuses its publication checks and draft protection; the five forum commits have been replayed unchanged onto the merged parent. Owned-agent discovery (#7122) is already merged. Split from #7114. Forum creation/templates, channel-less Notes and local-agent management are unchanged. Duplicate-name binding from #7133 is already merged and retained by this stack. Inviting does not start a remote agent or promise that it is online or will reply. ### Testing Forum composer lifecycle tests, including clipboard-settlement cases, and TypeScript/changed-file formatting checks passed after the restack. Earlier invitation, focus and transport-recovery browser coverage is retained, not claimed as a fresh full browser run on this head. See [live CI](https://github.com/block/buzz/pull/7125/checks) for current-head results. Browser evidence uses mock IPC; no full local `just ci` pass or native/live-relay validation is claimed. To try it: open a forum post or reply, select an owned nonmember agent and send. Cancel, then retry without reselecting; Invite should add that agent before posting. Deny the add to check the visible error and retained draft. Navigate away/back during a pending invitation or rejected send; no stale publication or overwrite of a newer draft should occur.  *Earlier mock-browser capture, not current-head runtime proof. [Error and successful-post captures](https://github.com/block/buzz/pull/7125#issuecomment-5513883462); no before-state/native capture available.* **Limits:** cancellation cannot undo accepted membership changes or dispatched posts; authorization and publication are not atomic. Recovery is same-window, subject to browser storage limits, and is not a durable in-flight send journal: reload/crash can lose a pending snapshot. Cross-window coordination and in-flight upload custody are unchanged. The parent's legacy member-agent compatibility does not establish ownership for nonmember invitations. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
d595806fc3 |
fix(desktop): authorize remote mentions at publication (#7124)
🤖
## Summary
In Buzz Desktop, you could own an agent running on another device but be
unable to mention it in a channel where it had not yet joined: it was
filtered out before you could invite it. A selected agent could also
disappear from the message's recipients when permissions changed. This
lets you select an eligible agent in the existing **@ menu**, invite it
from the message composer, and send to that agent—or see an error and
keep your draft rather than silently sending without it.
#### Where the experience changes
| Screen / control | Before → after |
| --- | --- |
| A channel's **Message #…** composer, or a message's **Reply in thread
to …** composer | Type `@` (or use the existing @ button), choose your
agent, write the message and press **Send message**. An owned agent not
yet in the channel can now reach the existing **“Mention people outside
this channel?”** dialog when its response settings allow you to address
it. |
| That dialog's **Invite** button | Previously the membership
requirement could block the agent before the invitation. Now Invite
checks permission to add it, adds it as an agent member of the
**channel** (not just the thread), then rechecks membership and response
permission before sending the waiting message. An agent already in the
channel needs no invitation. |
| Existing direct message, or the new-message screen with the **To:**
field | A mention is checked against the conversation the message will
actually enter, including a newly created direct message, rather than
the old or not-yet-created destination. This does not add an Invite
control to direct messages. |
| Editing a message / sending attachments | The selected agent remains
part of the send or edit attempt through attachment upload and the final
permission check. Lost permission produces a visible error instead of
dropping that recipient. |
**Invite is not the only chat choice.** The existing **Do nothing**
button sends the message *without inviting or notifying the nonmembers*;
their names remain references in the text. Where you cannot invite, that
choice is labelled **Send anyway**. To abandon the send instead, dismiss
the dialog with Escape. Invitation actions are disabled while
preparation is pending, preventing duplicate clicks.
**Leaving and returning must not resurrect a cancelled send.** Switching
threads or leaving the composer cancels its pending invitation, even if
you return to the same thread. Cancellation before dispatch sends no
message; an accepted membership change cannot be automatically undone.
An ordinary send without a pending invitation remains bound to its
original destination rather than following you into another
conversation.
**Failed sends must not overwrite your next draft.** If you leave a
thread, return and replace or deliberately clear its draft while an
older send is pending, the older failure cannot restore deleted text,
recipients or files; success cannot erase the newer draft—even if its
text is identical. An untouched draft cleared automatically for sending
remains recoverable on failure. This protection also covers reopening
the composer and starting a newer send.
The channel timeline also keeps its existing **new-messages / Jump to
latest** button available when newer messages are waiting to be
displayed. For example, after sharing a reply to the channel and closing
the thread panel, you can click the catch-up button to reveal buffered
messages. Closing the thread does **not** guarantee the shared row
appears automatically or force you away from reading history.
### Related issue
Built on [#7122](https://github.com/block/buzz/pull/7122), base branch
`split/owned-agent-discovery`, which lets Desktop find and verify owned
agents independently of this device. Current integration head:
`1144465d00273cf74b7c22544ae5a3299bd98560`, built on exact published
root `3a56d17824522580fe04cae463b54f4c7ba66021`. Root #7122 has its own
CI and security gates; this PR must not land ahead of that dependency.
Finding an agent is not channel membership, online status or a promise
of a reply. This PR changes what the existing message controls can do
with those agents; it adds no profile, presence, cloud marker or remote
start/stop UI.
Standalone forum post/reply **Invite / Cancel** is added separately in
[#7125](https://github.com/block/buzz/pull/7125); here those composers
only gain visible authorization errors. Same-name selection/binding
fixes ([#7133](https://github.com/block/buzz/pull/7133)) and mention
spacing ([#7128](https://github.com/block/buzz/pull/7128)) are not
included.
Extracted from [#7114](https://github.com/block/buzz/pull/7114)
(historical source `98fe33ec`). [Behavior and draft-recovery
contract](
|
||
|
|
ee883d73fb |
fix(desktop): bind duplicate mention selections to exact recipients (#7133)
## Summary Selecting two people or agents named Scout could replace the first recipient with the second even though the message still looked right. This binds each selection to its exact identity: the first keeps `@Scout`, and a conflicting selection gets `@Scout (<full public key>)`. Removing one no longer removes or redirects the other. - Reuse the existing **@ suggestion list**; team selection and automatic agent addressing reserve and reuse distinct labels too. Typing an ambiguous name manually shows an instruction to use the picker and preserves the draft without publishing—in chat, edits and standalone forums. - Share literal mention matching across recipient extraction, removal, display and editing so a shorter name cannot claim another recipient's longer or qualified label. - Rebuild authored references when a message is edited, and use the latest authorized snapshot when reopening or forwarding. Rendering/editing can recover qualified identities only from the message's recorded references, not from a key typed into its body. Unresolvable historical names remain literal rather than guessed recipients. - Wrap full-key labels within narrow/zoomed layouts while keeping their complete accessible label and ordinary mention icons. Edit activation waits for the action menu's focus cleanup before focusing the editor. ### Related issue Targets `main`; mention spacing (#7128) is already merged. Split from #7114. This is independent of the #7124 → #7125 remote-invitation stack and does not expand agent eligibility or invitation permissions. The separate Enter-selection suffix issue remains tracked in #7253. ### Testing The desktop unit suite and focused mock-Chromium checks passed on the published integration candidate, including pending-paste selection, edit/forwarding, copy and narrow-layout cases; formatting, types and frontend builds passed. On `0b3b18c0`, 64 focused trust/paste/selection unit tests and six mock-Chromium tests passed with zero browser retries, including actual timeline chip copy → fresh channel paste → send and mismatched-key rejection; TypeScript, changed-file Biome and an isolated E2E build passed. The broader browser run had copy failures before the focused repairs and is not claimed as wholly green. See [live CI](https://github.com/block/buzz/pull/7133/checks) for current-head results. No full local `just ci` pass, native/live-relay or cross-browser validation is claimed. To try it: select two same-name recipients, remove one, send, then edit/reopen and forward; only the intended identities should remain. Type an ambiguous name without choosing a suggestion and check that sending retains the draft with an error. Inspect full-key labels in a narrow window at 150% text size, and open Edit and type immediately.   *Earlier mock-browser captures, not current-head runtime proof. No new before-state capture; screenshots alone do not prove recipient delivery.* **Clipboard trust:** generated full-key-qualified mentions now retain their exact recipient after copy/paste when the full key matches the clipboard record and community directory/profile state independently vouches for the base alias, including numeric collision suffixes. A qualifier alone does not establish trust. Arbitrary historical labels still cannot always be reconstructed. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Bad Janet <150b20bdf6130418df9239dd1bd082c71612c8d653b47c277200365b9be215dc@buzz> |
||
|
|
b1f6b7ef77 |
feat(desktop): preserve mentions across copy and paste (#7228)
Copying a message out of the timeline lost the mention. The rendered chip drops the `@` for display, so the clipboard carried "John Smith" — two ordinary words no composer could bind back to a pubkey. Pasting into another channel produced dead text, and sending it tagged nobody. ## What changed Every Buzz copy now writes two clipboard flavors in one transaction: - **`text/plain`** — readable anywhere, sigils restored, no pubkeys. This is what TextEdit, Slack, and every other external app receive. - **`text/html`** — the same content with each mention wrapped in a span carrying `data-mention-pubkey` / `-label` / `-kind`. On paste, the composer harvests those records, registers each `name → pubkey` with the existing mention machinery, and inserts the content: the chip re-lights and the send path recovers the identity the author tagged. A marker attribute records what the plain flavor holds, so a Markdown copy pastes through the text pipeline and a rendered copy through the HTML one. **Covered surfaces:** timeline selection copy, thread-panel selection copy, forum post/reply selection copy, "Copy message", and composer copy/cut — plus paste in both the channel and forum composers. ## Trust boundary Clipboard HTML is untrusted, and the branch treats it that way: - Records are capped (50), labels bounded (200 chars), and a pubkey must be 64 hex before it can become a `p` tag. - **Only mentions the paste actually shows are registered.** An empty `<span data-mention-pubkey=… -label="Jane Doe">` would otherwise rebind that display name for the rest of the composer session, so a later hand-written @Jane Doe would chip-light convincingly against the attacker's key. Each branch registers only the records whose label appears in the text *it* inserts, matched with `getMentionOffsets` — the same matcher the send-time extractor uses. - **The visibility gate reads only what ProseMirror will insert.** `DOMParser` hard-drops `script`, `style`, `title`, `noscript`, `object`, and `head` content, so `visible<style>@Jane Doe</style>` beside an empty chip span used to smuggle a binding past the gate. Those elements are stripped before either output is derived. - **A partial chip never gains a sigil.** A selection crossing a chip boundary falls through to the browser's default copy, which serializes the full identity attributes around a slice of the text — pasting that invented "@Smith" out of "John Smith". Paste now leaves a fragment as plain text, tolerating only what a whole chip picks up in transit (restored sigil, author casing, U+00A0 swaps, and the label cap's own ellipsis). Both clipboard sides share one `matchChipTextToLabel` helper, in the module that owns the label attributes, so copy and paste cannot drift on what counts as a whole chip. ## Notes - Mention matching reuses `getMentionOffsets`, so code spans and fences are excluded and the longest display name wins. - The plain flavor inlines chip boxes before reading `innerText`; a chip is a flex container, so the browser's own copy split "@John Smith" onto its own line. - `MarkdownMention` and `MacEmacsTextShortcuts` are extracted verbatim from `markdown.tsx` and `useRichTextEditor.ts` to keep both files under the size gate. ## Testing - ~40 unit tests over the flavor builder/parser, the visibility filter, the ignored-tag sweep, and the chip-match verdicts. - A Playwright spec (`desktop/tests/e2e/mention-clipboard.spec.ts`) driving real copy/cut/paste DOM events: timeline and "Copy message" of a multi-word non-member mention pasted into another channel and sent with the original pubkey in its `p` tag; the forum copy → forum reply round trip; composer copy/cut; plain flavor asserted to contain no 64-hex string; the boundary-crossing drag; and the hidden-record and `<style>` smuggling vectors. - Every regression test is bound to a production seam and fails with its guard removed. - `just ci` / pre-push lanes green (5927 desktop unit tests, typecheck, lint, file-size gate). --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
752cbfc037 |
docs(nip-fi): add Blossom kind-24242 media possession-proof exception (#7278)
Adds the media/Blossom possession-proof exception section to
\`docs/nips/NIP-FI.md\`.
## What this changes
Encodes the condition set confirmed by Thufir's security review
(2026-09-03) as normative MUSTs in NIP-FI. Kind-24242 Blossom auth
events are admitted as the NIP-FI pairing possession proof for **media
routes only** — a named, bounded exception with an explicit precedent
fence against expansion.
### Scope fence
Kind-24242 proofs are valid only on:
| Proof type (`t` tag) | Valid route | Method |
|---|---|---|
| `upload` | `PUT /upload` (and temporary alias `PUT /media/upload`
until removed) | PUT |
| `get` | `GET /media/{hash…}`, `HEAD /media/{hash…}` | GET, HEAD |
All other protected routes MUST reject kind-24242 proofs.
### Upload proofs
Exactly one `x` tag over consumed body bytes; temporal check precedes
body consumption.
### Read proofs (the relaxation Will approved)
Host-wide MAY: no `x` required. Exactly one `server` tag matching the
resolved tenant host is a MUST. Optional `x` must match parent hash if
present.
Named residual (verbatim in spec): within at most 60 seconds from
minting (plus 5s future-skew), a captured full header set allows reading
any media blob on exactly one tenant host — read-only,
membership-checked, revocable, not state-changing, not cross-tenant.
### Freshness (Thufir option 2)
- `created_at <= now + 5s` (bounded future skew)
- `now - created_at <= 60s`
- Exactly one `expiration`, valid at admission, satisfying `expiration
<= created_at + 60s`
### Transport/cardinality
Exactly one each of `Authorization` (Nostr scheme), `t`, `expiration`,
`server`; `x` at most once; reject any duplicate, malformed, or
conflicting instance.
### Per-request pairing
Full assertion verification, exact key equality between assertion
`nostr_pubkey` and kind-24242 signer, deny-map enforcement on every
request. Stub gap named.
### Compliance note
PR #7264 implementation is explicitly non-compliant until the bounded
hardening task lands (named gaps: multi-tag acceptance, 3600s window,
optional `server`).
## Behavioral oracle
`FI-TRACE-HTTP-INGRESS` extended to cover kind-24242 admission and
denial cases.
## Scope
Docs-only. No code changes. The code hardening is a separate follow-on
task.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
5073e075d7 |
fix(desktop): unify owned-agent cloud provenance markers (#7129)
Unify presentation-only cloud provenance across agent identity surfaces. Keep successful local-inventory and verified-ownership gates; preserve main availability and mention spacing behavior. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com> |
||
|
|
d5a73b9f35 |
fix(desktop): derive agent availability from relay presence (#7127)
🤖 ## Requested rebase published — |
||
|
|
44d19f59a5 |
fix(desktop): preserve spacing after multi-word mentions (#7128)
🤖
## Summary
In Buzz Desktop, choosing a multi-word name and immediately continuing a
sentence could swallow the space after the mention: `Hey @Alice
Chenhello`. This keeps the separator, so the same action produces `Hey
@Alice Chen hello` without moving the caret or repairing the name by
hand.
The editor recognizes the complete selected label, including its
internal spaces, and settles the autocomplete caret after the trailing
separator. Deliberately moving left or clicking inside the label still
lets you edit there; this is not a rule that forces every caret to the
end of a mention.
### Related issue
Independent base: `main`. Child:
[#7133](https://github.com/block/buzz/pull/7133), whose disambiguated
labels also contain spaces. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).
[Behavior
contract](
|
||
|
|
c328202cb0 |
docs(nip-fi): document Git smart-HTTP credential exemption (#7268)
Amend NIP-FI HTTP ingress with an explicit Git smart-HTTP credential-helper exemption. The exception covers method binding, endpoint-URL binding, and the `payload` tag requirement for `info/refs`, `git-upload-pack`, and `git-receive-pack`, while preserving per-request NIP-FI assertion, key pairing, and deny-map enforcement. The spec records Git's credential-protocol limitation, the required compensating controls, and the rule that this exception is limited to these endpoints and is superseded by per-request signing. Related: [PR #7264](https://github.com/block/buzz/pull/7264) Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
187df22252 |
docs(nip-fi): adopt deny-until-TTL and extend enforcement to HTTP ingress (#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> |
||
|
|
83e5962984 |
fix(desktop): keep explicit agent profiles bound to their exact key (#7131)
🤖
## Summary
In Buzz Desktop, clicking a message from stopped agent A could open
running agent B—and B's controls—because both shared a persona (an agent
definition). This now opens the author you clicked and only that agent's
own controls, so you can inspect an old message without being redirected
to a different running agent.
An explicit public key—the identifier for one agent—now stays exact
across message authors, members, DMs, deep links and Instances rows,
including stopped, archived and relay-only agents. Local controls come
only from a matching local record for that key. A relay-only A cannot
borrow B's Start/Stop/Edit controls or configuration.
Deliberately opening a **persona** is different: it can still select a
representative that respects archived instances or offer Start when none
remains. The change removes competing historical-persona redirects
rather than adding another identity exception.
### Related issue
Independent base: `main`; no stack parent or child among the
replacements. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).
[Behavior
contract](
|
||
|
|
434dafe958 |
fix(desktop): discover authenticated owned relay agents (#7122)
🤖 ## Summary An agent you own could be missing from **New message → To:** and **Channel members → Add people and agents** on a machine that has never managed it. This PR lets those existing lists find your agent without requiring a shared channel first. Desktop now checks records proving you own it, rather than looking only at agents in channels you've already joined. **No new screen or control is added.** For example, an agent with verified ownership and **Who can send instructions → Only me (default)** can now appear even with no shared channels. Each screen still applies its existing access rules; this does not make every discovered agent selectable everywhere. | Screen / control | Before | After this PR alone | | --- | --- | --- | | **New message → To:** recipient picker | An owned agent absent from this machine and shared-channel bot lists could be missing. | Its named **agent** row can appear; selecting it adds a recipient chip. This is recipient selection, not a guarantee that a later message will reach or wake the agent. | | **Channel members → Add people and agents** | The same agent could be missing from **Not in this channel** search results. | Its row can appear with the existing **Add** button. If you can add members, that button submits the existing channel-membership request; finding the row alone changes no membership. | | **Stream / forum composer → @ suggestions** | An owned agent already in the channel under an ordinary member role could be missing from agent suggestions. | Its actual membership is recognized without requiring the bot role. Agents not managed on this device still need membership in that channel. | | **Pulse → Agents** | An agent absent from both local management and the server's agent list was omitted from the count and author lookup. | The count and feed's author lookup can include it; notes appear only if it has published them. | Being listed does **not** mean the agent is online, add it to a channel, or grant local Start/Edit controls. For agents not managed on this device, global **Search** still excludes those configured for “Only me”, and DM @ selection is not added here. DM @ selection and message-driven nonmember invitation are addressed in [#7124](https://github.com/block/buzz/pull/7124); the standalone forum **Invite / Cancel** flow is in [#7125](https://github.com/block/buzz/pull/7125). <details> <summary>Ownership and membership checks</summary> A discovery lead is not proof: the latest agent profile must have a valid signature and exactly one valid ownership attestation—the owner's signed link to that agent. Its response policy must be signed by that verified owner; an invalid latest policy cannot restore an older permission. Membership comes separately from the latest server-signed roster, including removals. Existing profile cards, owner labels and agent-avatar shapes also use this stricter verification: malformed or forged evidence must not supply ownership/agent classification on its own. Valid ownership was already recognized; no profile-picture or badge design changes. Attestation time conditions apply to the signed event's timestamp, not a live expiry timer. Existing legacy compatibility and builds requiring verified owner policy retain their respective rules. Discovery and sending remain separate operations, not an atomic permission check. </details> ### Review corrections - When runtime and owner policy overlap, **explicit online/away/offline from the verified latest runtime is retained**. Policy still supplies ownership/permissions; claimed runtime membership is not restored. Missing/unrecognized status stays unknown, and invalid latest policy cannot revive runtime permissions. - Discovery without runtime evidence is now **unknown**, not offline: native conversion, both IPC adapters, Pulse, Projects and profile/session consumers preserve that distinction. Unknown has no status dot and is not promoted to a deployed/running agent. - Both relay-only picker paths retain the authenticated owner, including the existing **managed by you** label. The analogous global Search projection is fixed without changing its existing “anyone” filter. - Authorized stored profile activity remains visible when liveness becomes unknown/absent or the active turn ends. History reads do not start a live subscription, grant access, or imply current availability. ### Related issue Independent base: `main`. Child: [#7124](https://github.com/block/buzz/pull/7124), then [#7125](https://github.com/block/buzz/pull/7125). Extracted from [#7114](https://github.com/block/buzz/pull/7114), retained as historical source (`98fe33ec`). [Behavior contract]( |
||
|
|
04babf0265 |
chore(db): drop Phase-A NIP-FI relay-side authority ledger (#7221)
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> |
||
|
|
d4420eb47f |
docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
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 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
42b42447b0 |
feat(mobile): prepare buzz-push-gateway for deployment (#7158)
A few things to prepare for deploying the Buzz push gateway as implemented in #6269: - Cut `buzz-push-gateway` chart `0.2.0` from the current config (certificate auth, dogfood client) - Datadog-compatible metrics - Align the desired schema, metrics docs, and production ingress guidance with the current dogfood deployment contract ## Links - [BUZZ-17: Publish the MVP-compatible gateway chart](https://linear.app/squareup/issue/BUZZ-17/publish-the-mvp-compatible-gateway-chart) - [BUZZ-26: Add Datadog-compatible push-gateway metrics and correct source drift](https://linear.app/squareup/issue/BUZZ-26/add-datadog-compatible-push-gateway-metrics-and-correct-source-drift) --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
3ed623bb21 |
feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229)
## Why A wedged relay boot pod holding a relation lock can park every other writer in the fleet behind it: DB load pins at pool capacity in `Lock:relation` waits while CPU stays flat, and nothing server-side releases the lock until the holder dies. We hit exactly this in production — ~1,400 sessions queued behind one crash-looping pod's boot transaction for ~20 minutes until kubelet killed the container. ## What Applies session-level Postgres timeouts to every **writer** connection inside the existing single `after_connect` hook in `buzz-db`, all env-tunable through the same `Config::from_env → DbConfig` path as the existing pool-size knobs: | Env var | GUC | Default | Effect | |---|---|---|---| | `BUZZ_DB_LOCK_TIMEOUT_MS` | `lock_timeout` | 5000 | statements waiting on any lock fail fast instead of parking behind a wedged holder | | `BUZZ_DB_IDLE_TXN_TIMEOUT_MS` | `idle_in_transaction_session_timeout` | 60000 | reaps wedged clients idling inside an open transaction while holding locks | | `BUZZ_DB_STATEMENT_TIMEOUT_MS` | `statement_timeout` | 0 (off) | opt-in runaway-statement cap; off by default because startup migrations/backfills legitimately run long statements | `0` disables a timeout (Postgres semantics) and deliberately passes through the env parsing — unlike the pool-size knobs where `0` falls back to the default. The reader pool is untouched: replica sessions never take contended locks and already fail acquire in 150 ms. Deployers tune these via plain env vars (`.env`, or `relay.extraEnv` in the Helm chart) — no code changes needed. ## Behavior change to note With the 5 s default `lock_timeout`, a boot-time migration or backfill that waits >5 s on a lock now errors (surfacing in logs / crash-looping the pod) instead of stalling silently. That is the intended visible-failure-over-fleet-stall tradeoff; deployers with slow contended migrations can set `BUZZ_DB_LOCK_TIMEOUT_MS=0`. ## Testing - `cargo test -p buzz-db -p buzz-relay` — buzz-db green; buzz-relay has 9 failures that also fail on clean `main` in this environment (api::admin/api::media/mesh_demo — unrelated, pre-existing). - New config test covers override / `0`-passthrough / invalid-fallback for all three env vars. - Extended the existing `writer_pool_safety_hook_is_single_and_composed` source-shape test so the timeouts can't drift out of the single `after_connect` hook (SQLx replaces hooks — a second hook would silently disarm the floor guard). - `cargo fmt --check` and `cargo clippy --all-targets` clean for the touched crates. Closest existing PR/issue: none found. --- **Update Aug 28, 17:06 EDT:** Rebased onto `main` at `a3730784fc` and addressed the latest correctness review. - Ported the timeout policy onto the refactored `buzz-db::runtime` pool constructor and kept the shared env overlay for relay, admin, deletion, and audit writers. - Migration/schema-destruction connections now disable `lock_timeout` and `statement_timeout` for their intentional long wait/DDL path. This supersedes the earlier “Behavior change to note”: contended boot migrations wait for the current migration owner rather than crash-looping after five seconds. - The audit worker now preserves and retries the same entry on PostgreSQL `55P03` lock timeouts, using exponential backoff capped at one second. Other database errors retain the existing terminal error behavior, and retries emit `buzz_audit_log_lock_retries_total`. - Added CI-backed PostgreSQL regressions for writer GUC installation/migration exemption, audit-pool lock timeouts, and worker recovery. The worker regression holds the real audit advisory lock past `lock_timeout`, observes a retry, releases the lock, and proves the original entry is appended exactly once. Current verification supersedes the earlier testing notes: workspace Rust clippy passed with warnings denied; all nine infrastructure-free backend unit-test lanes passed; all three focused PostgreSQL regressions passed against PostgreSQL 17; formatting, diff checks, file-size guards, and desktop frontend checks passed. The Linux Blox workstation could not run the unrelated Tauri native lane because `glib-2.0` is absent, so that platform check is left to PR CI. --- **Update Aug 31, 11:09 EDT:** Rebased onto current `main` at `c3132c3ee9` and reran the requested audit-lock contention scenario on Blox at head `896c3fe9ed`. - `git range-diff` reports both PR commits unchanged by the rebase; the branch remains two commits and the worktree is clean. - `cargo fmt --all -- --check` and clippy with warnings denied passed for `buzz-db`, `buzz-relay`, `buzz-admin`, and `buzz-deletion`. - All three focused PostgreSQL 17 regressions passed: writer session timeout/migration exemption, audit writer timeout bounds, and audit worker recovery of the original entry exactly once. - Live protocol verification used a head-built relay and CLI, native PostgreSQL 17/Redis, `BUZZ_DB_LOCK_TIMEOUT_MS=300`, and an eight-second hold on the community audit advisory lock. The real message was accepted and persisted once while the lock was held; its audit-row count remained zero during contention while retries accumulated. After release, exactly one `event_created` audit row appeared and remained exactly one after an additional two-second duplicate check. The run recorded nine lock-timeout retries, zero audit failures, and event ID `7f8c4ffae28e78555fcf2d56396d6e6c01b3712e5411288dc79e9a54af9d9444`. Generated with Codex --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> |
||
|
|
c432a111ca |
feat(mobile): push notifications MVP (#6269)
This PR implements MVP, iOS-only,
[NIP-PL](
|
||
|
|
86b9142a09 |
refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777)
Adds authenticated, role-based moderation to the relay admin API. On
`main` the admin API is read-only and gated only by `Host`/`Origin`
matching; this branch adds NIP-98 authentication, a two-tier
Operator/Moderator principal model, mutation and staffing endpoints, and
NIP-11 auto-discovery so clients never type the admin URL by hand.
## Authentication (`BUZZ_ADMIN_AUTH`)
`BUZZ_ADMIN_AUTH` accepts `nip98` or `disabled`. Leaving it unset
defaults to `nip98` (fail-secure). Configuration fails closed: any other
value aborts startup, while a lingering `BUZZ_ADMIN_TOKEN` is ignored
with a startup warning — token (bearer) authentication is not supported.
`Host`/`Origin` matching is retained in every mode as defense-in-depth.
- **`nip98`** (default) — per-request signed NIP-98 (kind 27235) events,
resolved to an Operator or Moderator principal with per-person
attribution and individual revocability. Read-write per resolved
principal.
- **`disabled`** — no credential; relies entirely on network-layer
controls (reverse proxy, VPN, firewall) and logs a `WARN` on every boot.
Always read-only: `authorize()` resolves no principal, so mutation and
staffing routes always `403`.
## Roles
Buzz has two independent authority axes after this change.
**Relay-level** roles (new here) are deployment-global: they act across
every community on the relay, through the admin API. **Community-level**
roles (pre-existing, unchanged by this PR) are tenant-scoped: they act
inside one community, through signed Nostr moderation commands.
### Relay level (new)
| Role | Description |
|---|---|
| **Operator** | Full control of the deployment's moderation surface:
read all reports, feedback, and attachments across every community;
resolve reports with enforcement (`delete`/`kick`/`ban`/`timeout`) or
decisions (`dismiss`/`escalate`); reopen and cancel; update feedback
status; and manage the Operator/Moderator roster via the staffing
endpoints. |
| **Moderator** | Day-to-day triage: everything an Operator can do
except staffing — cannot view or change the roster. |
How a pubkey acquires a relay role (resolution order; config always
outranks DB):
1. Listed in `RELAY_OPERATOR_PUBKEYS` → **Operator** (source `config`)
2. Equals `RELAY_OWNER_PUBKEY` while `RELAY_OPERATOR_PUBKEYS` is empty →
**Operator** (source `owner_fallback`, a break-glass grant for
self-hosters that deactivates once any operator is configured)
3. Row in the `relay_operators` table → **Operator** or **Moderator**
(source `db`, managed via the staffing endpoints)
4. No match → `403`
### Community level (pre-existing, unchanged)
| Role | Description |
|---|---|
| **Owner** (community) | Full authority within their community: every
moderation action (delete, kick, ban/unban, timeout/untimeout, resolve
reports, view queue) plus member, role, and invite management. No guard
rails. |
| **Admin** (community) | Same community-wide moderation capabilities as
owner, except an admin cannot ban or time out the owner or a fellow
admin — only the owner may action an admin. Manages members and invites;
only the owner grants the admin role. |
| **Member** (community) | Standard participant; no moderation
capability. |
| **Owner / Admin** (channel) | Channel-local authority only: delete
messages and kick users within their own channel. |
| **Member / Guest / Bot** (channel) | No moderation authority. |
There is no community-level Moderator tier in v1; relay-level Moderator
is the only role by that name.
## Escalation scoping
The operator report queue is an escalation backstop, not the community's
day-to-day triage surface (per `VISION_MODERATION`, the severe class is
the platform's to review rather than the community's). Two rules enforce
that:
- **Escalated-by-default listing.** `GET /reports` with no `status`
parameter returns only `escalated` reports. An explicit
`status=<open|resolved|dismissed|escalated>` filter is always honored as
given, and full visibility across every status stays available for
platform-safety and legal review via `scope=all` (which lists reports
regardless of status). `scope` accepts only `all` and is ignored when an
explicit `status` is present.
- **Auto-escalated `illegal` reports.** Member reports whose category is
`illegal` are ingested with `status=escalated` rather than `open`, so
the severe class reaches the operator backstop without waiting for a
community admin to forward it. Every other category still lands `open`.
Auto-escalation only sets the queue status — it records no moderator
decision and stamps no resolver, so an auto-escalated report is
indistinguishable downstream from an admin-escalated one: the reopen
route returns it to `open` on the same terms, keyed only on status,
never on how the report became escalated.
## Principal resolution and NIP-98 admission
`resolve_admin_principal()` returns `AdminPrincipal { pubkey, role,
source }` per the resolution order above; `None` never falls through as
a role. Admission is ordered so the replay guard is a privilege, not a
public surface: signature/URL/method/payload-hash verification first,
roster check second, and only then is the deployment-scoped replay id
atomically consumed — a validly-signing but unrostered key never
allocates a replay slot. Redis failure fails closed.
## Report resolution, recovery, and enforcement provenance
`POST /reports/{id}/resolve` is a crash-safe enforcement state machine:
decision-only outcomes (`dismiss`/`escalate`) are a single
CAS-plus-audit transaction; enforcement
(`delete`/`kick`/`ban`/`timeout`) claims the report
(`open`→`processing`), runs the durable mutation, then finalizes — a
re-drive resumes at the step marker and converges to exactly-one
enforcement, fenced by a lease and an outbox claim token.
Person-directed enforcement on an `event`-kind report derives its target
from the stored event's author (server-owned truth, never the reporter's
`p` tag) via a single `derive_enforcement_target` shared by the HTTP
driver and the recovery worker. If the reported event was purged before
its author could be read, person-directed actions are rejected pre-claim
and the report stays `open`; `delete` needs only the event id and is
exempt.
`GET /reports/{id}` and the resolve response carry an `activeAction`
field surfacing the enforcement that actually executed — a report
dismissed after a reopen still reports the ban that ran. `POST
/reports/{id}/reopen` returns a terminal report to `open` (idempotent on
`requestId`). `POST /reports/{id}/cancel` is the sole recovery path for
a pre-mutation `failed` action, attributed via
`relay_admin_actions.cancelled_by`.
## Feedback
`GET /feedback` and `/feedback/{id}` survive a tenant purge: provenance
columns are severed to `NULL` rather than cascade-deleted, and the
attachment path fails closed to `404` on a severed row. `PATCH
/feedback/{id}` updates lifecycle `status`
(`new`/`reviewed`/`archived`).
## Staffing and probe
`GET/PUT/DELETE /operators/{pubkey}` are Operator-only; mutating a
config-backed pubkey returns `409 Conflict`. `GET /operators` returns
the union of config and DB principals with per-entry `source`. `GET
/probe` reports auth mode, role, source, `canAct`, and `canStaff` for
the desktop console.
## NIP-11 auto-discovery
The NIP-11 relay-information document gains an optional `admin_api`
field carrying the canonical admin origin (`scheme://host[:port]`, no
path), present iff `BUZZ_ADMIN_HOST` is set and omitted otherwise. The
scheme follows the same loopback rule as NIP-98 `u`-tag verification via
a shared `scheme_for_host` helper, so the advertised origin and the
origin the relay verifies against can never diverge.
## Operator API origin decoupling
`RELAY_OPERATOR_API_ORIGIN` is no longer required at boot when
`RELAY_OPERATOR_PUBKEYS` is set — it is used only by the
community-provisioning endpoints, which fail closed at request time
(with a boot-time `WARN`) until it is set. The admin console needs no
origin.
## Admin-web adaptation
The standalone `admin-web` dashboard signs each request as a NIP-98
event via a NIP-07 browser extension, discovers the auth mode with a
single unauthenticated probe (`200` → `disabled`, anything else →
`nip98`, fail-secure), and carries no token entry surface. Playwright
coverage exercises the NIP-98 and CSP paths.
## Security hardening
Three findings from security review are folded in:
- **Append-only roster audit.** `PUT`/`DELETE /operators/{pubkey}`
mutate the deployment-wide root of trust, but the upsert overwrites
`role`/`added_by` in place and the delete removes the only row — so a
grant→revoke sequence left no trace of who was ever granted or by whom.
Each mutation now writes an `relay_operator_audit` row (actor, target,
`grant`/`revoke`, pre-image `prev_role`, `new_role`, timestamp) inside
the same transaction as the mutation. A per-target transaction-scoped
advisory lock serializes concurrent mutations of the same pubkey before
the pre-image read, so the recorded `prev_role` is always the true
predecessor even under a concurrent-grant race. Chronology is keyed on a
`BIGINT GENERATED ALWAYS AS IDENTITY` `seq` column, not the wall clock:
the serializing lock guarantees insertion order and `seq` captures it,
so ordered reads (`ORDER BY seq`) follow the true privilege chain even
across a backward NTP step that a `clock_timestamp()` ordering would
invert. `created_at` (`clock_timestamp()`) is retained as informational
occurrence time only. Append-only by construction — no `UPDATE`/`DELETE`
path and no API surface. A no-op delete writes nothing.
- **`expirationSecs` overflow.** The timeout path built `Utc::now() +
Duration::seconds(secs as i64)` from an attacker-controlled `u64`:
`i64::MAX` panicked the handler, and a wrapped-negative magnitude minted
a *past* expiry that still passed validation. `compute_timeout_until`
now rejects zero, rejects magnitudes above a documented
`MAX_TIMEOUT_SECS` (365 days), and uses checked
`try_seconds`/`checked_add_signed` so no input can panic or produce a
past expiry — over-cap, zero, `i64::MAX`, and wrapping-negative inputs
all return a clean `4xx`.
- **Uppercase-hex config-backed bypass.** Config pubkeys are lowercased
at parse, but the `409` immutability check raw-string-compared the path
param while `decode_hex_pubkey` accepted uppercase — so `PUT
/operators/{UPPERCASE}` skipped the guard and wrote a shadow row for the
same 32 bytes. The validated param is now canonicalized (lowercased)
before the `409` check, DB write, `DELETE`, and response body.
## Migrations
- `0035_relay_operators.sql` — `relay_operators` roster table
(deployment-global), `actor_authority` on `moderation_actions`,
`processing` status plus `active_action_id` on `moderation_reports`,
`status` on `product_feedback`.
- `0036_relay_admin_actions.sql` — enforcement-action table with a
`request_id` idempotency key, a `step_marker` for crash recovery, and a
`cancelled_by` attribution column.
- `0037_relay_admin_action_lease.sql` — lease fencing for the action
worker.
- `0038_relay_admin_outbox_claim_token.sql` — fenced claim token on the
outbox worker.
- `0039_relay_operator_audit.sql` — append-only `relay_operator_audit`
trail for roster mutations (see Security hardening).
`docs/admin/README.md` documents the full principal model, NIP-98 event
requirements, capabilities by role, the startup error matrix, and the
discovery field.
## Production blast radius
A relay without `BUZZ_ADMIN_HOST` is completely unaffected — the admin
surface stays disabled and `BUZZ_ADMIN_AUTH` is ignored; a lingering
`BUZZ_ADMIN_TOKEN` logs a startup warning and must be removed. Where
`BUZZ_ADMIN_HOST` **is** set, unset `BUZZ_ADMIN_AUTH` defaults to
`nip98` (per-person signed auth); `BUZZ_ADMIN_AUTH=disabled` reproduces
`main`'s prior `Host`/`Origin`-only gating but is read-only (mutation
routes `403`). The five migrations add tables and columns without
touching existing data.
---
Related: [block/buzz#4768](https://github.com/block/buzz/pull/4768)
(desktop admin console consuming the `admin_api` field),
[squareup/bb-public#339](https://github.com/squareup/bb-public/pull/339)
(Phase 4 rollout config)
---------
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>
|
||
|
|
953a1798b2 |
feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776)
The closed, provider-neutral contract layer at the root of the NIP-FI federated-identity dependency graph — Phase A, PR 1 of the plan. It has no dependencies on any other PR and defines no database schema, migration, runtime JWKS fetching, binding resolution, enrollment, or request/proof binding; those belong to later PRs. ## What this adds - **Multi-issuer assertion-policy config** (`IssuerRegistry`, `IssuerPolicy`) keyed by exact `iss`. Identity is issuer-qualified `(iss, sub)` throughout — equal `sub` under different `iss` are distinct identities. The subject coordinate is fixed to the JWT `sub` claim (`SUBJECT_CLAIM`), never configurable, so no deployment can seal a mutable attribute like `email` as identity. Issuer URL and audience remain deployment configuration. - **The two deterministic semantic contract identities** — `AssertionPolicyId = H(canonical assertion-policy contract)` and `TransportContractId = H(canonical transport contract)` — derived by length-prefixed, domain-separated SHA-256 so a semantic change moves exactly its owning ID while benign JWKS rotation never changes policy lineage. Set-valued policy inputs (audiences, algorithms, subject-class values, scope capture) are canonicalized before derivation, so the ID is invariant under permutation and duplication. Config fields that a freshness class never reads are rejected at construction (an `offline-jwt` policy cannot carry `maximum_status_age`), so the canonical encoding stays total over valid configs and semantically identical policies always derive one ID. - **The single canonical verifier** (`FederatedAssertionVerifier`, `FI-INV-16`) producing the origin-sealed, provider-neutral `VerifiedAssertion` normalized result. Its constructor is crate-private, so unverified claims cannot be promoted into authority. The issuer→JWKS authority is entirely crate-owned: `AssertionKeySet` has no public constructor and `IssuerKeySource` is sealed, so no downstream crate can relabel one issuer's keys as another's. The authenticated key set is bounded (`MAX_JWKS_KEYS`) and the bound is folded into `AssertionPolicyId`, so an unbounded attacker-controlled `kid` scan cannot be driven; every snapshot requires a finite positive hard deadline. - **Sealed revalidation dependencies** — `RevalidationDependencies` carries the key-snapshot hard deadline and a `ConfidentialAssertion` handle to the exact compact JWS (no `Debug`/`Display`/`serde` leak, sole read path `compact_jws()`), so a changed snapshot can revalidate the same evidence: a retained key revalidates, a removed key denies. - **The privacy-preserving four-class denial contract** (`DenialClass`, `FI-INV-13`) with the byte-exact Nostr text, HTTP status, body, `Content-Type`, and `WWW-Authenticate` values fixed by the spec's rejection table. An unreadable required current dependency maps to `authorization_unavailable`/503, never to rejected evidence, and rejected evidence never masquerades as a 503 at either end of the pipeline: all bounded, dependency-independent checks (compact structure, header, signature shape, policy, algorithm, token class) precede key-source lookup, and all offline validation (token-class, key, signature, audience, claims, time) completes before any status-witness deferral. A wrong-`typ` or structurally malformed token, or malformed/invalidly-signed input naming a current-status issuer, therefore denies with `evidence_rejected`/403 rather than being reported as a 503 availability signal. ## Corrections applied to the mined source Mined from the `buzz-auth` verifier core in [#1476](https://github.com/block/buzz/pull/1476) and corrected to the settled spec (the merged `docs/nips/NIP-FI*.md`, [#5946](https://github.com/block/buzz/pull/5946)), which settled after #1476 was written: - Token-class selection with exact `typ` enforcement. Two classes are offered: `at+jwt` and `nip-fi+jwt`. There is deliberately no generic/absent-`typ` "named compatibility" class: it cannot be proven disjoint from an OIDC ID token by claim presence alone (an issuer can mint an ID token carrying `client_id`), and the only authenticated discriminator is `typ`, which such a mode declines to constrain. - OIDC ID-token denial — denies even when `iss`, `aud`, and `sub` match, via exact `typ` mismatch against every accepted class. - The fixed `nostr_pubkey` claim accepted only as lowercase hex of exactly one 32-byte key; bech32 and other aliases deny. - Resource-owner / client-subject ambiguity denial and required `client_id` for `at+jwt`. - JWK admissibility: a key's `use` and, when present, `key_ops` must authorize signature verification (a key restricted to other operations such as `encrypt` is rejected), and the selected JOSE algorithm is bound to the key's required family and curve (ES256↔EC/P-256, ES384↔EC/P-384, EdDSA↔OKP/Ed25519, RS/PS↔RSA). The JWK `alg` is advisory; the actual key material is what signs, so a JWK declaring a matching `alg` over mismatched material (a different family or curve) is rejected before signature verification. - Spec-exact time arithmetic (`now < exp`, `iat <= now + skew`, `now < iat + maximum_assertion_age`, equality at expiry is expired). `exp`/`iat`/`nbf` accept finite integer or fractional RFC 7519 `NumericDate` values with checked, overflow-safe conversion; non-finite and out-of-range values deny. ## Verification In-crate tests in `crates/buzz-auth/src/nip_fi/verifier/tests.rs` sign real ES256 assertions against a fixed test key and cover: the happy path, exact-wire-text for all four denial classes, deterministic and semantic contract IDs, canonicalization invariance, token-class enforcement including ID-token denial, JWK `key_ops` rejection, `nostr_pubkey` hex handling, time bounds, and multi-issuer selection (same subject across distinct issuers yields distinct identities and policy IDs). They also cover the round-3 contracts: the key-set bound (oversized rejected, at-bound accepted, empty rejected), fixed-`sub` identity (identity is `sub` not a configured `email`; a token without `sub` denies), offline-before-deferral (invalid-signature, wrong-audience, malformed-claim, and expired under a current-status issuer each deny 403/`evidence_rejected`), and the revalidation contracts (dependencies carry the deadline and exact JWS; a retained key revalidates, a removed key denies). And the round-4 contracts: algorithm↔key-material binding, covered exactly per accepted algorithm (a table-driven matrix asserts every policy-acceptable algorithm — ES256, ES384, EdDSA, RS256/384/512, PS256/384/512 — matches only its required family/curve and rejects every other, so each mapping mutation fails individually; and an ES256 token against P-384, RSA, and Ed25519 material each deny `InvalidKey`); malformed evidence classified before key lookup (wrong-`typ`, two-segment, four-segment, empty-signature, and non-base64url-signature tokens against an empty source deny 403 not 503); `maximum_status_age` inapplicable under offline-jwt rejected at construction while current-status still requires it; and fractional `NumericDate` (finite fractional `iat`/`exp`/`nbf` within bounds verify, non-finite and out-of-range deny). Compile-fail doctests guard the sealed authority seam (`AssertionKeySet::new`, `IssuerKeySource`) and the absence of the named-compatibility token class. ## Notes - `Co-authored-by` attribution is preserved for the mined author. The #1476 `buzz-auth` commits are authored by Cea Stapleton Cordasco (the salvage map records #1476 under Franco; the git history on that PR is Cea's — surfacing for the attribution/closure record). - Amends `docs/nips/NIP-FI.md` to remove the "named compatibility access token" class and its `FI-TRACE-TOKEN-CLASS` oracle reference, so the normative spec matches the two-class root implementation (a generic/absent `typ` cannot be proven disjoint from an OIDC ID token). Removal-only, no neighbor redesign; a `docs/nips/` grep confirms no dangling cross-reference to the class remains. - Adds `jsonwebtoken 10.4.0` (`aws_lc_rs`) as a workspace dependency. Dependencies: none. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
8471049c43 |
feat(desktop): add KLIPY GIF search to composers (#5554)
## Summary - Supersedes #1913 with a KLIPY-hosted URL implementation. - Adds KLIPY GIF search and trending results to desktop message and forum composers. - Keeps selected GIFs hosted by KLIPY; Buzz stores only the external URL and media metadata (no imeta tag, since relays only accept hash-backed local `/media/` entries). - Aligns the Emoji/GIF picker with Buzz's standard segmented control, theme surfaces, and motion behavior. ## Relay-to-provider boundary - The relay proxies KLIPY search/share so the `BUZZ_KLIPY_API_KEY` never reaches the desktop; the key stays server-side behind a redacted `Debug` impl. - The dedicated GIF `reqwest` client sets `redirect::Policy::none()`. Because the API key rides in the request path, following a provider `3xx` could replay a key-bearing URL to an attacker-chosen host (an SSRF/key-disclosure primitive). With redirects disabled, a `3xx` returns as a non-success status that the handlers map to a generic `502`; the `Location` target is never read or forwarded. - Admission reuses the established NIP-98, membership, replay, and per-pubkey rate-limit gates, with an upstream response-size cap and allowlisting so KLIPY error bodies never cross the relay boundary. ## Accessibility - Under `prefers-reduced-motion: reduce`, the picker grid renders a static provider poster (a normalized `jpg` asset) instead of the animated preview, or a named static placeholder when no poster is available. It reacts to preference changes while mounted. `no-preference` keeps the animated preview. - Selected GIFs carry their title through `ImetaMedia`'s `displayLabel`, so the composer thumbnail, preview dialog, editor, lightbox, and remove control all derive one non-empty accessible name instead of an empty `Attachment ` label. Ordinary hashed uploads keep their existing hash-derived names. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> |
||
|
|
f249710331 |
Qualify canonical relay images for staged delivery (#6781)
## Why Phase 1 of progressive delivery needs an explicit eligibility boundary before deployment automation can safely consume relay artifacts. Today the canonical Docker workflow can publish independently of the same-SHA CI result, the relay does not expose its source/build identity, and the Helm chart only renders mutable tags. The existing `ghcr.io/block/buzz-staging-dev` workflow in #6709 remains a manual pre-merge preview lane. It is deliberately not a canonical promotion input. ## What changed - Compile the full source SHA and GitHub Actions build identity into canonical relay binaries and expose it at `/_status`. - Gate creation of tagged multi-architecture manifests on the latest exact-SHA `CI` push run succeeding on `main` or `release`. - Preserve the signed SLSA provenance and add a signed, release-only deployment-eligibility predicate with source, build run, qualifying CI run, and exact compatible Buzz chart version. - Keep failed or pending builds untagged and without an eligibility predicate; preview-package artifacts are excluded by package and signer contract. - Add optional `image.digest` support to Buzz chart `0.1.8`, retaining the existing tag fallback. - Document verification and the intentional boundary between image/chart compatibility and backwards-compatible database migrations. ## Risk and rollout Canonical tag publication now waits for same-SHA CI and fails closed when the newest matching run fails. Per-architecture blobs may remain untagged after a failed qualification; they are not promotion inputs. Debug manifests are also CI-gated but do not receive the release eligibility predicate. There are no migration or schema-orchestration changes. The companion BuilderBot infrastructure PR will pin a separately verified canonical main digest and expose a read-only running-deployment inspection command. Signed eligibility is available only for artifacts published after this workflow lands. ## Verification - Blox: workflow contract fixtures, including stale-green rejection and predicate structure - Blox: `actionlint` for `ci.yml` and `docker.yml` - Blox: 46 Helm unit tests, chart lint, and production/quickstart render fixtures - Blox: relay status unit test, workspace Rust formatting, and workspace all-target clippy - Live evidence: exact-SHA CI selector chose successful run `32857345153` for source `9d1e4b257657f382d3111ce748f3da8d063b7671` - Existing canonical digest provenance verified with `gh attestation verify` ## References - #6709 — developer-preview precursor; intentionally separate from canonical eligibility - `docs/deployment-identity.md` *Generated with Codex* --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> |
||
|
|
931747c9c4 |
Add staging dev relay image workflow (#6709)
## Summary Adds a manual, collaborator-triggered workflow for publishing pre-merge Buzz relay runtime images for bb-block staging. - Resolves a canonical `block/buzz` branch or tag to an immutable commit SHA before checkout. - Builds the relay runtime image for `linux/amd64` and `linux/arm64` and publishes a single OCI index. - Tags each publication uniquely as `dev-sha-<full SHA>-run-<run_id>-<run_attempt>` so no tag is ever reused against the immutable-tag utility ECR pull-through cache (rebuilds of the same SHA aren't byte-identical, given mutable base tags and `apt-get update`). - Uses the staging-only `ghcr.io/block/buzz-staging-dev` namespace, which maps to a distinct utility ECR pull-through path. - Restricts dispatch to `block/buzz` on `refs/heads/main`; job permissions are narrowly scoped to `contents: read` plus `packages: write`. - Emits a deployment summary with the exact BPCI `repository` and unique `tag` values, plus the merged manifest digest. - Keeps the existing production/release image workflow unchanged. The first real workflow run must confirm GHCR package creation/access and utility ECR pull-through import for the new package. --------- Signed-off-by: Brad Seiler <seiler@squareup.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: coder 0 <d97ebdbb198c7237c94f84ea8bb8a73583ea067407eebd0062abbb3962527fb1@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: tornquist <tornquist@squareup.com> |
||
|
|
2d280376ad |
perf(desktop): persist channel heads, collapse thread reads and reply sends (#6572)
## Summary
Lands the build-now items from the desktop latency plan
(#ui-performance-deep-dive) as one change. Every perceived-latency hot
path a user hits on launch, channel open, thread open, and reply send
drops one or more round trips.
**A1 — persisted channel heads (the big one).** Native WAL SQLite cache
(`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey,
relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap,
schema-version reset, corrupt-row tolerance, checkpointed on shutdown.
Three blocking-pool commands: `channel_head_cache_load` / `_store` /
`_clear`. On the renderer side, `CommunityQueryProvider` kicks off
hydration of up to 12 heads when it constructs the query client — the
app, splash and relay preconnect mount immediately; only
`useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then
consumes a one-shot hydrated gate so a hydrated channel pays **zero**
`get_channel_window` calls on mount and exactly **one** on the
post-subscription refresh, whose response replaces page zero wholesale.
That refresh fires whether live-subscription setup succeeds or fails,
and is sequenced behind hydration so it is always a distinct
authoritative fetch (see Review follow-ups). Bounds-only persisted heads
(zero rows) are not hydrated and take the cold loading path. The
timeline loading latch recognizes native-hydrated rows as restart-safe
so they paint immediately instead of holding a skeleton. The cache is a
paint accelerator only — the relay response is always authoritative.
Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401
lines).
Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or
`localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is
cleared on community removal and scoped per identity, so a replaced
signer never sees the previous identity's rows.
**B1 — thread aux in one response.** Relay thread filters accept
`include_aux`; the bridge appends the same authorized two-hop
reactions/edits/deletions closure a channel window gets
(`build_aux_query` shared with the window path). Renderer
`useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is
computed from reply-kind rows only since aux rows are unpaged.
Documented in `docs/bridge-channel-window.md`. Thread queries keep
`staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s,
which CI's `thread-unread.spec.ts` caught — once the user leaves a
channel, the live subscription stops feeding that thread's cache, so a
reopen must always take the (now single) authoritative read.
**B2 — cached root on reply send.** `send_channel_message` gains
`root_event_id`; when the renderer already holds the parent (channel or
thread cache) it passes the NIP-10 root, and native signs without the
relay round trip that `resolve_thread_ref` used to make. Strict hex
parse; `root_event_id` requires `parent_event_id`; absent root falls
back to the existing relay resolution. The renderer never sends a
guessed root.
**B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5**
relay preconnect fires as soon as identity is ready instead of waiting
for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts`
"service restart close resets accumulated backoff") had been relying on
the idle-callback batching to skip past its own seeded dial failures
before the channel list painted; `8133d70bb` makes it wait for the
connected state instead (test-only, still fails with the 1012 backoff
reset disabled). **B6** profile freshness 60s→10 min (both the in-memory
entry check and the query `staleTime`). Tradeoff: another user's
display-name/avatar edit can take up to 10 min to propagate to a client
that already holds their profile (relay reconnect refetches
`users-batch` but resolves from the still-fresh per-pubkey entry); your
own edits still evict the entry immediately (`evictUsersBatchEntries` in
`useUpdateProfileMutation`).
### Related issue
Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the
measurement instrument and is intentionally not folded in. No duplicate
PR found.
### Review follow-ups
Addressing Carl's reviews
[5001114109](https://github.com/block/buzz/pull/6572#pullrequestreview-5001114109)
and
[5002596542](https://github.com/block/buzz/pull/6572#pullrequestreview-5002596542),
each pushed as new commits (no rebase):
- `4f06b7770` fix(desktop): mount app while channel heads hydrate;
always revalidate — provider no longer gates children on the cache load;
`refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads
skipped at seed; seed merges into an existing window store. +3 tests.
- `35834cb31` fix(relay): drain aux closure hops across the page clamp —
`query_all_pages` walks the `(created_at, id)` keyset via
`until`/`before_id` until a short page (`AUX_PAGE_LIMIT` =
`DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so
one-shot `limit: 1000` newest-first no longer drops the oldest
edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated.
- `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no
overlap).
- `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind
channel head hydration — `refreshChannelWindowMessages` awaits
`channelHeadHydration()` and, for a hydration-seeded query (`data !==
undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before
invalidating. Without this, a subscription that settles before the
SQLite load invalidated a data-less in-flight query; TanStack dedupes
that onto the existing fetch (`query-core` `fetch()` only cancels when
`state.data` exists), which returned the seeded snapshot — 0
authoritative fetches. Regression test reproduces Carl's exact ordering
(fails at `35834cb31` with 0 calls), plus a cold-channel guard that the
fix does not double-fetch.
- `b129231c8` fix(desktop): let concurrent post-hydration refreshes
share one window fetch — found independently by Max and Wren reviewing
`5a5566c0f`: subscribe settlement + reconnect both wake on the same
snapshot promise and both invalidate; the second (default
`cancelRefetch: true`) cancelled and replaced the first authoritative
fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits
the relay). The seeded branch now invalidates with `cancelRefetch:
false` so a second waker joins the in-flight fetch; cold/warm keep the
default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window`
relies on it). Concurrent regression test fails at `5a5566c0f` with 3.
### Testing
At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD`
= `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.
At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.
At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib`
910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same
specs minus affordance); GitHub CI green on every job except Smoke (3)
(unrelated project-review row-count + messaging timing flake, per Carl)
and Unit Tests (sherpa cache skeleton, below).
Earlier, all at `8133d70bb` (this PR head is `0c492366d` =
|
||
|
|
d97780b477 |
docs(nips): comprehensive NIP-FI — core + claimable profiles (EDGE/LIFECYCLE/DELEG/CONF) (#5946)
## What Comprehensive NIP-FI against `main`: one normative core plus four separately claimable profiles, replacing the single-document structure of #3726 (which was based on #1485's branch, not `main`). Six documents, 1,975 lines, docs only. - **`NIP-FI.md` (core, 632 lines)** — issuer-qualified identity `(iss, sub)`, independent Nostr proof, client-attached assertion, partial bijection with durable tombstones, atomic final admission, bounded leases, private denials with a closed response vocabulary, retire/revoke/rotate, two contract identities (`assertion_policy_id`, `transport_contract_id`), per-policy `skew` / `maximum_assertion_age` / `maximum_status_age` with missing configuration denying, a closed token-class rule (`at+jwt`, `nip-fi+jwt`, named compatibility; ID tokens always deny), declared freshness class (`offline-jwt` | `current-status`), server-declared body authorization relevance (NIP-98 payload-binding fix, including a `payload` tag on an irrelevant-body operation), BCP 14, "equivalent" defined over identity / bounds / provenance classes, a compact non-normative worked wire example, and a non-normative comparison with DPoP, mTLS-bound tokens, and HTTP Message Signatures. FI-INV-01..16 are normative core text. The behavioral-oracle table lists exactly 30 oracle IDs, one per row, with no shorthand. - **`NIP-FI-EDGE.md`** — trusted-edge surface: the `trusted-proxy-hmac-v2` envelope + canonicalization, or a private authenticated-edge adapter under a reviewed contract; `authorization_domain_id` derivation (exact 16 RFC 9562 UUID bytes); `proof_transport_code` registry (0x01 NIP-42, 0x02 NIP-98; 0x03 Git smart-HTTP and 0x04 Blossom reserved pending their transport contracts; 0x05–0x7f unassigned pending published stable specifications) + extension procedure; body-acquisition bounds; three normative test vectors. An independent Nostr proof (the NIP-98 event in `Authorization`, which reaches the verifier byte-identical, or the NIP-42 event after connect) is the only decision input outside the MAC; absent or incomplete provenance on an edge-required route is `missing_evidence`, present-but-failing provenance is `evidence_rejected`. Header-trust-without-provenance is nonconformant. - **`NIP-FI-LIFECYCLE.md`** — provision / disable / re-enable / administrative expiry (`binding_not_after`) / pending-replacement lineage, one conformance trace per privileged transition; every binding-creating transition declares whether it continues or establishes a grant; a private-condition table for CONF enumeration agreement. - **`NIP-FI-DELEG.md`** — delegated agents; explicit temporal boundaries matching core's inclusive-`nbf`/exclusive-`exp` idiom, with the delegated `skew` configured by this profile; lease deadline anchored to the lease issue instant; strict path separation — a delegated request carries no assertion or provenance field, so it cannot traverse an edge-provenance route and uses ingress on which NIP-FI-EDGE is not required. - **`NIP-FI-CONF.md`** — conformance evidence: an immutable claim tuple including the governing document revision and exit fixture digest; the complete 16-row denial-fixture enumeration with three mechanical enumeration-agreement checks; mutation adequacy with a countable denominator — one retained killed mutant per literal oracle-table row (30 core + 6 EDGE + 11 LIFECYCLE + 7 DELEG + 4 CONF = 58), rows selected structurally by their first cell, never by section title, with the release gate and CONF's own oracle rows stated in the same listed-oracle terms and a mutant defined for CONF's own report- and suite-subject oracles; an interoperability exit test compared over signing inputs (per-transport NIP-01 serialization for the NIP-98 and NIP-42 proofs; decoded protected header and claims as JSON values for the assertion), with a shared exit fixture pinning complete pre-signature header/claim JSON and complete unsigned event fields for both transports, and mandatory negative controls. `FI-CONF-INTEROP-EXIT` is `deferred` with reason `no-independent-implementation` until a second independent implementation exists; the canonical fixture is editor-authored at `docs/nips/fixtures/nip-fi-conf-exit.json` and is **not in this PR** — until it is published a claim records `pending-canonical-fixture`, valid only while the exit test is deferred. Explicit not-applicable dispositions, including `offline-jwt` deployments for the two current-status oracles and absence of a revocation-bounded external capability projection for `FI-TRACE-CAPABILITY-REVOCATION`. - **`NIP-FI-MODEL.md`** — non-normative companion; defines no requirement or conformance claim and is not claimable. ## Why The prior draft rated 9 (soundness) / 6 (minimalness) / 7 (elegance) / 7 (correctness) in adversarial + comparative review. This restructure keeps the two-invariant spine untouched, makes everything else a claimable profile, and collapses five stacked versioning mechanisms into two contract identities. Mutation adequacy counts one mutant per literal oracle-table row — a set two implementers enumerate identically — instead of "each normative requirement," which had four defensible readings. Resolved product calls (owner-approved): 1. Enrollment/denial posture is private — boolean enrollment discovery, TOFU extension claim not self-advertised, `key_mismatch → authorization_denied` joins the denial anonymity set, and replayed evidence is classed `authorization_denied` so resubmission reveals nothing about commit. 2. Revocation honesty — only `current-status` deployments may advertise an unconditional residual-revocation bound; `offline-jwt` advertises unbounded/unknown. Access tokens keep RFC 9068 `at+jwt`; `nip-fi+jwt` is reserved for a separately minted Buzz assertion. ## Acceptance bar - Nothing in core is deletable without losing a stated core guarantee. - From the core document plus the CONF exit fixture, a second implementer can produce a valid request equal over the request compared object (signing inputs), and a byte-exact public denial per class — no reference implementation. - Every oracle-table row ships a retained killed mutant satisfying only the entry it was selected for. - Both deployment profiles (trusted proxy = EDGE, client-held OIDC = core client-attached) pass the same lifecycle conformance suite. ## Status Ready at head |