{"record":{"id":"9b638c155bccd3b7","repo":"moeru-ai/airi","slug":"permission-denied-details-area-details-actio","errorCode":null,"errorMessage":"Permission denied: ${details.area}.${details.action} \"${details.key}\"","messagePattern":"Permission denied: (.+?)\\.(.+?) \"(.+?)\"","errorType":"exception","errorClass":"PermissionDeniedError","httpStatus":null,"severity":"error","filePath":"packages/plugin-sdk/src/plugin-host/core.ts","lineNumber":527,"sourceCode":"\n  private createModuleKitRegistry(session: ExtensionSession, subscriptions: DisposableStore, moduleId: string): ExtensionModuleContext['kits'] {\n    return this.createKitRegistry(session, subscriptions, moduleId)\n  }\n\n  private assertExtensionPermission(\n    session: ExtensionSession,\n    input: ExtensionHostPermissionRequest,\n    moduleId?: string,\n  ) {\n    const grant = moduleId\n      ? session.modules.get(moduleId)?.permissions\n      : session.permissions.granted\n\n    if (grant && this.permissions.grantAllows(grant, input.area, input.action, input.key)) {\n      return\n    }\n\n    throw new PermissionDeniedError({\n      area: input.area,\n      action: input.action,\n      key: input.key,\n    })\n  }\n\n  private getExtensionSessionOrThrow(sessionId: string) {\n    const session = this.extensionSessionService.get(sessionId)\n    if (!session) {\n      throw new Error(`Unknown extension session: ${sessionId}`)\n    }\n\n    return session\n  }\n\n  private createInstallContext(): ExtensionHostInstallContext {\n    return {\n      registerKit: kit => this.registerKit(kit),","sourceCodeStart":509,"sourceCodeEnd":545,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/plugin-sdk/src/plugin-host/core.ts#L509-L545","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\n// manifest declares only storage.read\nawait host.storage.set('settings', value) // throws: storage.write \"settings\" denied\n\n// after\n// manifest.json permissions: [{ area: 'storage', action: 'write', key: 'settings' }]\nawait host.storage.set('settings', value)","handlingStrategy":"try-catch","validationCode":"// before performing the protected operation, ask for permission through the host flow\nconst granted = await host.requestPermission({ area: 'storage', action: 'write', key: 'settings' })\nif (granted) await host.storage.set('settings', value)","typeGuard":"function isPermissionDeniedError(e: unknown): e is { area: string, action: string, key: string } {\n  return e instanceof Error && e.name === 'PermissionDeniedError'\n}","tryCatchPattern":"import { PermissionDeniedError } from '<plugin-sdk>'\ntry {\n  await protectedOperation()\n} catch (error) {\n  if (error instanceof PermissionDeniedError) {\n    // surface a grant prompt or degrade; never crash the extension session\n    await offerPermissionUpgrade(error.area, error.action, error.key)\n    return\n  }\n  throw error\n}","preventionTips":["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."],"tags":["plugin-sdk","permissions","security","authorization","extension"],"backgroundTag":"permission-denied","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}