{"record":{"id":"1b7d8525b02e11ca","repo":"moeru-ai/airi","slug":"cannot-declare-permissions-for-unknown-plugin-e","errorCode":null,"errorMessage":"Cannot declare permissions for unknown plugin \"${extensionId}\".","messagePattern":"Cannot declare permissions for unknown plugin \"(.+?)\"\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/plugin-sdk/src/plugin-host/runtimes/shared/services/permissions.ts","lineNumber":322,"sourceCode":"    const persisted = options?.persisted ?? {}\n    const explicitGrant = options?.grant ?? requested\n    const mergedGrant = mergePermissions(persisted, explicitGrant)\n    const granted = intersectPermissions(requested, mergedGrant)\n    const previousRevision = this.store.get(extensionId)?.revision ?? 0\n    const snapshot: PermissionSnapshot = {\n      requested,\n      granted,\n      revision: previousRevision + 1,\n    }\n\n    this.store.set(extensionId, snapshot)\n    return snapshot\n  }\n\n  declare(extensionId: string, requestedDeclaration: ModulePermissionDeclaration) {\n    const existing = this.store.get(extensionId)\n    if (!existing) {\n      throw new Error(`Cannot declare permissions for unknown plugin \"${extensionId}\".`)\n    }\n\n    const requested = normalizeDeclaration(requestedDeclaration)\n    const snapshot: PermissionSnapshot = {\n      requested: mergePermissionDeclarations(existing.requested, requested),\n      granted: existing.granted,\n      revision: existing.revision + 1,\n    }\n\n    this.store.set(extensionId, snapshot)\n    return snapshot\n  }\n\n  grant(extensionId: string, grant: ModulePermissionGrant) {\n    const existing = this.store.get(extensionId)\n    if (!existing) {\n      throw new Error(`Cannot grant permissions to unknown plugin \"${extensionId}\".`)\n    }","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/plugin-sdk/src/plugin-host/runtimes/shared/services/permissions.ts#L304-L340","documentation":"Thrown by PermissionService.declare when store.get(extensionId) is unset. The in-memory permission store is populated only by initialize(extensionId, declaration, options) during session setup; declare is strictly an incremental merge on top of an initialized snapshot, so calling it for a plugin the service has never initialized is a state-machine violation.","triggerScenarios":"Invoking permissions.declare('ext-1', ...) before the host started that extension (initialize runs during start); using a different extensionId string than the one passed to initialize (casing, versioned id); calling declare on a freshly constructed PermissionService/Host in tests without bootstrapping; declaring after the host was recreated.","commonSituations":"Test helpers that exercise declare/grant directly; devtools that mutate permissions before the target extension session exists; extensionId drift between manifest, session registration, and permission tooling; multiple host instances sharing nothing in-memory.","solutions":["Initialize first: permissions.initialize(extensionId, declaration) (normally done by host.start) before any declare call.","Ensure the exact same extensionId is used for initialize and declare — compare against session.extension.id.","In tests/devtools, bootstrap via the host (start the extension) instead of hand-calling declare on a bare service.","Guard the call: if (!permissions.get(extensionId)) initialize, then declare."],"exampleFix":"// before\nsvc.declare('ext-1', { apis: [...] }) // fresh PermissionService -> throws\n\n// after\nsvc.initialize('ext-1', { apis: [...] })\nsvc.declare('ext-1', { resources: [...] })","handlingStrategy":"validation","validationCode":"// Before declare:\nif (!permissions.get(extensionId)) {\n  permissions.initialize(extensionId, requestedDeclaration)\n  return\n}\npermissions.declare(extensionId, requestedDeclaration)","typeGuard":"const isPermissionStateInitialized = (svc: PermissionService, extensionId: string): boolean => svc.get(extensionId) !== undefined","tryCatchPattern":"try {\n  svc.declare(extensionId, declaration)\n} catch (error) {\n  if (error instanceof Error && error.message.includes('unknown plugin')) {\n    svc.initialize(extensionId, declaration) // first-time declaration doubles as init\n    return\n  }\n  throw error\n}","preventionTips":["Drive all permission mutations through the host session lifecycle (start initializes) instead of calling the service directly.","In devtools/tests, check svc.get(extensionId) before enabling declare/grant actions.","Use one canonical extensionId (session.extension.id) everywhere."],"tags":["plugin-sdk","permissions","state-machine","initialization"],"backgroundTag":"uninitialized-state-access","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}