[lenny] Implement REG-5, REG-17, REG-18 - #6
Conversation
This PR implements MVP, iOS-only, [NIP-PL](https://github.com/block/buzz/blob/8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8/docs/nips/NIP-PL.md)-compliant push notifications. A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any message that appears in the in-app Notifications tab. The first time the client first connects to a relay with `BUZZ_PUSH_ENABLED`: ```mermaid sequenceDiagram autonumber participant App as Buzz iOS app participant iOS participant Relay as Buzz relay participant Attest as Apple App Attest participant Gateway as Push gateway App->>Relay: Fetch NIP-11 push capability Relay-->>App: Push profile, current relay public key, and limits par App->>iOS: Request notification permission iOS-->>App: Permission result and App->>iOS: Register for remote notifications iOS-->>App: Device token end App->>Gateway: Request installation challenge Gateway-->>App: Single-use challenge App->>Attest: Attest installation transcript Attest-->>App: Attestation proof App->>Gateway: Enroll device token and proof Gateway-->>App: Installation handle App->>Gateway: Request delegation challenge Gateway-->>App: Single-use challenge App->>Attest: Assert relay-key delegation Attest-->>App: Assertion App->>Gateway: Create delegation Gateway-->>App: Opaque endpoint grant App->>Relay: Publish encrypted push lease and filters Relay-->>App: Lease acknowledged ``` When a notification-eligible event is received by the relay: ```mermaid %%{init: { "sequence": { "actorMargin": 20, "width": 110, "messageMargin": 18, "diagramMarginX": 8, "wrap": true } }}%% sequenceDiagram autonumber participant Relay as Buzz relay participant Gateway as Push gateway participant APNs as Apple Push<br/>Notification service participant iOS participant NSE as Notification service<br/>extension Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload APNs-->>Gateway: 200 OK: request accepted Gateway-->>Relay: 200 OK: accepted status APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1 iOS->>NSE: Invoke extension<br/>original notification content NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003 NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target ``` relay → push gateway → APNs -> NSE -> Notification Center The APNs wake payload is intentionally constant and opaque: it contains no originating community or message identifier, in keeping with the implemented NIP-PL privacy design. The Notification Service Extension must therefore reconnect to the relay and resolve eligible messages after each wake. Around overlapping wakes, timing boundaries, or resolution windows, notification presentation may occasionally omit an expected message or display a message more than once. This best-effort behavior is deliberately accepted for the current implementation and will be measured during the internal rollout to determine whether the user experience is acceptable before any broader deployment; the implementation does not claim exactly-once presentation. Live end-to-end hardware validation used an internal remotely hosted development relay and push gateway, the APNs sandbox, and a physical iPhone 12 mini: - A second real Buzz client published a uniquely marked message through the hosted relay. - The relay matched the message and sent the constant opaque wake through the hosted gateway. The gateway made an actual APNs request; no `simctl push` or simulated notification was used. - The iPhone received the notification on its lock screen. The Notification Service Extension reconnected to the relay, fetched the event, verified its ID and signature, and replaced the placeholder content with the real notification title and body. - After the app populated its shared presentation cache, a final marked notification visibly showed the sender display name, sender avatar, and hashtag-prefixed channel name. - Tapping a lock-screen notification opened Buzz and exercised the notification-response path and navigated to the corresponding message. Final validation with a dogfood-signed artifact and production App Attest/APNs configuration remains a release step. - **First pass:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585) found missing transient retries, executor-key rotation suppression, duplicate installation renewal, and an unauthenticated challenge write amplifier. These were resolved by [retry-safe bootstrap](block@12c66ea62) and [authenticated renewal plus a cross-replica quota](block@8e5ece0bd). [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962) found delegation generation burning and an edited applied migration, resolved by [exact-generation revocation](block@c26d2159d) and a [forward-only migration](block@956c1d099). [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1) found no blockers. - **Exact-head re-review:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68), [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849), and [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb) independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also revalidated every remediation and the endpoint-specific App Attest enrollment bound. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Michael Feth <mfethe1@gmail.com>
Signed-off-by: Michael Feth <mfethe1@gmail.com>
…cret launch flags)
CI failure triage (2026-09-01)Baseline probe (PR #7, product/main @ e788bd2 + empty commit, now closed) proves product/main itself fails the same CI jobs:
This diff's own gates (run against origin/product/main): Rust fmt/clippy/test, dart format, mobile ratchet vs product/main, desktop/web file-size ratchets, file-size core tests, flutter test (45/45 relay, 6/6 golden), Harbor Buzz Orchestra (green on PR), Security Gate, helm/Mesh — all pass. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…stls can build its HTTP client in TLS-rootless images (REG-17 joined-trial enabler); add local qwen36 manifest/endpoint
CI accounting for head 9a2176f (CA-fix cherry-pick added)Green: Python tests+lint (REG-17 code), Harbor Buzz Orchestra, Security Gate, Mesh Lifecycle, helm/push-gateway charts, Desktop Core. Red, none caused by this diff:
Full detail in the registry ledger (ci-evidence.md). This PR's own changed-path gates are green. |
|
Correction to earlier triage (Desktop E2E Integration): the baseline probe's E2E job failed fail-fast without running tests, so it didn't prove that class. Direct local repro instead: the 3 failing integration tests (agents.spec.ts:2712, onboarding.spec.ts:1915/:2356) fail identically with our full desktop/ tree reverted to pure origin/product/main — i.e. they are baseline-owned failures, not caused by this PR (whose only desktop change is the TimelineAnnouncementRegion mount). Evidence in the registry ledger; update posted for accuracy. |
…lock#5706 card-click contract d12d825 (block#5706) deliberately made persona card clicks open the persona target instead of an explicit instance pubkey; these two tests still pinned the pre-change behavior and failed deterministically on product/main (which has no CI check-runs, so the break was silent). Update them to verify the new contract: card click -> profilePersona param + running instance primary action, with the exact-instance pick delegated to the panel Runtime tab -> Instances list.
|
Follow-up to the Desktop E2E Integration triage: root-caused and fixed the two deterministic baseline failures. Root cause:
product/main has 0 CI check-runs, so this trunk regression was silent. Fix (e785162): tests updated to pin the post-block#5706 contract — card click → persona target ( Note for reviewers: this is a test-only fix; the app behavior was intentional per block#5706. |
|
CI accounting for head ✅ Decisive result: Desktop E2E Integration (1/2) + (2/2) both SUCCESS — the two deterministic baseline failures ( Failures, all previously-triaged non-diff classes:
Net: the only checks this diff can influence (Desktop Core, Unit Tests, Desktop E2E Integration, type/lint gates) pass or are downstream of infra failures. |
|
Windows Rust attribution (completes the accounting for head |
* feat(mobile): push notifications MVP (block#6269) This PR implements MVP, iOS-only, [NIP-PL](https://github.com/block/buzz/blob/8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8/docs/nips/NIP-PL.md)-compliant push notifications. A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any message that appears in the in-app Notifications tab. The first time the client first connects to a relay with `BUZZ_PUSH_ENABLED`: ```mermaid sequenceDiagram autonumber participant App as Buzz iOS app participant iOS participant Relay as Buzz relay participant Attest as Apple App Attest participant Gateway as Push gateway App->>Relay: Fetch NIP-11 push capability Relay-->>App: Push profile, current relay public key, and limits par App->>iOS: Request notification permission iOS-->>App: Permission result and App->>iOS: Register for remote notifications iOS-->>App: Device token end App->>Gateway: Request installation challenge Gateway-->>App: Single-use challenge App->>Attest: Attest installation transcript Attest-->>App: Attestation proof App->>Gateway: Enroll device token and proof Gateway-->>App: Installation handle App->>Gateway: Request delegation challenge Gateway-->>App: Single-use challenge App->>Attest: Assert relay-key delegation Attest-->>App: Assertion App->>Gateway: Create delegation Gateway-->>App: Opaque endpoint grant App->>Relay: Publish encrypted push lease and filters Relay-->>App: Lease acknowledged ``` When a notification-eligible event is received by the relay: ```mermaid %%{init: { "sequence": { "actorMargin": 20, "width": 110, "messageMargin": 18, "diagramMarginX": 8, "wrap": true } }}%% sequenceDiagram autonumber participant Relay as Buzz relay participant Gateway as Push gateway participant APNs as Apple Push<br/>Notification service participant iOS participant NSE as Notification service<br/>extension Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload APNs-->>Gateway: 200 OK: request accepted Gateway-->>Relay: 200 OK: accepted status APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1 iOS->>NSE: Invoke extension<br/>original notification content NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003 NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target ``` relay → push gateway → APNs -> NSE -> Notification Center The APNs wake payload is intentionally constant and opaque: it contains no originating community or message identifier, in keeping with the implemented NIP-PL privacy design. The Notification Service Extension must therefore reconnect to the relay and resolve eligible messages after each wake. Around overlapping wakes, timing boundaries, or resolution windows, notification presentation may occasionally omit an expected message or display a message more than once. This best-effort behavior is deliberately accepted for the current implementation and will be measured during the internal rollout to determine whether the user experience is acceptable before any broader deployment; the implementation does not claim exactly-once presentation. Live end-to-end hardware validation used an internal remotely hosted development relay and push gateway, the APNs sandbox, and a physical iPhone 12 mini: - A second real Buzz client published a uniquely marked message through the hosted relay. - The relay matched the message and sent the constant opaque wake through the hosted gateway. The gateway made an actual APNs request; no `simctl push` or simulated notification was used. - The iPhone received the notification on its lock screen. The Notification Service Extension reconnected to the relay, fetched the event, verified its ID and signature, and replaced the placeholder content with the real notification title and body. - After the app populated its shared presentation cache, a final marked notification visibly showed the sender display name, sender avatar, and hashtag-prefixed channel name. - Tapping a lock-screen notification opened Buzz and exercised the notification-response path and navigated to the corresponding message. Final validation with a dogfood-signed artifact and production App Attest/APNs configuration remains a release step. - **First pass:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585) found missing transient retries, executor-key rotation suppression, duplicate installation renewal, and an unauthenticated challenge write amplifier. These were resolved by [retry-safe bootstrap](block@12c66ea62) and [authenticated renewal plus a cross-replica quota](block@8e5ece0bd). [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962) found delegation generation burning and an edited applied migration, resolved by [exact-generation revocation](block@c26d2159d) and a [forward-only migration](block@956c1d099). [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1) found no blockers. - **Exact-head re-review:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68), [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849), and [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb) independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also revalidated every remediation and the endpoint-specific App Attest enrollment bound. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com> * [lenny] implement: REG-17 qualification receipt core Signed-off-by: Michael Feth <mfethe1@gmail.com> * [lenny] implement: REG-18 timeline announcements Signed-off-by: Michael Feth <mfethe1@gmail.com> * [lenny] fix: REG-17 review findings (exact git SHA lengths, redact secret launch flags) * [lenny] fix: allowlist synthetic APNs test fixtures in gitleaks * [lenny] fix: dart format mobile * [lenny] fix: restore relay_session.dart under mobile file-size ratchet * [lenny] fix: keep relay_session.dart under mobile ratchet with guard restored * [lenny] fix: upload CA bundle into task containers so buzz-agent's rustls can build its HTTP client in TLS-rootless images (REG-17 joined-trial enabler); add local qwen36 manifest/endpoint * [lenny] fix(e2e): align persona gallery/profile tests with the post-block#5706 card-click contract d12d825 (block#5706) deliberately made persona card clicks open the persona target instead of an explicit instance pubkey; these two tests still pinned the pre-change behavior and failed deterministically on product/main (which has no CI check-runs, so the break was silent). Update them to verify the new contract: card click -> profilePersona param + running instance primary action, with the exact-instance pick delegated to the panel Runtime tab -> Instances list. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Signed-off-by: Michael Feth <mfethe1@gmail.com> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com>
…ontents (#35) * [lenny] fix(desktop): stop the timeline live region leaking spoiler contents `TimelineAnnouncementRegion` announced `message.body` verbatim into an `aria-live` status region. Any `||spoiler||` span — including the destination of a masked `[label](url)` link inside it — was therefore published to the accessibility tree, and read aloud by screen readers, before the reader chose to reveal it. Sighted users got the hidden-until-revealed guarantee; screen reader users did not. Redact spoiler spans at the announcement source via `redactSpoilers`, so the live region emits "spoiler hidden" in their place. This is a fork-local regression: `TimelineAnnouncementRegion.tsx` does not exist upstream (added by #6), while `spoiler.spec.ts` is byte-identical to upstream/main — our new component broke an inherited guarantee. Fixes the upstream E2E spec `spoiler.spec.ts:256` "masked link inside a hidden spoiler does not leak its URL until revealed", which asserted count 0 for the secret URL and received 1. Evidence: - spoiler.spec.ts (smoke): 7/7 pass (:256 now green; :166 flaky, passes on retry, and was already flaky before this change) - TimelineAnnouncementRegion.test.mjs: 9/9 pass, incl. 2 new regression tests covering redaction and the end-to-end live-region text - desktop unit suite: 6585/6585 pass (was 6583; +2 new) - pnpm run check exit 0, pnpm run typecheck exit 0 * [lenny] fix(desktop): clear the timeline live region after it announces The sr-only `role=status` region kept its last announcement in the DOM forever — `setAnnouncement` only ever overwrote, never cleared. That leaves a permanent second copy of a message body in the accessibility tree, which both pollutes screen-reader navigation and makes `getByText(body)` ambiguous. Drop the text again after a 500ms retention window: assistive tech announces on change, so the text has done its job once read. The retention timer goes through the injected scheduler, is cancelled on channel reset, and is cleared on unmount. Effects take `clearRetention` (a stable `useCallback` over a scheduler ref) rather than `scheduler.clear`, so they keep their existing dependency semantics instead of re-running per render. Evidence: - TimelineAnnouncementRegion.test.mjs: 10/10 pass, incl. a new test asserting the region empties after the retention window - desktop unit suite: 6586/6586 pass - spoiler.spec.ts (smoke): 7/7 pass - pnpm run check exit 0, pnpm run typecheck exit 0 Note: this does NOT fix thread-unread.spec.ts:962. That test hovers inside the retention window, so it still sees two matches. Fixing it needs a separate decision about whether the region should be exempt from text queries.
REG-5 push notifications (cherry-pick block#6269), REG-17 qualification receipts, REG-18 timeline accessibility. Full gates green locally; review findings fixed in b5a3d61.