Skip to content

Permissions

Permissions

PRYZM plugins declare permissions in their manifest. The set is locked at 7 for v1 per ADR-0038 §Decision A. Adding a permission is an additive 1.x change; removing or narrowing one requires 2.0.

The 7 permissions

PermissionGrantsWithout it
read:projectRead elements/views/selection from host.stores / host.views / host.selection.All proxy reads throw PluginPermissionError.
write:projectDispatch commands via host.commandBus.dispatch() AND invoke AI workflows via host.ai.runWorkflow().Dispatches and AI runs throw PluginPermissionError.
read:userRead displayName + email from ctx.user.ctx.user.displayName and ctx.user.email are null (a stable per-(plugin × user) id is always present).
network:fetchMake outbound fetch calls. Requires non-empty allowedOrigins. CSP connect-src is restricted to those origins.CSP sets connect-src 'none' — every fetch rejects.
register:toolContribute a tool (viewport entry).The host strips tool contributions from the manifest at install.
register:panelContribute a panel (docked surface).Panels stripped at install.
register:commandContribute a command (palette entry) AND register file-format importers/exporters via host.format.Commands stripped + format proxy throws on register.

Why no ai:invoke permission?

The set is locked at 7. AI workflows are gated by write:project because every workflow either mutates project state or reads enough that it is equivalent to a write impact (provenance + cost accounting attaches to the project owner per SPEC-28 §9).

The OAuth2 ai:invoke scope (declared in packages/api-spec/openapi.yaml) is a public-API namespace — unrelated to plugin permissions. The two are distinct concepts that share the same word.

Choosing the minimum set

The host enforces permissions at every proxy call. The user-visible “install” dialog lists permissions verbatim, so requesting more than you need is also a UX cost (users decline “wants to read all your data” plugins).

Heuristic:

What your plugin doesMinimum permissions
Sidebar widget showing project statsread:project + register:panel
Command that creates walls from a CSVread:project + write:project + register:command
Tool that selects+colors elementsread:project + write:project + register:tool
Panel that calls an AI workflowread:project + write:project + register:panel
Format plugin (.dxf ↔ project)read:project + write:project + register:command
Plugin that calls an external APIthe above + network:fetch + allowedOrigins: [...]
Plugin that personalizes by user identitythe above + read:user

What the host enforces, end to end

plugin code
│ ctx.hosts.commandBus.dispatch({ kind: 'wall.create', ... })
postMessage → iframe sandbox bridge
│ ① validate: is this kind allowed for the plugin direction? (sandbox/iframe-sandbox.ts)
host runtime (apps/editor/src/plugin-runtime/)
│ ② validate: does the manifest grant 'write:project'? (descriptor.ts permissions)
│ → if no: PluginPermissionError back across the bridge
command bus (packages/command-bus/)

Both checks happen host-side. A malicious plugin cannot bypass either: the iframe sandbox is <iframe sandbox="allow-scripts"> (no allow-same-origin), so no shared globals; the postMessage envelope is the only ingress and is type-narrowed at the boundary.

See Sandbox Model for the rest of the story.