notify is core, not grantable

it was an `app` permission and nothing granted it, so every Member's app
got a 403 the moment it tried to register its own push token — silently, at
launch, with no notification ever arriving and nothing saying why. four
member accounts on alpha, none of them able to receive anything. invisible
for days because the owner bypasses the check.

the grant was not what was missing. these are the caller's OWN devices, and
the send route takes its target from the authenticated header rather than
the body (resolve-user.ts), so an account can only ever reach its own
phone. "you may not subscribe to your own notifications" is not a coherent
thing to configure.

6 core, 3 app now. docs:check holds the count and the generated platform
skill was regenerated with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-24 15:52:54 +00:00
co-authored by Claude Opus 5
parent 40a03d987f
commit 16d02ffee1
2 changed files with 14 additions and 2 deletions
+1 -1
View File
@@ -212,7 +212,7 @@ ahead of everything, and it re-verifies the token itself so it covers routes tha
- `permissions/registry.ts` — the single enumeration of what the platform can do, in five kinds: - `permissions/registry.ts` — the single enumeration of what the platform can do, in five kinds:
`core` (every account, not deniable), `app` (**the grantable surface**), `confined` (grantable, but `core` (every account, not deniable), `app` (**the grantable surface**), `confined` (grantable, but
only to an account that has a Linux user), `execution` and `admin` (owner only, and `execution` is only to an account that has a Linux user), `execution` and `admin` (owner only, and `execution` is
never grantable at any level). **21 entries as of 2026-08-23** — verified by `bun run docs:check`, which fails if this number drifts again: 5 core, 4 app, 4 confined, 3 execution, 5 admin. It said 27 for nine days. The execution count dropped by one when Minesweeper became a plugin — see `plugins/minesweeper/manifest.ts` for why that was a fix rather than a downgrade. never grantable at any level). **21 entries as of 2026-08-24** — verified by `bun run docs:check`, which fails if this number drifts again: 6 core, 3 app, 4 confined, 3 execution, 5 admin. `notify` moved from `app` to `core` on 2026-08-24: it was grantable, nothing granted it, and every Member's app was silently 403'd registering its own push token. It said 27 for nine days. The execution count dropped by one when Minesweeper became a plugin — see `plugins/minesweeper/manifest.ts` for why that was a fix rather than a downgrade.
- `permissions/authorize.ts` — resolves "may this account do this". Owner short-circuits first; every - `permissions/authorize.ts` — resolves "may this account do this". Owner short-circuits first; every
other answer is role grants plus core, with `execution`/`admin` stripped even if a row grants them, other answer is role grants plus core, with `execution`/`admin` stripped even if a row grants them,
and `confined` stripped for an account with no `osUser`. and `confined` stripped for an account with no `osUser`.
+13 -1
View File
@@ -198,7 +198,19 @@ const CORE_REGISTRY: Permission[] = [
key: 'notify', key: 'notify',
label: 'Notifications', label: 'Notifications',
description: 'Push notifications to your devices', description: 'Push notifications to your devices',
kind: 'app', /*
CORE, not `app`, since 2026-08-24.
It was grantable and nothing granted it, so every Member's app got a 403 the moment it tried to
register its push token — silently, at launch, with no notification ever arriving and nothing
anywhere saying why. Invisible for days because the owner bypasses the check.
Making it grantable was the mistake rather than the missing grant. These are the caller's OWN
devices, and the send route resolves its target from the authenticated header rather than the body
(`sidecar/notify/resolve-user.ts`), so an account can only ever reach its own phone. There is no
version of "you may not subscribe to your own notifications" that is a coherent thing to configure.
*/
kind: 'core',
api: ['/notify'], api: ['/notify'],
// Device registration is the caller's own — a phone subscribing to its own push channel. // Device registration is the caller's own — a phone subscribing to its own push channel.
// //