moeru-ai/airi · error

Security-scoped Extension import access is not configured.

Error message

Security-scoped Extension import access is not configured.

What it means

withSecurityScopedAccess wraps an import operation in a macOS security-scoped bookmark session so the main process can read a folder the user picked in the sandboxed renderer. It throws when a bookmark exists but the injected startAccessingSecurityScopedResource hook is missing. This means the Electron main side was not wired with the browser-window security-scoped accessors, so the service cannot begin the scoped read it promised.

Solutions

  1. Wire the Electron main-process security-scoped accessors (dialog/bookmark based startAccessingSecurityScopedResource) into the service constructor/DI container where DirectoryImportService is created.
  2. Ensure a securityScopedBookmark is only stored on platforms that provide the accessor; clear the bookmark if the helper is unavailable so operation() runs without scoped access.
  3. Reconstruct the service with the full dependency object (check apps/stage-tamagotchi/src/main services wiring for the missing field).
  4. In tests, inject a no-op startAccessingSecurityScopedResource that returns a stop function.

Example fix

// before
new DirectoryImportService({ extensionsRoot, isExtensionInstalled })
// after
new DirectoryImportService({
  extensionsRoot,
  isExtensionInstalled,
  startAccessingSecurityScopedResource: bookmark => {
    const stop = airiStartAccessingSecurityScopedResource(bookmark)
    return () => stop()
  },
})
Defensive patterns

Strategy: try-catch

Validate before calling

if (!serviceHasSecurityScopedAccess) {
  // fall back to a non-bookmarked import path or reject before prepare
}

Type guard

function isSecurityScopedAccessConfigured(s: { startAccessingSecurityScopedResource?: (b: string) => () => void }): s is typeof s & { startAccessingSecurityScopedResource: (b: string) => () => void } {
  return typeof s.startAccessingSecurityScopedResource === 'function'
}

Try / catch

try {
  await importService.prepare(folderPath, bookmark)
} catch (error) {
  if (error.message.includes('Security-scoped Extension import access is not configured')) {
    // wire the Electron main helper or retry without a bookmark
  }
}

Prevention

When it happens

Trigger: Calling prepare/commitPreparedPlan on an extension import whose stored plan carries a securityScopedBookmark while the DirectoryImportService was constructed without startAccessingSecurityScopedResource (null/undefined dependency injection of the Electron main-process helper).

Common situations: Running on Linux/Windows where the bookmark helper is intentionally not wired but a bookmark value was still persisted from another platform; constructing the service in tests or secondary windows without the injeca-provided Electron helpers; refactors that dropped the startAccessingSecurityScopedResource argument.

Understand the failure class

Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17). Data as JSON: /api/errors/9a40f5b8a1c642df. Report an issue: GitHub.

Appendix: source

Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts:458

    await this.initialization
    await this.commitQueue
  }

  private assertActive(): void {
    if (this.disposed) {
      throw new Error('Extension directory importer is disposed.')
    }
  }

  private async withSecurityScopedAccess<TResult>(
    bookmark: string | undefined,
    operation: () => Promise<TResult>,
  ): Promise<TResult> {
    if (!bookmark) {
      return await operation()
    }
    if (!this.startAccessingSecurityScopedResource) {
      throw new Error('Security-scoped Extension import access is not configured.')
    }

    const stopAccessing = this.startAccessingSecurityScopedResource(bookmark)
    try {
      return await operation()
    }
    finally {
      stopAccessing()
    }
  }

  private async assertDestinationAvailable(extensionId: string): Promise<void> {
    const destination = join(this.extensionsRoot, extensionId)
    if (await this.isExtensionInstalled(extensionId) || await pathExists(destination)) {
      throw new Error(`Extension is already installed: ${extensionId}`)
    }
  }

View on GitHub (pinned to 438a067dde)