@try-works/dsh-tool-mode 0.0.0-stage → 0.5.45
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/LICENSE +205 -0
- package/README.md +303 -2
- package/bin/dsh-tool-mode-credentials.mjs +20 -0
- package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
- package/bin/dsh-tool-mode.mjs +211 -0
- package/builtins.lock.json +26 -0
- package/catalog/desert-ant-models.json +693 -0
- package/catalog/playbooks.json +531 -0
- package/client/client.model-tools.js +1910 -0
- package/client/index.js +4158 -0
- package/cordis.patch.yml +39 -0
- package/lib/adapters/index.d.ts +61 -0
- package/lib/adapters/index.d.ts.map +1 -0
- package/lib/adapters/index.js +82 -0
- package/lib/adapters/types.d.ts +106 -0
- package/lib/adapters/types.d.ts.map +1 -0
- package/lib/adapters/types.js +85 -0
- package/lib/approval.d.ts +189 -0
- package/lib/approval.d.ts.map +1 -0
- package/lib/approval.js +510 -0
- package/lib/asset-acquisition.d.ts +149 -0
- package/lib/asset-acquisition.d.ts.map +1 -0
- package/lib/asset-acquisition.js +175 -0
- package/lib/authority.d.ts +185 -0
- package/lib/authority.d.ts.map +1 -0
- package/lib/authority.js +223 -0
- package/lib/bindings.d.ts +144 -0
- package/lib/bindings.d.ts.map +1 -0
- package/lib/bindings.js +314 -0
- package/lib/builtins.d.ts +27 -0
- package/lib/builtins.d.ts.map +1 -0
- package/lib/builtins.js +37 -0
- package/lib/capability.d.ts +58 -0
- package/lib/capability.d.ts.map +1 -0
- package/lib/capability.js +192 -0
- package/lib/capture-path.d.ts +97 -0
- package/lib/capture-path.d.ts.map +1 -0
- package/lib/capture-path.js +152 -0
- package/lib/catalog.d.ts +37 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +274 -0
- package/lib/contract-payload.d.ts +39 -0
- package/lib/contract-payload.d.ts.map +1 -0
- package/lib/contract-payload.js +56 -0
- package/lib/custom-tools/compile.d.ts +25 -0
- package/lib/custom-tools/compile.d.ts.map +1 -0
- package/lib/custom-tools/compile.js +61 -0
- package/lib/custom-tools/composite-executor.d.ts +42 -0
- package/lib/custom-tools/composite-executor.d.ts.map +1 -0
- package/lib/custom-tools/composite-executor.js +113 -0
- package/lib/custom-tools/dispatch.d.ts +28 -0
- package/lib/custom-tools/dispatch.d.ts.map +1 -0
- package/lib/custom-tools/dispatch.js +34 -0
- package/lib/custom-tools/http-executor.d.ts +48 -0
- package/lib/custom-tools/http-executor.d.ts.map +1 -0
- package/lib/custom-tools/http-executor.js +241 -0
- package/lib/custom-tools/index.d.ts +12 -0
- package/lib/custom-tools/index.d.ts.map +1 -0
- package/lib/custom-tools/index.js +12 -0
- package/lib/custom-tools/mcp-surface.d.ts +14 -0
- package/lib/custom-tools/mcp-surface.d.ts.map +1 -0
- package/lib/custom-tools/mcp-surface.js +41 -0
- package/lib/custom-tools/prompt-executor.d.ts +39 -0
- package/lib/custom-tools/prompt-executor.d.ts.map +1 -0
- package/lib/custom-tools/prompt-executor.js +59 -0
- package/lib/custom-tools/registry.d.ts +69 -0
- package/lib/custom-tools/registry.d.ts.map +1 -0
- package/lib/custom-tools/registry.js +130 -0
- package/lib/custom-tools/schema.d.ts +26 -0
- package/lib/custom-tools/schema.d.ts.map +1 -0
- package/lib/custom-tools/schema.js +375 -0
- package/lib/custom-tools/service.d.ts +90 -0
- package/lib/custom-tools/service.d.ts.map +1 -0
- package/lib/custom-tools/service.js +203 -0
- package/lib/custom-tools/store.d.ts +61 -0
- package/lib/custom-tools/store.d.ts.map +1 -0
- package/lib/custom-tools/store.js +240 -0
- package/lib/custom-tools/types.d.ts +131 -0
- package/lib/custom-tools/types.d.ts.map +1 -0
- package/lib/custom-tools/types.js +11 -0
- package/lib/decision-combine.d.ts +113 -0
- package/lib/decision-combine.d.ts.map +1 -0
- package/lib/decision-combine.js +156 -0
- package/lib/decision-evaluate.d.ts +185 -0
- package/lib/decision-evaluate.d.ts.map +1 -0
- package/lib/decision-evaluate.js +227 -0
- package/lib/deployment-coordinator.d.ts +213 -0
- package/lib/deployment-coordinator.d.ts.map +1 -0
- package/lib/deployment-coordinator.js +274 -0
- package/lib/diagnostics.d.ts +169 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +136 -0
- package/lib/discovery-contracts.d.ts +14 -0
- package/lib/discovery-contracts.d.ts.map +1 -0
- package/lib/discovery-contracts.js +90 -0
- package/lib/discovery-describe.d.ts +35 -0
- package/lib/discovery-describe.d.ts.map +1 -0
- package/lib/discovery-describe.js +99 -0
- package/lib/embedding-index.d.ts +55 -0
- package/lib/embedding-index.d.ts.map +1 -0
- package/lib/embedding-index.js +126 -0
- package/lib/extension-dependencies.d.ts +355 -0
- package/lib/extension-dependencies.d.ts.map +1 -0
- package/lib/extension-dependencies.js +987 -0
- package/lib/extension-dispatch.d.ts +150 -0
- package/lib/extension-dispatch.d.ts.map +1 -0
- package/lib/extension-dispatch.js +178 -0
- package/lib/extension-page-state.d.ts +69 -0
- package/lib/extension-page-state.d.ts.map +1 -0
- package/lib/extension-page-state.js +82 -0
- package/lib/extension-registrar.d.ts +116 -0
- package/lib/extension-registrar.d.ts.map +1 -0
- package/lib/extension-registrar.js +144 -0
- package/lib/extensions-api.d.ts +346 -0
- package/lib/extensions-api.d.ts.map +1 -0
- package/lib/extensions-api.js +17 -0
- package/lib/extensions-remote.d.ts +57 -0
- package/lib/extensions-remote.d.ts.map +1 -0
- package/lib/extensions-remote.js +69 -0
- package/lib/extensions-service.d.ts +568 -0
- package/lib/extensions-service.d.ts.map +1 -0
- package/lib/extensions-service.js +2552 -0
- package/lib/harness-budgets.d.ts +158 -0
- package/lib/harness-budgets.d.ts.map +1 -0
- package/lib/harness-budgets.js +152 -0
- package/lib/harness-llm.d.ts +138 -0
- package/lib/harness-llm.d.ts.map +1 -0
- package/lib/harness-llm.js +218 -0
- package/lib/harness-serving.d.ts +115 -0
- package/lib/harness-serving.d.ts.map +1 -0
- package/lib/harness-serving.js +176 -0
- package/lib/headless.d.ts +97 -0
- package/lib/headless.d.ts.map +1 -0
- package/lib/headless.js +407 -0
- package/lib/http-acquisition.d.ts +27 -0
- package/lib/http-acquisition.d.ts.map +1 -0
- package/lib/http-acquisition.js +140 -0
- package/lib/index.d.ts +546 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +5273 -0
- package/lib/invocation-accounting.d.ts +169 -0
- package/lib/invocation-accounting.d.ts.map +1 -0
- package/lib/invocation-accounting.js +148 -0
- package/lib/jev.d.ts +218 -0
- package/lib/jev.d.ts.map +1 -0
- package/lib/jev.js +366 -0
- package/lib/legacy-migration.d.ts +95 -0
- package/lib/legacy-migration.d.ts.map +1 -0
- package/lib/legacy-migration.js +429 -0
- package/lib/license-basis.d.ts +29 -0
- package/lib/license-basis.d.ts.map +1 -0
- package/lib/license-basis.js +58 -0
- package/lib/managed-workers.d.ts +158 -0
- package/lib/managed-workers.d.ts.map +1 -0
- package/lib/managed-workers.js +269 -0
- package/lib/mcp/auth-selection.d.ts +35 -0
- package/lib/mcp/auth-selection.d.ts.map +1 -0
- package/lib/mcp/auth-selection.js +64 -0
- package/lib/mcp/client-grant.d.ts +89 -0
- package/lib/mcp/client-grant.d.ts.map +1 -0
- package/lib/mcp/client-grant.js +63 -0
- package/lib/mcp/credential-cli.d.ts +38 -0
- package/lib/mcp/credential-cli.d.ts.map +1 -0
- package/lib/mcp/credential-cli.js +148 -0
- package/lib/mcp/credential-store.d.ts +143 -0
- package/lib/mcp/credential-store.d.ts.map +1 -0
- package/lib/mcp/credential-store.js +461 -0
- package/lib/mcp/export-policy.d.ts +25 -0
- package/lib/mcp/export-policy.d.ts.map +1 -0
- package/lib/mcp/export-policy.js +31 -0
- package/lib/mcp/host-binding.d.ts +144 -0
- package/lib/mcp/host-binding.d.ts.map +1 -0
- package/lib/mcp/host-binding.js +280 -0
- package/lib/mcp/http.d.ts +180 -0
- package/lib/mcp/http.d.ts.map +1 -0
- package/lib/mcp/http.js +602 -0
- package/lib/mcp/oauth.d.ts +149 -0
- package/lib/mcp/oauth.d.ts.map +1 -0
- package/lib/mcp/oauth.js +396 -0
- package/lib/mcp/protocol.d.ts +329 -0
- package/lib/mcp/protocol.d.ts.map +1 -0
- package/lib/mcp/protocol.js +459 -0
- package/lib/mcp/rate-limit.d.ts +68 -0
- package/lib/mcp/rate-limit.d.ts.map +1 -0
- package/lib/mcp/rate-limit.js +114 -0
- package/lib/mcp/sdk-v2.d.ts +17 -0
- package/lib/mcp/sdk-v2.d.ts.map +1 -0
- package/lib/mcp/sdk-v2.js +204 -0
- package/lib/mcp/server.d.ts +149 -0
- package/lib/mcp/server.d.ts.map +1 -0
- package/lib/mcp/server.js +302 -0
- package/lib/mcp/stdio.d.ts +80 -0
- package/lib/mcp/stdio.d.ts.map +1 -0
- package/lib/mcp/stdio.js +184 -0
- package/lib/mcp/subscriptions.d.ts +177 -0
- package/lib/mcp/subscriptions.d.ts.map +1 -0
- package/lib/mcp/subscriptions.js +288 -0
- package/lib/mcp-status.d.ts +96 -0
- package/lib/mcp-status.d.ts.map +1 -0
- package/lib/mcp-status.js +70 -0
- package/lib/mcp-usage.d.ts +110 -0
- package/lib/mcp-usage.d.ts.map +1 -0
- package/lib/mcp-usage.js +189 -0
- package/lib/model-qualification.d.ts +192 -0
- package/lib/model-qualification.d.ts.map +1 -0
- package/lib/model-qualification.js +178 -0
- package/lib/model-runtime-lane.d.ts +69 -0
- package/lib/model-runtime-lane.d.ts.map +1 -0
- package/lib/model-runtime-lane.js +122 -0
- package/lib/model-runtime-recipe.d.ts +132 -0
- package/lib/model-runtime-recipe.d.ts.map +1 -0
- package/lib/model-runtime-recipe.js +291 -0
- package/lib/planning.d.ts +256 -0
- package/lib/planning.d.ts.map +1 -0
- package/lib/planning.js +471 -0
- package/lib/playbook.d.ts +42 -0
- package/lib/playbook.d.ts.map +1 -0
- package/lib/playbook.js +65 -0
- package/lib/prepared-assets.d.ts +269 -0
- package/lib/prepared-assets.d.ts.map +1 -0
- package/lib/prepared-assets.js +471 -0
- package/lib/provider-bridge.d.ts +107 -0
- package/lib/provider-bridge.d.ts.map +1 -0
- package/lib/provider-bridge.js +423 -0
- package/lib/ptc-authority.d.ts +154 -0
- package/lib/ptc-authority.d.ts.map +1 -0
- package/lib/ptc-authority.js +242 -0
- package/lib/ptc-confinement.d.ts +60 -0
- package/lib/ptc-confinement.d.ts.map +1 -0
- package/lib/ptc-confinement.js +212 -0
- package/lib/ptc-runtime.d.ts +166 -0
- package/lib/ptc-runtime.d.ts.map +1 -0
- package/lib/ptc-runtime.js +231 -0
- package/lib/publication.d.ts +161 -0
- package/lib/publication.d.ts.map +1 -0
- package/lib/publication.js +181 -0
- package/lib/receipt-store.d.ts +57 -0
- package/lib/receipt-store.d.ts.map +1 -0
- package/lib/receipt-store.js +143 -0
- package/lib/receipt.d.ts +231 -0
- package/lib/receipt.d.ts.map +1 -0
- package/lib/receipt.js +165 -0
- package/lib/registrar.d.ts +75 -0
- package/lib/registrar.d.ts.map +1 -0
- package/lib/registrar.js +152 -0
- package/lib/resource-governance.d.ts +204 -0
- package/lib/resource-governance.d.ts.map +1 -0
- package/lib/resource-governance.js +213 -0
- package/lib/server-lifecycle.d.ts +175 -0
- package/lib/server-lifecycle.d.ts.map +1 -0
- package/lib/server-lifecycle.js +269 -0
- package/lib/skills.d.ts +31 -0
- package/lib/skills.d.ts.map +1 -0
- package/lib/skills.js +170 -0
- package/lib/speech-bridge.d.ts +74 -0
- package/lib/speech-bridge.d.ts.map +1 -0
- package/lib/speech-bridge.js +115 -0
- package/lib/state.d.ts +100 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +160 -0
- package/lib/title-proposal.d.ts +194 -0
- package/lib/title-proposal.d.ts.map +1 -0
- package/lib/title-proposal.js +244 -0
- package/lib/tool-inventory.d.ts +90 -0
- package/lib/tool-inventory.d.ts.map +1 -0
- package/lib/tool-inventory.js +102 -0
- package/lib/tool-mode-run.d.ts +310 -0
- package/lib/tool-mode-run.d.ts.map +1 -0
- package/lib/tool-mode-run.js +411 -0
- package/lib/tool-sources.d.ts +80 -0
- package/lib/tool-sources.d.ts.map +1 -0
- package/lib/tool-sources.js +123 -0
- package/lib/tool-usage-projection.d.ts +390 -0
- package/lib/tool-usage-projection.d.ts.map +1 -0
- package/lib/tool-usage-projection.js +1038 -0
- package/lib/tool-usage-report.d.ts +153 -0
- package/lib/tool-usage-report.d.ts.map +1 -0
- package/lib/tool-usage-report.js +274 -0
- package/lib/transport.d.ts +158 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +332 -0
- package/lib/types.d.ts +134 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +8 -0
- package/lib/update-surface.d.ts +79 -0
- package/lib/update-surface.d.ts.map +1 -0
- package/lib/update-surface.js +224 -0
- package/lib/version.d.ts +3 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +27 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.d.ts +13 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.js +85 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +87 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +274 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +26 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +53 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +133 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +64 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +121 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +246 -0
- package/node_modules/@try-works/tool-mode-contracts/package.json +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +145 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +33 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +33 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +191 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +137 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +349 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +274 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +932 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +299 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +395 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +839 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts +44 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.js +255 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.d.ts +221 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.js +188 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +128 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.d.ts +41 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.js +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +157 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +688 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +54 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
- package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +460 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +647 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +61 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +1106 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +205 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +119 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.js +201 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +345 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1465 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +231 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +435 -0
- package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
- package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
- package/package.json +127 -4
- package/remote/spec.mjs +993 -0
- package/remote/typert.host.mjs +14 -0
- package/remote/typert.remote-client.mjs +16 -0
|
@@ -0,0 +1,2552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HOST half of the extension-management surface (Settings → Model as Tool).
|
|
3
|
+
*
|
|
4
|
+
* This module implements {@link ModelToolsRemote} from `extensions-api.ts` and is
|
|
5
|
+
* deliberately WIRE-AGNOSTIC: it imports `node:*` and the published SDK, never DSH,
|
|
6
|
+
* never the client page, and never the generated Typert artifact. That boundary is
|
|
7
|
+
* what lets the same implementation be exercised by unit tests without a Context,
|
|
8
|
+
* by the plugin at `apply`, and by whatever transports the browser half uses.
|
|
9
|
+
*
|
|
10
|
+
* The governing rule is TRUTHFULNESS over convenience, because every field here is
|
|
11
|
+
* shown to an operator who is about to change what code their host runs:
|
|
12
|
+
*
|
|
13
|
+
* - `store` is `null` when no extension store is configured, rather than an empty
|
|
14
|
+
* string that a caller could mistake for a mounted store.
|
|
15
|
+
* - `writable` is derived from a real access check on the store path and from the
|
|
16
|
+
* store lock being readable, not assumed from "the config mentions a directory".
|
|
17
|
+
* - `installed` means a record exists in the store lock; `active` means the
|
|
18
|
+
* persisted observation was made by THIS process; `integrity` is `unknown` when
|
|
19
|
+
* the installed bytes could not be digested, never `unchanged` by default.
|
|
20
|
+
* - `reason` names the FIRST condition that prevents install/activation, so a UI
|
|
21
|
+
* can show why an extension is not usable instead of guessing.
|
|
22
|
+
*
|
|
23
|
+
* INSTALLATION IS NOT ACTIVATION (EXT-07): `install` returns `active: false` and
|
|
24
|
+
* never calls the lifecycle. `enable`/`disable` are the only activation paths.
|
|
25
|
+
*
|
|
26
|
+
* FIRST-PARTY DEPENDENCIES (EXT-09): a source-directory install also supplies the
|
|
27
|
+
* SDK's dependency graph (`extension-dependencies.ts`), which resolves every
|
|
28
|
+
* declared runtime dependency from the host's own installed tree and refuses the
|
|
29
|
+
* install by name when one is missing. With the operator's preinstalled-content
|
|
30
|
+
* gate on, the resolved directory is copied by the SDK and recorded with
|
|
31
|
+
* `provenance: 'preinstalled'`; with it off (the default), the dependency is served
|
|
32
|
+
* as verified archive bytes and EXT-10's lifecycle-script refusal applies.
|
|
33
|
+
*
|
|
34
|
+
* PERSISTENCE, STATED HONESTLY:
|
|
35
|
+
*
|
|
36
|
+
* - Activation intent is durable through the SDK: `ExtensionLifecycle.activate`
|
|
37
|
+
* writes `desiredActivation: 'active'` and `deactivate` writes `'inactive'` into
|
|
38
|
+
* the profile state document, so a restart follows the operator's last toggle
|
|
39
|
+
* even when nothing else persists.
|
|
40
|
+
* - The operator's `disabledExtensions` list is a CONFIG value (Settings form). This
|
|
41
|
+
* service cannot write plugin config from the host: no config writer is injected.
|
|
42
|
+
* It therefore keeps this process's toggles as an in-memory override, seeded from
|
|
43
|
+
* `disabledExtensions`, and exposes `persistDisabledExtensions` as the seam a host
|
|
44
|
+
* with a config writer supplies. Without that seam, a toggle is truthfully
|
|
45
|
+
* reported as affecting this process only, and a `disable` survives restart via
|
|
46
|
+
* the SDK's durable `desiredActivation: 'inactive'` while an `enable` of an id
|
|
47
|
+
* still listed in config will be filtered by the restore gate on the next load.
|
|
48
|
+
*
|
|
49
|
+
* @module @try-works/dsh-tool-mode/extensions-service
|
|
50
|
+
*/
|
|
51
|
+
var _a;
|
|
52
|
+
import { accessSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
53
|
+
import { tmpdir } from 'node:os';
|
|
54
|
+
import { dirname, join, resolve } from 'node:path';
|
|
55
|
+
import { createTarGzipArchiveSource, digestInstalledContent, discoverExtension, ExtensionStore, installDependencyGraph, ManagementError, stagedInstall, stagedInstallFromDirectory, } from '@try-works/tool-mode-extension-sdk';
|
|
56
|
+
import { createFirstPartyDependencyGraph, createRegistryDependencyGraph, createRegistrySession, DEFAULT_REGISTRY_URL, MAX_DEPENDENCY_ARCHIVE_BYTES, RegistryError, resolveFirstPartyDependency, satisfiesRegistryRange, } from "./extension-dependencies.js";
|
|
57
|
+
import { createHttpSource } from "./http-acquisition.js";
|
|
58
|
+
import { assetSourceUnavailableReason, firstUnavailableAssetSource, } from "./prepared-assets.js";
|
|
59
|
+
/**
|
|
60
|
+
* The default dependency-graph depth bound for BOTH install arms (task-116).
|
|
61
|
+
*
|
|
62
|
+
* The SDK default (3) is too shallow for real trees. Preinstalled packages are
|
|
63
|
+
* NOT leaves for graph purposes: after copying one, the SDK reads that package's
|
|
64
|
+
* own declared dependencies and enqueues them one level deeper, so a host-tree
|
|
65
|
+
* graph is walked exactly like a registry graph. The real onnxruntime-node tree
|
|
66
|
+
* reaches `define-data-property` at depth 4 and
|
|
67
|
+
* `onnxruntime-node -> global-agent -> globalthis -> define-properties ->
|
|
68
|
+
* has-property-descriptors -> es-define-property` at depth 5, so the bound is a
|
|
69
|
+
* property of the graph, not of the arm that supplied its bytes. The package
|
|
70
|
+
* count is a separate bound (see {@link DEFAULT_REGISTRY_DEPENDENCY_PACKAGES}).
|
|
71
|
+
*/
|
|
72
|
+
export const DEFAULT_REGISTRY_DEPENDENCY_DEPTH = 12;
|
|
73
|
+
/**
|
|
74
|
+
* Operator identity recorded when a mutation requests an approval without one.
|
|
75
|
+
*
|
|
76
|
+
* The management service has no authenticated-operator argument on `enable`; naming
|
|
77
|
+
* the requester honestly as the host is better than inventing a person, and an
|
|
78
|
+
* explicit caller-supplied operator still wins (the request API takes one).
|
|
79
|
+
*/
|
|
80
|
+
const HOST_APPROVAL_OPERATOR = 'host';
|
|
81
|
+
/**
|
|
82
|
+
* The default dependency-graph PACKAGE bound for BOTH install arms.
|
|
83
|
+
*
|
|
84
|
+
* The SDK default (64) is too small for the real engine trees: the
|
|
85
|
+
* `@huggingface/transformers` route (with sharp, protobufjs and the platform
|
|
86
|
+
* binaries) and both minilm packages exceeded it, refusing an install that every
|
|
87
|
+
* other bound would have admitted. Raising the cap does not loosen resolution —
|
|
88
|
+
* every node still has to come from operator-completed host content or verified
|
|
89
|
+
* registry bytes — and the depth bound still shapes the graph.
|
|
90
|
+
*/
|
|
91
|
+
export const DEFAULT_REGISTRY_DEPENDENCY_PACKAGES = 256;
|
|
92
|
+
/**
|
|
93
|
+
* Exact dependency versions whose published tarballs ship their runtime
|
|
94
|
+
* artifacts and whose lifecycle scripts are not needed to run.
|
|
95
|
+
*
|
|
96
|
+
* These are admitted on the registry arm WITHOUT executing a lifecycle script.
|
|
97
|
+
* The package bytes are still integrity-verified and the package.json is left
|
|
98
|
+
* untouched. A version not listed here keeps EXT-10's refusal. This is a
|
|
99
|
+
* deliberately narrow prebuilt-content allowlist, not a script-execution gate.
|
|
100
|
+
*/
|
|
101
|
+
export const DEFAULT_TRUSTED_PREBUILT_DEPENDENCIES = Object.freeze([
|
|
102
|
+
'@desert-ant-labs/tongue@3.6.0',
|
|
103
|
+
'@bjorn3/browser_wasi_shim@0.3.0',
|
|
104
|
+
'onnxruntime-node@1.21.0',
|
|
105
|
+
'onnxruntime-node@1.30.0',
|
|
106
|
+
'onnxruntime-common@1.30.0',
|
|
107
|
+
'onnxruntime-common@1.21.0',
|
|
108
|
+
'onnxruntime-web@1.31.0-dev.20260914-8d85527a0',
|
|
109
|
+
'tar@7.5.22',
|
|
110
|
+
'@isaacs/fs-minipass@4.0.1',
|
|
111
|
+
'chownr@3.0.0',
|
|
112
|
+
'minipass@7.1.3',
|
|
113
|
+
'minizlib@3.1.0',
|
|
114
|
+
'yallist@5.0.0',
|
|
115
|
+
'sharp@0.34.5',
|
|
116
|
+
]);
|
|
117
|
+
/**
|
|
118
|
+
* The readiness values that permit activation (the SDK's callable set).
|
|
119
|
+
*
|
|
120
|
+
* `prepared-cold` is callable: the model loads on first use. Every other value is
|
|
121
|
+
* a concrete blocker this service names in a row reason and in the enable refusal.
|
|
122
|
+
*/
|
|
123
|
+
const CALLABLE_READINESS = new Set([
|
|
124
|
+
'prepared-cold',
|
|
125
|
+
'loading',
|
|
126
|
+
'ready',
|
|
127
|
+
'busy',
|
|
128
|
+
]);
|
|
129
|
+
/**
|
|
130
|
+
* The order an unmet requirement is surfaced in (row reason and enable preflight).
|
|
131
|
+
*
|
|
132
|
+
* Integrity and platform first (nothing the operator can do in the page will clear
|
|
133
|
+
* them), then what the operator CAN act on: dependencies, the harness model, the
|
|
134
|
+
* executable approval, model assets, activation cleanup; store/source last because
|
|
135
|
+
* an installed row already satisfied them.
|
|
136
|
+
*/
|
|
137
|
+
const REQUIREMENT_PRIORITY = [
|
|
138
|
+
'integrity',
|
|
139
|
+
'platform',
|
|
140
|
+
'dependencies',
|
|
141
|
+
'runtime',
|
|
142
|
+
'harness-model',
|
|
143
|
+
'executable',
|
|
144
|
+
'assets',
|
|
145
|
+
'activation',
|
|
146
|
+
'store',
|
|
147
|
+
'source',
|
|
148
|
+
];
|
|
149
|
+
/**
|
|
150
|
+
* Whether one UNMET requirement actually prevents this row from reaching active.
|
|
151
|
+
*
|
|
152
|
+
* A not-yet-installed row is blocked only by facts that would stop the INSTALL or
|
|
153
|
+
* would make the eventual activation impossible (store/source, dependencies,
|
|
154
|
+
* platform, harness model). Readiness, executable approval, integrity and
|
|
155
|
+
* activation state describe installed bytes, so they are not meaningful before an
|
|
156
|
+
* install and do not by themselves make the Install button impossible.
|
|
157
|
+
* @param installed - whether the store holds an installed copy.
|
|
158
|
+
* @param requirement - the requirement to classify.
|
|
159
|
+
* @returns whether the unmet requirement blocks the row.
|
|
160
|
+
*/
|
|
161
|
+
function blocksRow(installed, requirement) {
|
|
162
|
+
if (requirement.satisfied)
|
|
163
|
+
return false;
|
|
164
|
+
if (installed)
|
|
165
|
+
return true;
|
|
166
|
+
return requirement.kind === 'store'
|
|
167
|
+
|| requirement.kind === 'source'
|
|
168
|
+
|| requirement.kind === 'dependencies'
|
|
169
|
+
|| requirement.kind === 'platform'
|
|
170
|
+
|| requirement.kind === 'harness-model'
|
|
171
|
+
|| requirement.kind === 'runtime';
|
|
172
|
+
}
|
|
173
|
+
/** Build one requirement value. */
|
|
174
|
+
function requirement(kind, satisfied, reason, action = 'none') {
|
|
175
|
+
return { kind, satisfied, reason, action };
|
|
176
|
+
}
|
|
177
|
+
/** The highest-priority UNMET requirement that blocks this row, or undefined. */
|
|
178
|
+
function firstBlockingRequirement(requirements, installed) {
|
|
179
|
+
for (const kind of REQUIREMENT_PRIORITY) {
|
|
180
|
+
const found = requirements.find(candidate => candidate.kind === kind);
|
|
181
|
+
if (found !== undefined && blocksRow(installed, found))
|
|
182
|
+
return found;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Record that a restart refused an installed extension because its bytes changed.
|
|
188
|
+
*
|
|
189
|
+
* A restore that only LOGS the integrity failure leaves the previous profile row
|
|
190
|
+
* saying `active`, reserving the extension's tool names while nothing is published.
|
|
191
|
+
* This writes the truthful state first: failed, no tools.
|
|
192
|
+
*/
|
|
193
|
+
export function recordRestoreIntegrityFailure(store, extensionId) {
|
|
194
|
+
const states = store.readProfileState('default').value;
|
|
195
|
+
store.writeProfileState('default', states.map(state => state.extensionId === extensionId
|
|
196
|
+
? { ...state, observedActivation: 'failed', tools: [] }
|
|
197
|
+
: state));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A named refusal from the extension-management surface.
|
|
201
|
+
*
|
|
202
|
+
* The `code` is stable and machine-readable; the message is operator-facing and
|
|
203
|
+
* names the concrete condition (path, SDK code, or identity) rather than a generic
|
|
204
|
+
* "failed", because the Settings page shows it verbatim.
|
|
205
|
+
*/
|
|
206
|
+
export class ExtensionsServiceError extends Error {
|
|
207
|
+
name = 'ExtensionsServiceError';
|
|
208
|
+
code;
|
|
209
|
+
constructor(code, message, options) {
|
|
210
|
+
super(message, options);
|
|
211
|
+
this.code = code;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The directory a first-party checkout for one package identity occupies.
|
|
216
|
+
*
|
|
217
|
+
* A source directory holds PACKAGE DIRECTORIES, not scoped directories: the
|
|
218
|
+
* workspace layout is `packages/extension-laya`, whose package name is
|
|
219
|
+
* `@try-works/extension-laya`. The scope is therefore dropped and the final path
|
|
220
|
+
* segment is used, which is the only mapping that requires no registry lookup.
|
|
221
|
+
* @param extensionId - the extension identity, which equals its package name.
|
|
222
|
+
* @returns the directory name under the configured source directory.
|
|
223
|
+
*/
|
|
224
|
+
export function packageDirFor(extensionId) {
|
|
225
|
+
const slash = extensionId.lastIndexOf('/');
|
|
226
|
+
return slash === -1 ? extensionId : extensionId.slice(slash + 1);
|
|
227
|
+
}
|
|
228
|
+
/** A package identity: one optional scope segment, then one name segment. */
|
|
229
|
+
const PACKAGE_ID = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
230
|
+
/**
|
|
231
|
+
* Refuse an identity that cannot safely become a path component.
|
|
232
|
+
*
|
|
233
|
+
* `packageDirFor` is joined onto the source directory, so an id such as `..` or
|
|
234
|
+
* one carrying a separator would address a directory outside the configured root.
|
|
235
|
+
* @param extensionId - the requested id.
|
|
236
|
+
* @throws ExtensionsServiceError `extension-id-invalid`.
|
|
237
|
+
*/
|
|
238
|
+
function assertUsableId(extensionId) {
|
|
239
|
+
if (!PACKAGE_ID.test(extensionId)) {
|
|
240
|
+
throw new ExtensionsServiceError('extension-id-invalid', `${JSON.stringify(extensionId)} is not a package identity; expected a name like @scope/name`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** A message from an arbitrary thrown value. */
|
|
244
|
+
function messageOf(error) {
|
|
245
|
+
return error instanceof Error ? error.message : String(error);
|
|
246
|
+
}
|
|
247
|
+
/** Facts for a row with no readable package directory. */
|
|
248
|
+
const EMPTY_FACTS = {
|
|
249
|
+
packageName: null,
|
|
250
|
+
packageVersion: null,
|
|
251
|
+
packageDir: null,
|
|
252
|
+
tools: [],
|
|
253
|
+
models: [],
|
|
254
|
+
executionKind: undefined,
|
|
255
|
+
modelRuntimeRecipes: 0,
|
|
256
|
+
harnessRecipes: 0,
|
|
257
|
+
platforms: [],
|
|
258
|
+
dependencies: [],
|
|
259
|
+
optionalDependencies: [],
|
|
260
|
+
problem: null,
|
|
261
|
+
};
|
|
262
|
+
/** Read one package.json dependency field as name→range pairs; empty when unreadable. */
|
|
263
|
+
function readDeclaredDependencies(packageDir, field = 'dependencies') {
|
|
264
|
+
if (packageDir === null)
|
|
265
|
+
return [];
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
268
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
269
|
+
return [];
|
|
270
|
+
// SAFETY: the value came from JSON.parse and the object guard above; only
|
|
271
|
+
// string-valued dependency specs are kept, so an unexpected shape yields an
|
|
272
|
+
// empty declaration rather than a trusted one.
|
|
273
|
+
const dependencies = parsed[field];
|
|
274
|
+
if (typeof dependencies !== 'object' || dependencies === null || Array.isArray(dependencies))
|
|
275
|
+
return [];
|
|
276
|
+
const declared = [];
|
|
277
|
+
// SAFETY: the dependency container was checked to be a non-array object; only
|
|
278
|
+
// string entries become declared dependencies, and non-strings are ignored.
|
|
279
|
+
for (const [name, range] of Object.entries(dependencies)) {
|
|
280
|
+
if (typeof range === 'string' && range.length > 0)
|
|
281
|
+
declared.push({ name, range });
|
|
282
|
+
}
|
|
283
|
+
return declared;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Count the declared implementations by RECIPE KIND (PHASE2 § Host lane).
|
|
291
|
+
*
|
|
292
|
+
* The kind is read from the recipe file, because that is what the host branches on: a
|
|
293
|
+
* `model-runtime` recipe is served by its own factory and needs no harness provider, while a
|
|
294
|
+
* harness recipe (kind absent or `harness-model`) still does. An UNREADABLE recipe is counted
|
|
295
|
+
* as harness: demanding a provider the legacy lane will use is the safe direction, and the
|
|
296
|
+
* legacy lane refuses the unreadable recipe by name anyway.
|
|
297
|
+
* @param packageDir - the package directory.
|
|
298
|
+
* @param implementationIds - the manifest's implementations.
|
|
299
|
+
* @returns how many implementations each lane serves.
|
|
300
|
+
*/
|
|
301
|
+
function readDeclarativeRecipeKinds(packageDir, implementationIds) {
|
|
302
|
+
let modelRuntime = 0;
|
|
303
|
+
let harness = 0;
|
|
304
|
+
for (const id of implementationIds) {
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(readFileSync(join(packageDir, 'recipes', `${id}.json`), 'utf8'));
|
|
307
|
+
const kind = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
308
|
+
? parsed.kind
|
|
309
|
+
: undefined;
|
|
310
|
+
if (kind === 'model-runtime')
|
|
311
|
+
modelRuntime += 1;
|
|
312
|
+
else
|
|
313
|
+
harness += 1;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
harness += 1;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { modelRuntime, harness };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Read one package directory's declared facts without importing or executing it.
|
|
323
|
+
* @param packageDir - the installed copy or first-party checkout.
|
|
324
|
+
* @returns the facts, with `problem` set (and manifest facts empty) when discovery fails.
|
|
325
|
+
*/
|
|
326
|
+
function readExtensionFacts(packageDir) {
|
|
327
|
+
const dependencies = readDeclaredDependencies(packageDir);
|
|
328
|
+
const optionalDependencies = readDeclaredDependencies(packageDir, 'optionalDependencies');
|
|
329
|
+
try {
|
|
330
|
+
const discovery = discoverExtension(packageDir);
|
|
331
|
+
const manifest = discovery.manifest;
|
|
332
|
+
const recipeKinds = manifest.execution.kind === 'declarative'
|
|
333
|
+
? readDeclarativeRecipeKinds(packageDir, manifest.implementations.map(implementation => implementation.id))
|
|
334
|
+
: { modelRuntime: 0, harness: 0 };
|
|
335
|
+
return {
|
|
336
|
+
packageName: discovery.packageName,
|
|
337
|
+
packageVersion: discovery.packageVersion,
|
|
338
|
+
packageDir,
|
|
339
|
+
tools: manifest.tools.map(tool => tool.name),
|
|
340
|
+
models: manifest.models.map(model => model.id),
|
|
341
|
+
executionKind: manifest.execution.kind,
|
|
342
|
+
modelRuntimeRecipes: recipeKinds.modelRuntime,
|
|
343
|
+
harnessRecipes: recipeKinds.harness,
|
|
344
|
+
platforms: manifest.models.flatMap(model => model.platforms),
|
|
345
|
+
dependencies,
|
|
346
|
+
optionalDependencies,
|
|
347
|
+
problem: null,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return {
|
|
352
|
+
...EMPTY_FACTS,
|
|
353
|
+
packageName: null,
|
|
354
|
+
packageVersion: null,
|
|
355
|
+
packageDir,
|
|
356
|
+
dependencies,
|
|
357
|
+
optionalDependencies,
|
|
358
|
+
problem: `the package at ${packageDir} is not a usable extension: ${messageOf(error)}`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** A short human label for one declared platform. */
|
|
363
|
+
function platformLabel(platform) {
|
|
364
|
+
return `${platform.os}-${platform.arch}${platform.accelerator === undefined ? '' : `-${platform.accelerator}`}`;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Whether the host's own platform matches any declared deployment platform.
|
|
368
|
+
* @param platforms - the manifest's declared platforms.
|
|
369
|
+
* @returns whether this host can run the declared deployment.
|
|
370
|
+
*/
|
|
371
|
+
function hostPlatformMatches(platforms) {
|
|
372
|
+
return platforms.some(platform => platform.os === process.platform && platform.arch === process.arch);
|
|
373
|
+
}
|
|
374
|
+
/** The sidecar filename inside the extension store (host-owned, never inside a package). */
|
|
375
|
+
const RUNTIME_DEPENDENCY_FILE = 'runtime-dependencies.json';
|
|
376
|
+
/** Read the store's runtime-dependency sidecar; a missing/corrupt document is empty. */
|
|
377
|
+
function readRuntimeDependencyDocument(storeDir) {
|
|
378
|
+
try {
|
|
379
|
+
const parsed = JSON.parse(readFileSync(join(storeDir, RUNTIME_DEPENDENCY_FILE), 'utf8'));
|
|
380
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
381
|
+
return { updatedAt: '', extensions: {} };
|
|
382
|
+
const extensions = parsed.extensions;
|
|
383
|
+
if (typeof extensions !== 'object' || extensions === null || Array.isArray(extensions)) {
|
|
384
|
+
return { updatedAt: '', extensions: {} };
|
|
385
|
+
}
|
|
386
|
+
// SAFETY: the container was checked to be a non-array object; each entry is
|
|
387
|
+
// read defensively by the requirement model, and a malformed entry is ignored.
|
|
388
|
+
return { updatedAt: '', extensions: extensions };
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return { updatedAt: '', extensions: {} };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Merge one extension's runtime attempts into the store sidecar (atomic replace). */
|
|
395
|
+
function writeRuntimeDependencyDocument(storeDir, extensionId, version, optional) {
|
|
396
|
+
try {
|
|
397
|
+
const current = readRuntimeDependencyDocument(storeDir);
|
|
398
|
+
const document = {
|
|
399
|
+
updatedAt: new Date().toISOString(),
|
|
400
|
+
extensions: {
|
|
401
|
+
...current.extensions,
|
|
402
|
+
[extensionId]: { version, optional },
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
const target = join(storeDir, RUNTIME_DEPENDENCY_FILE);
|
|
406
|
+
const temporary = `${target}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
407
|
+
writeFileSync(temporary, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
|
|
408
|
+
rmSync(target, { force: true });
|
|
409
|
+
cpSync(temporary, target);
|
|
410
|
+
rmSync(temporary, { force: true });
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// Diagnostics only: failing to record an optional-runtime attempt must never
|
|
414
|
+
// turn a successful install into a failure. The requirement probes the tree.
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Read one installed dependency's package.json version, or undefined. */
|
|
418
|
+
function readInstalledDependencyVersion(installPath, name) {
|
|
419
|
+
try {
|
|
420
|
+
const parsed = JSON.parse(readFileSync(join(installPath, 'node_modules', ...name.split('/'), 'package.json'), 'utf8'));
|
|
421
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
422
|
+
return undefined;
|
|
423
|
+
const version = parsed.version;
|
|
424
|
+
return typeof version === 'string' ? version : undefined;
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Atomically REPLACE one installed record in the store lock.
|
|
432
|
+
*
|
|
433
|
+
* The SDK's `recordInstall` deliberately refuses an already-recorded identity
|
|
434
|
+
* (upgrade protection), and it exposes no update call. The host legitimately
|
|
435
|
+
* changes an installed copy AFTER activation-free materialization (optional tool
|
|
436
|
+
* runtimes): the recorded content digest must match the bytes the operator now
|
|
437
|
+
* has, and the lock is the host-owned document that carries it. The replacement is
|
|
438
|
+
* a same-file atomic rename, so a crash leaves either the old or the new record.
|
|
439
|
+
* @param storeDir - the configured store.
|
|
440
|
+
* @param updated - the record to store, keyed by identity and version.
|
|
441
|
+
* @returns whether the record was found and replaced.
|
|
442
|
+
*/
|
|
443
|
+
function replaceInstalledRecord(storeDir, updated) {
|
|
444
|
+
try {
|
|
445
|
+
const store = new ExtensionStore(storeDir);
|
|
446
|
+
const lock = store.readLock();
|
|
447
|
+
if (lock.corrupt)
|
|
448
|
+
return false;
|
|
449
|
+
const index = lock.value.extensions.findIndex(candidate => candidate.extensionId === updated.extensionId && candidate.version === updated.version);
|
|
450
|
+
if (index === -1)
|
|
451
|
+
return false;
|
|
452
|
+
const extensions = [...lock.value.extensions];
|
|
453
|
+
extensions[index] = updated;
|
|
454
|
+
const target = store.lockPath;
|
|
455
|
+
const temporary = `${target}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
456
|
+
writeFileSync(temporary, `${JSON.stringify({ lockVersion: 1, extensions }, null, 2)}\n`, 'utf8');
|
|
457
|
+
renameSync(temporary, target);
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Directories to search for OPERATOR-PREINSTALLED packages, deepest-nested first.
|
|
466
|
+
*
|
|
467
|
+
* A preinstalled package is a LEAF: its transitive dependencies already sit in its
|
|
468
|
+
* OWN `node_modules` (npm nests them when versions conflict), so root-level lookup
|
|
469
|
+
* alone misses e.g. `@huggingface/transformers/node_modules/onnxruntime-web`. This
|
|
470
|
+
* enumerates the preinstalled root, each top-level package directory, and (bounded)
|
|
471
|
+
* their nested package directories; the resolver tries each in order.
|
|
472
|
+
* @param root - the operator-configured preinstalled root (its `node_modules` is searched).
|
|
473
|
+
* @param depth - how many nested package levels to enumerate (bounded).
|
|
474
|
+
* @returns candidate package roots, root first.
|
|
475
|
+
*/
|
|
476
|
+
function preinstalledSearchRoots(root, depth = 3) {
|
|
477
|
+
const candidates = [root];
|
|
478
|
+
const visit = (nodeModules, remaining) => {
|
|
479
|
+
if (remaining <= 0)
|
|
480
|
+
return;
|
|
481
|
+
let entries;
|
|
482
|
+
try {
|
|
483
|
+
entries = readdirSync(nodeModules);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
for (const entry of entries) {
|
|
489
|
+
if (entry.startsWith('.'))
|
|
490
|
+
continue;
|
|
491
|
+
const entryPath = join(nodeModules, entry);
|
|
492
|
+
if (entry.startsWith('@')) {
|
|
493
|
+
let scoped;
|
|
494
|
+
try {
|
|
495
|
+
scoped = readdirSync(entryPath);
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
for (const name of scoped) {
|
|
501
|
+
const packageDir = join(entryPath, name);
|
|
502
|
+
candidates.push(packageDir);
|
|
503
|
+
visit(join(packageDir, 'node_modules'), remaining - 1);
|
|
504
|
+
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
candidates.push(entryPath);
|
|
508
|
+
visit(join(entryPath, 'node_modules'), remaining - 1);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
visit(join(root, 'node_modules'), depth);
|
|
512
|
+
return candidates;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Every declared deployment whose recorded readiness does not permit activation.
|
|
516
|
+
*
|
|
517
|
+
* A deployment with NO recorded readiness is reported as `needs-preparation`
|
|
518
|
+
* because that is the constant the SDK's lifecycle projects for an undeclared
|
|
519
|
+
* state (`#readinessOf` defaults every declared model to `needs-preparation`);
|
|
520
|
+
* reporting anything else would invent a fact the host does not hold.
|
|
521
|
+
* @param readiness - the persisted readiness map (may be empty).
|
|
522
|
+
* @param declaredModels - the manifest's model deployment ids.
|
|
523
|
+
* @returns one operator-facing line per blocker, most actionable phrasing first.
|
|
524
|
+
*/
|
|
525
|
+
function preparationBlockers(readiness, declaredModels) {
|
|
526
|
+
const blockers = [];
|
|
527
|
+
for (const modelId of declaredModels) {
|
|
528
|
+
const value = readiness[modelId] ?? 'needs-preparation';
|
|
529
|
+
if (CALLABLE_READINESS.has(value))
|
|
530
|
+
continue;
|
|
531
|
+
if (value === 'needs-preparation') {
|
|
532
|
+
blockers.push(`${modelId} needs its model assets prepared — run prepare for this extension`);
|
|
533
|
+
}
|
|
534
|
+
else if (value === 'preparing') {
|
|
535
|
+
blockers.push(`${modelId} is still preparing its model assets`);
|
|
536
|
+
}
|
|
537
|
+
else if (value === 'unavailable') {
|
|
538
|
+
blockers.push(`${modelId} is unavailable on this host — its runtime or assets are missing`);
|
|
539
|
+
}
|
|
540
|
+
else if (value === 'failed') {
|
|
541
|
+
blockers.push(`${modelId} failed inspection on this host — repair or reinstall its assets, then prepare again`);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
blockers.push(`${modelId} is ${value}, which does not permit activation`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return blockers;
|
|
548
|
+
}
|
|
549
|
+
/** Project the SDK/coordinator readiness map onto the wire readiness vocabulary. */
|
|
550
|
+
function readinessView(readiness, declaredModels) {
|
|
551
|
+
const view = {};
|
|
552
|
+
for (const modelId of declaredModels) {
|
|
553
|
+
const value = readiness[modelId];
|
|
554
|
+
// SAFETY: a readiness value read from the SDK's state is one of the SDK's
|
|
555
|
+
// ModelReadiness literals, which is the same union as ExtensionModelReadiness;
|
|
556
|
+
// an absent entry is projected as its documented default.
|
|
557
|
+
view[modelId] = value === undefined ? 'needs-preparation' : value;
|
|
558
|
+
}
|
|
559
|
+
return view;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Project the coordinator's plan onto the wire disclosure.
|
|
563
|
+
*
|
|
564
|
+
* The digest and identity fields the provider needs stay host-side; the page gets
|
|
565
|
+
* the bytes, sources, executable flags, totals, terms and revision an operator
|
|
566
|
+
* approves (the same fields the management channel discloses).
|
|
567
|
+
* @param plan - the resolved preparation plan.
|
|
568
|
+
* @returns the wire disclosure.
|
|
569
|
+
*/
|
|
570
|
+
function preparationPlanView(plan) {
|
|
571
|
+
return {
|
|
572
|
+
modelId: plan.identity.modelId,
|
|
573
|
+
modelRevision: plan.identity.modelRevision,
|
|
574
|
+
assets: plan.assets.map(asset => ({
|
|
575
|
+
assetId: asset.assetId,
|
|
576
|
+
bytes: asset.bytes,
|
|
577
|
+
source: asset.source,
|
|
578
|
+
executable: asset.executable,
|
|
579
|
+
})),
|
|
580
|
+
totalBytes: plan.totalBytes,
|
|
581
|
+
storageBytes: plan.storageBytes,
|
|
582
|
+
requiresExecutableApproval: plan.requiresExecutableApproval,
|
|
583
|
+
license: { name: plan.license.name, url: plan.license.url },
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Whether this process is likely able to create or write inside a directory.
|
|
588
|
+
*
|
|
589
|
+
* `access(W_OK)` is the strongest check available without performing a write, and
|
|
590
|
+
* it can still report success on a Windows ACL that later denies one. That gap is
|
|
591
|
+
* the reason every action names its write failure instead of trusting `writable`.
|
|
592
|
+
* @param directory - the store directory (it may not exist yet).
|
|
593
|
+
* @returns whether the nearest existing ancestor is a writable directory.
|
|
594
|
+
*/
|
|
595
|
+
function directoryWritable(directory) {
|
|
596
|
+
try {
|
|
597
|
+
let candidate = resolve(directory);
|
|
598
|
+
while (!existsSync(candidate)) {
|
|
599
|
+
const parent = dirname(candidate);
|
|
600
|
+
if (parent === candidate)
|
|
601
|
+
return false;
|
|
602
|
+
candidate = parent;
|
|
603
|
+
}
|
|
604
|
+
if (!statSync(candidate).isDirectory())
|
|
605
|
+
return false;
|
|
606
|
+
accessSync(candidate, constants.W_OK);
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* The SDK lifecycle state for one profile, indexed by extension id.
|
|
615
|
+
* @param store - the extension store.
|
|
616
|
+
* @param profile - the lifecycle profile.
|
|
617
|
+
* @returns the states and whether the document could be trusted.
|
|
618
|
+
*/
|
|
619
|
+
function readProfileStates(store, profile) {
|
|
620
|
+
const result = store.readProfileState(profile);
|
|
621
|
+
const byId = new Map();
|
|
622
|
+
for (const state of result.value)
|
|
623
|
+
byId.set(state.extensionId, state);
|
|
624
|
+
return { byId, corrupt: result.corrupt, diagnostic: result.diagnostic ?? null };
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* The activation-failure reason the store recorded for one extension, if any.
|
|
628
|
+
*
|
|
629
|
+
* Why this is read structurally rather than through `SdkExtensionState`: the field
|
|
630
|
+
* is ADDITIVE and OPTIONAL, so a profile document written by an older SDK has no
|
|
631
|
+
* such key (and must still parse), and a host whose SDK TYPE has not been rebuilt
|
|
632
|
+
* still receives the value at RUNTIME because the store round-trips the whole
|
|
633
|
+
* object. Reading it here keeps the row honest in both worlds without inventing a
|
|
634
|
+
* cause: only a non-blank string is trusted, and anything else — absent, empty,
|
|
635
|
+
* non-string — falls back to the generic failed-activation text.
|
|
636
|
+
* @param state - the persisted state for one extension, when the profile has it.
|
|
637
|
+
* @returns the recorded reason, or undefined when none is recorded.
|
|
638
|
+
*/
|
|
639
|
+
function recordedActivationError(state) {
|
|
640
|
+
if (state === undefined)
|
|
641
|
+
return undefined;
|
|
642
|
+
// SAFETY: the persisted document is this module's data boundary. `activationError`
|
|
643
|
+
// is declared optional on the SDK state and is additive across versions, so the
|
|
644
|
+
// property is read through an intersection with the SDK type (which keeps the cast
|
|
645
|
+
// comparable) rather than requiring a rebuilt SDK type; the `typeof` guard below
|
|
646
|
+
// is what makes the value safe to render.
|
|
647
|
+
const value = state.activationError;
|
|
648
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
|
|
649
|
+
}
|
|
650
|
+
/** Read a candidate package.json name for a directory whose discovery failed. */
|
|
651
|
+
function declaredPackageName(directory) {
|
|
652
|
+
try {
|
|
653
|
+
const parsed = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
|
|
654
|
+
if (parsed === null)
|
|
655
|
+
return undefined;
|
|
656
|
+
// SAFETY: the value was parsed from the package's own package.json, and a
|
|
657
|
+
// non-string `name` is rejected below rather than trusted. The null guard above
|
|
658
|
+
// is what keeps the property read total for this boundary.
|
|
659
|
+
const name = parsed.name;
|
|
660
|
+
return typeof name === 'string' && PACKAGE_ID.test(name) ? name : undefined;
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Discover every first-party package under a source directory.
|
|
668
|
+
*
|
|
669
|
+
* A directory whose package.json is unreadable is not listed at all: without an
|
|
670
|
+
* identity there is nothing truthful to name. A directory whose package.json names
|
|
671
|
+
* a package but whose manifest is unusable is listed WITH the reason, so the
|
|
672
|
+
* operator can see the broken checkout instead of an unexplained absence.
|
|
673
|
+
* @param sourceDir - the configured source directory.
|
|
674
|
+
* @returns packages in directory-name order.
|
|
675
|
+
*/
|
|
676
|
+
function listSourcePackages(sourceDir) {
|
|
677
|
+
let entries;
|
|
678
|
+
try {
|
|
679
|
+
entries = readdirSync(sourceDir, { withFileTypes: true });
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
const packages = [];
|
|
685
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
686
|
+
if (!entry.isDirectory())
|
|
687
|
+
continue;
|
|
688
|
+
const directory = join(sourceDir, entry.name);
|
|
689
|
+
const facts = readExtensionFacts(directory);
|
|
690
|
+
if (facts.packageName !== null && facts.problem === null) {
|
|
691
|
+
packages.push({
|
|
692
|
+
id: facts.packageName,
|
|
693
|
+
version: facts.packageVersion,
|
|
694
|
+
tools: facts.tools,
|
|
695
|
+
facts,
|
|
696
|
+
problem: null,
|
|
697
|
+
});
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const id = facts.packageName ?? declaredPackageName(directory);
|
|
701
|
+
if (id === undefined)
|
|
702
|
+
continue;
|
|
703
|
+
packages.push({
|
|
704
|
+
id,
|
|
705
|
+
version: facts.packageVersion,
|
|
706
|
+
tools: facts.tools,
|
|
707
|
+
facts: facts.problem === null
|
|
708
|
+
? { ...facts, problem: `the package at ${directory} could not be discovered as an extension` }
|
|
709
|
+
: facts,
|
|
710
|
+
problem: facts.problem ?? `the package at ${directory} could not be discovered as an extension`,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
return packages;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Select the current installed record for one identity.
|
|
717
|
+
*
|
|
718
|
+
* The EXT-30 active-version pointer wins when it names an installed version; a
|
|
719
|
+
* store without pointers (the pre-update world) uses the last recorded version,
|
|
720
|
+
* which is the order `recordInstall` appends in.
|
|
721
|
+
* @param records - every installed record.
|
|
722
|
+
* @param activeVersions - the EXT-30 pointer map.
|
|
723
|
+
* @param extensionId - the identity to select.
|
|
724
|
+
* @returns the selected record, or undefined when nothing is installed for the id.
|
|
725
|
+
*/
|
|
726
|
+
export function selectInstalled(records, activeVersions, extensionId) {
|
|
727
|
+
const versions = records.filter(record => record.extensionId === extensionId);
|
|
728
|
+
const newest = versions[versions.length - 1];
|
|
729
|
+
if (newest === undefined)
|
|
730
|
+
return undefined;
|
|
731
|
+
const pointedVersion = activeVersions[extensionId];
|
|
732
|
+
return pointedVersion === undefined
|
|
733
|
+
? newest
|
|
734
|
+
: versions.find(candidate => candidate.version === pointedVersion) ?? newest;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* The extension-management service: describe, install, enable, disable.
|
|
738
|
+
*
|
|
739
|
+
* Construct one per host and share it between the wire transport and any local
|
|
740
|
+
* surface; it owns the in-process override list and must not be duplicated, or two
|
|
741
|
+
* instances would report different disabled sets.
|
|
742
|
+
*/
|
|
743
|
+
export class ExtensionsService {
|
|
744
|
+
#options;
|
|
745
|
+
/**
|
|
746
|
+
* This process's disabled choices, layered over `config.disabledExtensions`.
|
|
747
|
+
*
|
|
748
|
+
* An override lives for the life of the process: a Settings save that rewrites
|
|
749
|
+
* the config list cannot silently undo a toggle the operator just made here, and
|
|
750
|
+
* the override is dropped on restart, when the persisted config is read again.
|
|
751
|
+
*/
|
|
752
|
+
#overrides = new Map();
|
|
753
|
+
constructor(options = {}) {
|
|
754
|
+
this.#options = options;
|
|
755
|
+
}
|
|
756
|
+
/** The configured store as an absolute path, or `''` when unconfigured. */
|
|
757
|
+
#storeDir() {
|
|
758
|
+
const configured = this.#options.storeDir ?? '';
|
|
759
|
+
return configured.trim().length === 0 ? '' : resolve(configured);
|
|
760
|
+
}
|
|
761
|
+
/** The configured source directory as an absolute path, or `''` when unconfigured. */
|
|
762
|
+
#sourceDir() {
|
|
763
|
+
const configured = this.#options.sourceDir ?? '';
|
|
764
|
+
return configured.trim().length === 0 ? '' : resolve(configured);
|
|
765
|
+
}
|
|
766
|
+
/** The lifecycle profile this service reads and writes. */
|
|
767
|
+
#profile() {
|
|
768
|
+
return this.#options.profile ?? 'default';
|
|
769
|
+
}
|
|
770
|
+
/** The mounted lifecycle, or undefined when the host mounted none. */
|
|
771
|
+
#lifecycle() {
|
|
772
|
+
return this.#options.lifecycle?.();
|
|
773
|
+
}
|
|
774
|
+
/** The mounted approval state machine, or undefined when the host mounted none. */
|
|
775
|
+
#approvals() {
|
|
776
|
+
return this.#options.approvals?.();
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Require the approval state machine.
|
|
780
|
+
*
|
|
781
|
+
* A request or a commit on a host that mounted none would create a record nothing
|
|
782
|
+
* can consume, or claim an approval that was never recorded; both are refusals.
|
|
783
|
+
* @returns the mounted store.
|
|
784
|
+
* @throws ExtensionsServiceError `extension-approval-required`.
|
|
785
|
+
*/
|
|
786
|
+
#requireApprovals() {
|
|
787
|
+
const approvals = this.#approvals();
|
|
788
|
+
if (approvals === undefined) {
|
|
789
|
+
throw new ExtensionsServiceError('extension-approval-required', 'no approval store is mounted, so a management-sensitive mutation cannot be requested or committed here');
|
|
790
|
+
}
|
|
791
|
+
return approvals;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Require a configured store.
|
|
795
|
+
* @throws ExtensionsServiceError `extension-store-unconfigured`.
|
|
796
|
+
* @returns the absolute store directory.
|
|
797
|
+
*/
|
|
798
|
+
#requireStoreDir() {
|
|
799
|
+
const storeDir = this.#storeDir();
|
|
800
|
+
if (storeDir === '') {
|
|
801
|
+
throw new ExtensionsServiceError('extension-store-unconfigured', 'no extension store is configured (config.extensionStore); install and activation state cannot be read or changed');
|
|
802
|
+
}
|
|
803
|
+
return storeDir;
|
|
804
|
+
}
|
|
805
|
+
/** The disabled set this process reports: configured ids, adjusted by local toggles. */
|
|
806
|
+
#effectiveDisabled() {
|
|
807
|
+
const ids = new Set(this.#options.disabledExtensions?.() ?? []);
|
|
808
|
+
for (const [id, disabled] of this.#overrides) {
|
|
809
|
+
if (disabled)
|
|
810
|
+
ids.add(id);
|
|
811
|
+
else
|
|
812
|
+
ids.delete(id);
|
|
813
|
+
}
|
|
814
|
+
return ids;
|
|
815
|
+
}
|
|
816
|
+
/** Record one toggle in this process. */
|
|
817
|
+
#rememberDisabled(extensionId, disabled) {
|
|
818
|
+
this.#overrides.set(extensionId, disabled);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Hand the effective disabled list to the host's writer, when one exists.
|
|
822
|
+
* @throws ExtensionsServiceError `extension-override-persist-failed` when the
|
|
823
|
+
* writer rejects; the caller reports the partial outcome.
|
|
824
|
+
*/
|
|
825
|
+
async #persistDisabled() {
|
|
826
|
+
const persist = this.#options.persistDisabledExtensions;
|
|
827
|
+
if (persist === undefined)
|
|
828
|
+
return;
|
|
829
|
+
const ids = [...this.#effectiveDisabled()].sort();
|
|
830
|
+
try {
|
|
831
|
+
await persist(ids);
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
throw new ExtensionsServiceError('extension-override-persist-failed', `the disabled list could not be persisted, so the change applies to this process only: ${messageOf(error)}`, { cause: error });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/** The content digest, through the test seam when one was supplied. */
|
|
838
|
+
#digest(directory) {
|
|
839
|
+
const digest = this.#options.digestContent ?? digestInstalledContent;
|
|
840
|
+
return digest(directory);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Select the installed version to operate on, refusing to guess over a corrupt store.
|
|
844
|
+
*
|
|
845
|
+
* A corrupt lock or profile document yields no records from the SDK's fail-closed
|
|
846
|
+
* read, and treating that as "not installed" would tell the operator the package
|
|
847
|
+
* is absent when the truth is that the store cannot be read.
|
|
848
|
+
* @param storeDir - the configured store.
|
|
849
|
+
* @param extensionId - the identity to select.
|
|
850
|
+
* @returns the current installed record.
|
|
851
|
+
* @throws ExtensionsServiceError `extension-store-unreadable` or `extension-not-installed`.
|
|
852
|
+
*/
|
|
853
|
+
#requireInstalled(storeDir, extensionId) {
|
|
854
|
+
const store = new ExtensionStore(storeDir);
|
|
855
|
+
const lock = store.readLock();
|
|
856
|
+
if (lock.corrupt) {
|
|
857
|
+
throw new ExtensionsServiceError('extension-store-unreadable', `the extension store lock at ${store.lockPath} is unreadable, so ${extensionId} cannot be managed: ${lock.diagnostic ?? 'unknown reason'}`);
|
|
858
|
+
}
|
|
859
|
+
const pointers = store.readProfilePointers(this.#profile());
|
|
860
|
+
if (pointers.corrupt) {
|
|
861
|
+
throw new ExtensionsServiceError('extension-store-unreadable', `the profile activation state for ${this.#profile()} is unreadable, so ${extensionId} cannot be managed: ${pointers.diagnostic ?? 'unknown reason'}`);
|
|
862
|
+
}
|
|
863
|
+
const selection = selectInstalled(lock.value.extensions, pointers.value.activeVersions, extensionId);
|
|
864
|
+
if (selection === undefined) {
|
|
865
|
+
throw new ExtensionsServiceError('extension-not-installed', `${extensionId} has no installed record in ${storeDir}; install it before changing its activation state`);
|
|
866
|
+
}
|
|
867
|
+
return selection;
|
|
868
|
+
}
|
|
869
|
+
/** The directory installer, through the test seam when one was supplied. */
|
|
870
|
+
#install(options) {
|
|
871
|
+
const install = this.#options.installDirectory ?? stagedInstallFromDirectory;
|
|
872
|
+
return install(options);
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* The dependency graph a first-party source install resolves against.
|
|
876
|
+
*
|
|
877
|
+
* The SDK calls it only when the package declares runtime dependencies; the
|
|
878
|
+
* default graph resolves each one from the host's own installed tree and refuses
|
|
879
|
+
* by name when it cannot. Constructing it is cheap and performs no I/O.
|
|
880
|
+
*
|
|
881
|
+
* DEPTH: the same bound as the registry arm (task-116). The SDK checks
|
|
882
|
+
* `entry.depth` for EVERY materialized dependency, and a preinstalled copy is not
|
|
883
|
+
* a leaf: after copying it the SDK reads that package's own declared
|
|
884
|
+
* dependencies and enqueues them one level deeper. Omitting maxDepth therefore
|
|
885
|
+
* inherits the SDK's 3-level default and refuses the real onnxruntime-node tree,
|
|
886
|
+
* whose first over-depth entry is `define-data-property` at depth 4 (the deepest
|
|
887
|
+
* is `es-define-property` at depth 5). The package bound
|
|
888
|
+
* ({@link DEFAULT_REGISTRY_DEPENDENCY_PACKAGES}) caps the graph independently.
|
|
889
|
+
* @param sourcePackageDir - the checkout being installed.
|
|
890
|
+
* @returns the graph the SDK's directory installer consumes.
|
|
891
|
+
*/
|
|
892
|
+
#dependencyGraph(sourcePackageDir) {
|
|
893
|
+
const supplied = this.#options.dependencyGraph;
|
|
894
|
+
if (supplied !== undefined)
|
|
895
|
+
return supplied(sourcePackageDir);
|
|
896
|
+
const root = this.#options.preinstalledDependenciesRoot;
|
|
897
|
+
const depth = this.#options.registryDependencyDepth ?? DEFAULT_REGISTRY_DEPENDENCY_DEPTH;
|
|
898
|
+
const packages = this.#options.registryDependencyPackages ?? DEFAULT_REGISTRY_DEPENDENCY_PACKAGES;
|
|
899
|
+
const base = {
|
|
900
|
+
sourcePackageDir,
|
|
901
|
+
allowPreinstalled: this.#options.allowPreinstalledDependencies === true,
|
|
902
|
+
trustedPrebuiltDependencies: this.#options.trustedPrebuiltDependencies ?? DEFAULT_TRUSTED_PREBUILT_DEPENDENCIES,
|
|
903
|
+
maxDepth: depth,
|
|
904
|
+
maxPackages: packages,
|
|
905
|
+
};
|
|
906
|
+
return (root === undefined
|
|
907
|
+
? createFirstPartyDependencyGraph(base)
|
|
908
|
+
: createFirstPartyDependencyGraph({ ...base, preinstalledRoot: root })).graph;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Verify an installed copy's content digest.
|
|
912
|
+
* @param storeDir - the configured store.
|
|
913
|
+
* @param record - the installed record, when there is one.
|
|
914
|
+
* @returns the integrity verdict plus the diagnostic text when it is not unchanged.
|
|
915
|
+
*/
|
|
916
|
+
#integrityOf(storeDir, record) {
|
|
917
|
+
if (record === undefined || storeDir === '')
|
|
918
|
+
return { integrity: 'unknown', reason: null };
|
|
919
|
+
try {
|
|
920
|
+
const verdict = new ExtensionStore(storeDir).verifyUnchanged(record);
|
|
921
|
+
return {
|
|
922
|
+
integrity: verdict.unchanged ? 'unchanged' : 'changed',
|
|
923
|
+
reason: verdict.unchanged
|
|
924
|
+
? null
|
|
925
|
+
: `the installed content is not the content that was recorded: ${verdict.diagnostic ?? 'it changed on disk after installation'}`,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
return {
|
|
930
|
+
integrity: 'unknown',
|
|
931
|
+
reason: `the installed content could not be digested, so its integrity is unknown: ${messageOf(error)}`,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Compute EVERY prerequisite this deployment checked, satisfied or not.
|
|
937
|
+
*
|
|
938
|
+
* The list is the answer to "why can't I enable this?" BEFORE an enable is
|
|
939
|
+
* attempted: each unmet requirement names the exact fact and (when one exists)
|
|
940
|
+
* the action that clears it. Nothing here executes extension code; the only I/O
|
|
941
|
+
* is the store read the caller already performed, discovery of the package
|
|
942
|
+
* directory, a host-tree dependency walk, and the preparation PLAN (not an
|
|
943
|
+
* acquisition) when an executable approval might be pending.
|
|
944
|
+
* @param input - the row's already-read facts.
|
|
945
|
+
* @returns the requirements in display order (store, source, dependencies,
|
|
946
|
+
* platform, integrity, harness-model, executable, assets, activation).
|
|
947
|
+
*/
|
|
948
|
+
#computeRequirements(input) {
|
|
949
|
+
const requirements = [];
|
|
950
|
+
// STORE: configured and readable. Writability is a surface property the page
|
|
951
|
+
// already receives as `writable`; a read-only mount still answers reads.
|
|
952
|
+
if (input.storeDir === '') {
|
|
953
|
+
requirements.push(requirement('store', false, 'the extension store is not configured (config.extensionStore)', 'config'));
|
|
954
|
+
}
|
|
955
|
+
else if (input.lockCorrupt) {
|
|
956
|
+
requirements.push(requirement('store', false, `the extension store lock is unreadable, so no installed extension can be trusted: ${input.lockDiagnostic ?? 'unknown reason'}`, 'config'));
|
|
957
|
+
}
|
|
958
|
+
else if (input.profileStates.corrupt) {
|
|
959
|
+
requirements.push(requirement('store', false, `the profile activation state is unreadable, so the host restores nothing: ${input.profileStates.diagnostic ?? 'unknown reason'}`, 'config'));
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
requirements.push(requirement('store', true, `the extension store at ${input.storeDir} is configured and readable`));
|
|
963
|
+
}
|
|
964
|
+
// SOURCE: where an install would get the bytes. The registry route is preferred
|
|
965
|
+
// by `auto`; the directory route (developer mode) is named when it applies.
|
|
966
|
+
if (input.installed) {
|
|
967
|
+
requirements.push(requirement('source', true, `the installed copy is the source of record (version ${input.record?.version ?? 'unknown'})`));
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
const installSource = this.#options.installSource ?? 'auto';
|
|
971
|
+
const registryUrl = this.#registryBaseUrl();
|
|
972
|
+
const directoryRoute = input.sourceDir !== ''
|
|
973
|
+
&& input.sourcePackage !== undefined
|
|
974
|
+
&& input.sourcePackage.problem === null
|
|
975
|
+
&& this.#options.allowSourceDirectories === true;
|
|
976
|
+
const registryRoute = installSource !== 'directory' && registryUrl !== '';
|
|
977
|
+
if (registryRoute) {
|
|
978
|
+
requirements.push(requirement('source', true, `the npm registry ${registryUrl} can supply ${input.id}@latest, resolved and sha512-verified at install time`
|
|
979
|
+
+ (installSource === 'auto' && directoryRoute
|
|
980
|
+
? '; a directory fallback is available if the registry is unreachable'
|
|
981
|
+
: ''), 'install'));
|
|
982
|
+
}
|
|
983
|
+
else if (installSource === 'registry') {
|
|
984
|
+
requirements.push(requirement('source', false, 'no npm registry is configured (config.extensionRegistry), and registry-only installs are selected (config.extensionInstallSource)', 'config'));
|
|
985
|
+
}
|
|
986
|
+
else if (input.sourcePackage === undefined) {
|
|
987
|
+
requirements.push(requirement('source', false, input.sourceDir === ''
|
|
988
|
+
? 'not installed, and no first-party source directory is configured (config.extensionSourceDir)'
|
|
989
|
+
: `no first-party package with this identity exists under ${input.sourceDir}`, input.sourceDir === '' ? 'config' : 'none'));
|
|
990
|
+
}
|
|
991
|
+
else if (input.sourcePackage.problem !== null) {
|
|
992
|
+
requirements.push(requirement('source', false, input.sourcePackage.problem, 'none'));
|
|
993
|
+
}
|
|
994
|
+
else if (this.#options.allowSourceDirectories !== true) {
|
|
995
|
+
requirements.push(requirement('source', false, 'installing from a source directory is disabled by configuration (extensions.allowSourceDirectories)', 'config'));
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
requirements.push(requirement('source', true, `the first-party package is available at ${join(input.sourceDir, packageDirFor(input.id))}`, 'install'));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// DEPENDENCIES: declared runtime dependencies must be resolvable/materialized.
|
|
1002
|
+
requirements.push(this.#dependencyRequirement(input));
|
|
1003
|
+
// PLATFORM: the manifest's declared deployment platforms must include this host.
|
|
1004
|
+
if (input.facts.problem !== null) {
|
|
1005
|
+
requirements.push(requirement('platform', false, input.facts.problem, 'none'));
|
|
1006
|
+
}
|
|
1007
|
+
else if (input.facts.platforms.length === 0) {
|
|
1008
|
+
requirements.push(requirement('platform', true, 'the extension declares no platform-specific model deployment'));
|
|
1009
|
+
}
|
|
1010
|
+
else if (hostPlatformMatches(input.facts.platforms)) {
|
|
1011
|
+
requirements.push(requirement('platform', true, `the declared deployment supports this host (${process.platform}/${process.arch}: ${input.facts.platforms.map(platformLabel).join(', ')})`));
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
requirements.push(requirement('platform', false, `the extension supports ${input.facts.platforms.map(platformLabel).join(', ')} but this host is ${process.platform}/${process.arch}`, 'none'));
|
|
1015
|
+
}
|
|
1016
|
+
// INTEGRITY: the installed bytes must still be the recorded bytes.
|
|
1017
|
+
if (!input.installed) {
|
|
1018
|
+
requirements.push(requirement('integrity', true, 'the extension is not installed, so there are no installed bytes to verify'));
|
|
1019
|
+
}
|
|
1020
|
+
else if (input.integrity === 'unchanged') {
|
|
1021
|
+
requirements.push(requirement('integrity', true, 'the installed content matches the recorded digest'));
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
requirements.push(requirement('integrity', false, input.integrityReason ?? `the installed content's integrity is ${input.integrity}`, 'install'));
|
|
1025
|
+
}
|
|
1026
|
+
// RUNTIME: optional tool-runtime packages must be materialized, or the
|
|
1027
|
+
// extension's tools load and fail at call time — which is not activation.
|
|
1028
|
+
requirements.push(this.#runtimeRequirement(input));
|
|
1029
|
+
// HARNESS MODEL: only declarative packages whose recipes the host BROKER executes.
|
|
1030
|
+
//
|
|
1031
|
+
// A `model-runtime` recipe (PHASE2) is executed by the package's own provider factory, so
|
|
1032
|
+
// requiring a harness LlmProvider for it would refuse a package that needs nothing from
|
|
1033
|
+
// the deployment. The requirement is skipped only when EVERY declared recipe is
|
|
1034
|
+
// model-runtime; a package with no declared recipe keeps the old behavior.
|
|
1035
|
+
const harnessServed = input.facts.harnessRecipes > 0 || input.facts.modelRuntimeRecipes === 0;
|
|
1036
|
+
if (input.facts.executionKind === 'declarative' && harnessServed) {
|
|
1037
|
+
requirements.push(this.#harnessModelRequirement());
|
|
1038
|
+
}
|
|
1039
|
+
// Readiness drives both the executable gate and the assets requirement.
|
|
1040
|
+
const blockers = preparationBlockers(input.state?.readiness ?? {}, input.facts.models);
|
|
1041
|
+
// EXECUTABLE: a downloaded engine needs the separate RES-02 approval.
|
|
1042
|
+
if (input.facts.models.length === 0) {
|
|
1043
|
+
requirements.push(requirement('executable', true, 'the extension declares no model deployment, so no executable asset can be planned'));
|
|
1044
|
+
}
|
|
1045
|
+
else if (!input.installed) {
|
|
1046
|
+
requirements.push(requirement('executable', true, 'not installed yet; the executable-asset approval is checked once the preparation plan can be resolved'));
|
|
1047
|
+
}
|
|
1048
|
+
else if (blockers.length === 0) {
|
|
1049
|
+
requirements.push(requirement('executable', true, 'every declared model deployment is already prepared or callable, so no executable approval is pending'));
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
requirements.push(this.#executableRequirement(input.id));
|
|
1053
|
+
}
|
|
1054
|
+
// ASSETS: each declared deployment must be in a callable readiness state.
|
|
1055
|
+
if (input.facts.models.length === 0) {
|
|
1056
|
+
requirements.push(requirement('assets', true, 'the extension declares no model deployment to prepare'));
|
|
1057
|
+
}
|
|
1058
|
+
else if (!input.installed) {
|
|
1059
|
+
requirements.push(requirement('assets', false, `not installed; ${input.facts.models.join(', ')} need${input.facts.models.length === 1 ? 's' : ''} model assets prepared after installation`, 'install'));
|
|
1060
|
+
}
|
|
1061
|
+
else if (this.#options.assetRestoreDiagnostic?.(input.id) !== undefined) {
|
|
1062
|
+
/**
|
|
1063
|
+
* The startup restore walk could not rebuild this extension's prepared handles.
|
|
1064
|
+
* Without this branch the row reads the generic "needs its model assets prepared"
|
|
1065
|
+
* and the operator has no way to tell a missing/size-mismatched cache file from a
|
|
1066
|
+
* model that was never prepared — the two need different actions.
|
|
1067
|
+
*/
|
|
1068
|
+
requirements.push(requirement('assets', false, `prepared assets could not be restored from the cache: ${this.#options.assetRestoreDiagnostic?.(input.id)}; run prepare for this extension`, 'prepare'));
|
|
1069
|
+
}
|
|
1070
|
+
else if (blockers.length > 0) {
|
|
1071
|
+
/**
|
|
1072
|
+
* A lock whose assets carry prose sources (no URL) cannot be prepared by this
|
|
1073
|
+
* host. Report action `none` so the page does not offer Prepare as if it could
|
|
1074
|
+
* succeed; the reason names the artifact and the documented export.
|
|
1075
|
+
*/
|
|
1076
|
+
const unavailable = this.#unavailableAssetSource(input.id);
|
|
1077
|
+
requirements.push(unavailable === undefined
|
|
1078
|
+
? requirement('assets', false, `activation blocked: ${blockers.join('; ')}`, 'prepare')
|
|
1079
|
+
: requirement('assets', false, `[ASSET_SOURCE_UNAVAILABLE] ${assetSourceUnavailableReason(unavailable.assetId, unavailable.source)}`, 'none'));
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
requirements.push(requirement('assets', true, `every declared model deployment is prepared or callable (${input.facts.models.join(', ')})`));
|
|
1083
|
+
}
|
|
1084
|
+
// ACTIVATION: a stored fault that a retry cannot clear.
|
|
1085
|
+
if (!input.installed) {
|
|
1086
|
+
requirements.push(requirement('activation', true, 'not installed; no activation state exists'));
|
|
1087
|
+
}
|
|
1088
|
+
else if (input.state?.observedActivation === 'quarantined') {
|
|
1089
|
+
requirements.push(requirement('activation', false, 'the host quarantined it: some contributions could not be withdrawn, so it is not fully stopped', 'none'));
|
|
1090
|
+
}
|
|
1091
|
+
else if (input.state?.observedActivation === 'activating' || input.state?.observedActivation === 'deactivating') {
|
|
1092
|
+
requirements.push(requirement('activation', false, `a ${input.state.observedActivation} operation was interrupted; the recorded state is not settled`, 'none'));
|
|
1093
|
+
}
|
|
1094
|
+
else if (input.state?.observedActivation === 'failed') {
|
|
1095
|
+
const explained = requirements.some(candidate => !candidate.satisfied);
|
|
1096
|
+
requirements.push(requirement('activation', true, explained
|
|
1097
|
+
? 'the last activation attempt failed; the unmet requirement above is the actionable blocker'
|
|
1098
|
+
: 'the last activation attempt failed, but no remaining prerequisite blocks a retry, so enable retries activation'));
|
|
1099
|
+
}
|
|
1100
|
+
else if (input.state === undefined) {
|
|
1101
|
+
requirements.push(requirement('activation', true, 'installed but never activated in this profile; no stored activation fault'));
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
requirements.push(requirement('activation', true, `the stored activation state is ${input.state.observedActivation}`));
|
|
1105
|
+
}
|
|
1106
|
+
return requirements;
|
|
1107
|
+
}
|
|
1108
|
+
/** The dependencies requirement: materialized in the installed copy, or resolvable in the host tree. */
|
|
1109
|
+
#dependencyRequirement(input) {
|
|
1110
|
+
if (input.facts.dependencies.length === 0) {
|
|
1111
|
+
return requirement('dependencies', true, 'the package declares no runtime dependencies');
|
|
1112
|
+
}
|
|
1113
|
+
const count = input.facts.dependencies.length;
|
|
1114
|
+
const missing = [];
|
|
1115
|
+
if (input.installed && input.record !== undefined) {
|
|
1116
|
+
for (const dependency of input.facts.dependencies) {
|
|
1117
|
+
const installed = join(input.record.installPath, 'node_modules', ...dependency.name.split('/'), 'package.json');
|
|
1118
|
+
if (!existsSync(installed))
|
|
1119
|
+
missing.push(`${dependency.name} (declared ${dependency.range})`);
|
|
1120
|
+
}
|
|
1121
|
+
return missing.length === 0
|
|
1122
|
+
? requirement('dependencies', true, `all ${count} declared runtime dependenc${count === 1 ? 'y is' : 'ies are'} materialized inside the extension's own installation`)
|
|
1123
|
+
: requirement('dependencies', false, `the installed copy is missing ${missing.length} declared runtime dependenc${missing.length === 1 ? 'y' : 'ies'}: ${missing.join(', ')}`, 'install');
|
|
1124
|
+
}
|
|
1125
|
+
if (input.facts.packageDir === null) {
|
|
1126
|
+
return requirement('dependencies', false, 'the declared runtime dependencies cannot be inspected because the package directory is unknown', 'none');
|
|
1127
|
+
}
|
|
1128
|
+
for (const dependency of input.facts.dependencies) {
|
|
1129
|
+
try {
|
|
1130
|
+
resolveFirstPartyDependency({ sourcePackageDir: input.facts.packageDir }, dependency.name, dependency.range);
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
missing.push(`${dependency.name} (declared ${dependency.range})`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return missing.length === 0
|
|
1137
|
+
? requirement('dependencies', true, `all ${count} declared runtime dependenc${count === 1 ? 'y resolves' : 'ies resolve'} in the host's installed tree`)
|
|
1138
|
+
: requirement('dependencies', false, `the host's tree is missing ${missing.length} declared runtime dependenc${missing.length === 1 ? 'y' : 'ies'}: ${missing.join(', ')}; install the dependency for the host first`, 'none');
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* The OPTIONAL tool-runtime requirement.
|
|
1142
|
+
*
|
|
1143
|
+
* Module extensions declare the packages their tools `require()` at call time as
|
|
1144
|
+
* optionalDependencies (e.g. tongue's `@desert-ant-labs/tongue`). If one is absent
|
|
1145
|
+
* the extension still activates and its tools fail at call time, so this
|
|
1146
|
+
* requirement BLOCKS activation instead: a tool that cannot load is not served.
|
|
1147
|
+
* The sidecar records why a materialization attempt failed (the lock cannot).
|
|
1148
|
+
*/
|
|
1149
|
+
#runtimeRequirement(input) {
|
|
1150
|
+
const declared = input.facts.optionalDependencies;
|
|
1151
|
+
if (declared.length === 0) {
|
|
1152
|
+
return requirement('runtime', true, 'the package declares no optional tool runtime');
|
|
1153
|
+
}
|
|
1154
|
+
if (!input.installed || input.record === undefined) {
|
|
1155
|
+
return requirement('runtime', true, 'not installed yet; optional tool runtimes are materialized and verified at install time');
|
|
1156
|
+
}
|
|
1157
|
+
const recorded = readRuntimeDependencyDocument(input.storeDir).extensions[input.id];
|
|
1158
|
+
const unresolved = [];
|
|
1159
|
+
const resolved = [];
|
|
1160
|
+
for (const dependency of declared) {
|
|
1161
|
+
const installedVersion = readInstalledDependencyVersion(input.record.installPath, dependency.name);
|
|
1162
|
+
if (installedVersion === undefined) {
|
|
1163
|
+
const attempt = recorded?.optional.find(entry => entry.name === dependency.name && !entry.satisfied);
|
|
1164
|
+
unresolved.push(`${dependency.name} ${dependency.range} is not installed inside the extension`
|
|
1165
|
+
+ (attempt?.error === undefined ? '' : `: ${attempt.error}`));
|
|
1166
|
+
}
|
|
1167
|
+
else if (!satisfiesRegistryRange(installedVersion, dependency.range)) {
|
|
1168
|
+
unresolved.push(`${dependency.name} is installed at ${installedVersion}, which does not satisfy ${dependency.range}`);
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
resolved.push(`${dependency.name}@${installedVersion}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (unresolved.length === 0) {
|
|
1175
|
+
return requirement('runtime', true, `the optional tool runtime${resolved.length === 1 ? ' is' : 's are'} materialized (${resolved.join(', ')})`);
|
|
1176
|
+
}
|
|
1177
|
+
return requirement('runtime', false, `the extension's tools cannot load: ${unresolved.join('; ')}`, 'install');
|
|
1178
|
+
}
|
|
1179
|
+
/** The harness-model requirement for a declarative package. */
|
|
1180
|
+
#harnessModelRequirement() {
|
|
1181
|
+
const status = this.#options.harnessProviderStatus?.();
|
|
1182
|
+
if (status === undefined) {
|
|
1183
|
+
return requirement('harness-model', false, 'the host could not report whether a harness model provider is available (provider status unknown); '
|
|
1184
|
+
+ 'a declarative extension needs a composed provider, for example config.harnessModel', 'config');
|
|
1185
|
+
}
|
|
1186
|
+
if (status.available) {
|
|
1187
|
+
const identity = [status.provider, status.model].filter((part) => part !== undefined && part.length > 0);
|
|
1188
|
+
return requirement('harness-model', true, identity.length === 0
|
|
1189
|
+
? 'a harness model provider is available for declarative extensions'
|
|
1190
|
+
: `a harness model provider is available (${identity.join(' / ')})`);
|
|
1191
|
+
}
|
|
1192
|
+
return requirement('harness-model', false, status.reason
|
|
1193
|
+
?? 'no harness model provider is configured, so declarative extensions cannot be served; set config.harnessModel to a runnable model', status.action ?? 'config');
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* The first planned asset with neither an http(s) source nor cached bytes.
|
|
1197
|
+
*
|
|
1198
|
+
* A prose lock source (laya's "split export of the pinned checkpoint") is not a
|
|
1199
|
+
* download location; preparation must refuse before any transfer. A cache hit is
|
|
1200
|
+
* still a source because acquisition verifies the cached digest.
|
|
1201
|
+
* @param extensionId - the installed extension.
|
|
1202
|
+
* @returns the unavailable asset, or undefined when every source is reachable.
|
|
1203
|
+
*/
|
|
1204
|
+
#unavailableAssetSource(extensionId) {
|
|
1205
|
+
const surface = this.#options.prepareSurface?.();
|
|
1206
|
+
if (surface === undefined)
|
|
1207
|
+
return undefined;
|
|
1208
|
+
try {
|
|
1209
|
+
const plan = surface.plan(extensionId);
|
|
1210
|
+
return firstUnavailableAssetSource(plan.assets.map(asset => ({ assetId: asset.assetId, source: asset.source, digest: asset.digest })), surface.registry.cacheDirectory);
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
return undefined;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/** The executable requirement for an installed, not-yet-callable model deployment. */
|
|
1217
|
+
#executableRequirement(extensionId) {
|
|
1218
|
+
const surface = this.#options.prepareSurface?.();
|
|
1219
|
+
if (surface === undefined) {
|
|
1220
|
+
/**
|
|
1221
|
+
* No plan exists to approve, so this requirement is NOT what blocks the row:
|
|
1222
|
+
* the assets requirement names the missing preparation. Reporting it unmet
|
|
1223
|
+
* here would replace the actionable "run prepare" text with a host gap the
|
|
1224
|
+
* row cannot fix.
|
|
1225
|
+
*/
|
|
1226
|
+
return requirement('executable', true, 'the prepared-assets surface is not mounted, so there is no preparation plan whose executable assets could be approved; '
|
|
1227
|
+
+ 'the assets requirement reports the missing preparation');
|
|
1228
|
+
}
|
|
1229
|
+
let plan;
|
|
1230
|
+
try {
|
|
1231
|
+
plan = surface.plan(extensionId);
|
|
1232
|
+
}
|
|
1233
|
+
catch (error) {
|
|
1234
|
+
return requirement('executable', true, `the preparation plan could not be inspected, so executable approval is not the blocker: ${messageOf(error)}`);
|
|
1235
|
+
}
|
|
1236
|
+
if (!plan.requiresExecutableApproval) {
|
|
1237
|
+
return requirement('executable', true, 'the preparation plan declares no executable assets');
|
|
1238
|
+
}
|
|
1239
|
+
return this.#options.allowExecutablePreparation?.() === true
|
|
1240
|
+
? requirement('executable', true, 'the preparation plan includes an executable asset and the operator approved executable preparation (config.allowExecutablePreparation)')
|
|
1241
|
+
: requirement('executable', false, 'activation blocked: the preparation plan includes an executable asset, and executable preparation is not approved for the client path '
|
|
1242
|
+
+ '(config.allowExecutablePreparation is off)', 'config');
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Read the current state of the extension surface.
|
|
1246
|
+
*
|
|
1247
|
+
* Every row is derived from the store, the profile state and the source
|
|
1248
|
+
* directory; nothing is cached between calls, so a page that re-reads after an
|
|
1249
|
+
* action sees the committed result.
|
|
1250
|
+
* @param signal - cancellation.
|
|
1251
|
+
* @returns the store/source configuration and one row per known extension.
|
|
1252
|
+
*/
|
|
1253
|
+
async describe(signal) {
|
|
1254
|
+
return this.#describe(signal, true);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Describe identity, state and tools WITHOUT digesting installed bytes.
|
|
1258
|
+
*
|
|
1259
|
+
* The Tool Insights inventory and owner attribution do not render integrity;
|
|
1260
|
+
* on a deployment with multi-GB extension trees the digest pass otherwise
|
|
1261
|
+
* stalls the Remote gateway for every inventory read.
|
|
1262
|
+
* @param signal - cancellation.
|
|
1263
|
+
* @returns the same row shape, with integrity reported as `unknown`.
|
|
1264
|
+
*/
|
|
1265
|
+
async describeFast(signal) {
|
|
1266
|
+
return this.#describe(signal, false);
|
|
1267
|
+
}
|
|
1268
|
+
async #describe(signal, verifyIntegrity) {
|
|
1269
|
+
signal?.throwIfAborted();
|
|
1270
|
+
const storeDir = this.#storeDir();
|
|
1271
|
+
const sourceDir = this.#sourceDir();
|
|
1272
|
+
const disabled = this.#effectiveDisabled();
|
|
1273
|
+
let lockCorrupt = false;
|
|
1274
|
+
let lockDiagnostic = null;
|
|
1275
|
+
let installed = [];
|
|
1276
|
+
let profileStates = { byId: new Map(), corrupt: false, diagnostic: null };
|
|
1277
|
+
let activeVersions = {};
|
|
1278
|
+
if (storeDir !== '') {
|
|
1279
|
+
const store = new ExtensionStore(storeDir);
|
|
1280
|
+
const lock = store.readLock();
|
|
1281
|
+
lockCorrupt = lock.corrupt;
|
|
1282
|
+
lockDiagnostic = lock.diagnostic ?? null;
|
|
1283
|
+
installed = lock.value.extensions;
|
|
1284
|
+
profileStates = readProfileStates(store, this.#profile());
|
|
1285
|
+
activeVersions = store.readProfilePointers(this.#profile()).value.activeVersions;
|
|
1286
|
+
}
|
|
1287
|
+
const sourcePackages = new Map();
|
|
1288
|
+
if (sourceDir !== '') {
|
|
1289
|
+
for (const candidate of listSourcePackages(sourceDir))
|
|
1290
|
+
sourcePackages.set(candidate.id, candidate);
|
|
1291
|
+
}
|
|
1292
|
+
const ids = new Set();
|
|
1293
|
+
for (const record of installed)
|
|
1294
|
+
ids.add(record.extensionId);
|
|
1295
|
+
for (const id of profileStates.byId.keys())
|
|
1296
|
+
ids.add(id);
|
|
1297
|
+
for (const id of sourcePackages.keys())
|
|
1298
|
+
ids.add(id);
|
|
1299
|
+
const extensions = [...ids].sort().map(id => this.#describeOne({
|
|
1300
|
+
id,
|
|
1301
|
+
installed,
|
|
1302
|
+
activeVersions,
|
|
1303
|
+
profileStates,
|
|
1304
|
+
sourcePackage: sourcePackages.get(id),
|
|
1305
|
+
disabled: disabled.has(id),
|
|
1306
|
+
storeDir,
|
|
1307
|
+
sourceDir,
|
|
1308
|
+
lockCorrupt,
|
|
1309
|
+
lockDiagnostic,
|
|
1310
|
+
verifyIntegrity,
|
|
1311
|
+
}));
|
|
1312
|
+
const writable = storeDir !== ''
|
|
1313
|
+
&& !lockCorrupt
|
|
1314
|
+
&& this.#options.readOnly !== true
|
|
1315
|
+
&& directoryWritable(storeDir);
|
|
1316
|
+
return {
|
|
1317
|
+
store: storeDir === '' ? null : storeDir,
|
|
1318
|
+
writable,
|
|
1319
|
+
sourceDir: sourceDir === '' ? null : sourceDir,
|
|
1320
|
+
extensions,
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
/** Build one row, keeping the reason precedence in one place. */
|
|
1324
|
+
#describeOne(view) {
|
|
1325
|
+
const selection = selectInstalled(view.installed, view.activeVersions, view.id);
|
|
1326
|
+
const record = selection;
|
|
1327
|
+
const state = view.profileStates.byId.get(view.id);
|
|
1328
|
+
const installed = record !== undefined;
|
|
1329
|
+
const integrity = view.verifyIntegrity
|
|
1330
|
+
? this.#integrityOf(view.storeDir, record)
|
|
1331
|
+
: { integrity: 'unknown', reason: null };
|
|
1332
|
+
const facts = record !== undefined
|
|
1333
|
+
? readExtensionFacts(record.installPath)
|
|
1334
|
+
: view.sourcePackage?.facts ?? EMPTY_FACTS;
|
|
1335
|
+
const active = state !== undefined
|
|
1336
|
+
&& state.observedActivation === 'active'
|
|
1337
|
+
&& (state.ownerPid === undefined || state.ownerPid === process.pid);
|
|
1338
|
+
const stateTools = state?.tools.map(tool => tool.toolName) ?? [];
|
|
1339
|
+
const tools = facts.tools.length > 0
|
|
1340
|
+
? facts.tools
|
|
1341
|
+
: stateTools.length > 0
|
|
1342
|
+
? stateTools
|
|
1343
|
+
: view.sourcePackage?.tools ?? [];
|
|
1344
|
+
const requirements = this.#computeRequirements({
|
|
1345
|
+
id: view.id,
|
|
1346
|
+
installed,
|
|
1347
|
+
record,
|
|
1348
|
+
state,
|
|
1349
|
+
integrity: integrity.integrity,
|
|
1350
|
+
integrityReason: integrity.reason,
|
|
1351
|
+
storeDir: view.storeDir,
|
|
1352
|
+
sourceDir: view.sourceDir,
|
|
1353
|
+
lockCorrupt: view.lockCorrupt,
|
|
1354
|
+
lockDiagnostic: view.lockDiagnostic,
|
|
1355
|
+
profileStates: view.profileStates,
|
|
1356
|
+
sourcePackage: view.sourcePackage,
|
|
1357
|
+
facts,
|
|
1358
|
+
});
|
|
1359
|
+
const blocked = requirements.some(candidate => blocksRow(installed, candidate));
|
|
1360
|
+
const version = record?.version ?? view.sourcePackage?.version ?? null;
|
|
1361
|
+
const reason = this.#reasonFor({
|
|
1362
|
+
...view,
|
|
1363
|
+
state,
|
|
1364
|
+
integrityReason: integrity.reason,
|
|
1365
|
+
active,
|
|
1366
|
+
installed,
|
|
1367
|
+
requirements,
|
|
1368
|
+
});
|
|
1369
|
+
return {
|
|
1370
|
+
id: view.id,
|
|
1371
|
+
package: view.id,
|
|
1372
|
+
version,
|
|
1373
|
+
installed,
|
|
1374
|
+
active,
|
|
1375
|
+
disabled: view.disabled,
|
|
1376
|
+
integrity: integrity.integrity,
|
|
1377
|
+
tools: [...tools],
|
|
1378
|
+
reason,
|
|
1379
|
+
requirements,
|
|
1380
|
+
blocked,
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
/** The first condition worth telling the operator about, or null when none is known. */
|
|
1384
|
+
#reasonFor(view) {
|
|
1385
|
+
if (view.lockCorrupt) {
|
|
1386
|
+
return `the extension store lock is unreadable, so no installed extension can be trusted: ${view.lockDiagnostic ?? 'unknown reason'}`;
|
|
1387
|
+
}
|
|
1388
|
+
if (view.integrityReason !== null)
|
|
1389
|
+
return view.integrityReason;
|
|
1390
|
+
if (view.profileStates.corrupt) {
|
|
1391
|
+
return `the profile activation state is unreadable, so the host restores nothing: ${view.profileStates.diagnostic ?? 'unknown reason'}`;
|
|
1392
|
+
}
|
|
1393
|
+
if (view.storeDir === '') {
|
|
1394
|
+
return 'the extension store is not configured (config.extensionStore)';
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* The highest-priority UNMET requirement that actually blocks this row is the
|
|
1398
|
+
* single summary. The wording is the requirement's own, so the page, the
|
|
1399
|
+
* preflight error and the requirement list never disagree.
|
|
1400
|
+
*/
|
|
1401
|
+
const unmet = firstBlockingRequirement(view.requirements, view.installed);
|
|
1402
|
+
if (view.state?.observedActivation === 'failed') {
|
|
1403
|
+
/**
|
|
1404
|
+
* PRECEDENCE: a blocking requirement stays the actionable explanation (the
|
|
1405
|
+
* operator can fix it), and the RECORDED cause is preferred over the generic
|
|
1406
|
+
* text when nothing blocks a retry. The fallback survives only for a state
|
|
1407
|
+
* written before the SDK recorded causes — never as a substitute for a reason
|
|
1408
|
+
* the store actually holds.
|
|
1409
|
+
*/
|
|
1410
|
+
return unmet?.reason
|
|
1411
|
+
?? recordedActivationError(view.state)
|
|
1412
|
+
?? 'activation failed earlier in this host; the store records no remaining preparation blocker, so retry enable to see whether it succeeds';
|
|
1413
|
+
}
|
|
1414
|
+
if (view.state?.observedActivation === 'quarantined') {
|
|
1415
|
+
return unmet?.reason
|
|
1416
|
+
?? 'the host quarantined it: some contributions could not be withdrawn, so it is not fully stopped';
|
|
1417
|
+
}
|
|
1418
|
+
if (view.state?.observedActivation === 'activating' || view.state?.observedActivation === 'deactivating') {
|
|
1419
|
+
return unmet?.reason
|
|
1420
|
+
?? `a ${view.state.observedActivation} operation was interrupted; the recorded state is not settled`;
|
|
1421
|
+
}
|
|
1422
|
+
if (view.disabled) {
|
|
1423
|
+
return 'disabled by the operator (config.disabledExtensions); the host does not restore it';
|
|
1424
|
+
}
|
|
1425
|
+
if (!view.installed) {
|
|
1426
|
+
if (view.sourcePackage?.problem !== null && view.sourcePackage?.problem !== undefined)
|
|
1427
|
+
return view.sourcePackage.problem;
|
|
1428
|
+
// The route-aware source requirement already explains where an install would
|
|
1429
|
+
// get the bytes (registry, directory, or why neither is available).
|
|
1430
|
+
const sourceRequirement = view.requirements.find(candidate => candidate.kind === 'source');
|
|
1431
|
+
if (sourceRequirement !== undefined)
|
|
1432
|
+
return sourceRequirement.reason;
|
|
1433
|
+
return 'not installed';
|
|
1434
|
+
}
|
|
1435
|
+
if (unmet !== undefined)
|
|
1436
|
+
return unmet.reason;
|
|
1437
|
+
if (!view.active) {
|
|
1438
|
+
if (view.state === undefined)
|
|
1439
|
+
return 'installed but never activated in this profile';
|
|
1440
|
+
return `installed; this host observes it as ${view.state.observedActivation} (desired ${view.state.desiredActivation})`;
|
|
1441
|
+
}
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Install one extension, choosing the route from the operator configuration.
|
|
1446
|
+
*
|
|
1447
|
+
* - An EXPLICIT `source` path is always a developer-mode directory install (the
|
|
1448
|
+
* existing source-directory gate and refusals apply).
|
|
1449
|
+
* - Otherwise `installSource` decides. `registry` resolves `id@latest` from the
|
|
1450
|
+
* npm registry. `directory` uses the configured source directory. `auto` (the
|
|
1451
|
+
* default) prefers the registry and falls back to the directory ONLY for a
|
|
1452
|
+
* transport-level registry failure (unreachable/timeout/5xx) when directory
|
|
1453
|
+
* installs are allowed — a package-level refusal (404, missing integrity, bad
|
|
1454
|
+
* tarball, digest mismatch) is always named and never retried locally.
|
|
1455
|
+
*
|
|
1456
|
+
* INSTALLATION IS NOT ACTIVATION: the result is always `active: false`, and it
|
|
1457
|
+
* names the route that actually supplied the bytes.
|
|
1458
|
+
* @param extensionId - the package identity to install.
|
|
1459
|
+
* @param source - an explicit source directory (developer override), or absent.
|
|
1460
|
+
* @param signal - cancellation.
|
|
1461
|
+
* @returns the installed identity, version and route.
|
|
1462
|
+
* @throws ExtensionsServiceError for every refusal and failure.
|
|
1463
|
+
*/
|
|
1464
|
+
async install(extensionId, source, signal) {
|
|
1465
|
+
try {
|
|
1466
|
+
const installed = await this.#installRequest(extensionId, source, signal);
|
|
1467
|
+
this.#options.onAudit?.({
|
|
1468
|
+
operation: 'install', extensionId, version: installed.version, outcome: 'succeeded',
|
|
1469
|
+
});
|
|
1470
|
+
return installed;
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
this.#options.onAudit?.({
|
|
1474
|
+
operation: 'install',
|
|
1475
|
+
extensionId,
|
|
1476
|
+
version: null,
|
|
1477
|
+
outcome: 'failed',
|
|
1478
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1479
|
+
});
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
async #installRequest(extensionId, source, signal) {
|
|
1484
|
+
signal?.throwIfAborted();
|
|
1485
|
+
assertUsableId(extensionId);
|
|
1486
|
+
const storeDir = this.#requireStoreDir();
|
|
1487
|
+
if (source !== undefined && source.trim().length > 0) {
|
|
1488
|
+
const requested = source.trim();
|
|
1489
|
+
if (/^https?:\/\//i.test(requested))
|
|
1490
|
+
return this.#installFromUrl(extensionId, requested, storeDir, signal);
|
|
1491
|
+
return this.#installFromDirectory(extensionId, resolve(requested), storeDir, signal);
|
|
1492
|
+
}
|
|
1493
|
+
const mode = this.#options.installSource ?? 'auto';
|
|
1494
|
+
if (mode === 'directory') {
|
|
1495
|
+
return this.#installFromDirectory(extensionId, undefined, storeDir, signal);
|
|
1496
|
+
}
|
|
1497
|
+
try {
|
|
1498
|
+
return await this.#installFromRegistry(extensionId, storeDir, signal);
|
|
1499
|
+
}
|
|
1500
|
+
catch (error) {
|
|
1501
|
+
const transportFallback = mode === 'auto'
|
|
1502
|
+
&& error instanceof ExtensionsServiceError
|
|
1503
|
+
&& (error.code === 'extension-registry-unavailable' || error.code === 'extension-registry-unconfigured')
|
|
1504
|
+
&& this.#options.allowSourceDirectories === true
|
|
1505
|
+
&& this.#directorySourceAvailable(extensionId);
|
|
1506
|
+
if (!transportFallback)
|
|
1507
|
+
throw error;
|
|
1508
|
+
return this.#installFromDirectory(extensionId, undefined, storeDir, signal);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/** The configured registry base URL, trimmed ('' when the operator set it empty). */
|
|
1512
|
+
#registryBaseUrl() {
|
|
1513
|
+
return (this.#options.registryUrl ?? DEFAULT_REGISTRY_URL).trim();
|
|
1514
|
+
}
|
|
1515
|
+
/** Whether the configured source directory holds a package directory for this id. */
|
|
1516
|
+
#directorySourceAvailable(extensionId) {
|
|
1517
|
+
const sourceDir = this.#sourceDir();
|
|
1518
|
+
if (sourceDir === '')
|
|
1519
|
+
return false;
|
|
1520
|
+
try {
|
|
1521
|
+
return statSync(join(sourceDir, packageDirFor(extensionId))).isDirectory();
|
|
1522
|
+
}
|
|
1523
|
+
catch {
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Map a registry failure onto this service's named codes.
|
|
1529
|
+
* @param error - an error from the registry session or the SDK install.
|
|
1530
|
+
* @returns the service refusal, preserving the message and cause.
|
|
1531
|
+
*/
|
|
1532
|
+
#mapRegistryFailure(error) {
|
|
1533
|
+
if (!(error instanceof RegistryError)) {
|
|
1534
|
+
return new ExtensionsServiceError('extension-install-failed', messageOf(error), { cause: error });
|
|
1535
|
+
}
|
|
1536
|
+
const codes = {
|
|
1537
|
+
'registry-not-configured': 'extension-registry-unconfigured',
|
|
1538
|
+
'registry-not-found': 'extension-registry-not-found',
|
|
1539
|
+
'registry-no-matching-version': 'extension-registry-not-found',
|
|
1540
|
+
'registry-integrity-missing': 'extension-registry-integrity-missing',
|
|
1541
|
+
'registry-invalid-metadata': 'extension-registry-invalid-metadata',
|
|
1542
|
+
'registry-tarball-fetch-failed': 'extension-registry-fetch-failed',
|
|
1543
|
+
'registry-tarball-too-large': 'extension-registry-fetch-failed',
|
|
1544
|
+
'registry-unavailable': 'extension-registry-unavailable',
|
|
1545
|
+
};
|
|
1546
|
+
return new ExtensionsServiceError(codes[error.code], error.message, { cause: error });
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Install from an HTTPS/HTTP npm-format tarball URL (EXT-06).
|
|
1550
|
+
*
|
|
1551
|
+
* Noninteractive URL installs REQUIRE a pinned integrity fragment
|
|
1552
|
+
* (`...tgz#sha512-<base64>`): unverified bytes are never installed. Redirects are
|
|
1553
|
+
* followed manually, bounded, and each destination must be http(s) with no embedded
|
|
1554
|
+
* credentials; the fetch carries no authorization header, so a redirect cannot
|
|
1555
|
+
* forward one. The bytes are size/time bounded and then go through the SAME staged
|
|
1556
|
+
* installer as every other source.
|
|
1557
|
+
* @param extensionId - the package identity to install.
|
|
1558
|
+
* @param rawUrl - the tarball URL plus pinned `#sha512-...` fragment.
|
|
1559
|
+
* @param storeDir - the configured store.
|
|
1560
|
+
* @param signal - cancellation.
|
|
1561
|
+
* @returns the installed identity/version and `source: 'url'`.
|
|
1562
|
+
*/
|
|
1563
|
+
async #installFromUrl(extensionId, rawUrl, storeDir, signal) {
|
|
1564
|
+
let url;
|
|
1565
|
+
try {
|
|
1566
|
+
url = new URL(rawUrl);
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', `${JSON.stringify(rawUrl)} is not a URL`, { cause: error });
|
|
1570
|
+
}
|
|
1571
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
1572
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', `URL source protocol ${url.protocol} is not http(s)`);
|
|
1573
|
+
}
|
|
1574
|
+
if (url.username !== '' || url.password !== '') {
|
|
1575
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', 'a URL source must not embed credentials');
|
|
1576
|
+
}
|
|
1577
|
+
let integrity;
|
|
1578
|
+
try {
|
|
1579
|
+
integrity = decodeURIComponent(url.hash.slice(1));
|
|
1580
|
+
}
|
|
1581
|
+
catch {
|
|
1582
|
+
integrity = url.hash.slice(1);
|
|
1583
|
+
}
|
|
1584
|
+
if (!/^sha512-[A-Za-z0-9+/=]+$/.test(integrity)) {
|
|
1585
|
+
throw new ExtensionsServiceError('extension-url-integrity-required', `${JSON.stringify(rawUrl)} has no pinned #sha512-... integrity; noninteractive URL installs require it`);
|
|
1586
|
+
}
|
|
1587
|
+
url.hash = '';
|
|
1588
|
+
const maxBytes = this.#options.registryTarballMaxBytes ?? MAX_DEPENDENCY_ARCHIVE_BYTES;
|
|
1589
|
+
const timeoutMs = this.#options.registryTarballTimeoutMs ?? 600_000;
|
|
1590
|
+
const bytes = await this.#fetchUrlTarball(url, maxBytes, timeoutMs, signal);
|
|
1591
|
+
let installed;
|
|
1592
|
+
try {
|
|
1593
|
+
installed = await stagedInstall({
|
|
1594
|
+
source: { kind: 'url', requested: rawUrl },
|
|
1595
|
+
storeDir,
|
|
1596
|
+
expectedSha512: integrity,
|
|
1597
|
+
archive: createTarGzipArchiveSource(async () => bytes),
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
catch (error) {
|
|
1601
|
+
if (error instanceof ManagementError && error.code === 'INTEGRITY_MISMATCH') {
|
|
1602
|
+
throw new ExtensionsServiceError('extension-url-integrity-mismatch', `${rawUrl}: ${error.message}`, { cause: error });
|
|
1603
|
+
}
|
|
1604
|
+
if (error instanceof ManagementError) {
|
|
1605
|
+
throw new ExtensionsServiceError('extension-install-failed', `[${error.code}] ${error.message}`, { cause: error });
|
|
1606
|
+
}
|
|
1607
|
+
throw new ExtensionsServiceError('extension-install-failed', messageOf(error), { cause: error });
|
|
1608
|
+
}
|
|
1609
|
+
if (installed.extensionId !== extensionId) {
|
|
1610
|
+
throw new ExtensionsServiceError('extension-source-mismatch', `the URL tarball requested as ${extensionId} contains ${JSON.stringify(installed.extensionId)}`);
|
|
1611
|
+
}
|
|
1612
|
+
if (installed.archiveDigest.trim().length === 0) {
|
|
1613
|
+
throw new ExtensionsServiceError('extension-digest-unavailable', `${installed.extensionId}@${installed.version} was installed without a content digest`);
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
id: installed.extensionId,
|
|
1617
|
+
version: installed.version,
|
|
1618
|
+
active: false,
|
|
1619
|
+
source: 'url',
|
|
1620
|
+
resolvedVersion: installed.version,
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Fetch one URL tarball with manual, bounded redirects and a byte/time cap.
|
|
1625
|
+
* @param initial - the validated URL (hash removed).
|
|
1626
|
+
* @param maxBytes - the byte cap.
|
|
1627
|
+
* @param timeoutMs - the idle/wall timeout.
|
|
1628
|
+
* @param signal - cancellation.
|
|
1629
|
+
* @returns the bounded bytes.
|
|
1630
|
+
*/
|
|
1631
|
+
async #fetchUrlTarball(initial, maxBytes, timeoutMs, signal) {
|
|
1632
|
+
const controller = new AbortController();
|
|
1633
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
1634
|
+
if (signal?.aborted === true) {
|
|
1635
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', 'the URL install was cancelled', { cause: signal.reason });
|
|
1636
|
+
}
|
|
1637
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1638
|
+
const timer = setTimeout(() => controller.abort(new Error(`URL fetch exceeded ${timeoutMs} ms`)), timeoutMs);
|
|
1639
|
+
try {
|
|
1640
|
+
let current = initial;
|
|
1641
|
+
const initialPrivate = _a.isPrivateDestination(initial.hostname);
|
|
1642
|
+
for (let hop = 0; hop <= 5; hop += 1) {
|
|
1643
|
+
if (current.protocol !== 'https:' && current.protocol !== 'http:') {
|
|
1644
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', `redirect to non-http(s) ${current.protocol}`);
|
|
1645
|
+
}
|
|
1646
|
+
if (current.username !== '' || current.password !== '') {
|
|
1647
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', 'redirect destination embeds credentials');
|
|
1648
|
+
}
|
|
1649
|
+
const response = await fetch(current, { redirect: 'manual', signal: controller.signal });
|
|
1650
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1651
|
+
const location = response.headers.get('location');
|
|
1652
|
+
if (location === null)
|
|
1653
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', 'a redirect carried no location');
|
|
1654
|
+
current = new URL(location, current);
|
|
1655
|
+
if (!initialPrivate && _a.isPrivateDestination(current.hostname)) {
|
|
1656
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', `a public URL redirected to the private/metadata destination ${current.hostname}, which is not permitted`);
|
|
1657
|
+
}
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (!response.ok) {
|
|
1661
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', `${current.toString()} answered HTTP ${response.status}`);
|
|
1662
|
+
}
|
|
1663
|
+
const declared = Number(response.headers.get('content-length') ?? '');
|
|
1664
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
1665
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', `declared ${declared} bytes exceeds the ${maxBytes}-byte cap`);
|
|
1666
|
+
}
|
|
1667
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
1668
|
+
if (data.byteLength > maxBytes) {
|
|
1669
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', `${data.byteLength} bytes exceeds the ${maxBytes}-byte cap`);
|
|
1670
|
+
}
|
|
1671
|
+
if (data.byteLength === 0)
|
|
1672
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', 'the URL returned no bytes');
|
|
1673
|
+
return data;
|
|
1674
|
+
}
|
|
1675
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', 'more than 5 redirects');
|
|
1676
|
+
}
|
|
1677
|
+
catch (error) {
|
|
1678
|
+
if (error instanceof ExtensionsServiceError)
|
|
1679
|
+
throw error;
|
|
1680
|
+
throw new ExtensionsServiceError('extension-url-fetch-failed', messageOf(error), { cause: error });
|
|
1681
|
+
}
|
|
1682
|
+
finally {
|
|
1683
|
+
clearTimeout(timer);
|
|
1684
|
+
signal?.removeEventListener('abort', onAbort);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Whether a hostname is a local/private/metadata destination (EXT-11).
|
|
1689
|
+
*
|
|
1690
|
+
* A caller-supplied source URL is itself explicit authorization for that destination.
|
|
1691
|
+
* A REDIRECT from a public source into one of these ranges is not: it would turn an
|
|
1692
|
+
* approved public URL into a local-network or cloud-metadata fetch.
|
|
1693
|
+
* @param hostname - the URL hostname.
|
|
1694
|
+
* @returns true for loopback, RFC1918, link-local, ULA and metadata hosts.
|
|
1695
|
+
*/
|
|
1696
|
+
static isPrivateDestination(hostname) {
|
|
1697
|
+
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
1698
|
+
if (host === 'localhost' || host.endsWith('.localhost') || host === '::1')
|
|
1699
|
+
return true;
|
|
1700
|
+
if (host === '169.254.169.254' || host === 'metadata.google.internal')
|
|
1701
|
+
return true;
|
|
1702
|
+
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host))
|
|
1703
|
+
return true;
|
|
1704
|
+
if (/^169\.254\./.test(host))
|
|
1705
|
+
return true;
|
|
1706
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host))
|
|
1707
|
+
return true;
|
|
1708
|
+
if (/^f[cd][0-9a-f]{2}:/.test(host) || /^fe80:/.test(host))
|
|
1709
|
+
return true;
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Install the extension package from the npm registry (task-104).
|
|
1714
|
+
*
|
|
1715
|
+
* The registry metadata is resolved to one exact version with a sha512 integrity;
|
|
1716
|
+
* the tarball is fetched bounded, verified by `stagedInstall` BEFORE extraction,
|
|
1717
|
+
* and installed through the SDK's tar-gzip ArchiveSource. The extension's runtime
|
|
1718
|
+
* dependencies are resolved from the SAME registry through the SDK dependency
|
|
1719
|
+
* graph (never the host-tree resolver), and EXT-10's install-script refusal is the
|
|
1720
|
+
* SDK's own.
|
|
1721
|
+
* @param extensionId - the package identity to install.
|
|
1722
|
+
* @param storeDir - the configured store.
|
|
1723
|
+
* @param signal - cancellation.
|
|
1724
|
+
* @returns the installed identity/version and `source: 'registry'`.
|
|
1725
|
+
* @throws ExtensionsServiceError for every refusal.
|
|
1726
|
+
*/
|
|
1727
|
+
async #installFromRegistry(extensionId, storeDir, signal) {
|
|
1728
|
+
const registryUrl = this.#registryBaseUrl();
|
|
1729
|
+
if (registryUrl === '') {
|
|
1730
|
+
throw new ExtensionsServiceError('extension-registry-unconfigured', 'no npm registry is configured (config.extensionRegistry), so registry installs are unavailable');
|
|
1731
|
+
}
|
|
1732
|
+
const session = createRegistrySession({
|
|
1733
|
+
registryUrl,
|
|
1734
|
+
archiveFactory: createTarGzipArchiveSource,
|
|
1735
|
+
maxTarballBytes: this.#options.registryTarballMaxBytes ?? MAX_DEPENDENCY_ARCHIVE_BYTES,
|
|
1736
|
+
...this.#options.registryMetadataTimeoutMs === undefined
|
|
1737
|
+
? {}
|
|
1738
|
+
: { metadataTimeoutMs: this.#options.registryMetadataTimeoutMs },
|
|
1739
|
+
...this.#options.registryTarballTimeoutMs === undefined
|
|
1740
|
+
? {}
|
|
1741
|
+
: { tarballTimeoutMs: this.#options.registryTarballTimeoutMs },
|
|
1742
|
+
});
|
|
1743
|
+
let resolved;
|
|
1744
|
+
try {
|
|
1745
|
+
// Initial slice: the Settings page does not carry a version yet, so the
|
|
1746
|
+
// configured route installs the registry's `latest` dist-tag. Exact versions
|
|
1747
|
+
// and semver ranges are supported by the resolver itself (dependency ranges
|
|
1748
|
+
// use them); a wire version field is a later slice.
|
|
1749
|
+
resolved = await session.resolve(extensionId, 'latest', signal);
|
|
1750
|
+
}
|
|
1751
|
+
catch (error) {
|
|
1752
|
+
throw this.#mapRegistryFailure(error);
|
|
1753
|
+
}
|
|
1754
|
+
let installed;
|
|
1755
|
+
try {
|
|
1756
|
+
installed = await stagedInstall({
|
|
1757
|
+
source: { kind: 'registry', requested: `${extensionId}@${resolved.version}` },
|
|
1758
|
+
storeDir,
|
|
1759
|
+
expectedSha512: resolved.integrity,
|
|
1760
|
+
archive: session.archive,
|
|
1761
|
+
// Registry metadata recorded AS DATA, never as trust: the bytes are
|
|
1762
|
+
// verified against the same sha512 before anything is extracted.
|
|
1763
|
+
provenance: { registryIntegrity: resolved.integrity },
|
|
1764
|
+
dependencies: this.#installGraph(session),
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
catch (error) {
|
|
1768
|
+
if (error instanceof RegistryError)
|
|
1769
|
+
throw this.#mapRegistryFailure(error);
|
|
1770
|
+
if (error instanceof ManagementError) {
|
|
1771
|
+
if (error.code === 'INTEGRITY_MISMATCH') {
|
|
1772
|
+
throw new ExtensionsServiceError('extension-registry-integrity-mismatch', `${extensionId}@${resolved.version}: ${error.message}`, { cause: error });
|
|
1773
|
+
}
|
|
1774
|
+
if (error.code === 'DEPENDENCY_UNSATISFIED') {
|
|
1775
|
+
throw new ExtensionsServiceError('extension-dependency-unsatisfied', `[DEPENDENCY_UNSATISFIED] ${error.message}`, { cause: error });
|
|
1776
|
+
}
|
|
1777
|
+
throw new ExtensionsServiceError('extension-install-failed', `[${error.code}] ${error.message}`, { cause: error });
|
|
1778
|
+
}
|
|
1779
|
+
throw new ExtensionsServiceError('extension-install-failed', messageOf(error), { cause: error });
|
|
1780
|
+
}
|
|
1781
|
+
if (installed.extensionId !== extensionId) {
|
|
1782
|
+
throw new ExtensionsServiceError('extension-source-mismatch', `the registry tarball requested as ${extensionId}@${resolved.version} contains ${JSON.stringify(installed.extensionId)}`);
|
|
1783
|
+
}
|
|
1784
|
+
if (installed.archiveDigest.trim().length === 0) {
|
|
1785
|
+
throw new ExtensionsServiceError('extension-digest-unavailable', `${installed.extensionId}@${installed.version} was installed without a content digest, so it cannot be verified before activation`);
|
|
1786
|
+
}
|
|
1787
|
+
// Optional tool runtimes are materialized AFTER the package install and are
|
|
1788
|
+
// NOT allowed to fail it: tongue/voz tools `require()` an optional package at
|
|
1789
|
+
// call time, and the requirement model blocks activation if it is absent.
|
|
1790
|
+
await this.#materializeOptionalRuntimes(storeDir, installed, this.#installGraph(session), signal);
|
|
1791
|
+
return {
|
|
1792
|
+
id: installed.extensionId,
|
|
1793
|
+
version: installed.version,
|
|
1794
|
+
active: false,
|
|
1795
|
+
source: 'registry',
|
|
1796
|
+
resolvedVersion: resolved.version,
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* The dependency graph used on the REGISTRY install path (regular AND optional).
|
|
1801
|
+
*
|
|
1802
|
+
* PRECEDENCE: when the operator's preinstalled gate is ON and the host's own
|
|
1803
|
+
* tree holds a package satisfying the range, that operator-completed content is
|
|
1804
|
+
* preferred. The SDK copies it with provenance `preinstalled` and records that
|
|
1805
|
+
* no lifecycle script was run. Anything NOT preinstalled falls to the registry
|
|
1806
|
+
* arm, where EXT-10 still refuses script-declaring packages.
|
|
1807
|
+
*
|
|
1808
|
+
* DEPTH: a preinstalled package is NOT a leaf. The SDK copies its bytes and then
|
|
1809
|
+
* reads that copy's own declared dependencies, enqueueing them one level deeper
|
|
1810
|
+
* (the same walk the registry arm does), because npm hoists most transitive
|
|
1811
|
+
* packages to the root `node_modules` rather than nesting them under the package.
|
|
1812
|
+
* Real trees (onnxruntime-node reaches `define-data-property` at depth 4 and
|
|
1813
|
+
* `es-define-property` at depth 5) therefore exceed the SDK's default 3-level
|
|
1814
|
+
* bound, so the host raises the default to
|
|
1815
|
+
* {@link DEFAULT_REGISTRY_DEPENDENCY_DEPTH} and makes it configurable.
|
|
1816
|
+
* @param session - the registry session.
|
|
1817
|
+
* @returns the graph.
|
|
1818
|
+
*/
|
|
1819
|
+
#installGraph(session) {
|
|
1820
|
+
const depth = this.#options.registryDependencyDepth ?? DEFAULT_REGISTRY_DEPENDENCY_DEPTH;
|
|
1821
|
+
const packages = this.#options.registryDependencyPackages ?? DEFAULT_REGISTRY_DEPENDENCY_PACKAGES;
|
|
1822
|
+
const registryGraph = {
|
|
1823
|
+
...createRegistryDependencyGraph(session),
|
|
1824
|
+
maxDepth: depth,
|
|
1825
|
+
maxPackages: packages,
|
|
1826
|
+
trustedPrebuiltDependencies: this.#options.trustedPrebuiltDependencies ?? DEFAULT_TRUSTED_PREBUILT_DEPENDENCIES,
|
|
1827
|
+
};
|
|
1828
|
+
const preinstalledRoot = this.#options.preinstalledDependenciesRoot ?? '';
|
|
1829
|
+
const allowPreinstalled = this.#options.allowPreinstalledDependencies === true && preinstalledRoot !== '';
|
|
1830
|
+
if (!allowPreinstalled)
|
|
1831
|
+
return registryGraph;
|
|
1832
|
+
const preinstalledRoots = preinstalledSearchRoots(preinstalledRoot);
|
|
1833
|
+
return {
|
|
1834
|
+
resolver: {
|
|
1835
|
+
async resolve(name, range) {
|
|
1836
|
+
// Prefer operator-completed content, root first and then nested package
|
|
1837
|
+
// trees: a preinstalled package's transitive deps live in ITS OWN
|
|
1838
|
+
// node_modules, so root-level lookup alone would fall through to the
|
|
1839
|
+
// registry and refuse a script-declaring leaf under EXT-10.
|
|
1840
|
+
for (const candidateRoot of preinstalledRoots) {
|
|
1841
|
+
try {
|
|
1842
|
+
const found = resolveFirstPartyDependency({ sourcePackageDir: candidateRoot }, name, range);
|
|
1843
|
+
return { name: found.name, version: found.version, dir: found.directory, provenance: 'preinstalled' };
|
|
1844
|
+
}
|
|
1845
|
+
catch {
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return registryGraph.resolver.resolve(name, range);
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1852
|
+
archive: registryGraph.archive,
|
|
1853
|
+
allowPreinstalled: true,
|
|
1854
|
+
preinstalledRoot,
|
|
1855
|
+
maxDepth: depth,
|
|
1856
|
+
maxPackages: packages,
|
|
1857
|
+
trustedPrebuiltDependencies: registryGraph.trustedPrebuiltDependencies,
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Materialize the extension's optionalDependencies without failing the install.
|
|
1862
|
+
*
|
|
1863
|
+
* Each declaration is resolved and materialized through the SAME SDK dependency
|
|
1864
|
+
* graph discipline as regular dependencies (bounded fetch, sha512 verification,
|
|
1865
|
+
* EXT-10 script refusal) into a scratch package, then merged into the
|
|
1866
|
+
* extension's own node_modules. A failure is RECORDED (store sidecar) and the
|
|
1867
|
+
* requirement model blocks activation instead; the package install stands.
|
|
1868
|
+
*
|
|
1869
|
+
* The store record's content digest is recomputed after a successful merge,
|
|
1870
|
+
* because the installed bytes legitimately changed; the archive digest (the
|
|
1871
|
+
* tarball identity) is untouched.
|
|
1872
|
+
* @param storeDir - the configured store.
|
|
1873
|
+
* @param installed - the just-installed package.
|
|
1874
|
+
* @param graph - the dependency graph for the install arm that ran: the registry
|
|
1875
|
+
* graph for a registry install, the host-tree graph for a source-directory
|
|
1876
|
+
* install. Passing the arm's own graph is what keeps the promise of each arm
|
|
1877
|
+
* (a directory install still downloads nothing).
|
|
1878
|
+
* @param signal - cancellation.
|
|
1879
|
+
*/
|
|
1880
|
+
async #materializeOptionalRuntimes(storeDir, installed, graph, signal) {
|
|
1881
|
+
const optional = readDeclaredDependencies(installed.installPath, 'optionalDependencies');
|
|
1882
|
+
if (optional.length === 0)
|
|
1883
|
+
return;
|
|
1884
|
+
const attempts = [];
|
|
1885
|
+
const locked = [];
|
|
1886
|
+
for (const dependency of optional) {
|
|
1887
|
+
signal?.throwIfAborted();
|
|
1888
|
+
const scratch = mkdtempSync(join(tmpdir(), 'dsh-optional-runtime-'));
|
|
1889
|
+
try {
|
|
1890
|
+
writeFileSync(join(scratch, 'package.json'), `${JSON.stringify({ name: 'dsh-optional-runtime-host', version: '0.0.0', dependencies: { [dependency.name]: dependency.range } }, null, 2)}\n`, 'utf8');
|
|
1891
|
+
const materialized = await installDependencyGraph(scratch, graph);
|
|
1892
|
+
const sourceNodeModules = join(scratch, 'node_modules');
|
|
1893
|
+
const targetNodeModules = join(installed.installPath, 'node_modules');
|
|
1894
|
+
if (existsSync(sourceNodeModules)) {
|
|
1895
|
+
/**
|
|
1896
|
+
* Enumerate PACKAGES, never top-level entries. A scoped dependency lives at
|
|
1897
|
+
* `node_modules/@scope/name`, and `node_modules/@scope` already exists as
|
|
1898
|
+
* soon as ANY package shares that scope — the runtime link always creates
|
|
1899
|
+
* `node_modules/@try-works`. Keying the merge on the scope directory
|
|
1900
|
+
* therefore skipped every later package in that scope while the record
|
|
1901
|
+
* below still marked it satisfied: a silently missing engine.
|
|
1902
|
+
*/
|
|
1903
|
+
const packageNames = [];
|
|
1904
|
+
for (const entry of readdirSync(sourceNodeModules)) {
|
|
1905
|
+
if (!entry.startsWith('@')) {
|
|
1906
|
+
packageNames.push(entry);
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
for (const scoped of readdirSync(join(sourceNodeModules, entry))) {
|
|
1910
|
+
packageNames.push(`${entry}/${scoped}`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
for (const name of packageNames) {
|
|
1914
|
+
const segments = name.split('/');
|
|
1915
|
+
const from = join(sourceNodeModules, ...segments);
|
|
1916
|
+
const to = join(targetNodeModules, ...segments);
|
|
1917
|
+
if (existsSync(to)) {
|
|
1918
|
+
const kept = readInstalledDependencyVersion(installed.installPath, name);
|
|
1919
|
+
const wanted = materialized.find(entry => entry.name === name);
|
|
1920
|
+
if (kept !== undefined && wanted !== undefined && kept !== wanted.version) {
|
|
1921
|
+
// A transitive runtime dependency may already be hoisted by an
|
|
1922
|
+
// earlier optional install (transformers brings global-agent 3,
|
|
1923
|
+
// onnxruntime asks for 4). Keep the existing tree: the engine
|
|
1924
|
+
// entrypoints are already present, and failing the whole optional
|
|
1925
|
+
// install here leaves the extension unactivatable. The mismatch is
|
|
1926
|
+
// a dependency-graph limitation, not a reason to discard a working
|
|
1927
|
+
// engine; a future nested-version layout can remove this tolerance.
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
mkdirSync(join(to, '..'), { recursive: true });
|
|
1933
|
+
cpSync(from, to, { recursive: true });
|
|
1934
|
+
}
|
|
1935
|
+
locked.push(...materialized);
|
|
1936
|
+
}
|
|
1937
|
+
const own = materialized.find(entry => entry.name === dependency.name);
|
|
1938
|
+
attempts.push({
|
|
1939
|
+
name: dependency.name,
|
|
1940
|
+
range: dependency.range,
|
|
1941
|
+
satisfied: true,
|
|
1942
|
+
...own === undefined ? {} : { version: own.version },
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
catch (error) {
|
|
1946
|
+
if (signal?.aborted === true)
|
|
1947
|
+
throw error;
|
|
1948
|
+
attempts.push({ name: dependency.name, range: dependency.range, satisfied: false, error: messageOf(error) });
|
|
1949
|
+
}
|
|
1950
|
+
finally {
|
|
1951
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
writeRuntimeDependencyDocument(storeDir, installed.extensionId, installed.version, attempts);
|
|
1955
|
+
if (locked.length === 0)
|
|
1956
|
+
return;
|
|
1957
|
+
const store = new ExtensionStore(storeDir);
|
|
1958
|
+
const record = store.listInstalled().value.find(candidate => candidate.extensionId === installed.extensionId && candidate.version === installed.version);
|
|
1959
|
+
if (record === undefined)
|
|
1960
|
+
return;
|
|
1961
|
+
const existing = new Set((record.dependencies ?? []).map(entry => entry.name));
|
|
1962
|
+
const additions = locked.filter(entry => !existing.has(entry.name));
|
|
1963
|
+
if (additions.length === 0)
|
|
1964
|
+
return;
|
|
1965
|
+
replaceInstalledRecord(storeDir, {
|
|
1966
|
+
...record,
|
|
1967
|
+
contentDigest: digestInstalledContent(installed.installPath),
|
|
1968
|
+
dependencies: [...(record.dependencies ?? []), ...additions],
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Install one extension from a local source directory (EXT-06 developer mode).
|
|
1973
|
+
*
|
|
1974
|
+
* Refusals are named and happen BEFORE the copy: an unconfigured store, an
|
|
1975
|
+
* unconfigured/missing source, a source whose package identity differs from the
|
|
1976
|
+
* requested id, a disallowed source-directory install, or content that cannot be
|
|
1977
|
+
* digested. The SDK then copies, validates, verifies and records the install; a
|
|
1978
|
+
* failure there is re-thrown as `extension-install-failed` with the SDK's own
|
|
1979
|
+
* named code in the message.
|
|
1980
|
+
* @param extensionId - the package identity to install.
|
|
1981
|
+
* @param explicitSource - an explicit source directory, or undefined for the configured one.
|
|
1982
|
+
* @param storeDir - the configured store.
|
|
1983
|
+
* @param signal - cancellation.
|
|
1984
|
+
* @returns the installed identity/version and `source: 'directory'`.
|
|
1985
|
+
* @throws ExtensionsServiceError for every refusal and failure.
|
|
1986
|
+
*/
|
|
1987
|
+
async #installFromDirectory(extensionId, explicitSource, storeDir, signal) {
|
|
1988
|
+
signal?.throwIfAborted();
|
|
1989
|
+
const sourceDir = explicitSource
|
|
1990
|
+
?? (() => {
|
|
1991
|
+
const configured = this.#sourceDir();
|
|
1992
|
+
if (configured === '') {
|
|
1993
|
+
throw new ExtensionsServiceError('extension-source-unconfigured', `no source was given for ${extensionId} and no first-party source directory is configured (config.extensionSourceDir)`);
|
|
1994
|
+
}
|
|
1995
|
+
return join(configured, packageDirFor(extensionId));
|
|
1996
|
+
})();
|
|
1997
|
+
let sourceIsDirectory = false;
|
|
1998
|
+
try {
|
|
1999
|
+
sourceIsDirectory = statSync(sourceDir).isDirectory();
|
|
2000
|
+
}
|
|
2001
|
+
catch {
|
|
2002
|
+
sourceIsDirectory = false;
|
|
2003
|
+
}
|
|
2004
|
+
if (!sourceIsDirectory) {
|
|
2005
|
+
throw new ExtensionsServiceError('extension-source-missing', `the source directory ${sourceDir} does not exist or is not a directory`);
|
|
2006
|
+
}
|
|
2007
|
+
let discovered;
|
|
2008
|
+
try {
|
|
2009
|
+
discovered = discoverExtension(sourceDir);
|
|
2010
|
+
}
|
|
2011
|
+
catch (error) {
|
|
2012
|
+
throw new ExtensionsServiceError('extension-source-invalid', `the source directory ${sourceDir} is not a usable extension package: ${messageOf(error)}`, { cause: error });
|
|
2013
|
+
}
|
|
2014
|
+
if (discovered.packageName !== extensionId) {
|
|
2015
|
+
throw new ExtensionsServiceError('extension-source-mismatch', `the source directory ${sourceDir} contains ${JSON.stringify(discovered.packageName)}, not ${JSON.stringify(extensionId)}`);
|
|
2016
|
+
}
|
|
2017
|
+
if (this.#options.allowSourceDirectories !== true) {
|
|
2018
|
+
throw new ExtensionsServiceError('extension-source-directories-not-allowed', 'source-directory installs are developer mode and are not allowed by the operator configuration (extensions.allowSourceDirectories)');
|
|
2019
|
+
}
|
|
2020
|
+
// Refuse BEFORE copying when the source cannot be digested: an installation the
|
|
2021
|
+
// store cannot verify later would be bytes nobody can attest to (EXT-12/EXT-33).
|
|
2022
|
+
try {
|
|
2023
|
+
this.#digest(sourceDir);
|
|
2024
|
+
}
|
|
2025
|
+
catch (error) {
|
|
2026
|
+
throw new ExtensionsServiceError('extension-digest-unavailable', `the content digest of ${sourceDir} could not be computed, so the install was refused before copying: ${messageOf(error)}`, { cause: error });
|
|
2027
|
+
}
|
|
2028
|
+
let installed;
|
|
2029
|
+
try {
|
|
2030
|
+
installed = await this.#install({
|
|
2031
|
+
sourceDir,
|
|
2032
|
+
storeDir,
|
|
2033
|
+
allowSourceDirectories: true,
|
|
2034
|
+
/**
|
|
2035
|
+
* The host-tree dependency graph is supplied only on this developer-mode
|
|
2036
|
+
* path. Registry installs resolve dependencies from the registry instead
|
|
2037
|
+
* (never from the host's tree).
|
|
2038
|
+
*/
|
|
2039
|
+
dependencies: this.#dependencyGraph(sourceDir),
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
catch (error) {
|
|
2043
|
+
if (error instanceof ManagementError && error.code === 'DEPENDENCY_UNSATISFIED') {
|
|
2044
|
+
throw new ExtensionsServiceError('extension-dependency-unsatisfied', `[DEPENDENCY_UNSATISFIED] ${error.message}`, { cause: error });
|
|
2045
|
+
}
|
|
2046
|
+
const named = error instanceof ManagementError ? `[${error.code}] ` : '';
|
|
2047
|
+
throw new ExtensionsServiceError('extension-install-failed', `${named}${messageOf(error)}`, { cause: error });
|
|
2048
|
+
}
|
|
2049
|
+
if (installed.archiveDigest.trim().length === 0) {
|
|
2050
|
+
throw new ExtensionsServiceError('extension-digest-unavailable', `${installed.extensionId}@${installed.version} was installed without a content digest, so it cannot be verified before activation`);
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* OPTIONAL RUNTIMES. The packages an extension declares in
|
|
2054
|
+
* `optionalDependencies` are the engine its tools `import()` at call time.
|
|
2055
|
+
* The REGISTRY arm already materialized them; this arm did not, so a
|
|
2056
|
+
* first-party install (EmbeddingGemma's onnxruntime-node and
|
|
2057
|
+
* @huggingface/transformers, Tongue's runtime) activated with the engine
|
|
2058
|
+
* absent and every tool reported needs-preparation naming a package that was
|
|
2059
|
+
* never installed. Resolution uses the HOST-TREE graph, so developer mode
|
|
2060
|
+
* still downloads nothing; a failure is recorded and blocks activation
|
|
2061
|
+
* instead of failing the package install.
|
|
2062
|
+
*/
|
|
2063
|
+
await this.#materializeOptionalRuntimes(storeDir, installed, this.#dependencyGraph(sourceDir), signal);
|
|
2064
|
+
return {
|
|
2065
|
+
id: installed.extensionId,
|
|
2066
|
+
version: installed.version,
|
|
2067
|
+
active: false,
|
|
2068
|
+
source: 'directory',
|
|
2069
|
+
resolvedVersion: installed.version,
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Activate an installed extension and clear its disabled override.
|
|
2074
|
+
*
|
|
2075
|
+
* Activation runs through the mounted lifecycle, which verifies the installed
|
|
2076
|
+
* bytes, publishes the contributions and persists `desiredActivation: 'active'`.
|
|
2077
|
+
* The disabled list is updated only after activation succeeds, so a refused
|
|
2078
|
+
* activation does not leave the operator's override claiming a state change that
|
|
2079
|
+
* did not happen.
|
|
2080
|
+
*
|
|
2081
|
+
* With an approval store mounted (`approvals`), this method does NOT activate: it
|
|
2082
|
+
* records a pending `extension-activate` request and refuses, and only
|
|
2083
|
+
* `commitApproval` applies the mutation. Without one, the direct path is the
|
|
2084
|
+
* documented pre-approved compatibility composition.
|
|
2085
|
+
* @param extensionId - the installed extension.
|
|
2086
|
+
* @param signal - cancellation.
|
|
2087
|
+
* @returns the id with the observed active/disabled state.
|
|
2088
|
+
* @throws ExtensionsServiceError for every refusal.
|
|
2089
|
+
*/
|
|
2090
|
+
async enable(extensionId, signal) {
|
|
2091
|
+
signal?.throwIfAborted();
|
|
2092
|
+
assertUsableId(extensionId);
|
|
2093
|
+
const storeDir = this.#requireStoreDir();
|
|
2094
|
+
this.#requireInstalled(storeDir, extensionId);
|
|
2095
|
+
/**
|
|
2096
|
+
* SEC-09: WITH an approval store mounted, activation is a management-sensitive
|
|
2097
|
+
* mutation and must pass an explicit operator decision. The request is RECORDED
|
|
2098
|
+
* here — so the operator has something to approve — and the call refuses
|
|
2099
|
+
* synchronously: nothing waits for a UI, and nothing mutates before a commit.
|
|
2100
|
+
* `commitApproval` applies the approved mutation.
|
|
2101
|
+
*/
|
|
2102
|
+
const approvals = this.#approvals();
|
|
2103
|
+
if (approvals !== undefined) {
|
|
2104
|
+
const pending = approvals.request({
|
|
2105
|
+
kind: 'extension-activate',
|
|
2106
|
+
scope: { kind: 'extension', id: extensionId },
|
|
2107
|
+
operator: HOST_APPROVAL_OPERATOR,
|
|
2108
|
+
reason: `activate ${extensionId} through the management surface`,
|
|
2109
|
+
});
|
|
2110
|
+
throw new ExtensionsServiceError('extension-approval-required', `approval/pending: ${pending.id} was recorded for extension ${extensionId}; approve it and commit before it can be activated`);
|
|
2111
|
+
}
|
|
2112
|
+
return await this.#enableApproved(extensionId, signal);
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* The activation itself, shared by the direct path and an approved commit.
|
|
2116
|
+
*
|
|
2117
|
+
* Private because every public entry point must first answer the approval question:
|
|
2118
|
+
* `enable` is the no-approval-store compatibility composition, and `commitApproval`
|
|
2119
|
+
* is the approved path. Neither the lifecycle nor the disabled override is touched
|
|
2120
|
+
* until that decision is made.
|
|
2121
|
+
* @param extensionId - the installed extension.
|
|
2122
|
+
* @param signal - cancellation.
|
|
2123
|
+
* @returns the id with the observed active/disabled state.
|
|
2124
|
+
*/
|
|
2125
|
+
async #enableApproved(extensionId, signal) {
|
|
2126
|
+
signal?.throwIfAborted();
|
|
2127
|
+
const storeDir = this.#requireStoreDir();
|
|
2128
|
+
const record = this.#requireInstalled(storeDir, extensionId);
|
|
2129
|
+
const lifecycle = this.#lifecycle();
|
|
2130
|
+
if (lifecycle === undefined) {
|
|
2131
|
+
throw new ExtensionsServiceError('extension-activation-unavailable', `the extension lifecycle is not mounted, so ${extensionId} cannot be activated by this host`);
|
|
2132
|
+
}
|
|
2133
|
+
const current = lifecycle.stateOf(extensionId);
|
|
2134
|
+
/**
|
|
2135
|
+
* THE PREFLIGHT. Every prerequisite the deployment can check is evaluated
|
|
2136
|
+
* BEFORE the lifecycle is touched: a deployment gap must never surface as the
|
|
2137
|
+
* SDK's generic EXTENSION_FAULT after an activation attempt. The refusal's code
|
|
2138
|
+
* maps the unmet requirement and its message is the requirement's own reason,
|
|
2139
|
+
* so the page, `describe()` and the error cannot disagree.
|
|
2140
|
+
*/
|
|
2141
|
+
const preflightIntegrity = this.#integrityOf(storeDir, record);
|
|
2142
|
+
const preflightFacts = readExtensionFacts(record.installPath);
|
|
2143
|
+
const requirements = this.#computeRequirements({
|
|
2144
|
+
id: extensionId,
|
|
2145
|
+
installed: true,
|
|
2146
|
+
record,
|
|
2147
|
+
state: current,
|
|
2148
|
+
integrity: preflightIntegrity.integrity,
|
|
2149
|
+
integrityReason: preflightIntegrity.reason,
|
|
2150
|
+
storeDir,
|
|
2151
|
+
sourceDir: this.#sourceDir(),
|
|
2152
|
+
lockCorrupt: false,
|
|
2153
|
+
lockDiagnostic: null,
|
|
2154
|
+
profileStates: readProfileStates(new ExtensionStore(storeDir), this.#profile()),
|
|
2155
|
+
sourcePackage: undefined,
|
|
2156
|
+
facts: preflightFacts,
|
|
2157
|
+
});
|
|
2158
|
+
const unmet = firstBlockingRequirement(requirements, true);
|
|
2159
|
+
if (unmet !== undefined) {
|
|
2160
|
+
if (unmet.kind === 'assets') {
|
|
2161
|
+
// The established preparation refusal keeps its own code and wording.
|
|
2162
|
+
throw new ExtensionsServiceError('extension-preparation-required', unmet.reason);
|
|
2163
|
+
}
|
|
2164
|
+
if (unmet.kind === 'integrity') {
|
|
2165
|
+
// The activation path named the SDK's integrity code; the preflight keeps
|
|
2166
|
+
// that classification while refusing before the lifecycle call.
|
|
2167
|
+
throw new ExtensionsServiceError('extension-activation-failed', `[INTEGRITY_MISMATCH] ${unmet.reason}`);
|
|
2168
|
+
}
|
|
2169
|
+
if (unmet.kind === 'runtime') {
|
|
2170
|
+
// Activation that cannot serve the extension's tools is not activation.
|
|
2171
|
+
throw new ExtensionsServiceError('extension-runtime-unavailable', unmet.reason);
|
|
2172
|
+
}
|
|
2173
|
+
throw new ExtensionsServiceError('extension-requirement-unmet', unmet.reason);
|
|
2174
|
+
}
|
|
2175
|
+
const ownedHere = current?.observedActivation === 'active'
|
|
2176
|
+
&& (current.ownerPid === undefined || current.ownerPid === process.pid);
|
|
2177
|
+
/**
|
|
2178
|
+
* RE-ACTIVATE EVEN WHEN THIS PROCESS ALREADY OWNS THE EXTENSION.
|
|
2179
|
+
*
|
|
2180
|
+
* `activate` reconciles the published surface, and the SDK documents
|
|
2181
|
+
* re-activation as the way a caller PICKS UP newly recorded readiness. Skipping
|
|
2182
|
+
* it here made `enable` a no-op after `prepare` or after an upgrade: the row
|
|
2183
|
+
* turned active while the registry still held the superseded (or empty) set of
|
|
2184
|
+
* tools, and only a disable→enable cycle made the new tool names appear.
|
|
2185
|
+
* Admission is still only needed on the first activation: the registrar refuses
|
|
2186
|
+
* a contribution from an extension outside its activated set, and a re-activation
|
|
2187
|
+
* of an already-admitted id must not be re-admitted.
|
|
2188
|
+
*/
|
|
2189
|
+
if (!ownedHere)
|
|
2190
|
+
this.#options.onActivationChange?.(extensionId, true);
|
|
2191
|
+
try {
|
|
2192
|
+
await lifecycle.activate(extensionId, record.version);
|
|
2193
|
+
}
|
|
2194
|
+
catch (error) {
|
|
2195
|
+
if (!ownedHere)
|
|
2196
|
+
this.#options.onActivationChange?.(extensionId, false);
|
|
2197
|
+
const named = error instanceof ManagementError ? `[${error.code}] ` : '';
|
|
2198
|
+
throw new ExtensionsServiceError('extension-activation-failed', `${named}${messageOf(error)}`, { cause: error });
|
|
2199
|
+
}
|
|
2200
|
+
this.#rememberDisabled(extensionId, false);
|
|
2201
|
+
await this.#persistDisabled();
|
|
2202
|
+
const observed = lifecycle.stateOf(extensionId);
|
|
2203
|
+
return {
|
|
2204
|
+
id: extensionId,
|
|
2205
|
+
active: observed?.observedActivation === 'active'
|
|
2206
|
+
&& (observed.ownerPid === undefined || observed.ownerPid === process.pid),
|
|
2207
|
+
disabled: this.#effectiveDisabled().has(extensionId),
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Record a pending approval for one management-sensitive mutation (SEC-09).
|
|
2212
|
+
*
|
|
2213
|
+
* The seam is REQUIRED here: creating a record on a host that mounted no store
|
|
2214
|
+
* would hand the operator an approval nothing can commit — the dead end this
|
|
2215
|
+
* wiring exists to remove.
|
|
2216
|
+
* @param input - the request facts (kind, exact scope, operator, reason, optional TTL).
|
|
2217
|
+
* @returns the pending record (a plain-JSON copy).
|
|
2218
|
+
* @throws ExtensionsServiceError when no approval store is mounted; ApprovalError for a bad request.
|
|
2219
|
+
*/
|
|
2220
|
+
requestApproval(input) {
|
|
2221
|
+
return this.#requireApprovals().request(input);
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Apply an explicit operator decision to a request (SEC-09).
|
|
2225
|
+
*
|
|
2226
|
+
* Decisions are the operator's alone; this method only forwards one, and the store
|
|
2227
|
+
* enforces that repeating the SAME decision is idempotent while the opposite
|
|
2228
|
+
* decision after a decision is refused.
|
|
2229
|
+
* @param id - the approval id.
|
|
2230
|
+
* @param decision - approve or deny, who decided, and an optional note.
|
|
2231
|
+
* @returns the decided record.
|
|
2232
|
+
* @throws ApprovalError for an unknown/expired/committed request or a conflicting decision.
|
|
2233
|
+
*/
|
|
2234
|
+
decideApproval(id, decision) {
|
|
2235
|
+
return this.#requireApprovals().decide(id, decision);
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Consume an approved activation and apply it EXACTLY once.
|
|
2239
|
+
*
|
|
2240
|
+
* Order is deliberate: the approval is consumed — and its scope checked against
|
|
2241
|
+
* `extensionId` by the store — BEFORE any mutation, so a wrong-scope or replayed
|
|
2242
|
+
* commit cannot touch the operator's extension. The store's one-shot commit is the
|
|
2243
|
+
* exactly-once guarantee; a second call is `approval/already-committed` and never
|
|
2244
|
+
* reaches the lifecycle. A mutation that fails AFTER the commit leaves the approval
|
|
2245
|
+
* spent: the operator re-requests rather than replaying an approval whose
|
|
2246
|
+
* conditions may have changed.
|
|
2247
|
+
* @param id - the approval id.
|
|
2248
|
+
* @param extensionId - the exact extension the caller is about to activate.
|
|
2249
|
+
* @param signal - cancellation.
|
|
2250
|
+
* @returns the activation result.
|
|
2251
|
+
* @throws ApprovalError for a pending/denied/expired/committed/mismatched approval.
|
|
2252
|
+
* @throws ExtensionsServiceError for the activation's own refusals.
|
|
2253
|
+
*/
|
|
2254
|
+
async commitApproval(id, extensionId, signal) {
|
|
2255
|
+
signal?.throwIfAborted();
|
|
2256
|
+
assertUsableId(extensionId);
|
|
2257
|
+
const approvals = this.#requireApprovals();
|
|
2258
|
+
const record = approvals.describe(id);
|
|
2259
|
+
if (record.kind !== 'extension-activate') {
|
|
2260
|
+
throw new ExtensionsServiceError('extension-approval-required', `approval ${id} covers ${record.kind}, which this management surface cannot commit`);
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Consumption happens BEFORE the mutation, so a scope mismatch, a denial, an
|
|
2264
|
+
* expiry or a replay refuses with the store's own named error and the extension
|
|
2265
|
+
* is never touched.
|
|
2266
|
+
*/
|
|
2267
|
+
approvals.commit(id, { kind: 'extension', id: extensionId });
|
|
2268
|
+
return await this.#enableApproved(extensionId, signal);
|
|
2269
|
+
}
|
|
2270
|
+
/** Live per-tool toggle through the running host (EXT-31 settings action). */
|
|
2271
|
+
async setToolEnabled(extensionId, toolName, enabled, signal) {
|
|
2272
|
+
signal?.throwIfAborted();
|
|
2273
|
+
assertUsableId(extensionId);
|
|
2274
|
+
const lifecycle = this.#lifecycle();
|
|
2275
|
+
if (lifecycle === undefined) {
|
|
2276
|
+
throw new ExtensionsServiceError('extension-activation-unavailable', `the extension lifecycle is not mounted, so ${extensionId}'s tools cannot be toggled by this host`);
|
|
2277
|
+
}
|
|
2278
|
+
const state = await lifecycle.setToolEnabled(extensionId, toolName, enabled);
|
|
2279
|
+
return {
|
|
2280
|
+
id: extensionId,
|
|
2281
|
+
toolName,
|
|
2282
|
+
enabled: state.tools.find(tool => tool.toolName === toolName)?.enabled ?? enabled,
|
|
2283
|
+
active: state.observedActivation === 'active',
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
/** Live uninstall; an active extension requires explicit deactivation (EXT-31 settings action). */
|
|
2287
|
+
async uninstall(extensionId, deactivate = false, signal) {
|
|
2288
|
+
signal?.throwIfAborted();
|
|
2289
|
+
assertUsableId(extensionId);
|
|
2290
|
+
this.#requireStoreDir();
|
|
2291
|
+
this.#requireInstalled(this.#requireStoreDir(), extensionId);
|
|
2292
|
+
const lifecycle = this.#lifecycle();
|
|
2293
|
+
if (lifecycle === undefined) {
|
|
2294
|
+
throw new ExtensionsServiceError('extension-activation-unavailable', `the extension lifecycle is not mounted, so ${extensionId} cannot be uninstalled by this host`);
|
|
2295
|
+
}
|
|
2296
|
+
try {
|
|
2297
|
+
await lifecycle.uninstall(extensionId, { deactivate });
|
|
2298
|
+
}
|
|
2299
|
+
catch (error) {
|
|
2300
|
+
if (error instanceof ManagementError && error.code === 'EXTENSION_IN_USE') {
|
|
2301
|
+
throw new ExtensionsServiceError('extension-in-use', messageOf(error), { cause: error });
|
|
2302
|
+
}
|
|
2303
|
+
throw error;
|
|
2304
|
+
}
|
|
2305
|
+
return { id: extensionId, removed: true, active: false };
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Record the failed activation attempt a fresh extension needs before its
|
|
2309
|
+
* readiness can be written.
|
|
2310
|
+
*
|
|
2311
|
+
* The SDK's `recordReadiness` refuses an extension the profile has never seen,
|
|
2312
|
+
* and a freshly installed extension has no profile state until an activation is
|
|
2313
|
+
* attempted. The probe therefore performs the SAME attempt the operator's failed
|
|
2314
|
+
* Enable performs: every declared deployment defaults to `needs-preparation`, so
|
|
2315
|
+
* the SDK refuses at the readiness assessment, records the state, and publishes
|
|
2316
|
+
* nothing. Any other refusal is a real problem and is reported.
|
|
2317
|
+
* @param lifecycle - the mounted lifecycle.
|
|
2318
|
+
* @param extensionId - the installed extension.
|
|
2319
|
+
* @param version - its installed version.
|
|
2320
|
+
* @throws ExtensionsServiceError when the probe fails for a reason other than the
|
|
2321
|
+
* expected preparation refusal.
|
|
2322
|
+
*/
|
|
2323
|
+
async #recordPreparationBlocker(lifecycle, extensionId, version) {
|
|
2324
|
+
try {
|
|
2325
|
+
await lifecycle.activate(extensionId, version, { recordFailure: false });
|
|
2326
|
+
}
|
|
2327
|
+
catch (error) {
|
|
2328
|
+
if (error instanceof ManagementError && error.code === 'EXTENSION_FAULT')
|
|
2329
|
+
return;
|
|
2330
|
+
throw new ExtensionsServiceError('extension-preparation-failed', `the extension's activation record could not be written before preparation: ${messageOf(error)}`, { cause: error });
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Acquire and verify an installed extension's model assets, then record the
|
|
2335
|
+
* resulting readiness.
|
|
2336
|
+
*
|
|
2337
|
+
* This is the management-channel `extensions-prepare-plan` + `extensions-prepare`
|
|
2338
|
+
* pair behind one call, over the SAME host surface: the plan is resolved from the
|
|
2339
|
+
* installed manifest's asset lock, the acquisition runs through the injectable
|
|
2340
|
+
* source (the real ranged HTTP source unless the host injects one), and a
|
|
2341
|
+
* successful acquisition records `prepared-cold` through the lifecycle — which is
|
|
2342
|
+
* what lets a following `enable` activate the extension.
|
|
2343
|
+
*
|
|
2344
|
+
* Success is reported ONLY when that readiness was actually recorded for the
|
|
2345
|
+
* planned deployment. The acquisition and the readiness are two different facts,
|
|
2346
|
+
* and they can disagree (a surface whose readiness write did not land, a host
|
|
2347
|
+
* that never mounted the lifecycle, an installed build the assets cannot make
|
|
2348
|
+
* callable); reporting `prepared: true` there is what makes Prepare look like a
|
|
2349
|
+
* no-op, because the page celebrates while the row keeps its blocker.
|
|
2350
|
+
*
|
|
2351
|
+
* The RES-02 executable gate is deliberately CLOSED here: the Remote call carries
|
|
2352
|
+
* no executable-approval argument, so a plan containing an executable asset is
|
|
2353
|
+
* returned as `prepared: false` with the registry's own reason rather than
|
|
2354
|
+
* acquired. The pinned EmbeddingGemma assets are not executable.
|
|
2355
|
+
* @param extensionId - the installed extension to prepare.
|
|
2356
|
+
* @param signal - cancellation.
|
|
2357
|
+
* @returns the disclosure that was executed and the resulting readiness.
|
|
2358
|
+
* @throws ExtensionsServiceError for every precondition and plan failure.
|
|
2359
|
+
*/
|
|
2360
|
+
async prepare(extensionId, signal) {
|
|
2361
|
+
try {
|
|
2362
|
+
const result = await this.#prepareRequest(extensionId, signal);
|
|
2363
|
+
this.#options.onAudit?.({
|
|
2364
|
+
operation: 'prepare', extensionId, version: null, outcome: 'succeeded',
|
|
2365
|
+
});
|
|
2366
|
+
return result;
|
|
2367
|
+
}
|
|
2368
|
+
catch (error) {
|
|
2369
|
+
this.#options.onAudit?.({
|
|
2370
|
+
operation: 'prepare',
|
|
2371
|
+
extensionId,
|
|
2372
|
+
version: null,
|
|
2373
|
+
outcome: 'failed',
|
|
2374
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2375
|
+
});
|
|
2376
|
+
throw error;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
async #prepareRequest(extensionId, signal) {
|
|
2380
|
+
signal?.throwIfAborted();
|
|
2381
|
+
assertUsableId(extensionId);
|
|
2382
|
+
const storeDir = this.#requireStoreDir();
|
|
2383
|
+
const record = this.#requireInstalled(storeDir, extensionId);
|
|
2384
|
+
const surface = this.#options.prepareSurface?.();
|
|
2385
|
+
if (surface === undefined) {
|
|
2386
|
+
throw new ExtensionsServiceError('extension-preparation-unavailable', `the prepared-assets surface is not mounted, so ${extensionId}'s model assets cannot be prepared by this host`);
|
|
2387
|
+
}
|
|
2388
|
+
const declaredModels = readExtensionFacts(record.installPath).models;
|
|
2389
|
+
if (declaredModels.length === 0) {
|
|
2390
|
+
throw new ExtensionsServiceError('extension-preparation-failed', `${extensionId} declares no model deployment to prepare`);
|
|
2391
|
+
}
|
|
2392
|
+
let plan;
|
|
2393
|
+
try {
|
|
2394
|
+
plan = surface.plan(extensionId);
|
|
2395
|
+
}
|
|
2396
|
+
catch (error) {
|
|
2397
|
+
throw new ExtensionsServiceError('extension-preparation-failed', `the preparation plan for ${extensionId} could not be resolved: ${messageOf(error)}`, { cause: error });
|
|
2398
|
+
}
|
|
2399
|
+
const lifecycle = this.#lifecycle();
|
|
2400
|
+
/**
|
|
2401
|
+
* A prose source (no URL, no cached bytes) is refused BEFORE the lifecycle
|
|
2402
|
+
* probe or any transfer: a URL-parse failure after waiting is the wrong answer.
|
|
2403
|
+
*/
|
|
2404
|
+
const unavailable = firstUnavailableAssetSource(plan.assets.map(asset => ({ assetId: asset.assetId, source: asset.source, digest: asset.digest })), surface.registry.cacheDirectory);
|
|
2405
|
+
if (unavailable !== undefined) {
|
|
2406
|
+
throw new ExtensionsServiceError('extension-asset-source-unavailable', `[ASSET_SOURCE_UNAVAILABLE] ${assetSourceUnavailableReason(unavailable.assetId, unavailable.source)}`);
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* REFUSE BEFORE THE TRANSFER when readiness has no writer. The acquisition is
|
|
2410
|
+
* the expensive half of preparation and the readiness write is the ONLY fact
|
|
2411
|
+
* that unblocks activation; without a mounted lifecycle the write cannot
|
|
2412
|
+
* happen, so a transfer would download the declared asset set and change
|
|
2413
|
+
* nothing. Naming the host gap costs nothing and acquires nothing.
|
|
2414
|
+
*/
|
|
2415
|
+
if (lifecycle === undefined) {
|
|
2416
|
+
throw new ExtensionsServiceError('extension-preparation-failed', `the extension lifecycle is not mounted, so ${extensionId}'s readiness cannot be recorded; `
|
|
2417
|
+
+ 'no assets were acquired');
|
|
2418
|
+
}
|
|
2419
|
+
if (lifecycle.stateOf(extensionId) === undefined) {
|
|
2420
|
+
await this.#recordPreparationBlocker(lifecycle, extensionId, record.version);
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* RES-02: the Remote path has no per-call executable argument, so the approval
|
|
2424
|
+
* is the operator's STANDING configuration value. With it off the registry
|
|
2425
|
+
* returns its exact refusal and NOTHING is acquired; with it on the same
|
|
2426
|
+
* digest-pinned plan is approved as a whole.
|
|
2427
|
+
*/
|
|
2428
|
+
const allowExecutable = this.#options.allowExecutablePreparation?.() === true;
|
|
2429
|
+
let outcome;
|
|
2430
|
+
try {
|
|
2431
|
+
outcome = await surface.prepare(extensionId, {
|
|
2432
|
+
source: (this.#options.preparationSource ?? createHttpSource)(),
|
|
2433
|
+
allowExecutable,
|
|
2434
|
+
signal: signal ?? new AbortController().signal,
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
catch (error) {
|
|
2438
|
+
throw new ExtensionsServiceError('extension-preparation-failed', `the preparation of ${extensionId} could not be recorded: ${messageOf(error)}`, { cause: error });
|
|
2439
|
+
}
|
|
2440
|
+
const state = lifecycle?.stateOf(extensionId);
|
|
2441
|
+
const readiness = readinessView(state?.readiness ?? {}, declaredModels);
|
|
2442
|
+
if (!outcome.ok) {
|
|
2443
|
+
return {
|
|
2444
|
+
id: extensionId,
|
|
2445
|
+
prepared: false,
|
|
2446
|
+
reason: outcome.reason,
|
|
2447
|
+
plan: preparationPlanView(plan),
|
|
2448
|
+
readiness,
|
|
2449
|
+
groups: [],
|
|
2450
|
+
activationReady: false,
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
const integrity = new ExtensionStore(storeDir).verifyUnchanged(record).unchanged;
|
|
2454
|
+
const blockers = preparationBlockers(state?.readiness ?? {}, declaredModels);
|
|
2455
|
+
/**
|
|
2456
|
+
* THE ACQUISITION IS NOT THE OUTCOME. `outcome.ok` says the surface acquired the
|
|
2457
|
+
* planned bytes; it says nothing about whether the READINESS that unblocks
|
|
2458
|
+
* activation was written. Only the planned deployment decides that (the surface
|
|
2459
|
+
* plans exactly one), so its recorded value is checked rather than assumed. A
|
|
2460
|
+
* `prepared: true` beside a "needs its model assets prepared" row is the
|
|
2461
|
+
* self-contradiction this check removes: the operator gets the recorded value
|
|
2462
|
+
* and the host gap, not a green result that changes nothing.
|
|
2463
|
+
*/
|
|
2464
|
+
const plannedModelId = plan.identity.modelId;
|
|
2465
|
+
const plannedReadiness = readiness[plannedModelId];
|
|
2466
|
+
if (plannedReadiness === undefined || !CALLABLE_READINESS.has(plannedReadiness)) {
|
|
2467
|
+
return {
|
|
2468
|
+
id: extensionId,
|
|
2469
|
+
prepared: false,
|
|
2470
|
+
reason: `${plannedModelId}'s assets were acquired, but its recorded readiness is `
|
|
2471
|
+
+ `${plannedReadiness ?? 'needs-preparation'}, which does not permit activation; `
|
|
2472
|
+
+ 'preparation cannot make this deployment callable until the host records a callable readiness '
|
|
2473
|
+
+ '(check that the installed build runs this model and that the host mounted the extension lifecycle)',
|
|
2474
|
+
plan: preparationPlanView(plan),
|
|
2475
|
+
readiness,
|
|
2476
|
+
groups: outcome.groups.map(group => group.groupId),
|
|
2477
|
+
activationReady: false,
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
return {
|
|
2481
|
+
id: extensionId,
|
|
2482
|
+
prepared: true,
|
|
2483
|
+
reason: null,
|
|
2484
|
+
plan: preparationPlanView(plan),
|
|
2485
|
+
readiness,
|
|
2486
|
+
groups: outcome.groups.map(group => group.groupId),
|
|
2487
|
+
activationReady: integrity && blockers.length === 0,
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Deactivate an installed extension and record it as disabled.
|
|
2492
|
+
*
|
|
2493
|
+
* The SDK's deactivation persists `desiredActivation: 'inactive'` BEFORE it
|
|
2494
|
+
* withdraws anything, so the durable answer survives a crash mid-withdrawal. The
|
|
2495
|
+
* disabled override is recorded only after the lifecycle call settles; a
|
|
2496
|
+
* deactivation that throws leaves the operator's list unchanged.
|
|
2497
|
+
* @param extensionId - the installed extension.
|
|
2498
|
+
* @param signal - cancellation.
|
|
2499
|
+
* @returns the id with the observed active/disabled state.
|
|
2500
|
+
* @throws ExtensionsServiceError for every refusal.
|
|
2501
|
+
*/
|
|
2502
|
+
async disable(extensionId, signal) {
|
|
2503
|
+
signal?.throwIfAborted();
|
|
2504
|
+
assertUsableId(extensionId);
|
|
2505
|
+
const storeDir = this.#requireStoreDir();
|
|
2506
|
+
// Not installed (or the store is unreadable) is still a refusal for disable:
|
|
2507
|
+
// there is no activation record this host could be switching off.
|
|
2508
|
+
this.#requireInstalled(storeDir, extensionId);
|
|
2509
|
+
const lifecycle = this.#lifecycle();
|
|
2510
|
+
if (lifecycle === undefined) {
|
|
2511
|
+
throw new ExtensionsServiceError('extension-activation-unavailable', `the extension lifecycle is not mounted, so ${extensionId} cannot be deactivated by this host`);
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* An installed extension that was never activated has no profile state, and
|
|
2515
|
+
* `deactivate` refuses such an id. That is not an error for the operator: the
|
|
2516
|
+
* extension is already inactive, and recording the disabled choice is enough.
|
|
2517
|
+
*/
|
|
2518
|
+
if (lifecycle.stateOf(extensionId) !== undefined) {
|
|
2519
|
+
try {
|
|
2520
|
+
await lifecycle.deactivate(extensionId);
|
|
2521
|
+
}
|
|
2522
|
+
catch (error) {
|
|
2523
|
+
if (!(error instanceof ManagementError && error.code === 'EXTENSION_INACTIVE')) {
|
|
2524
|
+
const named = error instanceof ManagementError ? `[${error.code}] ` : '';
|
|
2525
|
+
throw new ExtensionsServiceError('extension-deactivation-failed', `${named}${messageOf(error)}`, { cause: error });
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
this.#rememberDisabled(extensionId, true);
|
|
2530
|
+
await this.#persistDisabled();
|
|
2531
|
+
// The registrar's activated set follows the lifecycle: a deactivated extension
|
|
2532
|
+
// must not stay admitted to publish.
|
|
2533
|
+
this.#options.onActivationChange?.(extensionId, false);
|
|
2534
|
+
const observed = lifecycle.stateOf(extensionId);
|
|
2535
|
+
return {
|
|
2536
|
+
id: extensionId,
|
|
2537
|
+
active: observed?.observedActivation === 'active'
|
|
2538
|
+
&& (observed.ownerPid === undefined || observed.ownerPid === process.pid),
|
|
2539
|
+
disabled: this.#effectiveDisabled().has(extensionId),
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
_a = ExtensionsService;
|
|
2544
|
+
/**
|
|
2545
|
+
* Construct the extension-management service.
|
|
2546
|
+
* @param options - store/source configuration and the host's seams.
|
|
2547
|
+
* @returns a service satisfying {@link ModelToolsRemote}.
|
|
2548
|
+
*/
|
|
2549
|
+
export function createExtensionsService(options = {}) {
|
|
2550
|
+
return new ExtensionsService(options);
|
|
2551
|
+
}
|
|
2552
|
+
//# sourceMappingURL=extensions-service.js.map
|