@try-works/dsh-tool-mode 0.0.0-stage → 0.5.44

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