Permissions
A permission in WAMP does one concrete thing: it decides whether a namespace
appears on your extension’s API object. There is no permission prompt, no
runtime consent dialog, and no PermissionDeniedError. If you did not declare
cron, then ctx.api.cron is undefined — that is the entire enforcement
mechanism, and understanding it saves you from the failure mode it produces.
After this page you can tell which declarations actually withhold something, which are consent labels the runtime does not yet enforce, and how to write main-process code that fails loudly rather than quietly when a permission is missing.
Declaring them
Section titled “Declaring them”{ "permissions": [ "database", "cron", "notifications", "process", { "auth.outbound": ["https://api.example.com"] } ]}Eleven string values are valid — database, network, cron, ai,
terminal, filesystem, http-routes, notifications, process,
auth.identity, mcp — plus one structured entry, auth.outbound, which
carries data. There is no other form, and the array rejects anything else.
Permissions are read once, at registration. Changing them means editing
extension.json, which triggers a re-scan and a fresh context — the change
takes effect on the next activation, not on the current one.
Enforced permissions
Section titled “Enforced permissions”These withhold a capability. Declare them or the namespace is not there.
| Permission | Unlocks | Enforced where |
|---|---|---|
cron |
ctx.api.cron — scheduled jobs |
Main process |
http-routes |
ctx.api.http — local HTTP routes for webhooks |
Main process |
notifications |
ctx.api.notifications — system notifications |
Main process |
process |
ctx.api.process, ctx.api.pty, ctx.api.git |
Main process |
database |
ctx.api.cloud and pluginAPI.cloud |
Both halves |
auth.identity |
ctx.api.auth — identity of the signed-in user |
Main process |
{ "auth.outbound": [...] } |
The origins auth.fetch may target |
At every call |
mcp |
pluginAPI.mcp — list and start/stop MCP servers |
Main process |
Four of those deserve their own note.
process unlocks three namespaces
Section titled “process unlocks three namespaces”process (child processes), pty (interactive terminal sessions), and git
(worktrees and diffs) all sit behind the single process permission. Every
operation in git is a git subprocess spawn, so it grants nothing process
does not already imply, and a pty is a child process with a terminal
attached. One permission, three namespaces, deliberately — the vocabulary stays
shorter than the surface.
pty is the one to reach for when you are running a CLI: it negotiates size,
reports itself as xterm-256color, and lets a full terminal UI run the way it
does in a real terminal. Piping stdio through process instead gets you
line-buffered text with no resize and no interactivity.
database gates the cloud store, not the local one
Section titled “database gates the cloud store, not the local one”This is the one permission whose gate holds in the window as well as in the main process, and the one that does not gate what its name suggests.
ctx.api.dataandpluginAPI.data— the local per-extension typed store — are always available. No permission gates them. They never leave the machine.ctx.api.cloudandpluginAPI.cloud— records on the app’s own backend, shared across the user’s devices and, insharedscope, across users — requiredatabase.
The renderer path is re-checked in the main process on every call, so a modified renderer bundle cannot reach the backend without the declaration. The error is explicit:
Extension 'my-app' does not declare the 'database' permission,which cloud storage requires. Add "database" to `permissions` in extension.json.auth.identity and auth.outbound are two different things
Section titled “auth.identity and auth.outbound are two different things”auth.identity unlocks ctx.api.auth in the main process: read the signed-in
user, subscribe to session changes. Declaring it alone gets you read-only
identity.
auth.outbound is the origin allowlist for auth.fetch, the call that reaches
your own backend with a WAMP-signed, audience-bound token attached. Matching is
exact origin — scheme, host, and port must all match, and there are no
globs. Loopback origins are auto-allowed in a development build; a production
build is strict.
{ "permissions": [ "auth.identity", { "auth.outbound": ["https://api.example.com", "https://events.example.com"] } ]}The origin check runs inside the auth service, so it applies to the call
regardless of which half made it. Today, auth.identity itself gates only the
main-process ctx.api.auth; the renderer’s pluginAPI.auth.getSession is not
withheld when the permission is absent. Declare it anyway — the catalog shows
what you declared, and relying on the gap is relying on a bug.
mcp is narrow on purpose
Section titled “mcp is narrow on purpose”mcp unlocks pluginAPI.mcp, which can list MCP servers and start or stop
them app-wide. What it exposes about each server is deliberately limited to
name, display name, status, and tool count — never the server configuration,
which may carry credentials. The check runs in the main process, not the
renderer.
Declarative permissions
Section titled “Declarative permissions”These four are consent signals. They appear in the catalog and describe what the extension intends to touch, and the runtime does not currently withhold the matching capability.
| Permission | Describes |
|---|---|
ai |
Model calls — ctx.api.ai, pluginAPI.ai, agent delegation |
filesystem |
File reads and writes — ctx.api.fs, pluginAPI.fs |
network |
Outbound HTTP via pluginAPI.http.fetch |
terminal |
Command execution via pluginAPI.terminal.exec |
Always available, no permission needed
Section titled “Always available, no permission needed”Everything else on ctx.api is present regardless of what you declared:
workspace, ai, agents, fs, events, data, tools, secrets,
dialog, shell, ipc, tokens, services, commands, disposables,
storage, webviews, and settings.
Two of those are ungated for a reason worth knowing. dialog opens a native
file picker, which cannot happen without the user choosing a file — the
interaction is its own gate. tools.register adds a tool to the assistant’s
surface, which the user then sees in every tool card; hiding it behind a
permission would not make it more visible.
Capabilities are a separate list
Section titled “Capabilities are a separate list”capabilities in the manifest is not a permission list and does not overlap
with one. It carries four values — webviews.navigate,
webviews.executeScript, webviews.interceptRequests, and
webRequestRewrite — and it gates individual operations on
ctx.api.webviews rather than the namespace as a whole. Declaring any of the
three webviews.* values also selects the more capable webview backing, so it
changes what the webview is, not only what you may ask of it. An unrecognized
string is accepted and matches nothing. See
the manifest.
Absence, not refusal — and how to write for it
Section titled “Absence, not refusal — and how to write for it”Here is the trap, and it costs real time.
A missing permission does not produce an error naming the permission. The
namespace is not on the object, so the first thing that happens is a
TypeError about a property of undefined:
TypeError: Cannot read properties of undefined (reading 'schedule')That message names schedule. It does not name cron, and it does not mention
extension.json. In a 5-second activate() it reads like a host bug.
The SDK types are what save you: every gated namespace is declared optional
on the API type — cron?, http?, notifications?, process?, pty?,
git?, auth?, cloud?. So the compiler flags the access before you ever
run it. That compiler error is the permission check. Two ways of silencing it
are both worse than the error:
// Compiles. Throws at runtime with a message that names the wrong thing.ctx.api.cron!.schedule('daily', '0 9 * * *', run);
// Compiles. Does nothing, forever, silently. The extension activates// cleanly and the job never runs.ctx.api.cron?.schedule('daily', '0 9 * * *', run);Guard once, at the top of activate, and say what is missing:
import type { PluginContext } from '@wamp/extension-sdk';
export async function activate(ctx: PluginContext): Promise<void> { const { cron, notifications } = ctx.api; if (!cron || !notifications) { ctx.log.warn( 'reminders: needs the "cron" and "notifications" permissions in extension.json', ); return; }
// Past this point both are narrowed to non-optional. No `!`, no `?.`. ctx.api.disposables.add( cron.schedule('reminders.daily', '0 9 * * *', async () => { notifications.show({ title: 'Daily check', body: 'Nothing overdue.' }); }), );}The guard costs three lines and turns an unexplained TypeError into a log
line naming the fix.
Host-conditional namespaces are not permissions
Section titled “Host-conditional namespaces are not permissions”Two namespaces are optional for a reason that has nothing to do with what you declared, and they look identical from inside your code:
| Namespace | Absent when |
|---|---|
ctx.api.sessions |
The host has no user interface — nobody is looking at a session |
ctx.api.clientAffordances |
The host does not answer reverse calls from a remote core |
No permission makes either appear. Guard with ?. here — this is the one place
where the optional-chaining shortcut is the correct answer, because absence is
a legitimate deployment shape rather than a misconfiguration.
- The plugin API — the surface each permission unlocks, with a working snippet per namespace.
- The manifest — where permissions sit among the other twenty fields.