@ai-agent-forge/agent-forge 0.88.5 → 0.88.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/bundled-plugins/agent-loop-policy/agent-forge.json +1 -1
- package/bundled-plugins/agent-loop-policy/plugin.json +1 -1
- package/bundled-plugins/alignment/agent-forge.json +19 -0
- package/bundled-plugins/alignment/dist/baseline.d.ts +12 -0
- package/bundled-plugins/alignment/dist/baseline.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/baseline.js +68 -0
- package/bundled-plugins/alignment/dist/baseline.js.map +1 -0
- package/bundled-plugins/alignment/dist/engine.d.ts +102 -0
- package/bundled-plugins/alignment/dist/engine.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/engine.js +654 -0
- package/bundled-plugins/alignment/dist/engine.js.map +1 -0
- package/bundled-plugins/alignment/dist/entry.d.ts +14 -0
- package/bundled-plugins/alignment/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/entry.js +23 -0
- package/bundled-plugins/alignment/dist/entry.js.map +1 -0
- package/bundled-plugins/alignment/dist/export.d.ts +15 -0
- package/bundled-plugins/alignment/dist/export.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/export.js +122 -0
- package/bundled-plugins/alignment/dist/export.js.map +1 -0
- package/bundled-plugins/alignment/dist/http-server.d.ts +33 -0
- package/bundled-plugins/alignment/dist/http-server.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/http-server.js +119 -0
- package/bundled-plugins/alignment/dist/http-server.js.map +1 -0
- package/bundled-plugins/alignment/dist/index.d.ts +24 -0
- package/bundled-plugins/alignment/dist/index.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/index.js +719 -0
- package/bundled-plugins/alignment/dist/index.js.map +1 -0
- package/bundled-plugins/alignment/dist/page.d.ts +15 -0
- package/bundled-plugins/alignment/dist/page.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/page.js +832 -0
- package/bundled-plugins/alignment/dist/page.js.map +1 -0
- package/bundled-plugins/alignment/dist/render.d.ts +40 -0
- package/bundled-plugins/alignment/dist/render.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/render.js +194 -0
- package/bundled-plugins/alignment/dist/render.js.map +1 -0
- package/bundled-plugins/alignment/dist/types.d.ts +217 -0
- package/bundled-plugins/alignment/dist/types.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/types.js +30 -0
- package/bundled-plugins/alignment/dist/types.js.map +1 -0
- package/bundled-plugins/alignment/plugin.json +12 -0
- package/bundled-plugins/browser-playwright/MANUAL.md +60 -0
- package/bundled-plugins/browser-playwright/agent-forge.json +25 -0
- package/bundled-plugins/browser-playwright/dist/capability.d.ts +25 -0
- package/bundled-plugins/browser-playwright/dist/capability.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/capability.js +612 -0
- package/bundled-plugins/browser-playwright/dist/capability.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/entry.d.ts +13 -0
- package/bundled-plugins/browser-playwright/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/entry.js +14 -0
- package/bundled-plugins/browser-playwright/dist/entry.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/index.d.ts +7 -0
- package/bundled-plugins/browser-playwright/dist/index.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/index.js +5 -0
- package/bundled-plugins/browser-playwright/dist/index.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/operations.d.ts +69 -0
- package/bundled-plugins/browser-playwright/dist/operations.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/operations.js +149 -0
- package/bundled-plugins/browser-playwright/dist/operations.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.d.ts +275 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.js +953 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/setup.d.ts +88 -0
- package/bundled-plugins/browser-playwright/dist/setup.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/setup.js +252 -0
- package/bundled-plugins/browser-playwright/dist/setup.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/types.d.ts +270 -0
- package/bundled-plugins/browser-playwright/dist/types.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/types.js +2 -0
- package/bundled-plugins/browser-playwright/dist/types.js.map +1 -0
- package/bundled-plugins/browser-playwright/plugin.json +20 -0
- package/bundled-plugins/coding-tools/agent-forge.json +1 -1
- package/bundled-plugins/coding-tools/dist/bash.d.ts +2 -7
- package/bundled-plugins/coding-tools/dist/bash.d.ts.map +1 -1
- package/bundled-plugins/coding-tools/dist/bash.js +2 -34
- package/bundled-plugins/coding-tools/dist/bash.js.map +1 -1
- package/bundled-plugins/coding-tools/plugin.json +1 -1
- package/bundled-plugins/compaction-policy/agent-forge.json +1 -1
- package/bundled-plugins/compaction-policy/plugin.json +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.d.ts.map +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.js +84 -31
- package/bundled-plugins/mcp-http/dist/mcp-http.js.map +1 -1
- package/bundled-plugins/overflow-policy/agent-forge.json +1 -1
- package/bundled-plugins/overflow-policy/plugin.json +1 -1
- package/bundled-plugins/provider-failover/agent-forge.json +1 -1
- package/bundled-plugins/provider-failover/plugin.json +1 -1
- package/bundled-plugins/retry-policy/agent-forge.json +1 -1
- package/bundled-plugins/retry-policy/plugin.json +1 -1
- package/dist/bundle/.build-manifest.json +60 -58
- package/dist/bundle/chunks/{anthropic-messages-3FJHRAJL.js → anthropic-messages-NL4Y6WPB.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-GCL47IDS.js → azure-openai-responses-CPXC42HO.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/chunk-5LRQFZFE.js +2 -0
- package/dist/bundle/chunks/{chunk-HTEVUY7F.js → chunk-7HSJPQKN.js} +1 -1
- package/dist/bundle/chunks/{chunk-AC2FIS4G.js → chunk-EDO7Q6XR.js} +1 -1
- package/dist/bundle/chunks/{chunk-755VNWIF.js → chunk-ISLHFRER.js} +6 -6
- package/dist/bundle/chunks/{chunk-HV64ASMT.js → chunk-K3S6GTL7.js} +1 -1
- package/dist/bundle/chunks/{chunk-HEL6TAWE.js → chunk-POLDAUYA.js} +37 -36
- package/dist/bundle/chunks/{chunk-V3ACW7VD.js → chunk-WJSKB3FC.js} +1 -1
- package/dist/bundle/chunks/{chunk-32YDZI4J.js → chunk-ZS5JEI76.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-6BMXWA44.js → google-generative-ai-UMZ53EFA.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-6H7NLRTK.js → google-vertex-ZNR5NS6H.js} +1 -1
- package/dist/bundle/chunks/image-resize-worker.js +1 -1
- package/dist/bundle/chunks/{mistral-conversations-IXUHSFSJ.js → mistral-conversations-IT66WUNN.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-YHUAEVCM.js → openai-codex-responses-FOVCVG6I.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-ULQCFC72.js → openai-completions-FLQ7CBOL.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-NLLAJDPS.js → openai-responses-ABGYHS2T.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-EJTI2EJ3.js → pi-messages-6K5W6LUN.js} +1 -1
- package/dist/bundle/chunks/{virtual-modules-TQWEGNNZ.js → virtual-modules-2QGILFV7.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/client.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/client/remote-session.d.ts.map +1 -1
- package/dist/client/remote-session.js +65 -6
- package/dist/client/remote-session.js.map +1 -1
- package/dist/core/agent-session-store.d.ts.map +1 -1
- package/dist/core/agent-session-store.js +20 -3
- package/dist/core/agent-session-store.js.map +1 -1
- package/dist/core/extensions/agent-composition.d.ts.map +1 -1
- package/dist/core/extensions/agent-composition.js +20 -1
- package/dist/core/extensions/agent-composition.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.js +1 -0
- package/dist/core/i18n/locales/zh-cn/core-errors.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js +10 -3
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js.map +1 -1
- package/dist/core/keybindings.d.ts +35 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +34 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +62 -15
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +11 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +2 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/capability-mount.d.ts.map +1 -1
- package/dist/core/tools/capability-mount.js +4 -21
- package/dist/core/tools/capability-mount.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -2
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +2 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +25 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/changelog-pager.d.ts +53 -0
- package/dist/modes/interactive/components/changelog-pager.d.ts.map +1 -0
- package/dist/modes/interactive/components/changelog-pager.js +239 -0
- package/dist/modes/interactive/components/changelog-pager.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.d.ts +41 -11
- package/dist/modes/interactive/components/tool-run-group.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.js +126 -14
- package/dist/modes/interactive/components/tool-run-group.js.map +1 -1
- package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-commands.js +23 -13
- package/dist/modes/interactive/interactive-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-events.d.ts +14 -4
- package/dist/modes/interactive/interactive-events.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-events.js +94 -12
- package/dist/modes/interactive/interactive-events.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +6 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +21 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/plugins/ecosystem/node-local-package-source.d.ts +2 -1
- package/dist/plugins/ecosystem/node-local-package-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-local-package-source.js +110 -4
- package/dist/plugins/ecosystem/node-local-package-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.js +106 -39
- package/dist/plugins/ecosystem/node-package-install-host.js.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js +59 -2
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js.map +1 -1
- package/dist/suites/builtin-suites.d.ts.map +1 -1
- package/dist/suites/builtin-suites.js +2 -2
- package/dist/suites/builtin-suites.js.map +1 -1
- package/dist/utils/changelog.d.ts +27 -0
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +110 -0
- package/dist/utils/changelog.js.map +1 -1
- package/docs/keybindings.md +14 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/extensions/with-deps/plugin.json +1 -1
- package/package.json +1 -1
- package/dist/bundle/chunks/chunk-A3JYRWB6.js +0 -2
|
@@ -2,7 +2,8 @@ import { type NodeZipPackageEntryV1, type NodeZipPackageLimitsV1 } from "./node-
|
|
|
2
2
|
/**
|
|
3
3
|
* Walk a local directory into normalized package entries. Deterministic
|
|
4
4
|
* (code-unit sorted per level), symlink-free, and budget-bound against the
|
|
5
|
-
* shared package limits.
|
|
5
|
+
* shared package limits. A package.json `files` whitelist is honored with npm
|
|
6
|
+
* pack semantics (see above); without one, every eligible file is collected.
|
|
6
7
|
*/
|
|
7
8
|
export declare function collectLocalDirectoryPackageEntriesV1(rootDirectory: string, limitsInput?: NodeZipPackageLimitsV1, signal?: AbortSignal): Promise<readonly NodeZipPackageEntryV1[]>;
|
|
8
9
|
/** Read a local `.zip`/`.tgz` package file bounded by the shared archive budget. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-local-package-source.d.ts","sourceRoot":"","sources":["../../../src/plugins/ecosystem/node-local-package-source.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAG3B,MAAM,gCAAgC,CAAC;AAgExC;;;;GAIG;AACH,wBAAsB,qCAAqC,CAC1D,aAAa,EAAE,MAAM,EACrB,WAAW,GAAE,sBAA2B,EACxC,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,SAAS,qBAAqB,EAAE,CAAC,CAY3C;AAED,oFAAoF;AACpF,wBAAsB,sBAAsB,CAC3C,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,sBAA2B,EACxC,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,UAAU,CAAC,CAarB","sourcesContent":["import { lstat, readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n\tNodePackageInstallErrorV1,\n\ttype NodeZipPackageEntryV1,\n\ttype NodeZipPackageLimitsV1,\n\tnormalizedEntryName,\n\tresolveNodePackageLimitsV1,\n} from \"./node-package-install-host.ts\";\n\n/**\n * Local plugin package sources (design §4.6 \"本地\" row). A directory is walked\n * with the same budgets and entry-name rules as archive extraction (bounded\n * entries/bytes, no symlinks, shared name normalization), then packed by the\n * deterministic tar writer into the standard tgz package shape. \"Copy into\n * the plugin directory\" is realized by the install host writing extracted\n * entries into its own store; the original path is never referenced after\n * acquisition. A local `.zip`/`.tgz` file is read with the archive budget.\n */\n\nfunction isMissingError(error: unknown): boolean {\n\tif (typeof error !== \"object\" || error === null) return false;\n\treturn \"code\" in error && (error as { readonly code?: unknown }).code === \"ENOENT\";\n}\n\ninterface WalkBudgetV1 {\n\ttotalBytes: number;\n}\n\nasync function collectDirectory(\n\troot: string,\n\trelative: string,\n\tlimits: Required<NodeZipPackageLimitsV1>,\n\tentries: NodeZipPackageEntryV1[],\n\tbudget: WalkBudgetV1,\n\tsignal: AbortSignal | undefined,\n): Promise<void> {\n\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\tconst dirents = await readdir(relative.length === 0 ? root : join(root, ...relative.split(\"/\")), {\n\t\twithFileTypes: true,\n\t});\n\tdirents.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));\n\tfor (const dirent of dirents) {\n\t\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\tconst childRelative = relative.length === 0 ? dirent.name : `${relative}/${dirent.name}`;\n\t\t// Shared archive entry-name rules reject `..`, absolute segments, drive\n\t\t// letters, Windows reserved names, and trailing dots/spaces up front.\n\t\tnormalizedEntryName(childRelative);\n\t\tif (dirent.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tconst childPath = join(root, ...childRelative.split(\"/\"));\n\t\tconst stats = await lstat(childPath);\n\t\tif (stats.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (stats.isDirectory()) {\n\t\t\tawait collectDirectory(root, childRelative, limits, entries, budget, signal);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!stats.isFile()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (entries.length + 1 > limits.maxEntries) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tconst bytes = await readFile(childPath);\n\t\tif (bytes.byteLength > limits.maxEntryBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tbudget.totalBytes += bytes.byteLength;\n\t\tif (budget.totalBytes > limits.maxExtractedBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tentries.push(\n\t\t\tObject.freeze({\n\t\t\t\tname: childRelative,\n\t\t\t\tdirectory: false,\n\t\t\t\tbytes: new Uint8Array(bytes),\n\t\t\t}),\n\t\t);\n\t}\n}\n\n/**\n * Walk a local directory into normalized package entries. Deterministic\n * (code-unit sorted per level), symlink-free, and budget-bound against the\n * shared package limits.\n */\nexport async function collectLocalDirectoryPackageEntriesV1(\n\trootDirectory: string,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n\tsignal?: AbortSignal,\n): Promise<readonly NodeZipPackageEntryV1[]> {\n\tif (typeof rootDirectory !== \"string\" || rootDirectory.trim().length === 0)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst stats = await lstat(rootDirectory).catch((error: unknown) => {\n\t\tthrow isMissingError(error) ? new NodePackageInstallErrorV1(\"archive_invalid\") : error;\n\t});\n\tif (!stats.isDirectory() || stats.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tconst entries: NodeZipPackageEntryV1[] = [];\n\tconst budget: WalkBudgetV1 = { totalBytes: 0 };\n\tawait collectDirectory(rootDirectory, \"\", limits, entries, budget, signal);\n\treturn Object.freeze(entries);\n}\n\n/** Read a local `.zip`/`.tgz` package file bounded by the shared archive budget. */\nexport async function readLocalPackageFileV1(\n\tpath: string,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n\tsignal?: AbortSignal,\n): Promise<Uint8Array> {\n\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\tif (typeof path !== \"string\" || path.trim().length === 0) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst stats = await lstat(path).catch((error: unknown) => {\n\t\tthrow isMissingError(error) ? new NodePackageInstallErrorV1(\"archive_invalid\") : error;\n\t});\n\tif (stats.isSymbolicLink() || !stats.isFile()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tif (stats.size > limits.maxArchiveBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\tconst bytes = await readFile(path);\n\tif (bytes.byteLength !== stats.size || bytes.byteLength > limits.maxArchiveBytes)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\treturn new Uint8Array(bytes);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"node-local-package-source.d.ts","sourceRoot":"","sources":["../../../src/plugins/ecosystem/node-local-package-source.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAG3B,MAAM,gCAAgC,CAAC;AAsKxC;;;;;GAKG;AACH,wBAAsB,qCAAqC,CAC1D,aAAa,EAAE,MAAM,EACrB,WAAW,GAAE,sBAA2B,EACxC,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,SAAS,qBAAqB,EAAE,CAAC,CAa3C;AAED,oFAAoF;AACpF,wBAAsB,sBAAsB,CAC3C,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,sBAA2B,EACxC,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,UAAU,CAAC,CAarB","sourcesContent":["import { lstat, readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n\tNodePackageInstallErrorV1,\n\ttype NodeZipPackageEntryV1,\n\ttype NodeZipPackageLimitsV1,\n\tnormalizedEntryName,\n\tresolveNodePackageLimitsV1,\n} from \"./node-package-install-host.ts\";\n\n/**\n * Local plugin package sources (design §4.6 \"本地\" row). A directory is walked\n * with the same budgets and entry-name rules as archive extraction (bounded\n * entries/bytes, no symlinks, shared name normalization), then packed by the\n * deterministic tar writer into the standard tgz package shape. \"Copy into\n * the plugin directory\" is realized by the install host writing extracted\n * entries into its own store; the original path is never referenced after\n * acquisition. A local `.zip`/`.tgz` file is read with the archive budget.\n *\n * The walk aligns with npm pack semantics: when package.json carries a `files`\n * array, only whitelisted subtrees plus npm's implicit keeps (package.json,\n * README / LICENSE / LICENCE prefix files at the root) are collected;\n * `node_modules` and `.git` are never collected, and glob `files` entries are\n * rejected for directory installs (pack a `.tgz` for glob support).\n */\n\nfunction isMissingError(error: unknown): boolean {\n\tif (typeof error !== \"object\" || error === null) return false;\n\treturn \"code\" in error && (error as { readonly code?: unknown }).code === \"ENOENT\";\n}\n\ninterface WalkBudgetV1 {\n\ttotalBytes: number;\n}\n\n/** Directory names npm pack never collects, at any depth. */\nconst ALWAYS_EXCLUDED_DIRECTORY_NAMES_V1: ReadonlySet<string> = new Set([\"node_modules\", \".git\"]);\n\nconst GLOB_METACHARACTERS_V1 = /[*?[]/;\n\nfunction globFilesEntryErrorV1(entry: string): NodePackageInstallErrorV1 {\n\tconst error = new NodePackageInstallErrorV1(\"archive_invalid\");\n\terror.message = `Local directory install: package.json \"files\" entry \"${entry}\" uses glob syntax, which is not supported when installing from a directory; pack a .tgz instead`;\n\treturn error;\n}\n\n/**\n * Build the install error for a package.json read or JSON.parse failure. The\n * original error stays visible as `cause` and in the message, so an EACCES or\n * EIO is never silently degraded into \"no files whitelist\" collect-all.\n */\nfunction packageJsonAccessErrorV1(action: \"read\" | \"parse\", error: unknown): NodePackageInstallErrorV1 {\n\tconst detail = error instanceof Error ? error.message : String(error);\n\tconst installError = new NodePackageInstallErrorV1(\"archive_invalid\");\n\tinstallError.message = `Local directory install: failed to ${action} package.json: ${detail}`;\n\tinstallError.cause = error;\n\treturn installError;\n}\n\n/**\n * Read the package.json `files` whitelist. Returns `null` — keeping the\n * historic collect-all behavior — when package.json is absent (a legal\n * directory shape) or `files` is absent or not an array. A missing file is the\n * only tolerated error: any other read failure (permissions, I/O, locks) and a\n * malformed JSON body fail the acquisition instead of silently packing the\n * whole directory, matching npm pack. A returned array (including an empty\n * one) enables whitelist filtering.\n */\nasync function readPackageFilesWhitelistV1(rootDirectory: string): Promise<readonly string[] | null> {\n\tlet raw: string;\n\ttry {\n\t\traw = await readFile(join(rootDirectory, \"package.json\"), \"utf8\");\n\t} catch (error) {\n\t\tif (isMissingError(error)) return null;\n\t\tthrow packageJsonAccessErrorV1(\"read\", error);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (error) {\n\t\tthrow packageJsonAccessErrorV1(\"parse\", error);\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null) return null;\n\tconst files = (parsed as { readonly files?: unknown }).files;\n\tif (!Array.isArray(files)) return null;\n\tconst patterns: string[] = [];\n\tfor (const value of files) {\n\t\tif (typeof value !== \"string\") {\n\t\t\tconst error = new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\t\terror.message = 'Local directory install: package.json \"files\" must contain only strings';\n\t\t\tthrow error;\n\t\t}\n\t\t// npm ignores trailing separators; keeping them would break prefix matching.\n\t\tconst pattern = value.replace(/\\/+$/, \"\");\n\t\tif (pattern.length === 0) continue;\n\t\tif (GLOB_METACHARACTERS_V1.test(pattern)) throw globFilesEntryErrorV1(value);\n\t\tpatterns.push(pattern);\n\t}\n\treturn patterns;\n}\n\n/** npm implicit keeps at the package root: package.json plus README/LICENSE/LICENCE prefix names. */\nfunction isImplicitlyKeptPackageFileV1(relativePath: string): boolean {\n\tif (relativePath.includes(\"/\")) return false;\n\tconst name = relativePath.toUpperCase();\n\treturn (\n\t\tname === \"PACKAGE.JSON\" || name.startsWith(\"README\") || name.startsWith(\"LICENSE\") || name.startsWith(\"LICENCE\")\n\t);\n}\n\n/**\n * npm files-whitelist decision for one walk entry (posix separators). A path\n * is collected when a whitelist entry names it exactly or names a directory\n * above it; directories are also walked when some whitelist entry lives below\n * them, so single-file entries deeper in the tree stay reachable. Root-level\n * files additionally survive through npm's implicit keeps.\n */\nfunction isFilesWhitelistMatchV1(relativePath: string, isDirectory: boolean, patterns: readonly string[]): boolean {\n\tif (patterns.some((pattern) => relativePath === pattern || relativePath.startsWith(`${pattern}/`))) return true;\n\tif (isDirectory) return patterns.some((pattern) => pattern.startsWith(`${relativePath}/`));\n\treturn isImplicitlyKeptPackageFileV1(relativePath);\n}\n\nasync function collectDirectory(\n\troot: string,\n\trelative: string,\n\tlimits: Required<NodeZipPackageLimitsV1>,\n\tentries: NodeZipPackageEntryV1[],\n\tbudget: WalkBudgetV1,\n\tfilesWhitelist: readonly string[] | null,\n\tsignal: AbortSignal | undefined,\n): Promise<void> {\n\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\tconst dirents = await readdir(relative.length === 0 ? root : join(root, ...relative.split(\"/\")), {\n\t\twithFileTypes: true,\n\t});\n\tdirents.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));\n\tfor (const dirent of dirents) {\n\t\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\tconst childRelative = relative.length === 0 ? dirent.name : `${relative}/${dirent.name}`;\n\t\t// npm pack semantics first, before any budgeted work below: dependency\n\t\t// trees and VCS metadata are never packaged (the install store\n\t\t// re-materializes dependencies at staging), then the package.json\n\t\t// `files` whitelist.\n\t\tconst isDirectory = dirent.isDirectory();\n\t\tif (isDirectory && ALWAYS_EXCLUDED_DIRECTORY_NAMES_V1.has(dirent.name)) continue;\n\t\tif (filesWhitelist !== null && !isFilesWhitelistMatchV1(childRelative, isDirectory, filesWhitelist)) continue;\n\t\t// Shared archive entry-name rules reject `..`, absolute segments, drive\n\t\t// letters, Windows reserved names, and trailing dots/spaces up front.\n\t\tnormalizedEntryName(childRelative);\n\t\tif (dirent.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tconst childPath = join(root, ...childRelative.split(\"/\"));\n\t\tconst stats = await lstat(childPath);\n\t\tif (stats.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (stats.isDirectory()) {\n\t\t\tawait collectDirectory(root, childRelative, limits, entries, budget, filesWhitelist, signal);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!stats.isFile()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (entries.length + 1 > limits.maxEntries) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tconst bytes = await readFile(childPath);\n\t\tif (bytes.byteLength > limits.maxEntryBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tbudget.totalBytes += bytes.byteLength;\n\t\tif (budget.totalBytes > limits.maxExtractedBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tentries.push(\n\t\t\tObject.freeze({\n\t\t\t\tname: childRelative,\n\t\t\t\tdirectory: false,\n\t\t\t\tbytes: new Uint8Array(bytes),\n\t\t\t}),\n\t\t);\n\t}\n}\n\n/**\n * Walk a local directory into normalized package entries. Deterministic\n * (code-unit sorted per level), symlink-free, and budget-bound against the\n * shared package limits. A package.json `files` whitelist is honored with npm\n * pack semantics (see above); without one, every eligible file is collected.\n */\nexport async function collectLocalDirectoryPackageEntriesV1(\n\trootDirectory: string,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n\tsignal?: AbortSignal,\n): Promise<readonly NodeZipPackageEntryV1[]> {\n\tif (typeof rootDirectory !== \"string\" || rootDirectory.trim().length === 0)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst stats = await lstat(rootDirectory).catch((error: unknown) => {\n\t\tthrow isMissingError(error) ? new NodePackageInstallErrorV1(\"archive_invalid\") : error;\n\t});\n\tif (!stats.isDirectory() || stats.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tconst filesWhitelist = await readPackageFilesWhitelistV1(rootDirectory);\n\tconst entries: NodeZipPackageEntryV1[] = [];\n\tconst budget: WalkBudgetV1 = { totalBytes: 0 };\n\tawait collectDirectory(rootDirectory, \"\", limits, entries, budget, filesWhitelist, signal);\n\treturn Object.freeze(entries);\n}\n\n/** Read a local `.zip`/`.tgz` package file bounded by the shared archive budget. */\nexport async function readLocalPackageFileV1(\n\tpath: string,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n\tsignal?: AbortSignal,\n): Promise<Uint8Array> {\n\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\tif (typeof path !== \"string\" || path.trim().length === 0) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst stats = await lstat(path).catch((error: unknown) => {\n\t\tthrow isMissingError(error) ? new NodePackageInstallErrorV1(\"archive_invalid\") : error;\n\t});\n\tif (stats.isSymbolicLink() || !stats.isFile()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tif (stats.size > limits.maxArchiveBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\tconst bytes = await readFile(path);\n\tif (bytes.byteLength !== stats.size || bytes.byteLength > limits.maxArchiveBytes)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\treturn new Uint8Array(bytes);\n}\n"]}
|
|
@@ -9,13 +9,108 @@ import { NodePackageInstallErrorV1, normalizedEntryName, resolveNodePackageLimit
|
|
|
9
9
|
* the plugin directory" is realized by the install host writing extracted
|
|
10
10
|
* entries into its own store; the original path is never referenced after
|
|
11
11
|
* acquisition. A local `.zip`/`.tgz` file is read with the archive budget.
|
|
12
|
+
*
|
|
13
|
+
* The walk aligns with npm pack semantics: when package.json carries a `files`
|
|
14
|
+
* array, only whitelisted subtrees plus npm's implicit keeps (package.json,
|
|
15
|
+
* README / LICENSE / LICENCE prefix files at the root) are collected;
|
|
16
|
+
* `node_modules` and `.git` are never collected, and glob `files` entries are
|
|
17
|
+
* rejected for directory installs (pack a `.tgz` for glob support).
|
|
12
18
|
*/
|
|
13
19
|
function isMissingError(error) {
|
|
14
20
|
if (typeof error !== "object" || error === null)
|
|
15
21
|
return false;
|
|
16
22
|
return "code" in error && error.code === "ENOENT";
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
/** Directory names npm pack never collects, at any depth. */
|
|
25
|
+
const ALWAYS_EXCLUDED_DIRECTORY_NAMES_V1 = new Set(["node_modules", ".git"]);
|
|
26
|
+
const GLOB_METACHARACTERS_V1 = /[*?[]/;
|
|
27
|
+
function globFilesEntryErrorV1(entry) {
|
|
28
|
+
const error = new NodePackageInstallErrorV1("archive_invalid");
|
|
29
|
+
error.message = `Local directory install: package.json "files" entry "${entry}" uses glob syntax, which is not supported when installing from a directory; pack a .tgz instead`;
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the install error for a package.json read or JSON.parse failure. The
|
|
34
|
+
* original error stays visible as `cause` and in the message, so an EACCES or
|
|
35
|
+
* EIO is never silently degraded into "no files whitelist" collect-all.
|
|
36
|
+
*/
|
|
37
|
+
function packageJsonAccessErrorV1(action, error) {
|
|
38
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
39
|
+
const installError = new NodePackageInstallErrorV1("archive_invalid");
|
|
40
|
+
installError.message = `Local directory install: failed to ${action} package.json: ${detail}`;
|
|
41
|
+
installError.cause = error;
|
|
42
|
+
return installError;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read the package.json `files` whitelist. Returns `null` — keeping the
|
|
46
|
+
* historic collect-all behavior — when package.json is absent (a legal
|
|
47
|
+
* directory shape) or `files` is absent or not an array. A missing file is the
|
|
48
|
+
* only tolerated error: any other read failure (permissions, I/O, locks) and a
|
|
49
|
+
* malformed JSON body fail the acquisition instead of silently packing the
|
|
50
|
+
* whole directory, matching npm pack. A returned array (including an empty
|
|
51
|
+
* one) enables whitelist filtering.
|
|
52
|
+
*/
|
|
53
|
+
async function readPackageFilesWhitelistV1(rootDirectory) {
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = await readFile(join(rootDirectory, "package.json"), "utf8");
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (isMissingError(error))
|
|
60
|
+
return null;
|
|
61
|
+
throw packageJsonAccessErrorV1("read", error);
|
|
62
|
+
}
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(raw);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw packageJsonAccessErrorV1("parse", error);
|
|
69
|
+
}
|
|
70
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
71
|
+
return null;
|
|
72
|
+
const files = parsed.files;
|
|
73
|
+
if (!Array.isArray(files))
|
|
74
|
+
return null;
|
|
75
|
+
const patterns = [];
|
|
76
|
+
for (const value of files) {
|
|
77
|
+
if (typeof value !== "string") {
|
|
78
|
+
const error = new NodePackageInstallErrorV1("archive_invalid");
|
|
79
|
+
error.message = 'Local directory install: package.json "files" must contain only strings';
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
// npm ignores trailing separators; keeping them would break prefix matching.
|
|
83
|
+
const pattern = value.replace(/\/+$/, "");
|
|
84
|
+
if (pattern.length === 0)
|
|
85
|
+
continue;
|
|
86
|
+
if (GLOB_METACHARACTERS_V1.test(pattern))
|
|
87
|
+
throw globFilesEntryErrorV1(value);
|
|
88
|
+
patterns.push(pattern);
|
|
89
|
+
}
|
|
90
|
+
return patterns;
|
|
91
|
+
}
|
|
92
|
+
/** npm implicit keeps at the package root: package.json plus README/LICENSE/LICENCE prefix names. */
|
|
93
|
+
function isImplicitlyKeptPackageFileV1(relativePath) {
|
|
94
|
+
if (relativePath.includes("/"))
|
|
95
|
+
return false;
|
|
96
|
+
const name = relativePath.toUpperCase();
|
|
97
|
+
return (name === "PACKAGE.JSON" || name.startsWith("README") || name.startsWith("LICENSE") || name.startsWith("LICENCE"));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* npm files-whitelist decision for one walk entry (posix separators). A path
|
|
101
|
+
* is collected when a whitelist entry names it exactly or names a directory
|
|
102
|
+
* above it; directories are also walked when some whitelist entry lives below
|
|
103
|
+
* them, so single-file entries deeper in the tree stay reachable. Root-level
|
|
104
|
+
* files additionally survive through npm's implicit keeps.
|
|
105
|
+
*/
|
|
106
|
+
function isFilesWhitelistMatchV1(relativePath, isDirectory, patterns) {
|
|
107
|
+
if (patterns.some((pattern) => relativePath === pattern || relativePath.startsWith(`${pattern}/`)))
|
|
108
|
+
return true;
|
|
109
|
+
if (isDirectory)
|
|
110
|
+
return patterns.some((pattern) => pattern.startsWith(`${relativePath}/`));
|
|
111
|
+
return isImplicitlyKeptPackageFileV1(relativePath);
|
|
112
|
+
}
|
|
113
|
+
async function collectDirectory(root, relative, limits, entries, budget, filesWhitelist, signal) {
|
|
19
114
|
if (signal?.aborted)
|
|
20
115
|
throw new NodePackageInstallErrorV1("cancelled");
|
|
21
116
|
const dirents = await readdir(relative.length === 0 ? root : join(root, ...relative.split("/")), {
|
|
@@ -26,6 +121,15 @@ async function collectDirectory(root, relative, limits, entries, budget, signal)
|
|
|
26
121
|
if (signal?.aborted)
|
|
27
122
|
throw new NodePackageInstallErrorV1("cancelled");
|
|
28
123
|
const childRelative = relative.length === 0 ? dirent.name : `${relative}/${dirent.name}`;
|
|
124
|
+
// npm pack semantics first, before any budgeted work below: dependency
|
|
125
|
+
// trees and VCS metadata are never packaged (the install store
|
|
126
|
+
// re-materializes dependencies at staging), then the package.json
|
|
127
|
+
// `files` whitelist.
|
|
128
|
+
const isDirectory = dirent.isDirectory();
|
|
129
|
+
if (isDirectory && ALWAYS_EXCLUDED_DIRECTORY_NAMES_V1.has(dirent.name))
|
|
130
|
+
continue;
|
|
131
|
+
if (filesWhitelist !== null && !isFilesWhitelistMatchV1(childRelative, isDirectory, filesWhitelist))
|
|
132
|
+
continue;
|
|
29
133
|
// Shared archive entry-name rules reject `..`, absolute segments, drive
|
|
30
134
|
// letters, Windows reserved names, and trailing dots/spaces up front.
|
|
31
135
|
normalizedEntryName(childRelative);
|
|
@@ -36,7 +140,7 @@ async function collectDirectory(root, relative, limits, entries, budget, signal)
|
|
|
36
140
|
if (stats.isSymbolicLink())
|
|
37
141
|
throw new NodePackageInstallErrorV1("archive_invalid");
|
|
38
142
|
if (stats.isDirectory()) {
|
|
39
|
-
await collectDirectory(root, childRelative, limits, entries, budget, signal);
|
|
143
|
+
await collectDirectory(root, childRelative, limits, entries, budget, filesWhitelist, signal);
|
|
40
144
|
continue;
|
|
41
145
|
}
|
|
42
146
|
if (!stats.isFile())
|
|
@@ -59,7 +163,8 @@ async function collectDirectory(root, relative, limits, entries, budget, signal)
|
|
|
59
163
|
/**
|
|
60
164
|
* Walk a local directory into normalized package entries. Deterministic
|
|
61
165
|
* (code-unit sorted per level), symlink-free, and budget-bound against the
|
|
62
|
-
* shared package limits.
|
|
166
|
+
* shared package limits. A package.json `files` whitelist is honored with npm
|
|
167
|
+
* pack semantics (see above); without one, every eligible file is collected.
|
|
63
168
|
*/
|
|
64
169
|
export async function collectLocalDirectoryPackageEntriesV1(rootDirectory, limitsInput = {}, signal) {
|
|
65
170
|
if (typeof rootDirectory !== "string" || rootDirectory.trim().length === 0)
|
|
@@ -70,9 +175,10 @@ export async function collectLocalDirectoryPackageEntriesV1(rootDirectory, limit
|
|
|
70
175
|
if (!stats.isDirectory() || stats.isSymbolicLink())
|
|
71
176
|
throw new NodePackageInstallErrorV1("archive_invalid");
|
|
72
177
|
const limits = resolveNodePackageLimitsV1(limitsInput);
|
|
178
|
+
const filesWhitelist = await readPackageFilesWhitelistV1(rootDirectory);
|
|
73
179
|
const entries = [];
|
|
74
180
|
const budget = { totalBytes: 0 };
|
|
75
|
-
await collectDirectory(rootDirectory, "", limits, entries, budget, signal);
|
|
181
|
+
await collectDirectory(rootDirectory, "", limits, entries, budget, filesWhitelist, signal);
|
|
76
182
|
return Object.freeze(entries);
|
|
77
183
|
}
|
|
78
184
|
/** Read a local `.zip`/`.tgz` package file bounded by the shared archive budget. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-local-package-source.js","sourceRoot":"","sources":["../../../src/plugins/ecosystem/node-local-package-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n\tNodePackageInstallErrorV1,\n\ttype NodeZipPackageEntryV1,\n\ttype NodeZipPackageLimitsV1,\n\tnormalizedEntryName,\n\tresolveNodePackageLimitsV1,\n} from \"./node-package-install-host.ts\";\n\n/**\n * Local plugin package sources (design §4.6 \"本地\" row). A directory is walked\n * with the same budgets and entry-name rules as archive extraction (bounded\n * entries/bytes, no symlinks, shared name normalization), then packed by the\n * deterministic tar writer into the standard tgz package shape. \"Copy into\n * the plugin directory\" is realized by the install host writing extracted\n * entries into its own store; the original path is never referenced after\n * acquisition. A local `.zip`/`.tgz` file is read with the archive budget.\n */\n\nfunction isMissingError(error: unknown): boolean {\n\tif (typeof error !== \"object\" || error === null) return false;\n\treturn \"code\" in error && (error as { readonly code?: unknown }).code === \"ENOENT\";\n}\n\ninterface WalkBudgetV1 {\n\ttotalBytes: number;\n}\n\nasync function collectDirectory(\n\troot: string,\n\trelative: string,\n\tlimits: Required<NodeZipPackageLimitsV1>,\n\tentries: NodeZipPackageEntryV1[],\n\tbudget: WalkBudgetV1,\n\tsignal: AbortSignal | undefined,\n): Promise<void> {\n\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\tconst dirents = await readdir(relative.length === 0 ? root : join(root, ...relative.split(\"/\")), {\n\t\twithFileTypes: true,\n\t});\n\tdirents.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));\n\tfor (const dirent of dirents) {\n\t\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\tconst childRelative = relative.length === 0 ? dirent.name : `${relative}/${dirent.name}`;\n\t\t// Shared archive entry-name rules reject `..`, absolute segments, drive\n\t\t// letters, Windows reserved names, and trailing dots/spaces up front.\n\t\tnormalizedEntryName(childRelative);\n\t\tif (dirent.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tconst childPath = join(root, ...childRelative.split(\"/\"));\n\t\tconst stats = await lstat(childPath);\n\t\tif (stats.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (stats.isDirectory()) {\n\t\t\tawait collectDirectory(root, childRelative, limits, entries, budget, signal);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!stats.isFile()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (entries.length + 1 > limits.maxEntries) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tconst bytes = await readFile(childPath);\n\t\tif (bytes.byteLength > limits.maxEntryBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tbudget.totalBytes += bytes.byteLength;\n\t\tif (budget.totalBytes > limits.maxExtractedBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tentries.push(\n\t\t\tObject.freeze({\n\t\t\t\tname: childRelative,\n\t\t\t\tdirectory: false,\n\t\t\t\tbytes: new Uint8Array(bytes),\n\t\t\t}),\n\t\t);\n\t}\n}\n\n/**\n * Walk a local directory into normalized package entries. Deterministic\n * (code-unit sorted per level), symlink-free, and budget-bound against the\n * shared package limits.\n */\nexport async function collectLocalDirectoryPackageEntriesV1(\n\trootDirectory: string,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n\tsignal?: AbortSignal,\n): Promise<readonly NodeZipPackageEntryV1[]> {\n\tif (typeof rootDirectory !== \"string\" || rootDirectory.trim().length === 0)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst stats = await lstat(rootDirectory).catch((error: unknown) => {\n\t\tthrow isMissingError(error) ? new NodePackageInstallErrorV1(\"archive_invalid\") : error;\n\t});\n\tif (!stats.isDirectory() || stats.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tconst entries: NodeZipPackageEntryV1[] = [];\n\tconst budget: WalkBudgetV1 = { totalBytes: 0 };\n\tawait collectDirectory(rootDirectory, \"\", limits, entries, budget, signal);\n\treturn Object.freeze(entries);\n}\n\n/** Read a local `.zip`/`.tgz` package file bounded by the shared archive budget. */\nexport async function readLocalPackageFileV1(\n\tpath: string,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n\tsignal?: AbortSignal,\n): Promise<Uint8Array> {\n\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\tif (typeof path !== \"string\" || path.trim().length === 0) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst stats = await lstat(path).catch((error: unknown) => {\n\t\tthrow isMissingError(error) ? new NodePackageInstallErrorV1(\"archive_invalid\") : error;\n\t});\n\tif (stats.isSymbolicLink() || !stats.isFile()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tif (stats.size > limits.maxArchiveBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\tconst bytes = await readFile(path);\n\tif (bytes.byteLength !== stats.size || bytes.byteLength > limits.maxArchiveBytes)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\treturn new Uint8Array(bytes);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"node-local-package-source.js","sourceRoot":"","sources":["../../../src/plugins/ecosystem/node-local-package-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n\tNodePackageInstallErrorV1,\n\ttype NodeZipPackageEntryV1,\n\ttype NodeZipPackageLimitsV1,\n\tnormalizedEntryName,\n\tresolveNodePackageLimitsV1,\n} from \"./node-package-install-host.ts\";\n\n/**\n * Local plugin package sources (design §4.6 \"本地\" row). A directory is walked\n * with the same budgets and entry-name rules as archive extraction (bounded\n * entries/bytes, no symlinks, shared name normalization), then packed by the\n * deterministic tar writer into the standard tgz package shape. \"Copy into\n * the plugin directory\" is realized by the install host writing extracted\n * entries into its own store; the original path is never referenced after\n * acquisition. A local `.zip`/`.tgz` file is read with the archive budget.\n *\n * The walk aligns with npm pack semantics: when package.json carries a `files`\n * array, only whitelisted subtrees plus npm's implicit keeps (package.json,\n * README / LICENSE / LICENCE prefix files at the root) are collected;\n * `node_modules` and `.git` are never collected, and glob `files` entries are\n * rejected for directory installs (pack a `.tgz` for glob support).\n */\n\nfunction isMissingError(error: unknown): boolean {\n\tif (typeof error !== \"object\" || error === null) return false;\n\treturn \"code\" in error && (error as { readonly code?: unknown }).code === \"ENOENT\";\n}\n\ninterface WalkBudgetV1 {\n\ttotalBytes: number;\n}\n\n/** Directory names npm pack never collects, at any depth. */\nconst ALWAYS_EXCLUDED_DIRECTORY_NAMES_V1: ReadonlySet<string> = new Set([\"node_modules\", \".git\"]);\n\nconst GLOB_METACHARACTERS_V1 = /[*?[]/;\n\nfunction globFilesEntryErrorV1(entry: string): NodePackageInstallErrorV1 {\n\tconst error = new NodePackageInstallErrorV1(\"archive_invalid\");\n\terror.message = `Local directory install: package.json \"files\" entry \"${entry}\" uses glob syntax, which is not supported when installing from a directory; pack a .tgz instead`;\n\treturn error;\n}\n\n/**\n * Build the install error for a package.json read or JSON.parse failure. The\n * original error stays visible as `cause` and in the message, so an EACCES or\n * EIO is never silently degraded into \"no files whitelist\" collect-all.\n */\nfunction packageJsonAccessErrorV1(action: \"read\" | \"parse\", error: unknown): NodePackageInstallErrorV1 {\n\tconst detail = error instanceof Error ? error.message : String(error);\n\tconst installError = new NodePackageInstallErrorV1(\"archive_invalid\");\n\tinstallError.message = `Local directory install: failed to ${action} package.json: ${detail}`;\n\tinstallError.cause = error;\n\treturn installError;\n}\n\n/**\n * Read the package.json `files` whitelist. Returns `null` — keeping the\n * historic collect-all behavior — when package.json is absent (a legal\n * directory shape) or `files` is absent or not an array. A missing file is the\n * only tolerated error: any other read failure (permissions, I/O, locks) and a\n * malformed JSON body fail the acquisition instead of silently packing the\n * whole directory, matching npm pack. A returned array (including an empty\n * one) enables whitelist filtering.\n */\nasync function readPackageFilesWhitelistV1(rootDirectory: string): Promise<readonly string[] | null> {\n\tlet raw: string;\n\ttry {\n\t\traw = await readFile(join(rootDirectory, \"package.json\"), \"utf8\");\n\t} catch (error) {\n\t\tif (isMissingError(error)) return null;\n\t\tthrow packageJsonAccessErrorV1(\"read\", error);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (error) {\n\t\tthrow packageJsonAccessErrorV1(\"parse\", error);\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null) return null;\n\tconst files = (parsed as { readonly files?: unknown }).files;\n\tif (!Array.isArray(files)) return null;\n\tconst patterns: string[] = [];\n\tfor (const value of files) {\n\t\tif (typeof value !== \"string\") {\n\t\t\tconst error = new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\t\terror.message = 'Local directory install: package.json \"files\" must contain only strings';\n\t\t\tthrow error;\n\t\t}\n\t\t// npm ignores trailing separators; keeping them would break prefix matching.\n\t\tconst pattern = value.replace(/\\/+$/, \"\");\n\t\tif (pattern.length === 0) continue;\n\t\tif (GLOB_METACHARACTERS_V1.test(pattern)) throw globFilesEntryErrorV1(value);\n\t\tpatterns.push(pattern);\n\t}\n\treturn patterns;\n}\n\n/** npm implicit keeps at the package root: package.json plus README/LICENSE/LICENCE prefix names. */\nfunction isImplicitlyKeptPackageFileV1(relativePath: string): boolean {\n\tif (relativePath.includes(\"/\")) return false;\n\tconst name = relativePath.toUpperCase();\n\treturn (\n\t\tname === \"PACKAGE.JSON\" || name.startsWith(\"README\") || name.startsWith(\"LICENSE\") || name.startsWith(\"LICENCE\")\n\t);\n}\n\n/**\n * npm files-whitelist decision for one walk entry (posix separators). A path\n * is collected when a whitelist entry names it exactly or names a directory\n * above it; directories are also walked when some whitelist entry lives below\n * them, so single-file entries deeper in the tree stay reachable. Root-level\n * files additionally survive through npm's implicit keeps.\n */\nfunction isFilesWhitelistMatchV1(relativePath: string, isDirectory: boolean, patterns: readonly string[]): boolean {\n\tif (patterns.some((pattern) => relativePath === pattern || relativePath.startsWith(`${pattern}/`))) return true;\n\tif (isDirectory) return patterns.some((pattern) => pattern.startsWith(`${relativePath}/`));\n\treturn isImplicitlyKeptPackageFileV1(relativePath);\n}\n\nasync function collectDirectory(\n\troot: string,\n\trelative: string,\n\tlimits: Required<NodeZipPackageLimitsV1>,\n\tentries: NodeZipPackageEntryV1[],\n\tbudget: WalkBudgetV1,\n\tfilesWhitelist: readonly string[] | null,\n\tsignal: AbortSignal | undefined,\n): Promise<void> {\n\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\tconst dirents = await readdir(relative.length === 0 ? root : join(root, ...relative.split(\"/\")), {\n\t\twithFileTypes: true,\n\t});\n\tdirents.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));\n\tfor (const dirent of dirents) {\n\t\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\tconst childRelative = relative.length === 0 ? dirent.name : `${relative}/${dirent.name}`;\n\t\t// npm pack semantics first, before any budgeted work below: dependency\n\t\t// trees and VCS metadata are never packaged (the install store\n\t\t// re-materializes dependencies at staging), then the package.json\n\t\t// `files` whitelist.\n\t\tconst isDirectory = dirent.isDirectory();\n\t\tif (isDirectory && ALWAYS_EXCLUDED_DIRECTORY_NAMES_V1.has(dirent.name)) continue;\n\t\tif (filesWhitelist !== null && !isFilesWhitelistMatchV1(childRelative, isDirectory, filesWhitelist)) continue;\n\t\t// Shared archive entry-name rules reject `..`, absolute segments, drive\n\t\t// letters, Windows reserved names, and trailing dots/spaces up front.\n\t\tnormalizedEntryName(childRelative);\n\t\tif (dirent.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tconst childPath = join(root, ...childRelative.split(\"/\"));\n\t\tconst stats = await lstat(childPath);\n\t\tif (stats.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (stats.isDirectory()) {\n\t\t\tawait collectDirectory(root, childRelative, limits, entries, budget, filesWhitelist, signal);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!stats.isFile()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (entries.length + 1 > limits.maxEntries) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tconst bytes = await readFile(childPath);\n\t\tif (bytes.byteLength > limits.maxEntryBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tbudget.totalBytes += bytes.byteLength;\n\t\tif (budget.totalBytes > limits.maxExtractedBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tentries.push(\n\t\t\tObject.freeze({\n\t\t\t\tname: childRelative,\n\t\t\t\tdirectory: false,\n\t\t\t\tbytes: new Uint8Array(bytes),\n\t\t\t}),\n\t\t);\n\t}\n}\n\n/**\n * Walk a local directory into normalized package entries. Deterministic\n * (code-unit sorted per level), symlink-free, and budget-bound against the\n * shared package limits. A package.json `files` whitelist is honored with npm\n * pack semantics (see above); without one, every eligible file is collected.\n */\nexport async function collectLocalDirectoryPackageEntriesV1(\n\trootDirectory: string,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n\tsignal?: AbortSignal,\n): Promise<readonly NodeZipPackageEntryV1[]> {\n\tif (typeof rootDirectory !== \"string\" || rootDirectory.trim().length === 0)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst stats = await lstat(rootDirectory).catch((error: unknown) => {\n\t\tthrow isMissingError(error) ? new NodePackageInstallErrorV1(\"archive_invalid\") : error;\n\t});\n\tif (!stats.isDirectory() || stats.isSymbolicLink()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tconst filesWhitelist = await readPackageFilesWhitelistV1(rootDirectory);\n\tconst entries: NodeZipPackageEntryV1[] = [];\n\tconst budget: WalkBudgetV1 = { totalBytes: 0 };\n\tawait collectDirectory(rootDirectory, \"\", limits, entries, budget, filesWhitelist, signal);\n\treturn Object.freeze(entries);\n}\n\n/** Read a local `.zip`/`.tgz` package file bounded by the shared archive budget. */\nexport async function readLocalPackageFileV1(\n\tpath: string,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n\tsignal?: AbortSignal,\n): Promise<Uint8Array> {\n\tif (signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\tif (typeof path !== \"string\" || path.trim().length === 0) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst stats = await lstat(path).catch((error: unknown) => {\n\t\tthrow isMissingError(error) ? new NodePackageInstallErrorV1(\"archive_invalid\") : error;\n\t});\n\tif (stats.isSymbolicLink() || !stats.isFile()) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tif (stats.size > limits.maxArchiveBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\tconst bytes = await readFile(path);\n\tif (bytes.byteLength !== stats.size || bytes.byteLength > limits.maxArchiveBytes)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\treturn new Uint8Array(bytes);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-package-install-host.d.ts","sourceRoot":"","sources":["../../../src/plugins/ecosystem/node-package-install-host.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { mkdir, mkdtemp, rename, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { isAbsolute, join } from \"node:path\";\nimport { inflateRawSync } from \"node:zlib\";\nimport { parseCapabilityPluginManifest } from \"../../core/extensions/capability-plugin.ts\";\nimport { t } from \"../../core/i18n/index.ts\";\nimport type { PluginDistributionArtifactV1 } from \"./distribution.ts\";\nimport type { PluginInstallationScopeV1, PluginInstallationV1, PluginInstallHostV1 } from \"./installer.ts\";\nimport {\n\tcreateNodePackageInstallPersistenceV1,\n\ttype NodePackageInstallPersistenceV1,\n\ttype NodePackageInstallPersistentRecordV1,\n} from \"./node-package-install-persistence.ts\";\nimport { parseNodeTarGzPackageV1 } from \"./node-tar-package.ts\";\nimport type { PluginManifestV1, PluginUpdateRecordV1 } from \"./types.ts\";\nimport { pluginManifestDigestV1 } from \"./updates.ts\";\n\nconst NODE_PACKAGE_INSTALL_HOST_VERSION_V1 = 1 as const;\nexport const NODE_PACKAGE_FORMAT_VERSION_V1 = 1 as const;\nconst DEFAULT_MAX_ARCHIVE_BYTES_V1 = 128 * 1024 * 1024;\nconst MAX_ARCHIVE_BYTES_V1 = 512 * 1024 * 1024;\nconst DEFAULT_MAX_EXTRACTED_BYTES_V1 = 256 * 1024 * 1024;\nconst MAX_EXTRACTED_BYTES_V1 = 1024 * 1024 * 1024;\nconst DEFAULT_MAX_ENTRY_BYTES_V1 = 64 * 1024 * 1024;\nconst MAX_ENTRY_BYTES_V1 = 256 * 1024 * 1024;\nconst DEFAULT_MAX_ENTRIES_V1 = 2048;\nconst MAX_ENTRIES_V1 = 8192;\nconst MAX_PACKAGE_MANIFEST_BYTES_V1 = 64 * 1024;\nconst ECOSYSTEM_MANIFEST_FILE_V1 = \"agent-forge.json\";\nconst CAPABILITY_MANIFEST_FILE_V1 = \"plugin.json\";\n/**\n * Host-supplied runtime dependency specifiers: at plugin load time these\n * resolve to the host's own copies through jiti virtual modules (supply\n * contract in core/extensions/virtual-modules.ts, mirrored by\n * HOST_SUPPLIED_PLUGIN_SPECIFIERS in scripts/check-plugin-boundary.mjs), so\n * they are never materialized into a store copy. Both the workspace scope\n * (source/checkout packages) and the published-name scope of the same\n * packages (scripts/release-packages.mjs name map) are covered; a future\n * plugin-to-plugin runtime dependency must therefore not use these scopes.\n */\nconst HOST_SUPPLIED_DEPENDENCY_PREFIXES_V1: readonly string[] = [\"@agent-forge/\", \"@ai-agent-forge/\"];\nconst HOST_SUPPLIED_DEPENDENCIES_V1: ReadonlySet<string> = new Set([\"typebox\"]);\n\nconst LOCAL_FILE_HEADER_V1 = 0x04034b50;\nconst CENTRAL_FILE_HEADER_V1 = 0x02014b50;\nconst END_OF_CENTRAL_DIRECTORY_V1 = 0x06054b50;\nconst ZIP64_END_OF_CENTRAL_DIRECTORY_V1 = 0x06064b50;\nconst ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_V1 = 0x07064b50;\n\nexport type NodePackageInstallErrorCodeV1 =\n\t| \"cancelled\"\n\t| \"host_closed\"\n\t| \"invalid_artifact\"\n\t| \"unsupported_media_type\"\n\t| \"archive_invalid\"\n\t| \"archive_budget_exceeded\"\n\t| \"package_manifest_missing\"\n\t| \"package_manifest_invalid\"\n\t| \"package_manifest_mismatch\"\n\t| \"dependency_install_failed\"\n\t| \"installation_unknown\"\n\t| \"installation_identity_mismatch\"\n\t| \"installation_io_failed\";\n\nexport class NodePackageInstallErrorV1 extends Error {\n\treadonly code: NodePackageInstallErrorCodeV1;\n\n\tconstructor(code: NodePackageInstallErrorCodeV1) {\n\t\tsuper(t(\"Node package installation failed: {code}\", { code }));\n\t\tthis.name = \"NodePackageInstallErrorV1\";\n\t\tthis.code = code;\n\t}\n}\n\nexport interface NodeZipPackageEntryV1 {\n\treadonly name: string;\n\treadonly directory: boolean;\n\treadonly bytes: Uint8Array;\n}\n\nexport interface NodeZipPackageLimitsV1 {\n\treadonly maxArchiveBytes?: number;\n\treadonly maxExtractedBytes?: number;\n\treadonly maxEntryBytes?: number;\n\treadonly maxEntries?: number;\n}\n\nexport interface NodePackageInstallHostOptionsV1 extends NodeZipPackageLimitsV1 {\n\t/** An opaque artifact reader owned by the distribution host. */\n\treadonly openArtifact: (input: {\n\t\treadonly artifact: PluginDistributionArtifactV1;\n\t\treadonly signal?: AbortSignal;\n\t}) => AsyncIterable<Uint8Array>;\n\t/** Parent directory owned by the host; no local path leaves this adapter. */\n\treadonly installationDirectory?: string;\n\treadonly supportedScopes?: readonly PluginInstallationScopeV1[];\n\t/** Persistent host-owned index; omitted keeps the test-friendly ephemeral mode. */\n\treadonly persistence?: \"ephemeral\" | \"persistent\";\n\treadonly persistenceLockTimeoutMs?: number;\n\t/**\n\t * Host-only runtime assembly seam. The directory and manifest path never\n\t * cross the public installer DTO or model/tool context.\n\t */\n\treadonly loadPlugin?: (input: {\n\t\treadonly installation: PluginInstallationV1;\n\t\treadonly record?: PluginUpdateRecordV1;\n\t\treadonly directory: string;\n\t\treadonly manifestPath: string;\n\t\treadonly signal: AbortSignal;\n\t}) => Promise<{ readonly dispose?: () => void | Promise<void> } | void>;\n\t/**\n\t * Omitted keeps the production behavior: declared non-host-supplied\n\t * runtime dependencies are materialized into the staging copy before the\n\t * atomic rename. Deterministic offline fixture chains\n\t * (test/*-package-fixtures) pass \"skip\" so installing real first-party\n\t * packages through the acquisition chain stays registry-free.\n\t */\n\treadonly dependencyMaterialization?: \"skip\";\n}\n\n/** Minimal structural bridge for an AgentSession-owned capability loader. */\nexport interface NodePackageInstallCapabilityPluginSessionV1 {\n\treadonly loadCapabilityPluginManifest: (manifestPath: string) => Promise<{\n\t\treadonly dispose: () => void | Promise<void>;\n\t}>;\n}\n\n/**\n * Adapt an AgentSession-like loader to the host-only runtime seam. The\n * manifest path is never returned as a DTO or sent through model context.\n */\nexport function createNodePackageInstallSessionLoaderV1(\n\tsession: NodePackageInstallCapabilityPluginSessionV1,\n): NonNullable<NodePackageInstallHostOptionsV1[\"loadPlugin\"]> {\n\treturn async ({ manifestPath }) => {\n\t\tconst handle = await session.loadCapabilityPluginManifest(manifestPath);\n\t\treturn { dispose: handle.dispose };\n\t};\n}\n\n/**\n * Create a package host with an explicit AgentSession-compatible runtime\n * loader. This is an opt-in composition helper; it does not alter Core's\n * default session construction or install untrusted package code.\n */\nexport async function createNodePackageInstallHostForSessionV1(\n\toptions: NodePackageInstallHostOptionsV1,\n\tsession: NodePackageInstallCapabilityPluginSessionV1,\n): Promise<NodePackageInstallHostV1> {\n\tif (options.loadPlugin !== undefined) throw new Error(t(\"loadPlugin must be omitted when binding a session\"));\n\treturn await createNodePackageInstallHostV1({\n\t\t...options,\n\t\tloadPlugin: createNodePackageInstallSessionLoaderV1(session),\n\t});\n}\n\nexport interface NodePackageInstallHostV1 extends PluginInstallHostV1 {\n\treadonly version: typeof NODE_PACKAGE_INSTALL_HOST_VERSION_V1;\n\treadonly uninstall: NonNullable<PluginInstallHostV1[\"uninstall\"]>;\n\t/** Host-only bridge for local runtime assembly; never put this path in a DTO or transcript. */\n\treadonly getInstallationDirectory: (input: { readonly installation: PluginInstallationV1 }) => string;\n\treadonly close: () => Promise<void>;\n}\n\ninterface ZipEntryDescriptorV1 {\n\treadonly name: string;\n\treadonly directory: boolean;\n\treadonly flags: number;\n\treadonly method: number;\n\treadonly compressedSize: number;\n\treadonly uncompressedSize: number;\n\treadonly crc32: number;\n\treadonly localOffset: number;\n}\n\ninterface ZipLocalSpanV1 {\n\treadonly start: number;\n\treadonly end: number;\n}\n\ninterface InstalledRecordV1 {\n\treadonly installationRef: string;\n\treadonly directory: string;\n\treadonly pluginId: string;\n\treadonly pluginVersion: string;\n\treadonly manifestDigest: string;\n\treadonly scope: PluginInstallationScopeV1;\n\treadonly scopeRef: string;\n\tstate: \"installed\" | \"active\" | \"retained\";\n}\n\nfunction positiveLimit(value: number | undefined, fallback: number, maximum: number, label: string): number {\n\tconst result = value ?? fallback;\n\tif (!Number.isSafeInteger(result) || result < 1 || result > maximum)\n\t\tthrow new Error(t(\"{label} must be a positive integer at most {maximum}\", { label, maximum }));\n\treturn result;\n}\n\n/** Resolve the archive budget limits shared by the ZIP and tgz package parsers. */\nexport function resolveNodePackageLimitsV1(limitsInput: NodeZipPackageLimitsV1): Required<NodeZipPackageLimitsV1> {\n\treturn {\n\t\tmaxArchiveBytes: positiveLimit(\n\t\t\tlimitsInput.maxArchiveBytes,\n\t\t\tDEFAULT_MAX_ARCHIVE_BYTES_V1,\n\t\t\tMAX_ARCHIVE_BYTES_V1,\n\t\t\t\"maxArchiveBytes\",\n\t\t),\n\t\tmaxExtractedBytes: positiveLimit(\n\t\t\tlimitsInput.maxExtractedBytes,\n\t\t\tDEFAULT_MAX_EXTRACTED_BYTES_V1,\n\t\t\tMAX_EXTRACTED_BYTES_V1,\n\t\t\t\"maxExtractedBytes\",\n\t\t),\n\t\tmaxEntryBytes: positiveLimit(\n\t\t\tlimitsInput.maxEntryBytes,\n\t\t\tDEFAULT_MAX_ENTRY_BYTES_V1,\n\t\t\tMAX_ENTRY_BYTES_V1,\n\t\t\t\"maxEntryBytes\",\n\t\t),\n\t\tmaxEntries: positiveLimit(limitsInput.maxEntries, DEFAULT_MAX_ENTRIES_V1, MAX_ENTRIES_V1, \"maxEntries\"),\n\t};\n}\n\nfunction readU16(bytes: Uint8Array, offset: number): number {\n\tif (offset < 0 || offset + 2 > bytes.byteLength) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\treturn bytes[offset]! | (bytes[offset + 1]! << 8);\n}\n\nfunction readU32(bytes: Uint8Array, offset: number): number {\n\tif (offset < 0 || offset + 4 > bytes.byteLength) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\treturn (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;\n}\n\nfunction decodeName(bytes: Uint8Array, utf8: boolean): string {\n\ttry {\n\t\tconst value = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n\t\tif (!utf8 && /[^\\x00-\\x7f]/u.test(value)) throw new Error(t(\"non-ascii zip name\"));\n\t\treturn value;\n\t} catch {\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t}\n}\n\n/** Normalized archive entry name shared by the ZIP and tgz package parsers. */\nexport function normalizedEntryName(value: string): { readonly name: string; readonly directory: boolean } {\n\tif (value.length === 0 || value.includes(\"\\\\\") || /[\\u0000-\\u001f\\u007f]/u.test(value))\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst directory = value.endsWith(\"/\");\n\tif (value.startsWith(\"/\") || /^[a-z]:/iu.test(value)) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst parts = value.split(\"/\");\n\tif (directory) parts.pop();\n\tif (\n\t\tparts.length === 0 ||\n\t\tparts.some(\n\t\t\t(part) =>\n\t\t\t\tpart === \"\" ||\n\t\t\t\tpart === \".\" ||\n\t\t\t\tpart === \"..\" ||\n\t\t\t\tpart.includes(\":\") ||\n\t\t\t\t/[ .]$/u.test(part) ||\n\t\t\t\t/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\..*)?$/iu.test(part),\n\t\t)\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\treturn { name: parts.join(\"/\"), directory };\n}\n\nfunction crcTable(): Uint32Array {\n\tconst table = new Uint32Array(256);\n\tfor (let index = 0; index < table.length; index += 1) {\n\t\tlet value = index;\n\t\tfor (let bit = 0; bit < 8; bit += 1) value = (value & 1) === 0 ? value >>> 1 : (value >>> 1) ^ 0xedb88320;\n\t\ttable[index] = value >>> 0;\n\t}\n\treturn table;\n}\n\nconst CRC_TABLE_V1 = crcTable();\n\nfunction crc32(bytes: Uint8Array): number {\n\tlet value = 0xffffffff;\n\tfor (const byte of bytes) value = (value >>> 8) ^ CRC_TABLE_V1[(value ^ byte) & 0xff]!;\n\treturn (value ^ 0xffffffff) >>> 0;\n}\n\nfunction findEndOfCentralDirectory(bytes: Uint8Array): {\n\treadonly offset: number;\n\treadonly entries: number;\n\treadonly size: number;\n\treadonly start: number;\n} {\n\tconst minimum = Math.max(0, bytes.byteLength - 0xffff - 22);\n\tfor (let offset = bytes.byteLength - 22; offset >= minimum; offset -= 1) {\n\t\tif (readU32(bytes, offset) !== END_OF_CENTRAL_DIRECTORY_V1) continue;\n\t\tconst disk = readU16(bytes, offset + 4);\n\t\tconst centralDisk = readU16(bytes, offset + 6);\n\t\tconst entries = readU16(bytes, offset + 10);\n\t\tconst centralEntries = readU16(bytes, offset + 8);\n\t\tconst size = readU32(bytes, offset + 12);\n\t\tconst start = readU32(bytes, offset + 16);\n\t\tconst commentLength = readU16(bytes, offset + 20);\n\t\tif (\n\t\t\tdisk !== 0 ||\n\t\t\tcentralDisk !== 0 ||\n\t\t\tentries !== centralEntries ||\n\t\t\toffset + 22 + commentLength !== bytes.byteLength\n\t\t)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (entries === 0xffff || size === 0xffffffff || start === 0xffffffff)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (start + size > offset) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\treturn { offset, entries, size, start };\n\t}\n\tfor (let offset = Math.max(0, bytes.byteLength - 64); offset < bytes.byteLength - 3; offset += 1) {\n\t\tconst signature = readU32(bytes, offset);\n\t\tif (signature === ZIP64_END_OF_CENTRAL_DIRECTORY_V1 || signature === ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_V1)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t}\n\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n}\n\nfunction parseCentralDirectory(\n\tbytes: Uint8Array,\n\tlimits: Required<NodeZipPackageLimitsV1>,\n): readonly ZipEntryDescriptorV1[] {\n\tconst end = findEndOfCentralDirectory(bytes);\n\tif (end.entries > limits.maxEntries || end.size > bytes.byteLength || end.start + end.size > end.offset)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\tconst entries: ZipEntryDescriptorV1[] = [];\n\tconst names = new Set<string>();\n\tlet offset = end.start;\n\tfor (let index = 0; index < end.entries; index += 1) {\n\t\tif (readU32(bytes, offset) !== CENTRAL_FILE_HEADER_V1) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tconst madeBy = readU16(bytes, offset + 4) >>> 8;\n\t\tconst flags = readU16(bytes, offset + 8);\n\t\tconst method = readU16(bytes, offset + 10);\n\t\tconst crc = readU32(bytes, offset + 16);\n\t\tconst compressedSize = readU32(bytes, offset + 20);\n\t\tconst uncompressedSize = readU32(bytes, offset + 24);\n\t\tconst nameLength = readU16(bytes, offset + 28);\n\t\tconst extraLength = readU16(bytes, offset + 30);\n\t\tconst commentLength = readU16(bytes, offset + 32);\n\t\tconst disk = readU16(bytes, offset + 34);\n\t\tconst externalAttributes = readU32(bytes, offset + 38);\n\t\tconst localOffset = readU32(bytes, offset + 42);\n\t\tconst nameStart = offset + 46;\n\t\tconst recordEnd = nameStart + nameLength + extraLength + commentLength;\n\t\tif (\n\t\t\trecordEnd > end.start + end.size ||\n\t\t\tdisk !== 0 ||\n\t\t\t(flags & ~(0x8 | 0x800)) !== 0 ||\n\t\t\t(flags & 1) !== 0 ||\n\t\t\t(flags & 0x40) !== 0\n\t\t)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (method !== 0 && method !== 8) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (uncompressedSize > limits.maxEntryBytes || compressedSize > limits.maxArchiveBytes)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tconst name = normalizedEntryName(\n\t\t\tdecodeName(bytes.subarray(nameStart, nameStart + nameLength), (flags & 0x800) !== 0),\n\t\t);\n\t\tif (names.has(name.name)) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tnames.add(name.name);\n\t\tconst unixMode = madeBy === 3 ? (externalAttributes >>> 16) & 0xffff : 0;\n\t\tif ((unixMode & 0xf000) === 0xa000) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tentries.push({\n\t\t\tname: name.name,\n\t\t\tdirectory: name.directory,\n\t\t\tflags,\n\t\t\tmethod,\n\t\t\tcompressedSize,\n\t\t\tuncompressedSize,\n\t\t\tcrc32: crc,\n\t\t\tlocalOffset,\n\t\t});\n\t\toffset = recordEnd;\n\t}\n\tif (offset !== end.start + end.size) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t// Validate the local-file regions before extraction. A forged central\n\t// directory must not be able to point into another entry or into the\n\t// central directory/EOCD; otherwise extraction could reinterpret metadata\n\t// bytes as package contents.\n\tconst spans: ZipLocalSpanV1[] = [];\n\tfor (const entry of entries) {\n\t\tif (entry.localOffset < 0 || entry.localOffset + 30 > end.start)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (readU32(bytes, entry.localOffset) !== LOCAL_FILE_HEADER_V1)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tconst localNameLength = readU16(bytes, entry.localOffset + 26);\n\t\tconst localExtraLength = readU16(bytes, entry.localOffset + 28);\n\t\tconst dataStart = entry.localOffset + 30 + localNameLength + localExtraLength;\n\t\tconst dataEnd = dataStart + entry.compressedSize;\n\t\tif (dataStart < entry.localOffset + 30 || dataEnd < dataStart || dataEnd > end.start)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tspans.push({ start: entry.localOffset, end: dataEnd });\n\t}\n\tspans.sort((left, right) => left.start - right.start || left.end - right.end);\n\tfor (let index = 1; index < spans.length; index += 1) {\n\t\tif (spans[index - 1]!.end > spans[index]!.start) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t}\n\treturn entries;\n}\n\nfunction extractEntry(bytes: Uint8Array, entry: ZipEntryDescriptorV1, maxEntryBytes: number): Uint8Array {\n\tif (entry.directory) {\n\t\tif (entry.compressedSize !== 0 || entry.uncompressedSize !== 0)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\treturn new Uint8Array();\n\t}\n\tif (entry.localOffset + 30 > bytes.byteLength) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tif (readU32(bytes, entry.localOffset) !== LOCAL_FILE_HEADER_V1)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst localFlags = readU16(bytes, entry.localOffset + 6);\n\tconst localMethod = readU16(bytes, entry.localOffset + 8);\n\tconst nameLength = readU16(bytes, entry.localOffset + 26);\n\tconst extraLength = readU16(bytes, entry.localOffset + 28);\n\tconst localName = decodeName(\n\t\tbytes.subarray(entry.localOffset + 30, entry.localOffset + 30 + nameLength),\n\t\t(entry.flags & 0x800) !== 0,\n\t);\n\tif (normalizedEntryName(localName).name !== entry.name || localFlags !== entry.flags || localMethod !== entry.method)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst localCompressedSize = readU32(bytes, entry.localOffset + 18);\n\tconst localUncompressedSize = readU32(bytes, entry.localOffset + 22);\n\tif (\n\t\t(entry.flags & 8) === 0 &&\n\t\t(localCompressedSize !== entry.compressedSize || localUncompressedSize !== entry.uncompressedSize)\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst dataStart = entry.localOffset + 30 + nameLength + extraLength;\n\tconst dataEnd = dataStart + entry.compressedSize;\n\tif (dataStart > bytes.byteLength || dataEnd > bytes.byteLength)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst compressed = bytes.subarray(dataStart, dataEnd);\n\tlet output: Uint8Array;\n\ttry {\n\t\toutput =\n\t\t\tentry.method === 0\n\t\t\t\t? new Uint8Array(compressed)\n\t\t\t\t: inflateRawSync(compressed, { maxOutputLength: maxEntryBytes });\n\t} catch {\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t}\n\tif (\n\t\toutput.byteLength !== entry.uncompressedSize ||\n\t\toutput.byteLength > maxEntryBytes ||\n\t\tcrc32(output) !== entry.crc32\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\treturn output;\n}\n\nexport function parseNodeZipPackageV1(\n\tinput: Uint8Array,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n): readonly NodeZipPackageEntryV1[] {\n\tif (!(input instanceof Uint8Array)) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tif (input.byteLength > limits.maxArchiveBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\tconst descriptors = parseCentralDirectory(input, limits);\n\tlet extractedBytes = 0;\n\treturn Object.freeze(\n\t\tdescriptors.map((descriptor) => {\n\t\t\tconst bytes = extractEntry(input, descriptor, limits.maxEntryBytes);\n\t\t\textractedBytes += bytes.byteLength;\n\t\t\tif (extractedBytes > limits.maxExtractedBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\t\treturn Object.freeze({ name: descriptor.name, directory: descriptor.directory, bytes });\n\t\t}),\n\t);\n}\n\nfunction manifestKeys(value: Record<string, unknown>): boolean {\n\tconst allowed = new Set([\n\t\t\"packageFormatVersion\",\n\t\t\"id\",\n\t\t\"pluginVersion\",\n\t\t\"apiVersion\",\n\t\t\"minHostVersion\",\n\t\t\"maxHostVersion\",\n\t\t\"capabilityRefs\",\n\t\t\"requires\",\n\t\t\"platform\",\n\t\t\"testOnly\",\n\t]);\n\treturn Object.keys(value).every((key) => allowed.has(key));\n}\n\nfunction packageManifest(value: unknown, record: PluginUpdateRecordV1): void {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\tconst manifest = value as Record<string, unknown>;\n\tif (\n\t\t!manifestKeys(manifest) ||\n\t\tmanifest.packageFormatVersion !== NODE_PACKAGE_FORMAT_VERSION_V1 ||\n\t\tmanifest.id !== record.pluginId ||\n\t\tmanifest.pluginVersion !== record.pluginVersion\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n\tif (typeof manifest.apiVersion !== \"string\" || manifest.apiVersion !== record.manifest.apiVersion)\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n\ttry {\n\t\tif (pluginManifestDigestV1(manifest as unknown as PluginManifestV1) !== record.manifestDigest)\n\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n\t} catch (error) {\n\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t}\n}\n\nfunction capabilityManifest(\n\tvalue: unknown,\n\trecord: PluginUpdateRecordV1,\n\tentries: readonly NodeZipPackageEntryV1[],\n): void {\n\tlet manifest: ReturnType<typeof parseCapabilityPluginManifest>;\n\ttry {\n\t\tmanifest = parseCapabilityPluginManifest(value, \"<installed plugin.json>\");\n\t} catch {\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t}\n\tif (manifest.id !== record.pluginId || manifest.version !== record.pluginVersion)\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n\tconst entryValue = manifest.entry.startsWith(\"./\") ? manifest.entry.slice(2) : manifest.entry;\n\tconst entry = normalizedEntryName(entryValue);\n\tif (entry.directory || !entries.some((candidate) => candidate.name === entry.name && !candidate.directory))\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n}\n\nfunction opaqueScope(value: string): string {\n\tif (\n\t\ttypeof value !== \"string\" ||\n\t\tvalue.trim() === \"\" ||\n\t\tvalue.length > 2048 ||\n\t\t/\\s|[\\u0000-\\u001f\\u007f]/u.test(value)\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\treturn value;\n}\n\nfunction installationKey(scope: PluginInstallationScopeV1, scopeRef: string, pluginId: string): string {\n\treturn `${scope}\\u0000${scopeRef}\\u0000${pluginId}`;\n}\n\nfunction identityMatches(\n\trecord: InstalledRecordV1,\n\tinput: {\n\t\treadonly pluginId: string;\n\t\treadonly pluginVersion: string;\n\t\treadonly manifestDigest: string;\n\t\treadonly scope: PluginInstallationScopeV1;\n\t\treadonly scopeRef: string;\n\t},\n): boolean {\n\treturn (\n\t\trecord.pluginId === input.pluginId &&\n\t\trecord.pluginVersion === input.pluginVersion &&\n\t\trecord.manifestDigest === input.manifestDigest &&\n\t\trecord.scope === input.scope &&\n\t\trecord.scopeRef === input.scopeRef\n\t);\n}\n\nfunction artifactIdentityMatchesRecord(artifact: PluginDistributionArtifactV1, record: PluginUpdateRecordV1): boolean {\n\treturn (\n\t\tartifact.version === 1 &&\n\t\tartifact.pluginId === record.pluginId &&\n\t\tartifact.pluginVersion === record.pluginVersion &&\n\t\tartifact.manifestDigest === record.manifestDigest\n\t);\n}\n\nfunction expectedArtifactDigest(value: string): { readonly algorithm: \"sha256\" | \"sha512\"; readonly hex: string } {\n\tconst prefixed = /^(sha256|sha512):([0-9a-f]+)$/iu.exec(value);\n\tif (prefixed !== null) {\n\t\tconst algorithm = prefixed[1]!.toLowerCase() as \"sha256\" | \"sha512\";\n\t\tconst hex = prefixed[2]!.toLowerCase();\n\t\tif ((algorithm === \"sha256\" && hex.length === 64) || (algorithm === \"sha512\" && hex.length === 128))\n\t\t\treturn { algorithm, hex };\n\t}\n\tif (/^[0-9a-f]{64}$/iu.test(value)) return { algorithm: \"sha256\", hex: value.toLowerCase() };\n\tif (/^[0-9a-f]{128}$/iu.test(value)) return { algorithm: \"sha512\", hex: value.toLowerCase() };\n\tthrow new NodePackageInstallErrorV1(\"invalid_artifact\");\n}\n\nasync function collectArtifact(\n\topenArtifact: NodePackageInstallHostOptionsV1[\"openArtifact\"],\n\tartifact: PluginDistributionArtifactV1,\n\tmaxBytes: number,\n\tsignal: AbortSignal,\n): Promise<Uint8Array> {\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor await (const chunk of openArtifact({ artifact, signal })) {\n\t\tif (signal.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\tif (!(chunk instanceof Uint8Array)) throw new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\tif (chunk.byteLength > maxBytes - total) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tchunks.push(new Uint8Array(chunk));\n\t\ttotal += chunk.byteLength;\n\t}\n\tconst result = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tresult.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\treturn result;\n}\n\nfunction packagePath(root: string, key: string): string {\n\treturn join(root, createHash(\"sha256\").update(key).digest(\"hex\"));\n}\n\nfunction isHostSuppliedDependencyV1(name: string): boolean {\n\treturn (\n\t\tHOST_SUPPLIED_DEPENDENCIES_V1.has(name) ||\n\t\tHOST_SUPPLIED_DEPENDENCY_PREFIXES_V1.some((prefix) => name.startsWith(prefix))\n\t);\n}\n\n/**\n * Install declared runtime dependencies inside the staging directory before\n * the atomic rename (2026-10-07 user ruling): npm tarballs never carry\n * node_modules, so a store copy of a plugin with runtime dependencies\n * (e.g. memory's onnxruntime-node) would otherwise have no dependency tree\n * at load time. A failed dependency install fails the whole installation\n * while staging is still private — no half-installed store copy is ever\n * renamed into place, and the next startup retry heals it.\n */\nasync function installStagingRuntimeDependencies(\n\tstaging: string,\n\tentries: readonly NodeZipPackageEntryV1[],\n\tsignal: AbortSignal,\n): Promise<void> {\n\tconst packageEntry = entries.find((entry) => entry.name === \"package.json\" && !entry.directory);\n\tif (packageEntry === undefined) return;\n\tlet manifest: unknown;\n\ttry {\n\t\tmanifest = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(packageEntry.bytes));\n\t} catch {\n\t\t// Package identity is already fully validated by the two manifests;\n\t\t// a package without a parseable package.json installs unchanged.\n\t\treturn;\n\t}\n\tif (typeof manifest !== \"object\" || manifest === null || Array.isArray(manifest)) return;\n\tconst record = manifest as Record<string, unknown>;\n\tconst dependencies = record.dependencies;\n\tif (typeof dependencies !== \"object\" || dependencies === null || Array.isArray(dependencies)) return;\n\tconst runtime: Record<string, unknown> = Object.fromEntries(\n\t\tObject.entries(dependencies as Record<string, unknown>).filter(([name]) => !isHostSuppliedDependencyV1(name)),\n\t);\n\tif (Object.keys(runtime).length === 0) return;\n\t// Rewrite the staging package.json to the runtime closure. Host-supplied\n\t// specifiers stay out (they resolve through jiti virtual modules and the\n\t// source-form names do not exist on the registry), and devDependencies are\n\t// dropped — npm resolves root devDependency metadata even under --omit=dev,\n\t// so unpublished dev names would fail the whole install with a 404.\n\tawait writeFile(\n\t\tjoin(staging, \"package.json\"),\n\t\tJSON.stringify({ ...record, dependencies: runtime, devDependencies: undefined }),\n\t\t{ flag: \"w\", mode: 0o600 },\n\t);\n\tconst win32 = process.platform === \"win32\";\n\tawait new Promise<void>((resolve, reject) => {\n\t\t// Node >= 18.20 refuses to spawn .cmd shims directly (EINVAL); the\n\t\t// fixed argument list contains no shell metacharacters, so the shell\n\t\t// join is safe (scripts/release.mjs precedent).\n\t\tconst child = spawn(\n\t\t\twin32 ? \"npm.cmd\" : \"npm\",\n\t\t\t[\"install\", \"--omit=dev\", \"--ignore-scripts\", \"--no-audit\", \"--no-fund\", \"--loglevel=error\"],\n\t\t\t// Inherits process.env so registry/proxy configuration follows\n\t\t\t// the host; abort kills the child and surfaces as a rejection.\n\t\t\t{ cwd: staging, signal, shell: win32, stdio: \"ignore\" },\n\t\t);\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"close\", (code) => {\n\t\t\tif (code === 0) {\n\t\t\t\tresolve();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treject(new NodePackageInstallErrorV1(\"dependency_install_failed\"));\n\t\t});\n\t});\n}\n\nasync function writeEntries(\n\troot: string,\n\tentries: readonly NodeZipPackageEntryV1[],\n\tsignal: AbortSignal,\n): Promise<void> {\n\tawait mkdir(root, { recursive: true, mode: 0o700 });\n\tfor (const entry of entries) {\n\t\tif (signal.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\tconst target = join(root, ...entry.name.split(\"/\"));\n\t\tif (entry.directory) {\n\t\t\tawait mkdir(target, { recursive: true, mode: 0o700 });\n\t\t\tcontinue;\n\t\t}\n\t\tconst parent = join(root, ...entry.name.split(\"/\").slice(0, -1));\n\t\tawait mkdir(parent, { recursive: true, mode: 0o700 });\n\t\tawait writeFile(target, entry.bytes, { flag: \"wx\", mode: 0o600 });\n\t}\n}\n\n/**\n * Host-owned ZIP/tgz package installer. It extracts only a bounded,\n * non-encrypted ZIP (\"application/zip\") or gzip-compressed tar\n * (\"application/gzip\") package and never executes package contents. The\n * returned installation handle is opaque; paths stay inside the host-owned\n * instance directory.\n */\nexport async function createNodePackageInstallHostV1(\n\toptions: NodePackageInstallHostOptionsV1,\n): Promise<NodePackageInstallHostV1> {\n\tif (typeof options.openArtifact !== \"function\") throw new Error(t(\"openArtifact is required\"));\n\tconst limits = resolveNodePackageLimitsV1(options);\n\tconst parent = options.installationDirectory ?? tmpdir();\n\tif (!isAbsolute(parent)) throw new Error(t(\"installationDirectory must be absolute\"));\n\tif (options.persistence !== undefined && options.persistence !== \"ephemeral\" && options.persistence !== \"persistent\")\n\t\tthrow new Error(t(\"persistence mode is invalid\"));\n\tconst supportedScopes = options.supportedScopes ?? [\"global\", \"project\"];\n\tif (\n\t\t!Array.isArray(supportedScopes) ||\n\t\tsupportedScopes.length === 0 ||\n\t\tnew Set(supportedScopes).size !== supportedScopes.length ||\n\t\tsupportedScopes.some((scope) => scope !== \"global\" && scope !== \"project\")\n\t)\n\t\tthrow new Error(t(\"supportedScopes is invalid\"));\n\tawait mkdir(parent, { recursive: true, mode: 0o700 });\n\tconst persistent = options.persistence === \"persistent\";\n\tconst instanceRoot = persistent\n\t\t? join(parent, \"agent-forge-plugin-install-store-v1\")\n\t\t: await mkdtemp(join(parent, \"agent-forge-plugin-install-\"));\n\tconst persistence: NodePackageInstallPersistenceV1 | undefined = persistent\n\t\t? await createNodePackageInstallPersistenceV1({\n\t\t\t\trootDirectory: instanceRoot,\n\t\t\t\tlockTimeoutMs: options.persistenceLockTimeoutMs,\n\t\t\t})\n\t\t: undefined;\n\tconst records = new Map<string, InstalledRecordV1>();\n\tconst activeByKey = new Map<string, string>();\n\tif (persistence !== undefined) {\n\t\tfor (const record of await persistence.load()) records.set(record.installationRef, { ...record });\n\t}\n\tfor (const record of records.values()) {\n\t\tif (record.state === \"active\")\n\t\t\tactiveByKey.set(installationKey(record.scope, record.scopeRef, record.pluginId), record.installationRef);\n\t}\n\tlet closed = false;\n\tconst activeOperations = new Set<AbortController>();\n\tconst runtimeHandles = new Map<string, { readonly dispose?: () => void | Promise<void> }>();\n\tconst applyPersistentSnapshot = (snapshot: readonly NodePackageInstallPersistentRecordV1[]): void => {\n\t\trecords.clear();\n\t\tactiveByKey.clear();\n\t\tfor (const value of snapshot) records.set(value.installationRef, { ...value });\n\t\tfor (const value of records.values()) {\n\t\t\tif (value.state === \"active\")\n\t\t\t\tactiveByKey.set(installationKey(value.scope, value.scopeRef, value.pluginId), value.installationRef);\n\t\t}\n\t};\n\tconst commitRecords = async (\n\t\tupdate: (values: Map<string, NodePackageInstallPersistentRecordV1>) => void | Promise<void>,\n\t): Promise<void> => {\n\t\tif (persistence === undefined) {\n\t\t\tawait update(records);\n\t\t\treturn;\n\t\t}\n\t\tapplyPersistentSnapshot(await persistence.mutate({ update }));\n\t};\n\tlet drained: (() => void) | undefined;\n\tlet closePromise: Promise<void> | undefined;\n\n\tconst begin = (\n\t\tparentSignal: AbortSignal | undefined,\n\t): { readonly controller: AbortController; readonly signal: AbortSignal } => {\n\t\tif (closed) throw new NodePackageInstallErrorV1(\"host_closed\");\n\t\tconst controller = new AbortController();\n\t\tif (parentSignal?.aborted) controller.abort(parentSignal.reason);\n\t\telse parentSignal?.addEventListener(\"abort\", () => controller.abort(parentSignal.reason), { once: true });\n\t\tactiveOperations.add(controller);\n\t\treturn { controller, signal: controller.signal };\n\t};\n\tconst end = (controller: AbortController): void => {\n\t\tactiveOperations.delete(controller);\n\t\tif (activeOperations.size === 0) {\n\t\t\tdrained?.();\n\t\t\tdrained = undefined;\n\t\t}\n\t};\n\tconst waitDrained = async (): Promise<void> => {\n\t\tif (activeOperations.size === 0) return;\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tdrained = resolve;\n\t\t});\n\t};\n\tconst getRecord = (\n\t\tinstallation: PluginInstallationV1,\n\t\trecord: PluginUpdateRecordV1,\n\t\tscope: PluginInstallationScopeV1,\n\t\tscopeRef: string,\n\t): InstalledRecordV1 => {\n\t\tconst value = records.get(installation.installationRef);\n\t\tif (\n\t\t\tvalue === undefined ||\n\t\t\tinstallation.pluginId !== record.pluginId ||\n\t\t\tinstallation.pluginVersion !== record.pluginVersion ||\n\t\t\tinstallation.manifestDigest !== record.manifestDigest ||\n\t\t\tinstallation.scope !== scope ||\n\t\t\tinstallation.scopeRef !== scopeRef ||\n\t\t\t!identityMatches(value, {\n\t\t\t\tpluginId: record.pluginId,\n\t\t\t\tpluginVersion: record.pluginVersion,\n\t\t\t\tmanifestDigest: record.manifestDigest,\n\t\t\t\tscope,\n\t\t\t\tscopeRef,\n\t\t\t})\n\t\t)\n\t\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\treturn value;\n\t};\n\tconst disposeRuntime = async (installationRef: string): Promise<void> => {\n\t\tconst handle = runtimeHandles.get(installationRef);\n\t\tif (handle === undefined) return;\n\t\tif (handle.dispose !== undefined) await Promise.resolve(handle.dispose());\n\t\truntimeHandles.delete(installationRef);\n\t};\n\tconst loadRuntime = async (\n\t\tinstallation: PluginInstallationV1,\n\t\trecord: PluginUpdateRecordV1 | undefined,\n\t\tdirectory: string,\n\t\tsignal: AbortSignal,\n\t): Promise<{ readonly dispose?: () => void | Promise<void> } | undefined> => {\n\t\tif (options.loadPlugin === undefined) return undefined;\n\t\ttry {\n\t\t\tconst loaded = await options.loadPlugin({\n\t\t\t\tinstallation,\n\t\t\t\trecord,\n\t\t\t\tdirectory,\n\t\t\t\tmanifestPath: join(directory, CAPABILITY_MANIFEST_FILE_V1),\n\t\t\t\tsignal,\n\t\t\t});\n\t\t\treturn loaded ?? undefined;\n\t\t} catch (error) {\n\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t}\n\t};\n\n\tconst host: NodePackageInstallHostV1 = {\n\t\tversion: NODE_PACKAGE_INSTALL_HOST_VERSION_V1,\n\t\tsupportedScopes: Object.freeze([...supportedScopes]),\n\t\tgetInstallationDirectory(input) {\n\t\t\tconst record = records.get(input.installation.installationRef);\n\t\t\tif (\n\t\t\t\trecord === undefined ||\n\t\t\t\trecord.pluginId !== input.installation.pluginId ||\n\t\t\t\trecord.pluginVersion !== input.installation.pluginVersion ||\n\t\t\t\trecord.manifestDigest !== input.installation.manifestDigest ||\n\t\t\t\trecord.scope !== input.installation.scope ||\n\t\t\t\trecord.scopeRef !== input.installation.scopeRef\n\t\t\t)\n\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_unknown\");\n\t\t\treturn record.directory;\n\t\t},\n\t\tasync install(input) {\n\t\t\tconst operation = begin(input.signal);\n\t\t\tlet staging: string | undefined;\n\t\t\ttry {\n\t\t\t\tif (input.artifact === undefined) throw new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\t\t\tif (!artifactIdentityMatchesRecord(input.artifact, input.record))\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\t\t\tconst mediaType = input.artifact.mediaType.toLowerCase();\n\t\t\t\tif (mediaType !== \"application/zip\" && mediaType !== \"application/gzip\")\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"unsupported_media_type\");\n\t\t\t\tconst scopeRef = opaqueScope(input.scopeRef);\n\t\t\t\tconst bytes = await collectArtifact(\n\t\t\t\t\toptions.openArtifact,\n\t\t\t\t\tinput.artifact,\n\t\t\t\t\tlimits.maxArchiveBytes,\n\t\t\t\t\toperation.signal,\n\t\t\t\t);\n\t\t\t\tif (input.artifact.sizeBytes !== bytes.byteLength) throw new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\t\t\tconst digest = expectedArtifactDigest(input.artifact.artifactDigest);\n\t\t\t\tif (createHash(digest.algorithm).update(bytes).digest(\"hex\") !== digest.hex)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\t\t\tconst entries =\n\t\t\t\t\tmediaType === \"application/gzip\"\n\t\t\t\t\t\t? await parseNodeTarGzPackageV1(bytes, limits)\n\t\t\t\t\t\t: parseNodeZipPackageV1(bytes, limits);\n\t\t\t\tconst manifestEntry = entries.find(\n\t\t\t\t\t(entry) => entry.name === ECOSYSTEM_MANIFEST_FILE_V1 && !entry.directory,\n\t\t\t\t);\n\t\t\t\tif (manifestEntry === undefined) throw new NodePackageInstallErrorV1(\"package_manifest_missing\");\n\t\t\t\tif (manifestEntry.bytes.byteLength > MAX_PACKAGE_MANIFEST_BYTES_V1)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t\t\t\tlet manifest: unknown;\n\t\t\t\ttry {\n\t\t\t\t\tmanifest = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(manifestEntry.bytes));\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t\t\t\t}\n\t\t\t\tpackageManifest(manifest, input.record);\n\t\t\t\tconst capabilityEntry = entries.find(\n\t\t\t\t\t(entry) => entry.name === CAPABILITY_MANIFEST_FILE_V1 && !entry.directory,\n\t\t\t\t);\n\t\t\t\tif (capabilityEntry === undefined) throw new NodePackageInstallErrorV1(\"package_manifest_missing\");\n\t\t\t\tif (capabilityEntry.bytes.byteLength > MAX_PACKAGE_MANIFEST_BYTES_V1)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t\t\t\tlet capability: unknown;\n\t\t\t\ttry {\n\t\t\t\t\tcapability = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(capabilityEntry.bytes));\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t\t\t\t}\n\t\t\t\tcapabilityManifest(capability, input.record, entries);\n\t\t\t\tconst key = installationKey(input.scope, scopeRef, input.record.pluginId);\n\t\t\t\tstaging = join(instanceRoot, `.staging-${randomUUID()}`);\n\t\t\t\tawait writeEntries(staging, entries, operation.signal);\n\t\t\t\t// Materialize declared runtime dependencies while staging is\n\t\t\t\t// still private; a failure throws before the rename, so no\n\t\t\t\t// half-installed copy ever reaches the store.\n\t\t\t\tif (options.dependencyMaterialization !== \"skip\") {\n\t\t\t\t\tawait installStagingRuntimeDependencies(staging, entries, operation.signal);\n\t\t\t\t}\n\t\t\t\tconst installRef = `plugin-install://node-zip/${randomUUID()}`;\n\t\t\t\tconst destination = packagePath(instanceRoot, `${key}\\u0000${installRef}`);\n\t\t\t\tawait rename(staging, destination);\n\t\t\t\tstaging = undefined;\n\t\t\t\tconst installedRecord: InstalledRecordV1 = {\n\t\t\t\t\tinstallationRef: installRef,\n\t\t\t\t\tdirectory: destination,\n\t\t\t\t\tpluginId: input.record.pluginId,\n\t\t\t\t\tpluginVersion: input.record.pluginVersion,\n\t\t\t\t\tmanifestDigest: input.record.manifestDigest,\n\t\t\t\t\tscope: input.scope,\n\t\t\t\t\tscopeRef,\n\t\t\t\t\tstate: \"installed\",\n\t\t\t\t};\n\t\t\t\ttry {\n\t\t\t\t\tawait commitRecords((values) => {\n\t\t\t\t\t\tvalues.set(installRef, installedRecord);\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tawait rm(destination, { recursive: true, force: true }).catch(() => undefined);\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\treturn { installationRef: installRef, scope: input.scope, scopeRef };\n\t\t\t} catch (error) {\n\t\t\t\tif (operation.signal.aborted || input.signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t\t} finally {\n\t\t\t\tif (staging !== undefined) await rm(staging, { recursive: true, force: true }).catch(() => undefined);\n\t\t\t\tend(operation.controller);\n\t\t\t}\n\t\t},\n\t\tasync activate(input) {\n\t\t\tconst operation = begin(input.signal);\n\t\t\tlet candidate: { readonly dispose?: () => void | Promise<void> } | undefined;\n\t\t\ttry {\n\t\t\t\tconst scopeRef = opaqueScope(input.scopeRef);\n\t\t\t\tconst record = getRecord(input.installation, input.record, input.scope, scopeRef);\n\t\t\t\tif (record.state !== \"installed\") throw new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\t\t\tcandidate = await loadRuntime(input.installation, input.record, record.directory, operation.signal);\n\t\t\t\tconst key = installationKey(input.scope, scopeRef, input.record.pluginId);\n\t\t\t\tconst previousRef = activeByKey.get(key);\n\t\t\t\tif (previousRef !== undefined) {\n\t\t\t\t\tconst previous = records.get(previousRef);\n\t\t\t\t\tif (previous !== undefined) {\n\t\t\t\t\t\tawait disposeRuntime(previousRef);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tawait commitRecords((values) => {\n\t\t\t\t\tconst candidateRecord = values.get(input.installation.installationRef);\n\t\t\t\t\tif (candidateRecord === undefined) throw new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\t\t\t\tif (previousRef !== undefined) {\n\t\t\t\t\t\tconst previousRecord = values.get(previousRef);\n\t\t\t\t\t\tif (previousRecord !== undefined) values.set(previousRef, { ...previousRecord, state: \"retained\" });\n\t\t\t\t\t}\n\t\t\t\t\tvalues.set(input.installation.installationRef, { ...candidateRecord, state: \"active\" });\n\t\t\t\t});\n\t\t\t\tactiveByKey.set(key, input.installation.installationRef);\n\t\t\t\tif (candidate !== undefined) runtimeHandles.set(input.installation.installationRef, candidate);\n\t\t\t\tcandidate = undefined;\n\t\t\t} catch (error) {\n\t\t\t\tif (candidate?.dispose !== undefined) await Promise.resolve(candidate.dispose()).catch(() => undefined);\n\t\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t\t} finally {\n\t\t\t\tend(operation.controller);\n\t\t\t}\n\t\t},\n\t\tasync rollback(input) {\n\t\t\tconst operation = begin(input.signal);\n\t\t\tlet candidate: { readonly dispose?: () => void | Promise<void> } | undefined;\n\t\t\ttry {\n\t\t\t\tconst scopeRef = opaqueScope(input.scopeRef);\n\t\t\t\tconst failed = records.get(input.failed.installationRef);\n\t\t\t\tif (\n\t\t\t\t\tfailed === undefined ||\n\t\t\t\t\t!identityMatches(failed, {\n\t\t\t\t\t\tpluginId: input.failed.pluginId,\n\t\t\t\t\t\tpluginVersion: input.failed.pluginVersion,\n\t\t\t\t\t\tmanifestDigest: input.failed.manifestDigest,\n\t\t\t\t\t\tscope: input.scope,\n\t\t\t\t\t\tscopeRef,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\t\t\tconst previous = records.get(input.previous.installationRef);\n\t\t\t\tif (\n\t\t\t\t\tprevious === undefined ||\n\t\t\t\t\t!identityMatches(previous, {\n\t\t\t\t\t\tpluginId: input.previous.pluginId,\n\t\t\t\t\t\tpluginVersion: input.previous.pluginVersion,\n\t\t\t\t\t\tmanifestDigest: input.previous.manifestDigest,\n\t\t\t\t\t\tscope: input.scope,\n\t\t\t\t\t\tscopeRef,\n\t\t\t\t\t}) ||\n\t\t\t\t\t(previous.state !== \"active\" && previous.state !== \"retained\") ||\n\t\t\t\t\tfailed.pluginId !== previous.pluginId\n\t\t\t\t)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\t\t\tconst previousInstallation: PluginInstallationV1 = {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tpluginId: previous.pluginId,\n\t\t\t\t\tpluginVersion: previous.pluginVersion,\n\t\t\t\t\tmanifestDigest: previous.manifestDigest,\n\t\t\t\t\tscope: previous.scope,\n\t\t\t\t\tscopeRef: previous.scopeRef,\n\t\t\t\t\tinstallationRef: input.previous.installationRef,\n\t\t\t\t\tstate: \"active\",\n\t\t\t\t};\n\t\t\t\tcandidate = await loadRuntime(previousInstallation, undefined, previous.directory, operation.signal);\n\t\t\t\tawait disposeRuntime(input.failed.installationRef);\n\t\t\t\tawait rm(failed.directory, { recursive: true, force: true });\n\t\t\t\tawait commitRecords((values) => {\n\t\t\t\t\tvalues.delete(input.failed.installationRef);\n\t\t\t\t\tconst previousRecord = values.get(input.previous.installationRef);\n\t\t\t\t\tif (previousRecord !== undefined)\n\t\t\t\t\t\tvalues.set(input.previous.installationRef, { ...previousRecord, state: \"active\" });\n\t\t\t\t});\n\t\t\t\tactiveByKey.set(installationKey(input.scope, scopeRef, failed.pluginId), input.previous.installationRef);\n\t\t\t\tif (candidate !== undefined) runtimeHandles.set(input.previous.installationRef, candidate);\n\t\t\t\tcandidate = undefined;\n\t\t\t} catch (error) {\n\t\t\t\tif (candidate?.dispose !== undefined) await Promise.resolve(candidate.dispose()).catch(() => undefined);\n\t\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t\t} finally {\n\t\t\t\tend(operation.controller);\n\t\t\t}\n\t\t},\n\t\tasync uninstall(input) {\n\t\t\tconst operation = begin(input.signal);\n\t\t\ttry {\n\t\t\t\tconst scopeRef = opaqueScope(input.scopeRef);\n\t\t\t\tconst record = records.get(input.installation.installationRef);\n\t\t\t\tif (\n\t\t\t\t\trecord === undefined ||\n\t\t\t\t\t!identityMatches(record, {\n\t\t\t\t\t\tpluginId: input.installation.pluginId,\n\t\t\t\t\t\tpluginVersion: input.installation.pluginVersion,\n\t\t\t\t\t\tmanifestDigest: input.installation.manifestDigest,\n\t\t\t\t\t\tscope: input.scope,\n\t\t\t\t\t\tscopeRef,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_unknown\");\n\t\t\t\tawait disposeRuntime(input.installation.installationRef);\n\t\t\t\t// force:false keeps rm strict, but a directory that is already\n\t\t\t\t// gone (ghost record from an interrupted uninstall) is the\n\t\t\t\t// desired end state — continue to the index commit instead of\n\t\t\t\t// bricking every later store operation on ENOENT.\n\t\t\t\tawait rm(record.directory, { recursive: true, force: false }).catch((error: unknown) => {\n\t\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return;\n\t\t\t\t\tthrow error;\n\t\t\t\t});\n\t\t\t\tawait commitRecords((values) => {\n\t\t\t\t\tvalues.delete(input.installation.installationRef);\n\t\t\t\t});\n\t\t\t\tconst key = installationKey(input.scope, scopeRef, record.pluginId);\n\t\t\t\tif (activeByKey.get(key) === input.installation.installationRef) activeByKey.delete(key);\n\t\t\t} finally {\n\t\t\t\tend(operation.controller);\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\tif (closePromise !== undefined) return await closePromise;\n\t\t\tclosed = true;\n\t\t\tfor (const operation of activeOperations) operation.abort();\n\t\t\tclosePromise = (async () => {\n\t\t\t\tawait waitDrained();\n\t\t\t\tlet disposeError = false;\n\t\t\t\tfor (const installationRef of runtimeHandles.keys()) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait disposeRuntime(installationRef);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tdisposeError = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (persistence === undefined) await rm(instanceRoot, { recursive: true, force: true });\n\t\t\t\telse await persistence.close();\n\t\t\t\trecords.clear();\n\t\t\t\tactiveByKey.clear();\n\t\t\t\tif (disposeError) throw new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t\t})();\n\t\t\treturn await closePromise;\n\t\t},\n\t};\n\tif (options.loadPlugin !== undefined) {\n\t\ttry {\n\t\t\tfor (const record of records.values()) {\n\t\t\t\tif (record.state !== \"active\") continue;\n\t\t\t\tconst installation: PluginInstallationV1 = {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tpluginId: record.pluginId,\n\t\t\t\t\tpluginVersion: record.pluginVersion,\n\t\t\t\t\tmanifestDigest: record.manifestDigest,\n\t\t\t\t\tscope: record.scope,\n\t\t\t\t\tscopeRef: record.scopeRef,\n\t\t\t\t\tinstallationRef: record.installationRef,\n\t\t\t\t\tstate: \"active\",\n\t\t\t\t};\n\t\t\t\tconst runtime = await loadRuntime(installation, undefined, record.directory, new AbortController().signal);\n\t\t\t\tif (runtime !== undefined) runtimeHandles.set(record.installationRef, runtime);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tawait host.close().catch(() => undefined);\n\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t}\n\t}\n\treturn host;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"node-package-install-host.d.ts","sourceRoot":"","sources":["../../../src/plugins/ecosystem/node-package-install-host.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { mkdir, mkdtemp, rename, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { isAbsolute, join } from \"node:path\";\nimport { inflateRawSync } from \"node:zlib\";\nimport { parseCapabilityPluginManifest } from \"../../core/extensions/capability-plugin.ts\";\nimport { t } from \"../../core/i18n/index.ts\";\nimport type { PluginDistributionArtifactV1 } from \"./distribution.ts\";\nimport type { PluginInstallationScopeV1, PluginInstallationV1, PluginInstallHostV1 } from \"./installer.ts\";\nimport {\n\tcreateNodePackageInstallPersistenceV1,\n\ttype NodePackageInstallPersistenceV1,\n\ttype NodePackageInstallPersistentRecordV1,\n} from \"./node-package-install-persistence.ts\";\nimport { parseNodeTarGzPackageV1 } from \"./node-tar-package.ts\";\nimport type { PluginManifestV1, PluginUpdateRecordV1 } from \"./types.ts\";\nimport { pluginManifestDigestV1 } from \"./updates.ts\";\n\nconst NODE_PACKAGE_INSTALL_HOST_VERSION_V1 = 1 as const;\nexport const NODE_PACKAGE_FORMAT_VERSION_V1 = 1 as const;\nconst DEFAULT_MAX_ARCHIVE_BYTES_V1 = 128 * 1024 * 1024;\nconst MAX_ARCHIVE_BYTES_V1 = 512 * 1024 * 1024;\nconst DEFAULT_MAX_EXTRACTED_BYTES_V1 = 256 * 1024 * 1024;\nconst MAX_EXTRACTED_BYTES_V1 = 1024 * 1024 * 1024;\nconst DEFAULT_MAX_ENTRY_BYTES_V1 = 64 * 1024 * 1024;\nconst MAX_ENTRY_BYTES_V1 = 256 * 1024 * 1024;\nconst DEFAULT_MAX_ENTRIES_V1 = 2048;\nconst MAX_ENTRIES_V1 = 8192;\nconst MAX_PACKAGE_MANIFEST_BYTES_V1 = 64 * 1024;\nconst ECOSYSTEM_MANIFEST_FILE_V1 = \"agent-forge.json\";\nconst CAPABILITY_MANIFEST_FILE_V1 = \"plugin.json\";\n/**\n * Host-supplied runtime dependency specifiers: at plugin load time these\n * resolve to the host's own copies through jiti virtual modules (supply\n * contract in core/extensions/virtual-modules.ts, mirrored by\n * HOST_SUPPLIED_PLUGIN_SPECIFIERS in scripts/check-plugin-boundary.mjs), so\n * they are never materialized into a store copy. Both the workspace scope\n * (source/checkout packages) and the published-name scope of the same\n * packages (scripts/release-packages.mjs name map) are covered; a future\n * plugin-to-plugin runtime dependency must therefore not use these scopes.\n */\nconst HOST_SUPPLIED_DEPENDENCY_PREFIXES_V1: readonly string[] = [\"@agent-forge/\", \"@ai-agent-forge/\"];\nconst HOST_SUPPLIED_DEPENDENCIES_V1: ReadonlySet<string> = new Set([\"typebox\"]);\n\nconst LOCAL_FILE_HEADER_V1 = 0x04034b50;\nconst CENTRAL_FILE_HEADER_V1 = 0x02014b50;\nconst END_OF_CENTRAL_DIRECTORY_V1 = 0x06054b50;\nconst ZIP64_END_OF_CENTRAL_DIRECTORY_V1 = 0x06064b50;\nconst ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_V1 = 0x07064b50;\n\nexport type NodePackageInstallErrorCodeV1 =\n\t| \"cancelled\"\n\t| \"host_closed\"\n\t| \"invalid_artifact\"\n\t| \"unsupported_media_type\"\n\t| \"archive_invalid\"\n\t| \"archive_budget_exceeded\"\n\t| \"package_manifest_missing\"\n\t| \"package_manifest_invalid\"\n\t| \"package_manifest_mismatch\"\n\t| \"dependency_install_failed\"\n\t| \"installation_unknown\"\n\t| \"installation_identity_mismatch\"\n\t| \"installation_io_failed\";\n\nexport class NodePackageInstallErrorV1 extends Error {\n\treadonly code: NodePackageInstallErrorCodeV1;\n\n\tconstructor(code: NodePackageInstallErrorCodeV1) {\n\t\tsuper(t(\"Node package installation failed: {code}\", { code }));\n\t\tthis.name = \"NodePackageInstallErrorV1\";\n\t\tthis.code = code;\n\t}\n}\n\nexport interface NodeZipPackageEntryV1 {\n\treadonly name: string;\n\treadonly directory: boolean;\n\treadonly bytes: Uint8Array;\n}\n\nexport interface NodeZipPackageLimitsV1 {\n\treadonly maxArchiveBytes?: number;\n\treadonly maxExtractedBytes?: number;\n\treadonly maxEntryBytes?: number;\n\treadonly maxEntries?: number;\n}\n\nexport interface NodePackageInstallHostOptionsV1 extends NodeZipPackageLimitsV1 {\n\t/** An opaque artifact reader owned by the distribution host. */\n\treadonly openArtifact: (input: {\n\t\treadonly artifact: PluginDistributionArtifactV1;\n\t\treadonly signal?: AbortSignal;\n\t}) => AsyncIterable<Uint8Array>;\n\t/** Parent directory owned by the host; no local path leaves this adapter. */\n\treadonly installationDirectory?: string;\n\treadonly supportedScopes?: readonly PluginInstallationScopeV1[];\n\t/** Persistent host-owned index; omitted keeps the test-friendly ephemeral mode. */\n\treadonly persistence?: \"ephemeral\" | \"persistent\";\n\treadonly persistenceLockTimeoutMs?: number;\n\t/**\n\t * Host-only runtime assembly seam. The directory and manifest path never\n\t * cross the public installer DTO or model/tool context.\n\t */\n\treadonly loadPlugin?: (input: {\n\t\treadonly installation: PluginInstallationV1;\n\t\treadonly record?: PluginUpdateRecordV1;\n\t\treadonly directory: string;\n\t\treadonly manifestPath: string;\n\t\treadonly signal: AbortSignal;\n\t}) => Promise<{ readonly dispose?: () => void | Promise<void> } | void>;\n\t/**\n\t * Omitted keeps the production behavior: declared non-host-supplied\n\t * runtime dependencies are materialized into the staging copy before the\n\t * atomic rename. Deterministic offline fixture chains\n\t * (test/*-package-fixtures) pass \"skip\" so installing real first-party\n\t * packages through the acquisition chain stays registry-free.\n\t */\n\treadonly dependencyMaterialization?: \"skip\";\n}\n\n/** Minimal structural bridge for an AgentSession-owned capability loader. */\nexport interface NodePackageInstallCapabilityPluginSessionV1 {\n\treadonly loadCapabilityPluginManifest: (manifestPath: string) => Promise<{\n\t\treadonly dispose: () => void | Promise<void>;\n\t}>;\n}\n\n/**\n * Adapt an AgentSession-like loader to the host-only runtime seam. The\n * manifest path is never returned as a DTO or sent through model context.\n */\nexport function createNodePackageInstallSessionLoaderV1(\n\tsession: NodePackageInstallCapabilityPluginSessionV1,\n): NonNullable<NodePackageInstallHostOptionsV1[\"loadPlugin\"]> {\n\treturn async ({ manifestPath }) => {\n\t\tconst handle = await session.loadCapabilityPluginManifest(manifestPath);\n\t\treturn { dispose: handle.dispose };\n\t};\n}\n\n/**\n * Create a package host with an explicit AgentSession-compatible runtime\n * loader. This is an opt-in composition helper; it does not alter Core's\n * default session construction or install untrusted package code.\n */\nexport async function createNodePackageInstallHostForSessionV1(\n\toptions: NodePackageInstallHostOptionsV1,\n\tsession: NodePackageInstallCapabilityPluginSessionV1,\n): Promise<NodePackageInstallHostV1> {\n\tif (options.loadPlugin !== undefined) throw new Error(t(\"loadPlugin must be omitted when binding a session\"));\n\treturn await createNodePackageInstallHostV1({\n\t\t...options,\n\t\tloadPlugin: createNodePackageInstallSessionLoaderV1(session),\n\t});\n}\n\nexport interface NodePackageInstallHostV1 extends PluginInstallHostV1 {\n\treadonly version: typeof NODE_PACKAGE_INSTALL_HOST_VERSION_V1;\n\treadonly uninstall: NonNullable<PluginInstallHostV1[\"uninstall\"]>;\n\t/** Host-only bridge for local runtime assembly; never put this path in a DTO or transcript. */\n\treadonly getInstallationDirectory: (input: { readonly installation: PluginInstallationV1 }) => string;\n\treadonly close: () => Promise<void>;\n}\n\ninterface ZipEntryDescriptorV1 {\n\treadonly name: string;\n\treadonly directory: boolean;\n\treadonly flags: number;\n\treadonly method: number;\n\treadonly compressedSize: number;\n\treadonly uncompressedSize: number;\n\treadonly crc32: number;\n\treadonly localOffset: number;\n}\n\ninterface ZipLocalSpanV1 {\n\treadonly start: number;\n\treadonly end: number;\n}\n\ninterface InstalledRecordV1 {\n\treadonly installationRef: string;\n\treadonly directory: string;\n\treadonly pluginId: string;\n\treadonly pluginVersion: string;\n\treadonly manifestDigest: string;\n\treadonly scope: PluginInstallationScopeV1;\n\treadonly scopeRef: string;\n\tstate: \"installed\" | \"active\" | \"retained\";\n}\n\nfunction positiveLimit(value: number | undefined, fallback: number, maximum: number, label: string): number {\n\tconst result = value ?? fallback;\n\tif (!Number.isSafeInteger(result) || result < 1 || result > maximum)\n\t\tthrow new Error(t(\"{label} must be a positive integer at most {maximum}\", { label, maximum }));\n\treturn result;\n}\n\n/** Resolve the archive budget limits shared by the ZIP and tgz package parsers. */\nexport function resolveNodePackageLimitsV1(limitsInput: NodeZipPackageLimitsV1): Required<NodeZipPackageLimitsV1> {\n\treturn {\n\t\tmaxArchiveBytes: positiveLimit(\n\t\t\tlimitsInput.maxArchiveBytes,\n\t\t\tDEFAULT_MAX_ARCHIVE_BYTES_V1,\n\t\t\tMAX_ARCHIVE_BYTES_V1,\n\t\t\t\"maxArchiveBytes\",\n\t\t),\n\t\tmaxExtractedBytes: positiveLimit(\n\t\t\tlimitsInput.maxExtractedBytes,\n\t\t\tDEFAULT_MAX_EXTRACTED_BYTES_V1,\n\t\t\tMAX_EXTRACTED_BYTES_V1,\n\t\t\t\"maxExtractedBytes\",\n\t\t),\n\t\tmaxEntryBytes: positiveLimit(\n\t\t\tlimitsInput.maxEntryBytes,\n\t\t\tDEFAULT_MAX_ENTRY_BYTES_V1,\n\t\t\tMAX_ENTRY_BYTES_V1,\n\t\t\t\"maxEntryBytes\",\n\t\t),\n\t\tmaxEntries: positiveLimit(limitsInput.maxEntries, DEFAULT_MAX_ENTRIES_V1, MAX_ENTRIES_V1, \"maxEntries\"),\n\t};\n}\n\nfunction readU16(bytes: Uint8Array, offset: number): number {\n\tif (offset < 0 || offset + 2 > bytes.byteLength) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\treturn bytes[offset]! | (bytes[offset + 1]! << 8);\n}\n\nfunction readU32(bytes: Uint8Array, offset: number): number {\n\tif (offset < 0 || offset + 4 > bytes.byteLength) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\treturn (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;\n}\n\nfunction decodeName(bytes: Uint8Array, utf8: boolean): string {\n\ttry {\n\t\tconst value = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n\t\tif (!utf8 && /[^\\x00-\\x7f]/u.test(value)) throw new Error(t(\"non-ascii zip name\"));\n\t\treturn value;\n\t} catch {\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t}\n}\n\n/** Normalized archive entry name shared by the ZIP and tgz package parsers. */\nexport function normalizedEntryName(value: string): { readonly name: string; readonly directory: boolean } {\n\tif (value.length === 0 || value.includes(\"\\\\\") || /[\\u0000-\\u001f\\u007f]/u.test(value))\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst directory = value.endsWith(\"/\");\n\tif (value.startsWith(\"/\") || /^[a-z]:/iu.test(value)) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst parts = value.split(\"/\");\n\tif (directory) parts.pop();\n\tif (\n\t\tparts.length === 0 ||\n\t\tparts.some(\n\t\t\t(part) =>\n\t\t\t\tpart === \"\" ||\n\t\t\t\tpart === \".\" ||\n\t\t\t\tpart === \"..\" ||\n\t\t\t\tpart.includes(\":\") ||\n\t\t\t\t/[ .]$/u.test(part) ||\n\t\t\t\t/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\..*)?$/iu.test(part),\n\t\t)\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\treturn { name: parts.join(\"/\"), directory };\n}\n\nfunction crcTable(): Uint32Array {\n\tconst table = new Uint32Array(256);\n\tfor (let index = 0; index < table.length; index += 1) {\n\t\tlet value = index;\n\t\tfor (let bit = 0; bit < 8; bit += 1) value = (value & 1) === 0 ? value >>> 1 : (value >>> 1) ^ 0xedb88320;\n\t\ttable[index] = value >>> 0;\n\t}\n\treturn table;\n}\n\nconst CRC_TABLE_V1 = crcTable();\n\nfunction crc32(bytes: Uint8Array): number {\n\tlet value = 0xffffffff;\n\tfor (const byte of bytes) value = (value >>> 8) ^ CRC_TABLE_V1[(value ^ byte) & 0xff]!;\n\treturn (value ^ 0xffffffff) >>> 0;\n}\n\nfunction findEndOfCentralDirectory(bytes: Uint8Array): {\n\treadonly offset: number;\n\treadonly entries: number;\n\treadonly size: number;\n\treadonly start: number;\n} {\n\tconst minimum = Math.max(0, bytes.byteLength - 0xffff - 22);\n\tfor (let offset = bytes.byteLength - 22; offset >= minimum; offset -= 1) {\n\t\tif (readU32(bytes, offset) !== END_OF_CENTRAL_DIRECTORY_V1) continue;\n\t\tconst disk = readU16(bytes, offset + 4);\n\t\tconst centralDisk = readU16(bytes, offset + 6);\n\t\tconst entries = readU16(bytes, offset + 10);\n\t\tconst centralEntries = readU16(bytes, offset + 8);\n\t\tconst size = readU32(bytes, offset + 12);\n\t\tconst start = readU32(bytes, offset + 16);\n\t\tconst commentLength = readU16(bytes, offset + 20);\n\t\tif (\n\t\t\tdisk !== 0 ||\n\t\t\tcentralDisk !== 0 ||\n\t\t\tentries !== centralEntries ||\n\t\t\toffset + 22 + commentLength !== bytes.byteLength\n\t\t)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (entries === 0xffff || size === 0xffffffff || start === 0xffffffff)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (start + size > offset) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\treturn { offset, entries, size, start };\n\t}\n\tfor (let offset = Math.max(0, bytes.byteLength - 64); offset < bytes.byteLength - 3; offset += 1) {\n\t\tconst signature = readU32(bytes, offset);\n\t\tif (signature === ZIP64_END_OF_CENTRAL_DIRECTORY_V1 || signature === ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_V1)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t}\n\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n}\n\nfunction parseCentralDirectory(\n\tbytes: Uint8Array,\n\tlimits: Required<NodeZipPackageLimitsV1>,\n): readonly ZipEntryDescriptorV1[] {\n\tconst end = findEndOfCentralDirectory(bytes);\n\tif (end.entries > limits.maxEntries || end.size > bytes.byteLength || end.start + end.size > end.offset)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\tconst entries: ZipEntryDescriptorV1[] = [];\n\tconst names = new Set<string>();\n\tlet offset = end.start;\n\tfor (let index = 0; index < end.entries; index += 1) {\n\t\tif (readU32(bytes, offset) !== CENTRAL_FILE_HEADER_V1) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tconst madeBy = readU16(bytes, offset + 4) >>> 8;\n\t\tconst flags = readU16(bytes, offset + 8);\n\t\tconst method = readU16(bytes, offset + 10);\n\t\tconst crc = readU32(bytes, offset + 16);\n\t\tconst compressedSize = readU32(bytes, offset + 20);\n\t\tconst uncompressedSize = readU32(bytes, offset + 24);\n\t\tconst nameLength = readU16(bytes, offset + 28);\n\t\tconst extraLength = readU16(bytes, offset + 30);\n\t\tconst commentLength = readU16(bytes, offset + 32);\n\t\tconst disk = readU16(bytes, offset + 34);\n\t\tconst externalAttributes = readU32(bytes, offset + 38);\n\t\tconst localOffset = readU32(bytes, offset + 42);\n\t\tconst nameStart = offset + 46;\n\t\tconst recordEnd = nameStart + nameLength + extraLength + commentLength;\n\t\tif (\n\t\t\trecordEnd > end.start + end.size ||\n\t\t\tdisk !== 0 ||\n\t\t\t(flags & ~(0x8 | 0x800)) !== 0 ||\n\t\t\t(flags & 1) !== 0 ||\n\t\t\t(flags & 0x40) !== 0\n\t\t)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (method !== 0 && method !== 8) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (uncompressedSize > limits.maxEntryBytes || compressedSize > limits.maxArchiveBytes)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tconst name = normalizedEntryName(\n\t\t\tdecodeName(bytes.subarray(nameStart, nameStart + nameLength), (flags & 0x800) !== 0),\n\t\t);\n\t\tif (names.has(name.name)) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tnames.add(name.name);\n\t\tconst unixMode = madeBy === 3 ? (externalAttributes >>> 16) & 0xffff : 0;\n\t\tif ((unixMode & 0xf000) === 0xa000) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tentries.push({\n\t\t\tname: name.name,\n\t\t\tdirectory: name.directory,\n\t\t\tflags,\n\t\t\tmethod,\n\t\t\tcompressedSize,\n\t\t\tuncompressedSize,\n\t\t\tcrc32: crc,\n\t\t\tlocalOffset,\n\t\t});\n\t\toffset = recordEnd;\n\t}\n\tif (offset !== end.start + end.size) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t// Validate the local-file regions before extraction. A forged central\n\t// directory must not be able to point into another entry or into the\n\t// central directory/EOCD; otherwise extraction could reinterpret metadata\n\t// bytes as package contents.\n\tconst spans: ZipLocalSpanV1[] = [];\n\tfor (const entry of entries) {\n\t\tif (entry.localOffset < 0 || entry.localOffset + 30 > end.start)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tif (readU32(bytes, entry.localOffset) !== LOCAL_FILE_HEADER_V1)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tconst localNameLength = readU16(bytes, entry.localOffset + 26);\n\t\tconst localExtraLength = readU16(bytes, entry.localOffset + 28);\n\t\tconst dataStart = entry.localOffset + 30 + localNameLength + localExtraLength;\n\t\tconst dataEnd = dataStart + entry.compressedSize;\n\t\tif (dataStart < entry.localOffset + 30 || dataEnd < dataStart || dataEnd > end.start)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\tspans.push({ start: entry.localOffset, end: dataEnd });\n\t}\n\tspans.sort((left, right) => left.start - right.start || left.end - right.end);\n\tfor (let index = 1; index < spans.length; index += 1) {\n\t\tif (spans[index - 1]!.end > spans[index]!.start) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\t}\n\treturn entries;\n}\n\nfunction extractEntry(bytes: Uint8Array, entry: ZipEntryDescriptorV1, maxEntryBytes: number): Uint8Array {\n\tif (entry.directory) {\n\t\tif (entry.compressedSize !== 0 || entry.uncompressedSize !== 0)\n\t\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t\treturn new Uint8Array();\n\t}\n\tif (entry.localOffset + 30 > bytes.byteLength) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tif (readU32(bytes, entry.localOffset) !== LOCAL_FILE_HEADER_V1)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst localFlags = readU16(bytes, entry.localOffset + 6);\n\tconst localMethod = readU16(bytes, entry.localOffset + 8);\n\tconst nameLength = readU16(bytes, entry.localOffset + 26);\n\tconst extraLength = readU16(bytes, entry.localOffset + 28);\n\tconst localName = decodeName(\n\t\tbytes.subarray(entry.localOffset + 30, entry.localOffset + 30 + nameLength),\n\t\t(entry.flags & 0x800) !== 0,\n\t);\n\tif (normalizedEntryName(localName).name !== entry.name || localFlags !== entry.flags || localMethod !== entry.method)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst localCompressedSize = readU32(bytes, entry.localOffset + 18);\n\tconst localUncompressedSize = readU32(bytes, entry.localOffset + 22);\n\tif (\n\t\t(entry.flags & 8) === 0 &&\n\t\t(localCompressedSize !== entry.compressedSize || localUncompressedSize !== entry.uncompressedSize)\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst dataStart = entry.localOffset + 30 + nameLength + extraLength;\n\tconst dataEnd = dataStart + entry.compressedSize;\n\tif (dataStart > bytes.byteLength || dataEnd > bytes.byteLength)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst compressed = bytes.subarray(dataStart, dataEnd);\n\tlet output: Uint8Array;\n\ttry {\n\t\toutput =\n\t\t\tentry.method === 0\n\t\t\t\t? new Uint8Array(compressed)\n\t\t\t\t: inflateRawSync(compressed, { maxOutputLength: maxEntryBytes });\n\t} catch {\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\t}\n\tif (\n\t\toutput.byteLength !== entry.uncompressedSize ||\n\t\toutput.byteLength > maxEntryBytes ||\n\t\tcrc32(output) !== entry.crc32\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"archive_invalid\");\n\treturn output;\n}\n\nexport function parseNodeZipPackageV1(\n\tinput: Uint8Array,\n\tlimitsInput: NodeZipPackageLimitsV1 = {},\n): readonly NodeZipPackageEntryV1[] {\n\tif (!(input instanceof Uint8Array)) throw new NodePackageInstallErrorV1(\"archive_invalid\");\n\tconst limits = resolveNodePackageLimitsV1(limitsInput);\n\tif (input.byteLength > limits.maxArchiveBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\tconst descriptors = parseCentralDirectory(input, limits);\n\tlet extractedBytes = 0;\n\treturn Object.freeze(\n\t\tdescriptors.map((descriptor) => {\n\t\t\tconst bytes = extractEntry(input, descriptor, limits.maxEntryBytes);\n\t\t\textractedBytes += bytes.byteLength;\n\t\t\tif (extractedBytes > limits.maxExtractedBytes) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\t\treturn Object.freeze({ name: descriptor.name, directory: descriptor.directory, bytes });\n\t\t}),\n\t);\n}\n\nfunction manifestKeys(value: Record<string, unknown>): boolean {\n\tconst allowed = new Set([\n\t\t\"packageFormatVersion\",\n\t\t\"id\",\n\t\t\"pluginVersion\",\n\t\t\"apiVersion\",\n\t\t\"minHostVersion\",\n\t\t\"maxHostVersion\",\n\t\t\"capabilityRefs\",\n\t\t\"requires\",\n\t\t\"platform\",\n\t\t\"testOnly\",\n\t]);\n\treturn Object.keys(value).every((key) => allowed.has(key));\n}\n\nfunction packageManifest(value: unknown, record: PluginUpdateRecordV1): void {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\tconst manifest = value as Record<string, unknown>;\n\tif (\n\t\t!manifestKeys(manifest) ||\n\t\tmanifest.packageFormatVersion !== NODE_PACKAGE_FORMAT_VERSION_V1 ||\n\t\tmanifest.id !== record.pluginId ||\n\t\tmanifest.pluginVersion !== record.pluginVersion\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n\tif (typeof manifest.apiVersion !== \"string\" || manifest.apiVersion !== record.manifest.apiVersion)\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n\ttry {\n\t\tif (pluginManifestDigestV1(manifest as unknown as PluginManifestV1) !== record.manifestDigest)\n\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n\t} catch (error) {\n\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t}\n}\n\nfunction capabilityManifest(\n\tvalue: unknown,\n\trecord: PluginUpdateRecordV1,\n\tentries: readonly NodeZipPackageEntryV1[],\n): void {\n\tlet manifest: ReturnType<typeof parseCapabilityPluginManifest>;\n\ttry {\n\t\tmanifest = parseCapabilityPluginManifest(value, \"<installed plugin.json>\");\n\t} catch {\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t}\n\tif (manifest.id !== record.pluginId || manifest.version !== record.pluginVersion)\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n\tconst entryValue = manifest.entry.startsWith(\"./\") ? manifest.entry.slice(2) : manifest.entry;\n\tconst entry = normalizedEntryName(entryValue);\n\tif (entry.directory || !entries.some((candidate) => candidate.name === entry.name && !candidate.directory))\n\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_mismatch\");\n}\n\nfunction opaqueScope(value: string): string {\n\tif (\n\t\ttypeof value !== \"string\" ||\n\t\tvalue.trim() === \"\" ||\n\t\tvalue.length > 2048 ||\n\t\t/\\s|[\\u0000-\\u001f\\u007f]/u.test(value)\n\t)\n\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\treturn value;\n}\n\nfunction installationKey(scope: PluginInstallationScopeV1, scopeRef: string, pluginId: string): string {\n\treturn `${scope}\\u0000${scopeRef}\\u0000${pluginId}`;\n}\n\nfunction identityMatches(\n\trecord: InstalledRecordV1,\n\tinput: {\n\t\treadonly pluginId: string;\n\t\treadonly pluginVersion: string;\n\t\treadonly manifestDigest: string;\n\t\treadonly scope: PluginInstallationScopeV1;\n\t\treadonly scopeRef: string;\n\t},\n): boolean {\n\treturn (\n\t\trecord.pluginId === input.pluginId &&\n\t\trecord.pluginVersion === input.pluginVersion &&\n\t\trecord.manifestDigest === input.manifestDigest &&\n\t\trecord.scope === input.scope &&\n\t\trecord.scopeRef === input.scopeRef\n\t);\n}\n\nfunction artifactIdentityMatchesRecord(artifact: PluginDistributionArtifactV1, record: PluginUpdateRecordV1): boolean {\n\treturn (\n\t\tartifact.version === 1 &&\n\t\tartifact.pluginId === record.pluginId &&\n\t\tartifact.pluginVersion === record.pluginVersion &&\n\t\tartifact.manifestDigest === record.manifestDigest\n\t);\n}\n\nfunction expectedArtifactDigest(value: string): { readonly algorithm: \"sha256\" | \"sha512\"; readonly hex: string } {\n\tconst prefixed = /^(sha256|sha512):([0-9a-f]+)$/iu.exec(value);\n\tif (prefixed !== null) {\n\t\tconst algorithm = prefixed[1]!.toLowerCase() as \"sha256\" | \"sha512\";\n\t\tconst hex = prefixed[2]!.toLowerCase();\n\t\tif ((algorithm === \"sha256\" && hex.length === 64) || (algorithm === \"sha512\" && hex.length === 128))\n\t\t\treturn { algorithm, hex };\n\t}\n\tif (/^[0-9a-f]{64}$/iu.test(value)) return { algorithm: \"sha256\", hex: value.toLowerCase() };\n\tif (/^[0-9a-f]{128}$/iu.test(value)) return { algorithm: \"sha512\", hex: value.toLowerCase() };\n\tthrow new NodePackageInstallErrorV1(\"invalid_artifact\");\n}\n\nasync function collectArtifact(\n\topenArtifact: NodePackageInstallHostOptionsV1[\"openArtifact\"],\n\tartifact: PluginDistributionArtifactV1,\n\tmaxBytes: number,\n\tsignal: AbortSignal,\n): Promise<Uint8Array> {\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\tfor await (const chunk of openArtifact({ artifact, signal })) {\n\t\tif (signal.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\tif (!(chunk instanceof Uint8Array)) throw new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\tif (chunk.byteLength > maxBytes - total) throw new NodePackageInstallErrorV1(\"archive_budget_exceeded\");\n\t\tchunks.push(new Uint8Array(chunk));\n\t\ttotal += chunk.byteLength;\n\t}\n\tconst result = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tresult.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\treturn result;\n}\n\nfunction packagePath(root: string, key: string): string {\n\treturn join(root, createHash(\"sha256\").update(key).digest(\"hex\"));\n}\n\nfunction isHostSuppliedDependencyV1(name: string): boolean {\n\treturn (\n\t\tHOST_SUPPLIED_DEPENDENCIES_V1.has(name) ||\n\t\tHOST_SUPPLIED_DEPENDENCY_PREFIXES_V1.some((prefix) => name.startsWith(prefix))\n\t);\n}\n\n/**\n * Install declared runtime dependencies inside the staging directory before\n * the atomic rename (2026-10-07 user ruling): npm tarballs never carry\n * node_modules, so a store copy of a plugin with runtime dependencies\n * (e.g. memory's onnxruntime-node) would otherwise have no dependency tree\n * at load time. A failed dependency install fails the whole installation\n * while staging is still private — no half-installed store copy is ever\n * renamed into place, and the next startup retry heals it.\n */\nasync function installStagingRuntimeDependencies(\n\tstaging: string,\n\tentries: readonly NodeZipPackageEntryV1[],\n\tsignal: AbortSignal,\n): Promise<void> {\n\tconst packageEntry = entries.find((entry) => entry.name === \"package.json\" && !entry.directory);\n\tif (packageEntry === undefined) return;\n\tlet manifest: unknown;\n\ttry {\n\t\tmanifest = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(packageEntry.bytes));\n\t} catch {\n\t\t// Unlike the two manifests, a package.json with parse errors is not\n\t\t// part of install validation: the package installs unchanged and the\n\t\t// missing runtime dependencies surface later as module-not-found at\n\t\t// plugin load (registered boundary in the distribution design).\n\t\treturn;\n\t}\n\tif (typeof manifest !== \"object\" || manifest === null || Array.isArray(manifest)) return;\n\tconst record = manifest as Record<string, unknown>;\n\tconst dependencies = record.dependencies;\n\tif (typeof dependencies !== \"object\" || dependencies === null || Array.isArray(dependencies)) return;\n\tconst runtime: Record<string, unknown> = Object.fromEntries(\n\t\tObject.entries(dependencies as Record<string, unknown>).filter(([name]) => !isHostSuppliedDependencyV1(name)),\n\t);\n\tif (Object.keys(runtime).length === 0) return;\n\t// Rewrite the staging package.json to the runtime closure. Host-supplied\n\t// specifiers stay out (they resolve through jiti virtual modules and the\n\t// source-form names do not exist on the registry), and devDependencies are\n\t// dropped — npm resolves root devDependency metadata even under --omit=dev,\n\t// so unpublished dev names would fail the whole install with a 404.\n\tawait writeFile(\n\t\tjoin(staging, \"package.json\"),\n\t\tJSON.stringify({ ...record, dependencies: runtime, devDependencies: undefined }),\n\t\t{ flag: \"w\", mode: 0o600 },\n\t);\n\tconst win32 = process.platform === \"win32\";\n\tawait new Promise<void>((resolve, reject) => {\n\t\t// Node >= 18.20 refuses to spawn .cmd shims directly (EINVAL); the\n\t\t// fixed argument list contains no shell metacharacters, so the shell\n\t\t// join is safe (scripts/release.mjs precedent).\n\t\tconst child = spawn(\n\t\t\twin32 ? \"npm.cmd\" : \"npm\",\n\t\t\t[\"install\", \"--omit=dev\", \"--ignore-scripts\", \"--no-audit\", \"--no-fund\", \"--loglevel=error\"],\n\t\t\t// Inherits process.env so registry/proxy configuration follows\n\t\t\t// the host; abort kills the child and surfaces as a rejection.\n\t\t\t{ cwd: staging, signal, shell: win32, stdio: \"ignore\" },\n\t\t);\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"close\", (code) => {\n\t\t\tif (code === 0) {\n\t\t\t\tresolve();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treject(new NodePackageInstallErrorV1(\"dependency_install_failed\"));\n\t\t});\n\t});\n}\n\nasync function writeEntries(\n\troot: string,\n\tentries: readonly NodeZipPackageEntryV1[],\n\tsignal: AbortSignal,\n): Promise<void> {\n\tawait mkdir(root, { recursive: true, mode: 0o700 });\n\tfor (const entry of entries) {\n\t\tif (signal.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\tconst target = join(root, ...entry.name.split(\"/\"));\n\t\tif (entry.directory) {\n\t\t\tawait mkdir(target, { recursive: true, mode: 0o700 });\n\t\t\tcontinue;\n\t\t}\n\t\tconst parent = join(root, ...entry.name.split(\"/\").slice(0, -1));\n\t\tawait mkdir(parent, { recursive: true, mode: 0o700 });\n\t\tawait writeFile(target, entry.bytes, { flag: \"wx\", mode: 0o600 });\n\t}\n}\n\n/**\n * Host-owned ZIP/tgz package installer. It extracts only a bounded,\n * non-encrypted ZIP (\"application/zip\") or gzip-compressed tar\n * (\"application/gzip\") package and never executes package contents. The\n * returned installation handle is opaque; paths stay inside the host-owned\n * instance directory.\n */\nexport async function createNodePackageInstallHostV1(\n\toptions: NodePackageInstallHostOptionsV1,\n): Promise<NodePackageInstallHostV1> {\n\tif (typeof options.openArtifact !== \"function\") throw new Error(t(\"openArtifact is required\"));\n\tconst limits = resolveNodePackageLimitsV1(options);\n\tconst parent = options.installationDirectory ?? tmpdir();\n\tif (!isAbsolute(parent)) throw new Error(t(\"installationDirectory must be absolute\"));\n\tif (options.persistence !== undefined && options.persistence !== \"ephemeral\" && options.persistence !== \"persistent\")\n\t\tthrow new Error(t(\"persistence mode is invalid\"));\n\tconst supportedScopes = options.supportedScopes ?? [\"global\", \"project\"];\n\tif (\n\t\t!Array.isArray(supportedScopes) ||\n\t\tsupportedScopes.length === 0 ||\n\t\tnew Set(supportedScopes).size !== supportedScopes.length ||\n\t\tsupportedScopes.some((scope) => scope !== \"global\" && scope !== \"project\")\n\t)\n\t\tthrow new Error(t(\"supportedScopes is invalid\"));\n\tawait mkdir(parent, { recursive: true, mode: 0o700 });\n\tconst persistent = options.persistence === \"persistent\";\n\tconst instanceRoot = persistent\n\t\t? join(parent, \"agent-forge-plugin-install-store-v1\")\n\t\t: await mkdtemp(join(parent, \"agent-forge-plugin-install-\"));\n\tconst persistence: NodePackageInstallPersistenceV1 | undefined = persistent\n\t\t? await createNodePackageInstallPersistenceV1({\n\t\t\t\trootDirectory: instanceRoot,\n\t\t\t\tlockTimeoutMs: options.persistenceLockTimeoutMs,\n\t\t\t})\n\t\t: undefined;\n\tconst records = new Map<string, InstalledRecordV1>();\n\tconst activeByKey = new Map<string, string>();\n\tif (persistence !== undefined) {\n\t\tfor (const record of await persistence.load()) records.set(record.installationRef, { ...record });\n\t}\n\tfor (const record of records.values()) {\n\t\tif (record.state === \"active\")\n\t\t\tactiveByKey.set(installationKey(record.scope, record.scopeRef, record.pluginId), record.installationRef);\n\t}\n\tlet closed = false;\n\tconst activeOperations = new Set<AbortController>();\n\tconst runtimeHandles = new Map<string, { readonly dispose?: () => void | Promise<void> }>();\n\t// Activation serialization: NodePackageInstallHostV1 is a public API and\n\t// third-party callers may activate the same installation key concurrently.\n\t// Two interleaved activations can both pass the state guard and overwrite\n\t// each other's runtimeHandles entry — the losing dispose stops its own\n\t// handle while the ledger still reads \"active\", silently leaking the\n\t// surviving runtime, because the persistence layer only serializes\n\t// commits and not this in-memory path. Each activation chains onto the\n\t// previous one for its key, preserving the existing success semantics: a\n\t// queued activation starts only after the previous one settles and is\n\t// then rejected by the state guard like any sequential re-activation.\n\t// rollback/uninstall stay unchained; their interleavings with activate\n\t// are tolerated by the persistence snapshot and the ENOENT handling in\n\t// uninstall.\n\tconst activationChains = new Map<string, Promise<void>>();\n\tconst applyPersistentSnapshot = (snapshot: readonly NodePackageInstallPersistentRecordV1[]): void => {\n\t\trecords.clear();\n\t\tactiveByKey.clear();\n\t\tfor (const value of snapshot) records.set(value.installationRef, { ...value });\n\t\tfor (const value of records.values()) {\n\t\t\tif (value.state === \"active\")\n\t\t\t\tactiveByKey.set(installationKey(value.scope, value.scopeRef, value.pluginId), value.installationRef);\n\t\t}\n\t};\n\tconst commitRecords = async (\n\t\tupdate: (values: Map<string, NodePackageInstallPersistentRecordV1>) => void | Promise<void>,\n\t): Promise<void> => {\n\t\tif (persistence === undefined) {\n\t\t\tawait update(records);\n\t\t\treturn;\n\t\t}\n\t\tapplyPersistentSnapshot(await persistence.mutate({ update }));\n\t};\n\tlet drained: (() => void) | undefined;\n\tlet closePromise: Promise<void> | undefined;\n\n\tconst begin = (\n\t\tparentSignal: AbortSignal | undefined,\n\t): { readonly controller: AbortController; readonly signal: AbortSignal } => {\n\t\tif (closed) throw new NodePackageInstallErrorV1(\"host_closed\");\n\t\tconst controller = new AbortController();\n\t\tif (parentSignal?.aborted) controller.abort(parentSignal.reason);\n\t\telse parentSignal?.addEventListener(\"abort\", () => controller.abort(parentSignal.reason), { once: true });\n\t\tactiveOperations.add(controller);\n\t\treturn { controller, signal: controller.signal };\n\t};\n\tconst end = (controller: AbortController): void => {\n\t\tactiveOperations.delete(controller);\n\t\tif (activeOperations.size === 0) {\n\t\t\tdrained?.();\n\t\t\tdrained = undefined;\n\t\t}\n\t};\n\tconst waitDrained = async (): Promise<void> => {\n\t\tif (activeOperations.size === 0) return;\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tdrained = resolve;\n\t\t});\n\t};\n\tconst getRecord = (\n\t\tinstallation: PluginInstallationV1,\n\t\trecord: PluginUpdateRecordV1,\n\t\tscope: PluginInstallationScopeV1,\n\t\tscopeRef: string,\n\t): InstalledRecordV1 => {\n\t\tconst value = records.get(installation.installationRef);\n\t\tif (\n\t\t\tvalue === undefined ||\n\t\t\tinstallation.pluginId !== record.pluginId ||\n\t\t\tinstallation.pluginVersion !== record.pluginVersion ||\n\t\t\tinstallation.manifestDigest !== record.manifestDigest ||\n\t\t\tinstallation.scope !== scope ||\n\t\t\tinstallation.scopeRef !== scopeRef ||\n\t\t\t!identityMatches(value, {\n\t\t\t\tpluginId: record.pluginId,\n\t\t\t\tpluginVersion: record.pluginVersion,\n\t\t\t\tmanifestDigest: record.manifestDigest,\n\t\t\t\tscope,\n\t\t\t\tscopeRef,\n\t\t\t})\n\t\t)\n\t\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\treturn value;\n\t};\n\tconst disposeRuntime = async (installationRef: string): Promise<void> => {\n\t\tconst handle = runtimeHandles.get(installationRef);\n\t\tif (handle === undefined) return;\n\t\tif (handle.dispose !== undefined) await Promise.resolve(handle.dispose());\n\t\truntimeHandles.delete(installationRef);\n\t};\n\t// A dispose that throws keeps its entry in runtimeHandles (disposeRuntime\n\t// deletes only after a successful dispose), so re-registering the same\n\t// installation ref must not silently overwrite the stale leftover: the\n\t// combined handle releases the new runtime first and the stale one in\n\t// `finally`, so a failure on either side still reclaims the other.\n\tconst registerRuntimeHandle = (\n\t\tinstallationRef: string,\n\t\thandle: { readonly dispose?: () => void | Promise<void> },\n\t): void => {\n\t\tconst existing = runtimeHandles.get(installationRef);\n\t\tif (existing === undefined) {\n\t\t\truntimeHandles.set(installationRef, handle);\n\t\t\treturn;\n\t\t}\n\t\truntimeHandles.set(installationRef, {\n\t\t\tdispose: async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait Promise.resolve(handle.dispose?.());\n\t\t\t\t} finally {\n\t\t\t\t\tawait Promise.resolve(existing.dispose?.());\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n\tconst loadRuntime = async (\n\t\tinstallation: PluginInstallationV1,\n\t\trecord: PluginUpdateRecordV1 | undefined,\n\t\tdirectory: string,\n\t\tsignal: AbortSignal,\n\t): Promise<{ readonly dispose?: () => void | Promise<void> } | undefined> => {\n\t\tif (options.loadPlugin === undefined) return undefined;\n\t\ttry {\n\t\t\tconst loaded = await options.loadPlugin({\n\t\t\t\tinstallation,\n\t\t\t\trecord,\n\t\t\t\tdirectory,\n\t\t\t\tmanifestPath: join(directory, CAPABILITY_MANIFEST_FILE_V1),\n\t\t\t\tsignal,\n\t\t\t});\n\t\t\treturn loaded ?? undefined;\n\t\t} catch (error) {\n\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t}\n\t};\n\n\tconst host: NodePackageInstallHostV1 = {\n\t\tversion: NODE_PACKAGE_INSTALL_HOST_VERSION_V1,\n\t\tsupportedScopes: Object.freeze([...supportedScopes]),\n\t\tgetInstallationDirectory(input) {\n\t\t\tconst record = records.get(input.installation.installationRef);\n\t\t\tif (\n\t\t\t\trecord === undefined ||\n\t\t\t\trecord.pluginId !== input.installation.pluginId ||\n\t\t\t\trecord.pluginVersion !== input.installation.pluginVersion ||\n\t\t\t\trecord.manifestDigest !== input.installation.manifestDigest ||\n\t\t\t\trecord.scope !== input.installation.scope ||\n\t\t\t\trecord.scopeRef !== input.installation.scopeRef\n\t\t\t)\n\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_unknown\");\n\t\t\treturn record.directory;\n\t\t},\n\t\tasync install(input) {\n\t\t\tconst operation = begin(input.signal);\n\t\t\tlet staging: string | undefined;\n\t\t\ttry {\n\t\t\t\tif (input.artifact === undefined) throw new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\t\t\tif (!artifactIdentityMatchesRecord(input.artifact, input.record))\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\t\t\tconst mediaType = input.artifact.mediaType.toLowerCase();\n\t\t\t\tif (mediaType !== \"application/zip\" && mediaType !== \"application/gzip\")\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"unsupported_media_type\");\n\t\t\t\tconst scopeRef = opaqueScope(input.scopeRef);\n\t\t\t\tconst bytes = await collectArtifact(\n\t\t\t\t\toptions.openArtifact,\n\t\t\t\t\tinput.artifact,\n\t\t\t\t\tlimits.maxArchiveBytes,\n\t\t\t\t\toperation.signal,\n\t\t\t\t);\n\t\t\t\tif (input.artifact.sizeBytes !== bytes.byteLength) throw new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\t\t\tconst digest = expectedArtifactDigest(input.artifact.artifactDigest);\n\t\t\t\tif (createHash(digest.algorithm).update(bytes).digest(\"hex\") !== digest.hex)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"invalid_artifact\");\n\t\t\t\tconst entries =\n\t\t\t\t\tmediaType === \"application/gzip\"\n\t\t\t\t\t\t? await parseNodeTarGzPackageV1(bytes, limits)\n\t\t\t\t\t\t: parseNodeZipPackageV1(bytes, limits);\n\t\t\t\tconst manifestEntry = entries.find(\n\t\t\t\t\t(entry) => entry.name === ECOSYSTEM_MANIFEST_FILE_V1 && !entry.directory,\n\t\t\t\t);\n\t\t\t\tif (manifestEntry === undefined) throw new NodePackageInstallErrorV1(\"package_manifest_missing\");\n\t\t\t\tif (manifestEntry.bytes.byteLength > MAX_PACKAGE_MANIFEST_BYTES_V1)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t\t\t\tlet manifest: unknown;\n\t\t\t\ttry {\n\t\t\t\t\tmanifest = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(manifestEntry.bytes));\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t\t\t\t}\n\t\t\t\tpackageManifest(manifest, input.record);\n\t\t\t\tconst capabilityEntry = entries.find(\n\t\t\t\t\t(entry) => entry.name === CAPABILITY_MANIFEST_FILE_V1 && !entry.directory,\n\t\t\t\t);\n\t\t\t\tif (capabilityEntry === undefined) throw new NodePackageInstallErrorV1(\"package_manifest_missing\");\n\t\t\t\tif (capabilityEntry.bytes.byteLength > MAX_PACKAGE_MANIFEST_BYTES_V1)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t\t\t\tlet capability: unknown;\n\t\t\t\ttry {\n\t\t\t\t\tcapability = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(capabilityEntry.bytes));\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"package_manifest_invalid\");\n\t\t\t\t}\n\t\t\t\tcapabilityManifest(capability, input.record, entries);\n\t\t\t\tconst key = installationKey(input.scope, scopeRef, input.record.pluginId);\n\t\t\t\tstaging = join(instanceRoot, `.staging-${randomUUID()}`);\n\t\t\t\tawait writeEntries(staging, entries, operation.signal);\n\t\t\t\t// Materialize declared runtime dependencies while staging is\n\t\t\t\t// still private; a failure throws before the rename, so no\n\t\t\t\t// half-installed copy ever reaches the store.\n\t\t\t\tif (options.dependencyMaterialization !== \"skip\") {\n\t\t\t\t\tawait installStagingRuntimeDependencies(staging, entries, operation.signal);\n\t\t\t\t}\n\t\t\t\tconst installRef = `plugin-install://node-zip/${randomUUID()}`;\n\t\t\t\tconst destination = packagePath(instanceRoot, `${key}\\u0000${installRef}`);\n\t\t\t\tawait rename(staging, destination);\n\t\t\t\tstaging = undefined;\n\t\t\t\tconst installedRecord: InstalledRecordV1 = {\n\t\t\t\t\tinstallationRef: installRef,\n\t\t\t\t\tdirectory: destination,\n\t\t\t\t\tpluginId: input.record.pluginId,\n\t\t\t\t\tpluginVersion: input.record.pluginVersion,\n\t\t\t\t\tmanifestDigest: input.record.manifestDigest,\n\t\t\t\t\tscope: input.scope,\n\t\t\t\t\tscopeRef,\n\t\t\t\t\tstate: \"installed\",\n\t\t\t\t};\n\t\t\t\ttry {\n\t\t\t\t\tawait commitRecords((values) => {\n\t\t\t\t\t\tvalues.set(installRef, installedRecord);\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tawait rm(destination, { recursive: true, force: true }).catch(() => undefined);\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\treturn { installationRef: installRef, scope: input.scope, scopeRef };\n\t\t\t} catch (error) {\n\t\t\t\tif (operation.signal.aborted || input.signal?.aborted) throw new NodePackageInstallErrorV1(\"cancelled\");\n\t\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t\t} finally {\n\t\t\t\tif (staging !== undefined) await rm(staging, { recursive: true, force: true }).catch(() => undefined);\n\t\t\t\tend(operation.controller);\n\t\t\t}\n\t\t},\n\t\tasync activate(input) {\n\t\t\t// Validate before queueing: a malformed request fails at call time\n\t\t\t// instead of waiting behind pending activations.\n\t\t\tconst key = installationKey(input.scope, opaqueScope(input.scopeRef), input.record.pluginId);\n\t\t\tconst activateBody = async (): Promise<void> => {\n\t\t\t\tconst operation = begin(input.signal);\n\t\t\t\tlet candidate: { readonly dispose?: () => void | Promise<void> } | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tconst scopeRef = opaqueScope(input.scopeRef);\n\t\t\t\t\tconst record = getRecord(input.installation, input.record, input.scope, scopeRef);\n\t\t\t\t\tif (record.state !== \"installed\") throw new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\t\t\t\tcandidate = await loadRuntime(input.installation, input.record, record.directory, operation.signal);\n\t\t\t\t\tconst previousRef = activeByKey.get(key);\n\t\t\t\t\t// Commit the persisted ledger before stopping the previous\n\t\t\t\t\t// runtime: if the commit fails, the old runtime is still\n\t\t\t\t\t// running and still matches the ledger, so the caller can\n\t\t\t\t\t// retry or roll back from a consistent state. Between the\n\t\t\t\t\t// commit and the dispose below the old and new runtimes\n\t\t\t\t\t// coexist briefly — the previous plugin keeps serving until\n\t\t\t\t\t// it is replaced here, which is the correct activation\n\t\t\t\t\t// window. The new handle is registered before the old\n\t\t\t\t\t// runtime is disposed, so even if disposeRuntime propagates\n\t\t\t\t\t// a failing old-runtime dispose, the committed new version\n\t\t\t\t\t// stays live and the error surfaces through the catch below.\n\t\t\t\t\tawait commitRecords((values) => {\n\t\t\t\t\t\tconst candidateRecord = values.get(input.installation.installationRef);\n\t\t\t\t\t\tif (candidateRecord === undefined)\n\t\t\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\t\t\t\t\tif (previousRef !== undefined) {\n\t\t\t\t\t\t\tconst previousRecord = values.get(previousRef);\n\t\t\t\t\t\t\tif (previousRecord !== undefined)\n\t\t\t\t\t\t\t\tvalues.set(previousRef, { ...previousRecord, state: \"retained\" });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvalues.set(input.installation.installationRef, { ...candidateRecord, state: \"active\" });\n\t\t\t\t\t});\n\t\t\t\t\tactiveByKey.set(key, input.installation.installationRef);\n\t\t\t\t\tif (candidate !== undefined) registerRuntimeHandle(input.installation.installationRef, candidate);\n\t\t\t\t\tcandidate = undefined;\n\t\t\t\t\tif (previousRef !== undefined) {\n\t\t\t\t\t\tconst previous = records.get(previousRef);\n\t\t\t\t\t\tif (previous !== undefined) {\n\t\t\t\t\t\t\tawait disposeRuntime(previousRef);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (candidate?.dispose !== undefined) await Promise.resolve(candidate.dispose()).catch(() => undefined);\n\t\t\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t\t\t} finally {\n\t\t\t\t\tend(operation.controller);\n\t\t\t\t}\n\t\t\t};\n\t\t\t// Chain onto the previous activation for this key; the queued body\n\t\t\t// runs only after it settles, whatever its outcome.\n\t\t\tconst queued = activationChains.get(key) ?? Promise.resolve();\n\t\t\tconst run = queued.catch(() => undefined).then(() => activateBody());\n\t\t\tactivationChains.set(key, run);\n\t\t\ttry {\n\t\t\t\tawait run;\n\t\t\t} finally {\n\t\t\t\t// Drop the chain entry only while it is still the newest run, so\n\t\t\t\t// a queued successor keeps later arrivals chained onto it.\n\t\t\t\tif (activationChains.get(key) === run) activationChains.delete(key);\n\t\t\t}\n\t\t},\n\t\tasync rollback(input) {\n\t\t\tconst operation = begin(input.signal);\n\t\t\tlet candidate: { readonly dispose?: () => void | Promise<void> } | undefined;\n\t\t\ttry {\n\t\t\t\tconst scopeRef = opaqueScope(input.scopeRef);\n\t\t\t\tconst failed = records.get(input.failed.installationRef);\n\t\t\t\tif (\n\t\t\t\t\tfailed === undefined ||\n\t\t\t\t\t!identityMatches(failed, {\n\t\t\t\t\t\tpluginId: input.failed.pluginId,\n\t\t\t\t\t\tpluginVersion: input.failed.pluginVersion,\n\t\t\t\t\t\tmanifestDigest: input.failed.manifestDigest,\n\t\t\t\t\t\tscope: input.scope,\n\t\t\t\t\t\tscopeRef,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\t\t\tconst previous = records.get(input.previous.installationRef);\n\t\t\t\tif (\n\t\t\t\t\tprevious === undefined ||\n\t\t\t\t\t!identityMatches(previous, {\n\t\t\t\t\t\tpluginId: input.previous.pluginId,\n\t\t\t\t\t\tpluginVersion: input.previous.pluginVersion,\n\t\t\t\t\t\tmanifestDigest: input.previous.manifestDigest,\n\t\t\t\t\t\tscope: input.scope,\n\t\t\t\t\t\tscopeRef,\n\t\t\t\t\t}) ||\n\t\t\t\t\t(previous.state !== \"active\" && previous.state !== \"retained\") ||\n\t\t\t\t\tfailed.pluginId !== previous.pluginId\n\t\t\t\t)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_identity_mismatch\");\n\t\t\t\tconst previousInstallation: PluginInstallationV1 = {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tpluginId: previous.pluginId,\n\t\t\t\t\tpluginVersion: previous.pluginVersion,\n\t\t\t\t\tmanifestDigest: previous.manifestDigest,\n\t\t\t\t\tscope: previous.scope,\n\t\t\t\t\tscopeRef: previous.scopeRef,\n\t\t\t\t\tinstallationRef: input.previous.installationRef,\n\t\t\t\t\tstate: \"active\",\n\t\t\t\t};\n\t\t\t\tcandidate = await loadRuntime(previousInstallation, undefined, previous.directory, operation.signal);\n\t\t\t\tawait disposeRuntime(input.failed.installationRef);\n\t\t\t\tawait rm(failed.directory, { recursive: true, force: true });\n\t\t\t\tawait commitRecords((values) => {\n\t\t\t\t\tvalues.delete(input.failed.installationRef);\n\t\t\t\t\tconst previousRecord = values.get(input.previous.installationRef);\n\t\t\t\t\tif (previousRecord !== undefined)\n\t\t\t\t\t\tvalues.set(input.previous.installationRef, { ...previousRecord, state: \"active\" });\n\t\t\t\t});\n\t\t\t\tactiveByKey.set(installationKey(input.scope, scopeRef, failed.pluginId), input.previous.installationRef);\n\t\t\t\tif (candidate !== undefined) registerRuntimeHandle(input.previous.installationRef, candidate);\n\t\t\t\tcandidate = undefined;\n\t\t\t} catch (error) {\n\t\t\t\tif (candidate?.dispose !== undefined) await Promise.resolve(candidate.dispose()).catch(() => undefined);\n\t\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t\t} finally {\n\t\t\t\tend(operation.controller);\n\t\t\t}\n\t\t},\n\t\tasync uninstall(input) {\n\t\t\tconst operation = begin(input.signal);\n\t\t\ttry {\n\t\t\t\tconst scopeRef = opaqueScope(input.scopeRef);\n\t\t\t\tconst record = records.get(input.installation.installationRef);\n\t\t\t\tif (\n\t\t\t\t\trecord === undefined ||\n\t\t\t\t\t!identityMatches(record, {\n\t\t\t\t\t\tpluginId: input.installation.pluginId,\n\t\t\t\t\t\tpluginVersion: input.installation.pluginVersion,\n\t\t\t\t\t\tmanifestDigest: input.installation.manifestDigest,\n\t\t\t\t\t\tscope: input.scope,\n\t\t\t\t\t\tscopeRef,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t\t\tthrow new NodePackageInstallErrorV1(\"installation_unknown\");\n\t\t\t\tawait disposeRuntime(input.installation.installationRef);\n\t\t\t\t// force:false keeps rm strict, but a directory that is already\n\t\t\t\t// gone (ghost record from an interrupted uninstall) is the\n\t\t\t\t// desired end state — continue to the index commit instead of\n\t\t\t\t// bricking every later store operation on ENOENT.\n\t\t\t\tawait rm(record.directory, { recursive: true, force: false }).catch((error: unknown) => {\n\t\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return;\n\t\t\t\t\tthrow error;\n\t\t\t\t});\n\t\t\t\tawait commitRecords((values) => {\n\t\t\t\t\tvalues.delete(input.installation.installationRef);\n\t\t\t\t});\n\t\t\t\tconst key = installationKey(input.scope, scopeRef, record.pluginId);\n\t\t\t\tif (activeByKey.get(key) === input.installation.installationRef) activeByKey.delete(key);\n\t\t\t} finally {\n\t\t\t\tend(operation.controller);\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\tif (closePromise !== undefined) return await closePromise;\n\t\t\tclosed = true;\n\t\t\tfor (const operation of activeOperations) operation.abort();\n\t\t\tclosePromise = (async () => {\n\t\t\t\tawait waitDrained();\n\t\t\t\tlet disposeError = false;\n\t\t\t\tfor (const installationRef of runtimeHandles.keys()) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait disposeRuntime(installationRef);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tdisposeError = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (persistence === undefined) await rm(instanceRoot, { recursive: true, force: true });\n\t\t\t\telse await persistence.close();\n\t\t\t\trecords.clear();\n\t\t\t\tactiveByKey.clear();\n\t\t\t\tif (disposeError) throw new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t\t})();\n\t\t\treturn await closePromise;\n\t\t},\n\t};\n\tif (options.loadPlugin !== undefined) {\n\t\ttry {\n\t\t\tfor (const record of records.values()) {\n\t\t\t\tif (record.state !== \"active\") continue;\n\t\t\t\tconst installation: PluginInstallationV1 = {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tpluginId: record.pluginId,\n\t\t\t\t\tpluginVersion: record.pluginVersion,\n\t\t\t\t\tmanifestDigest: record.manifestDigest,\n\t\t\t\t\tscope: record.scope,\n\t\t\t\t\tscopeRef: record.scopeRef,\n\t\t\t\t\tinstallationRef: record.installationRef,\n\t\t\t\t\tstate: \"active\",\n\t\t\t\t};\n\t\t\t\tconst runtime = await loadRuntime(installation, undefined, record.directory, new AbortController().signal);\n\t\t\t\tif (runtime !== undefined) runtimeHandles.set(record.installationRef, runtime);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tawait host.close().catch(() => undefined);\n\t\t\tif (error instanceof NodePackageInstallErrorV1) throw error;\n\t\t\tthrow new NodePackageInstallErrorV1(\"installation_io_failed\");\n\t\t}\n\t}\n\treturn host;\n}\n"]}
|