{"record":{"id":"2f74f1c6eb4045ea","repo":"moeru-ai/airi","slug":"extension-entrypoint-escapes-the-package-folder-entrypoint","errorCode":null,"errorMessage":"Extension entrypoint escapes the package folder: ${entrypoint}","messagePattern":"Extension entrypoint escapes the package folder: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts","lineNumber":243,"sourceCode":"    }\n    throw error\n  }\n\n  const parsedManifest = parseExtensionManifest(rawManifest)\n  if (!parsedManifest.success) {\n    throw new Error(`Extension manifest is invalid: ${formatManifestDiagnostics(parsedManifest.diagnostics)}`)\n  }\n\n  for (const entrypoint of Object.values(parsedManifest.manifest.entrypoints)) {\n    if (!entrypoint) {\n      continue\n    }\n    if (isAbsolute(entrypoint)) {\n      throw new Error(`Imported Extension entrypoints must be relative paths: ${entrypoint}`)\n    }\n    const resolvedEntrypoint = resolve(sourceRealPath, entrypoint)\n    if (!isContainedPath(sourceRealPath, resolvedEntrypoint)) {\n      throw new Error(`Extension entrypoint escapes the package folder: ${entrypoint}`)\n    }\n    await assertRegularFile(resolvedEntrypoint, 'Extension entrypoint')\n    const entrypointRealPath = await realpath(resolvedEntrypoint)\n    if (!isContainedPath(sourceRealPath, entrypointRealPath)) {\n      throw new Error(`Extension entrypoint resolves outside the package folder: ${entrypoint}`)\n    }\n  }\n\n  const fingerprint = createHash('sha256')\n  for (const directory of directories) {\n    fingerprint.update(`directory\\0${directory}\\0`)\n  }\n  for (const file of files) {\n    fingerprint.update(`file\\0${file.relativePath}\\0${file.size}\\0`)\n    if (file.relativePath === manifestRelativePath) {\n      fingerprint.update(manifestContents)\n    }\n    else {","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L225-L261","documentation":"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.","triggerScenarios":"`inspectExtensionDirectory` encounters a manifest entrypoint like `\"../shared/index.js\"` or `\"../../elsewhere/main.js\"`; `resolve(sourceRealPath, entrypoint)` then lands outside `sourceRealPath` and `isContainedPath` returns false.","commonSituations":"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.","solutions":["Move the file(s) the entrypoint references inside the extension package folder and point the entrypoint at them relatively.","Remove any `../` segments from entrypoint paths in the manifest so the resolved path stays under the package root.","Bundle external/shared code into the package at build time so the entrypoint is a local file."],"exampleFix":"// before (manifest)\n{ \"entrypoints\": { \"main\": \"../shared/ui.js\" } }\n// after: copy shared/ui.js into the package, then\n{ \"entrypoints\": { \"main\": \"./shared/ui.js\" } }","handlingStrategy":"validation","validationCode":"import { resolve, isAbsolute, sep } from 'node:path'\nfunction isContainedPath(root, candidate) {\n  const rel = resolve(root, candidate)\n  return rel === root || rel.startsWith(root + sep)\n}\nfunction validateNoEscape(manifest, root) {\n  for (const ep of Object.values(manifest.entrypoints ?? {})) {\n    if (ep && !isContainedPath(root, ep))\n      throw new Error(`entrypoint escapes package folder: ${ep}`)\n  }\n}","typeGuard":"const isInsidePackage = (root, ep) => typeof ep === 'string' && !ep.split(/[\\\\/]/).includes('..')","tryCatchPattern":"try {\n  await importExtension(folder)\n} catch (err) {\n  if (String(err.message).includes('escapes the package folder')) {\n    showHint('Move all entrypoint-referenced files inside the package and use relative paths.')\n  } else throw err\n}","preventionTips":["Never reference files outside the extension root from the manifest; bundle shared code in.","Sanity-check entrypoints with a `..` segment check before distributing the extension.","Test-import your extension folder before publishing."],"tags":["extensions","manifest","path-traversal","security"],"backgroundTag":"path-traversal-blocked","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"}