{"record":{"id":"9a40f5b8a1c642df","repo":"moeru-ai/airi","slug":"security-scoped-extension-import-access-is-not-configured","errorCode":null,"errorMessage":"Security-scoped Extension import access is not configured.","messagePattern":"Security-scoped Extension import access is not configured\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts","lineNumber":458,"sourceCode":"    await this.initialization\n    await this.commitQueue\n  }\n\n  private assertActive(): void {\n    if (this.disposed) {\n      throw new Error('Extension directory importer is disposed.')\n    }\n  }\n\n  private async withSecurityScopedAccess<TResult>(\n    bookmark: string | undefined,\n    operation: () => Promise<TResult>,\n  ): Promise<TResult> {\n    if (!bookmark) {\n      return await operation()\n    }\n    if (!this.startAccessingSecurityScopedResource) {\n      throw new Error('Security-scoped Extension import access is not configured.')\n    }\n\n    const stopAccessing = this.startAccessingSecurityScopedResource(bookmark)\n    try {\n      return await operation()\n    }\n    finally {\n      stopAccessing()\n    }\n  }\n\n  private async assertDestinationAvailable(extensionId: string): Promise<void> {\n    const destination = join(this.extensionsRoot, extensionId)\n    if (await this.isExtensionInstalled(extensionId) || await pathExists(destination)) {\n      throw new Error(`Extension is already installed: ${extensionId}`)\n    }\n  }\n","sourceCodeStart":440,"sourceCodeEnd":476,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L440-L476","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Wire the Electron main-process security-scoped accessors (dialog/bookmark based startAccessingSecurityScopedResource) into the service constructor/DI container where DirectoryImportService is created.","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.","Reconstruct the service with the full dependency object (check apps/stage-tamagotchi/src/main services wiring for the missing field).","In tests, inject a no-op startAccessingSecurityScopedResource that returns a stop function."],"exampleFix":"// before\nnew DirectoryImportService({ extensionsRoot, isExtensionInstalled })\n// after\nnew DirectoryImportService({\n  extensionsRoot,\n  isExtensionInstalled,\n  startAccessingSecurityScopedResource: bookmark => {\n    const stop = airiStartAccessingSecurityScopedResource(bookmark)\n    return () => stop()\n  },\n})","handlingStrategy":"try-catch","validationCode":"if (!serviceHasSecurityScopedAccess) {\n  // fall back to a non-bookmarked import path or reject before prepare\n}","typeGuard":"function isSecurityScopedAccessConfigured(s: { startAccessingSecurityScopedResource?: (b: string) => () => void }): s is typeof s & { startAccessingSecurityScopedResource: (b: string) => () => void } {\n  return typeof s.startAccessingSecurityScopedResource === 'function'\n}","tryCatchPattern":"try {\n  await importService.prepare(folderPath, bookmark)\n} catch (error) {\n  if (error.message.includes('Security-scoped Extension import access is not configured')) {\n    // wire the Electron main helper or retry without a bookmark\n  }\n}","preventionTips":["Always construct the service with the full Electron main-process dependency set via injeca.","Only persist securityScopedBookmark values on platforms where the accessor exists.","Inject a no-op accessor in tests so bookmarked paths are exercisable."],"tags":["electron","macos","security-scoped-bookmark","dependency-injection"],"backgroundTag":"missing-dependency","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}