moeru-ai/airi · error · PermissionDeniedError
Permission denied: .
Error message
Permission denied: ${details.area}.${details.action} "${details.key}" What it means
plugin-host gates protected host operations with an assertion (core.ts): it resolves the permission grant for the session (or for a module, via the module's intersected grants) and checks grantAllows(grant, area, action, key). If no grant covers the exact area/action/key triple, it throws PermissionDeniedError with those details in the message. This is the host's security boundary — extension code attempted something the user never granted, either via the manifest permission declaration or an interactive grant.
Solutions
- Declare the needed permission (exact area, action, and key) in the extension manifest so the grant covers the call.
- If permission is interactive, run the permission request flow first and only proceed when granted — or degrade gracefully when denied.
- For module-scoped calls, pass input.permissions at ctx.modules.register that include the areas the module actually uses (intersection can only narrow).
- Double-check the key string (exact match, no wildcarding unless the grant system defines one) and the action verb (read vs write).
Example fix
// before
// manifest declares only storage.read
await host.storage.set('settings', value) // throws: storage.write "settings" denied
// after
// manifest.json permissions: [{ area: 'storage', action: 'write', key: 'settings' }]
await host.storage.set('settings', value) Defensive patterns
Strategy: try-catch
Validate before calling
// before performing the protected operation, ask for permission through the host flow
const granted = await host.requestPermission({ area: 'storage', action: 'write', key: 'settings' })
if (granted) await host.storage.set('settings', value) Type guard
function isPermissionDeniedError(e: unknown): e is { area: string, action: string, key: string } {
return e instanceof Error && e.name === 'PermissionDeniedError'
} Try / catch
import { PermissionDeniedError } from '<plugin-sdk>'
try {
await protectedOperation()
} catch (error) {
if (error instanceof PermissionDeniedError) {
// surface a grant prompt or degrade; never crash the extension session
await offerPermissionUpgrade(error.area, error.action, error.key)
return
}
throw error
} Prevention
- Declare every permissioned area/action/key the extension uses in the manifest up front; keep it in sync when adding host API calls.
- Run the interactive permission request before the protected call instead of catching denials after the fact.
- Remember module-level permissions intersect with session grants — intersection only narrows, so module declarations must cover everything the module does.
When it happens
Trigger: An extension calling a permissioned host API (storage read, network, window control) whose area/action/key is not in its manifest-declared permissions; module-level code whose module was registered with narrower permissions than the action needs (the intersect shrinks the effective grant); key-scoped access like a specific resource key that was granted for a different key; permission grants persisted for an older manifest that no longer covers a newly added action.
Common situations: Adding a new host API call to an extension without updating manifest permissions; users denying an interactive permission prompt; typos in the permission key; partial grants (read granted, write attempted); revoked permissions after an update.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Cannot declare permissions for unknown plugin
- Cannot grant permissions to unknown plugin
- Extension entrypoint id
- Extension module ` ` is already registered for session .
- Unknown extension session
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-08-18).
Data as JSON: /api/errors/9b638c155bccd3b7.
Report an issue: GitHub.
Appendix: source
Thrown at packages/plugin-sdk/src/plugin-host/core.ts:527
private createModuleKitRegistry(session: ExtensionSession, subscriptions: DisposableStore, moduleId: string): ExtensionModuleContext['kits'] {
return this.createKitRegistry(session, subscriptions, moduleId)
}
private assertExtensionPermission(
session: ExtensionSession,
input: ExtensionHostPermissionRequest,
moduleId?: string,
) {
const grant = moduleId
? session.modules.get(moduleId)?.permissions
: session.permissions.granted
if (grant && this.permissions.grantAllows(grant, input.area, input.action, input.key)) {
return
}
throw new PermissionDeniedError({
area: input.area,
action: input.action,
key: input.key,
})
}
private getExtensionSessionOrThrow(sessionId: string) {
const session = this.extensionSessionService.get(sessionId)
if (!session) {
throw new Error(`Unknown extension session: ${sessionId}`)
}
return session
}
private createInstallContext(): ExtensionHostInstallContext {
return {
registerKit: kit => this.registerKit(kit),View on GitHub (pinned to 438a067dde)