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

  1. Declare the needed permission (exact area, action, and key) in the extension manifest so the grant covers the call.
  2. If permission is interactive, run the permission request flow first and only proceed when granted — or degrade gracefully when denied.
  3. For module-scoped calls, pass input.permissions at ctx.modules.register that include the areas the module actually uses (intersection can only narrow).
  4. 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

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


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)