Scopes are Perch’s entire permission model. Your manifest declares them; the Workshop page and the settings mod card render them verbatim with per-scope revocation; the sandbox installs exactly the APIs they grant. An undeclared capability doesn’t error — it simply doesn’t exist inside your Lua state.
The scope table
| Scope | Grants | Notes |
|---|---|---|
| (none) | perch.time.*, perch.json.*, host.g, the host API | pure — always available |
timer | host.timer / cancel_timer | floors enforce the idle budget |
storage | perch.storage.* | mod-scoped KV, ≤64 KB |
media | perch.media.read + on_media | the now-playing model |
media.control | perch.media.control | transport verbs only |
discord | perch.discord.status + on_call on_dnote on_ring on_status | read side |
discord.control | perch.discord.control / .reconnect | the five call verbs |
weather | perch.weather.read / .set_config + on_weather | native met.no pipeline |
system.stats | perch.system.stats.read | CPU/RAM/GPU/temps |
system.transients | on_volume on_brightness on_nightlight on_connect + timer events | the HUD stream |
volume.control | perch.volume.control | up / down / set:<0-100> |
system.power | on_power | battery events |
system.downloads | on_download | download progress |
system.notifications | perch.mirror.set_config + on_note | opt-in mirror; loud wording |
apps | perch.apps.* | recent/top + launch of recorded refs only, never arbitrary exec |
open | perch.open.url / .steam | https:// URLs and numeric appids only; audit-logged |
net.http:<host> | perch.http.get pinned to <host> | one scope per host; ≤30 req/h, ≤1 MB |
Dotted names narrow: discord reads, discord.control acts. Declare both
if you do both.
What the gates actually enforce
- Verbs are whitelists.
media.controlaccepts exactly the transport verb set;open.urlaccepts exactlyhttps://;apps.launchlaunches only refs the tracker recorded. There is no string that reaches a shell, a browser, or an RPC socket unchecked. - HTTP is pinned.
net.http:api.github.comreachesapi.github.com— exact host match, https only, no wandering off into other hosts. Want two hosts? Declare two scopes; the user sees both. - The carve-outs are absolute. Perch’s own preference directory (secrets, config, caches) is denied at the API layer regardless of any scope. Trust surfaces — the mic-live indicator, the ownership gate, suppression — live in core where no scope reaches.
The social contract
Rules of thumb:
- Narrow beats broad:
net.http:api.open-meteo.com, never a generic proxy host you control. - Late beats early: don’t declare scopes for the feature you might build next month.
- Explain in your Workshop description why each non-obvious scope exists — the users who read scope lists are exactly the users who write reviews.
Revocation
Users can revoke any scope per-mod from the settings card. Your mod is not
told — the APIs simply stop existing (calls become no-ops, events stop).
Write logic that degrades: a weather widget without weather should show
its waiting state, not error.