moeru-ai/airi · error · Error
Cannot grant permissions to unknown plugin
Error message
Cannot grant permissions to unknown plugin "${extensionId}". What it means
Thrown by PermissionService.grant when store.get(extensionId) is unset. Grants can only intersect with an existing snapshot (requested ceiling plus granted set); with no initialize call there is nothing to intersect with, so granting to an unknown plugin id is rejected. Same lifecycle rule as declare: initialize during session start must precede any grant.
Solutions
- Start/initialize the extension (host.start populates the snapshot) before enabling grant actions for it.
- Guard in the UI layer: disable 'Grant' until permissions.get(extensionId) returns a snapshot.
- Confirm the extensionId matches session.extension.id exactly.
- After host recreation, re-initialize permission state (optionally passing persisted grants via initialize options) instead of granting into the void.
Example fix
// before
function onGrantClick(extensionId: string, grant: ModulePermissionGrant) {
svc.grant(extensionId, grant) // throws when not initialized
}
// after
function onGrantClick(extensionId: string, grant: ModulePermissionGrant) {
if (!svc.get(extensionId)) {
svc.initialize(extensionId, {})
}
svc.grant(extensionId, grant)
} Defensive patterns
Strategy: validation
Validate before calling
// Before granting from a UI:
if (!permissions.get(extensionId)) {
throw new Error(`Start extension '${extensionId}' before granting permissions`)
}
const snapshot = permissions.grant(extensionId, grant) Type guard
const canGrant = (svc: PermissionService, extensionId: string): boolean => svc.get(extensionId) !== undefined
Try / catch
try {
svc.grant(extensionId, grant)
} catch (error) {
if (error instanceof Error && error.message.includes('unknown plugin')) {
// session not started yet: queue the grant and apply it after initialize
pendingGrants.set(extensionId, grant)
return
}
throw error
} Prevention
- Disable grant controls for extensions whose permission snapshot is absent.
- On host restart, re-initialize permission state (pass persisted grants through initialize options) before granting.
- Re-apply queued grants right after host.start completes.
When it happens
Trigger: Calling grant('ext-1', {...}) from a permissions UI before the extension session was started; granting after a host restart wiped the in-memory store while the UI still had the old extension list; extensionId mismatch between the UI row and the initialized id; granting during teardown after the snapshot was cleared.
Common situations: Permission-management UIs operating on installed-but-not-started extensions; dev flows that recreate the host per request; id drift between the installer registry and the session service.
Related errors
- Cannot declare permissions for unknown plugin
- Permission denied: .
- 1. Microphone permission not granted - browser should…
- AudioContext not initialized
- AudioContext or worklets not ready
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/c35c06f2e4c6c62d.
Report an issue: GitHub.
Appendix: source
Thrown at packages/plugin-sdk/src/plugin-host/runtimes/shared/services/permissions.ts:339
if (!existing) {
throw new Error(`Cannot declare permissions for unknown plugin "${extensionId}".`)
}
const requested = normalizeDeclaration(requestedDeclaration)
const snapshot: PermissionSnapshot = {
requested: mergePermissionDeclarations(existing.requested, requested),
granted: existing.granted,
revision: existing.revision + 1,
}
this.store.set(extensionId, snapshot)
return snapshot
}
grant(extensionId: string, grant: ModulePermissionGrant) {
const existing = this.store.get(extensionId)
if (!existing) {
throw new Error(`Cannot grant permissions to unknown plugin "${extensionId}".`)
}
const mergedGranted = mergePermissions(existing.granted, grant)
const snapshot: PermissionSnapshot = {
requested: existing.requested,
granted: intersectPermissions(existing.requested, mergedGranted),
revision: existing.revision + 1,
}
this.store.set(extensionId, snapshot)
return snapshot
}
get(extensionId: string) {
return this.store.get(extensionId)
}
isAllowed(extensionId: string, area: ModulePermissionArea, action: string, key: string) {
const snapshot = this.store.get(extensionId)View on GitHub (pinned to 677329427f)