fix(electron): grant the dashboard the notifications permission so its OS toasts work - #8312
Conversation
…s OS toasts work The renderer already fires page-context new Notification() for new unacked notifications and approval requests, and it works in a plain browser where Chromium prompts and grants. The Electron permission handlers denied 'notifications' wholesale, pinning Notification.permission to 'denied' and silently no-oping the same code in the packaged app. Generalize the fullscreen grant into TRUSTED_PAGE_PERMISSIONS (fullscreen, notifications): trust-gated (untrusted embedded browser views stay denied, and the persist:kirocrew-browser partition keeps its own deny-all handlers), origin-gated, and answered before the media rule so neither capability ever touches the macOS TCC leg. Fixes #8308
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix at the right layer: the existing renderer code is unblocked by the one handler that denied it, gated identically to the fullscreen precedent. The set-widening is earned (two real consumers, the old single-constant rationale updated in place), the untrusted-view/foreign-origin/subframe denials keep every trust boundary the handler already enforced, and the deliberate fullscreen-vs-notifications frame asymmetry is documented and pinned by tests. No design-level concerns. [DESIGN-REVIEWED] 8fc9dfd |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS Polished, self-explaining share flow — but a failed card export dies silently, and on the intent path it strands a blank tab. Watch
Suggestions
[UX-REVIEWED] 8fc9dfd |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of First-Principles-Verdict: CONCERNS The notifications grant is derived (issue #8308) and cause-level; only the one-member What this change shipsIntent: let desktop-app users get the OS notification toasts the dashboard already fires in a browser — a FIX (#8308). Note: the diff artifact was cut against a stale base — its non-electron hunks (SSE relay, credential-redaction notice, share cards, ratchet audit, ~25 files) match commits already in the base checkout's
WatchThe description's gate list ("trust-gated… origin-gated… TCC-free") predates the tip commit: the shipped grant is also frame-gated, and fullscreen deliberately is not ( No unfixed siblings: grepped SubtractionsDrop the [FIRST-PRINCIPLES-REVIEWED] 8fc9dfd |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
A subframe shares its parent's webContents and isAppOrigin treats any localhost origin as the app, so an iframe embedded by the dashboard (a scripted localhost preview, a widget) inherited the notifications grant and could post OS toasts wearing this app's identity. Gate the grant on details.isMainFrame === true in both the request and check handlers, fail-closed when the field is absent. Fullscreen deliberately stays frame-agnostic: inline-video fullscreen from an embedded frame was the pre-existing granted behavior and must not regress. Addresses the GPT review finding on permission-handler.js:250. Fixes #8308
|
Finding: GPT 5.6 (BLOCKING) — Disposition: FIXED in 8fc9dfd. The |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
bolichen97
left a comment
There was a problem hiding this comment.
Scope is exactly two files — permission-handler.js and its test — with zero renderer changes, so this unblocks the new Notification() calls that already ship rather than adding a notification feature. The generalization of FULLSCREEN_PERMISSION into TRUSTED_PAGE_PERMISSIONS is a faithful refactor: both branches keep the same !isUntrusted(wc) && originOk(wc, origin) verdict, and both remain answered before the media rule so a toast is never gated on or prompted for the microphone (pinned by the test that throws from getMicAccessStatus/askForMicAccess/onMicBlocked).
The security-relevant part is handled correctly. Widening a permission set needs the gate to still hold on every axis, and each one has a test: untrusted embedded-browser view denied, foreign origin denied, and — the non-obvious one — subframes denied via MAIN_FRAME_ONLY_PERMISSIONS, which matters because a subframe shares the parent's webContents and isAppOrigin accepts any localhost origin, so without it an embedded localhost preview would inherit the grant and could post OS toasts under the app's identity. frameOk is fail-closed (details?.isMainFrame === true), and the test asserts denial for {} and undefined, not just {isMainFrame:false}. Scoping the frame gate to notifications only, with an explicit test that fullscreen stays frame-agnostic, is right — an inline
The request/check pair is the actual bug surface and is covered as such: Notification.permission is the check handler's verdict, and the renderer never constructs a toast unless it reads 'granted', so a request-side-only grant would have left the hook dead. The suite pins the check directly (including the null-wc frameless case) and asserts the two handlers agree under both trust states. Removing notifications from the three deny-sweep loops is required by the new behavior, and each of those loops retains other members so none becomes vacuous. All checks green.
Summary
Fixes #8308.
The dashboard already fires page-context
new Notification()toasts —useNativeNotification.tsfor new unacked notifications,useWebSocket.tsfor approval requests — and they work in a plain browser, where Chromium prompts and grants. In the Electron app the permission handlers deniednotificationswholesale, pinningNotification.permissionto'denied', so the exact same code no-oped silently. Desktop users got fewer OS notifications than browser-tab users.Change
electron/permission-handler.js: generalize the existing fullscreen grant into aTRUSTED_PAGE_PERMISSIONSset (fullscreen,notifications). Both members share one shape — requested by the dashboard's own code, no OS resource Electron must broker, silent renderer failure when denied. The grant is:persist:kirocrew-browserpartition keeps its own deny-all handlers;Zero renderer changes: with permission granted, Electron renders the existing constructor calls as real OS notifications.
Tests
New
notificationsdescribe block inelectron/test/permission-handler.test.jsmirroring the fullscreen-grant pattern: dashboard grant (request + check), null-wccheck on origin (the verdictNotification.permissionreflects), untrusted-view denial, foreign-origin denial, no-TCC-path guard, request/check agreement. The untrusted-view sweep inpermission-untrusted.test.jspasses unchanged — the identity gate outranks the new grant, which is exactly what it pins.node --test test/permission-handler.test.js test/permission-untrusted.test.js: 50/50 pass. Full electron suite matches the clean-main host baseline (failures identical on pristine HEAD, all in unrelated auto-update/petOverlays files).Not in scope
#4667's broader feature asks (per-category config, click-to-navigate) — this PR only unblocks the notification code that already ships.
Pattern harvest
Rule candidate: audit checklist
Pattern: a renderer calls a permission-gated Web API (
new Notification()) that the Electron permission handlers deny by default, so the feature silently no-ops only in the packaged app. When adding or reviewing page-context Web API usage inwebsite/src/, cross-checkelectron/permission-handler.jsgrants (TRUSTED_PAGE_PERMISSIONS / media rule) so the app-origin allowlist stays in sync with the APIs the dashboard actually uses.