moeru-ai/airi · error · Error

Extension folder import is available only from the…

Error message

Extension folder import is available only from the Extension management window.

What it means

This error is thrown by the AIRI Electron main process when an IPC request to import an extension folder arrives from a renderer that is not the authorized Extension management window. The main process compares the sender's webContents id against the id registered as the extension-management window; any mismatch (or a missing event/authorization) is rejected to prevent arbitrary renderers from opening native directory pickers and importing extensions.

Solutions

  1. Invoke the extension folder-import handler only from the Extension management window renderer.
  2. Ensure the Extension management window registers its webContents id via getExtensionManagementWebContentsId before issuing import requests.
  3. After a window reload, re-register the new webContents id with the main process.
  4. Do not wrap or forward this IPC call from other windows or background pages.

Example fix

// before
// called from an arbitrary renderer window
await window.ipc.invoke('extension:import-folder', path)

// after
// only in the Extension management window renderer
if (isExtensionManagementWindow) {
  await window.ipc.invoke('extension:import-folder', path)
}
Defensive patterns

Strategy: try-catch

Validate before calling

const authorizedId = await window.ipc.invoke('extension:get-management-webcontents-id')
if (window.__webContentsId !== authorizedId) throw new Error('not the extension management window')

Type guard

function isAuthorizedSender(event: Electron.IpcMainEvent, authorizedId: number | undefined): boolean {
  return typeof authorizedId === 'number' && event.sender.id === authorizedId
}

Try / catch

try {
  await window.ipc.invoke('extension:import-folder')
} catch (err) {
  if (String((err as Error).message).includes('Extension folder import is available only')) {
    // route the action to the extension management window instead
  }
}

Prevention

When it happens

Trigger: An IPC event reaches requireExtensionManagementEvent when: the invoke options carry no raw ipcMainEvent, no extension-management webContents id has been registered, or event.sender.id differs from that registered id — i.e. any renderer other than the Extension management window invokes the extension folder-import handler.

Common situations: Calling the import API from a secondary window, a devtools page, or an embedded webview; triggering the import before the Extension management window registers its webContents id; stale window replaced after reload so the registered id no longer matches the live sender.

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-09-17). Data as JSON: /api/errors/1b038874ec0149eb. Report an issue: GitHub.

Appendix: source

Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/index.ts:47

  electronPluginListAgentTools,
  electronPluginListXsaiTools,
  electronPluginToolsChanged,
} from '../../../../shared/eventa/plugin/tools'
import { onAppBeforeQuit } from '../../../libs/bootkit/lifecycle'
import { setupExtensionHostServiceInternal } from './host'

interface ElectronInvokeOptions {
  raw?: { ipcMainEvent?: IpcMainEvent }
}

function requireExtensionManagementEvent(
  invokeOptions: ElectronInvokeOptions | undefined,
  getAuthorizedWebContentsId: SetupExtensionHostOptions['getExtensionManagementWebContentsId'],
): IpcMainEvent {
  const event = invokeOptions?.raw?.ipcMainEvent
  const authorizedWebContentsId = getAuthorizedWebContentsId?.()
  if (!event || authorizedWebContentsId === undefined || event.sender.id !== authorizedWebContentsId) {
    throw new Error('Extension folder import is available only from the Extension management window.')
  }
  return event
}

function assertImportPlanOwner(planOwners: Map<string, number>, planId: string, senderId: number): void {
  if (planOwners.get(planId) !== senderId) {
    throw new Error('Extension import plan is not available to this renderer.')
  }
}

function cancelDirectoryImportsForOwner(
  hostService: Awaited<ReturnType<typeof setupExtensionHostServiceInternal>>,
  planOwners: Map<string, number>,
  ownerId: number,
): void {
  for (const [planId, planOwnerId] of planOwners) {
    if (planOwnerId !== ownerId) {
      continue

View on GitHub (pinned to 438a067dde)