moeru-ai/airi · error

Extension entrypoint escapes the package folder

Error message

Extension entrypoint escapes the package folder: ${entrypoint}

What it means

During directory import, each relative entrypoint from the manifest is resolved against the package folder's real path and AIRI verifies the result stays inside that folder. This error is thrown when the resolved path escapes the package directory (e.g. via `..` segments), which is treated as a security risk since imported extensions must be self-contained.

Solutions

  1. Move the file(s) the entrypoint references inside the extension package folder and point the entrypoint at them relatively.
  2. Remove any `../` segments from entrypoint paths in the manifest so the resolved path stays under the package root.
  3. Bundle external/shared code into the package at build time so the entrypoint is a local file.

Example fix

// before (manifest)
{ "entrypoints": { "main": "../shared/ui.js" } }
// after: copy shared/ui.js into the package, then
{ "entrypoints": { "main": "./shared/ui.js" } }
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, isAbsolute, sep } from 'node:path'
function isContainedPath(root, candidate) {
  const rel = resolve(root, candidate)
  return rel === root || rel.startsWith(root + sep)
}
function validateNoEscape(manifest, root) {
  for (const ep of Object.values(manifest.entrypoints ?? {})) {
    if (ep && !isContainedPath(root, ep))
      throw new Error(`entrypoint escapes package folder: ${ep}`)
  }
}

Type guard

const isInsidePackage = (root, ep) => typeof ep === 'string' && !ep.split(/[\\/]/).includes('..')

Try / catch

try {
  await importExtension(folder)
} catch (err) {
  if (String(err.message).includes('escapes the package folder')) {
    showHint('Move all entrypoint-referenced files inside the package and use relative paths.')
  } else throw err
}

Prevention

When it happens

Trigger: `inspectExtensionDirectory` encounters a manifest entrypoint like `"../shared/index.js"` or `"../../elsewhere/main.js"`; `resolve(sourceRealPath, entrypoint)` then lands outside `sourceRealPath` and `isContainedPath` returns false.

Common situations: Extension authors sharing code with a sibling folder via `..` paths; monorepo-style packages referencing assets outside the extension root; typos in entrypoint paths adding extra `../` segments; manifests written for a different layout than the actual folder structure.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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

Appendix: source

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

    }
    throw error
  }

  const parsedManifest = parseExtensionManifest(rawManifest)
  if (!parsedManifest.success) {
    throw new Error(`Extension manifest is invalid: ${formatManifestDiagnostics(parsedManifest.diagnostics)}`)
  }

  for (const entrypoint of Object.values(parsedManifest.manifest.entrypoints)) {
    if (!entrypoint) {
      continue
    }
    if (isAbsolute(entrypoint)) {
      throw new Error(`Imported Extension entrypoints must be relative paths: ${entrypoint}`)
    }
    const resolvedEntrypoint = resolve(sourceRealPath, entrypoint)
    if (!isContainedPath(sourceRealPath, resolvedEntrypoint)) {
      throw new Error(`Extension entrypoint escapes the package folder: ${entrypoint}`)
    }
    await assertRegularFile(resolvedEntrypoint, 'Extension entrypoint')
    const entrypointRealPath = await realpath(resolvedEntrypoint)
    if (!isContainedPath(sourceRealPath, entrypointRealPath)) {
      throw new Error(`Extension entrypoint resolves outside the package folder: ${entrypoint}`)
    }
  }

  const fingerprint = createHash('sha256')
  for (const directory of directories) {
    fingerprint.update(`directory\0${directory}\0`)
  }
  for (const file of files) {
    fingerprint.update(`file\0${file.relativePath}\0${file.size}\0`)
    if (file.relativePath === manifestRelativePath) {
      fingerprint.update(manifestContents)
    }
    else {

View on GitHub (pinned to 438a067dde)