@try-works/dsh-model-as-tool 0.1.2 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/bin/dsh-tool-mode-credentials.mjs +20 -0
  2. package/bin/dsh-tool-mode.mjs +202 -0
  3. package/catalog/desert-ant-models.json +693 -693
  4. package/client/client.model-tools.js +592 -0
  5. package/client/index.js +1679 -0
  6. package/lib/adapters/index.d.ts.map +1 -1
  7. package/lib/adapters/index.js +4 -4
  8. package/lib/bindings.d.ts.map +1 -1
  9. package/lib/bindings.js +26 -25
  10. package/lib/capability.d.ts.map +1 -1
  11. package/lib/capability.js +17 -0
  12. package/lib/capture-path.d.ts +97 -0
  13. package/lib/capture-path.d.ts.map +1 -0
  14. package/lib/capture-path.js +152 -0
  15. package/lib/diagnostics.d.ts +166 -0
  16. package/lib/diagnostics.d.ts.map +1 -0
  17. package/lib/diagnostics.js +135 -0
  18. package/lib/extension-dependencies.d.ts +197 -0
  19. package/lib/extension-dependencies.d.ts.map +1 -0
  20. package/lib/extension-dependencies.js +369 -0
  21. package/lib/extension-dispatch.d.ts +117 -0
  22. package/lib/extension-dispatch.d.ts.map +1 -0
  23. package/lib/extension-dispatch.js +124 -0
  24. package/lib/extension-registrar.d.ts +100 -0
  25. package/lib/extension-registrar.d.ts.map +1 -0
  26. package/lib/extension-registrar.js +134 -0
  27. package/lib/extensions-api.d.ts +217 -0
  28. package/lib/extensions-api.d.ts.map +1 -0
  29. package/lib/extensions-api.js +17 -0
  30. package/lib/extensions-remote.d.ts +54 -0
  31. package/lib/extensions-remote.d.ts.map +1 -0
  32. package/lib/extensions-remote.js +66 -0
  33. package/lib/extensions-service.d.ts +334 -0
  34. package/lib/extensions-service.d.ts.map +1 -0
  35. package/lib/extensions-service.js +1351 -0
  36. package/lib/harness-llm.d.ts +138 -0
  37. package/lib/harness-llm.d.ts.map +1 -0
  38. package/lib/harness-llm.js +218 -0
  39. package/lib/harness-serving.d.ts +95 -0
  40. package/lib/harness-serving.d.ts.map +1 -0
  41. package/lib/harness-serving.js +157 -0
  42. package/lib/headless.d.ts +97 -0
  43. package/lib/headless.d.ts.map +1 -0
  44. package/lib/headless.js +349 -0
  45. package/lib/http-acquisition.d.ts +27 -0
  46. package/lib/http-acquisition.d.ts.map +1 -0
  47. package/lib/http-acquisition.js +136 -0
  48. package/lib/index.d.ts +250 -7
  49. package/lib/index.d.ts.map +1 -1
  50. package/lib/index.js +2625 -70
  51. package/lib/invocation-accounting.d.ts +169 -0
  52. package/lib/invocation-accounting.d.ts.map +1 -0
  53. package/lib/invocation-accounting.js +148 -0
  54. package/lib/legacy-migration.d.ts +95 -0
  55. package/lib/legacy-migration.d.ts.map +1 -0
  56. package/lib/legacy-migration.js +429 -0
  57. package/lib/managed-workers.d.ts +151 -0
  58. package/lib/managed-workers.d.ts.map +1 -0
  59. package/lib/managed-workers.js +249 -0
  60. package/lib/mcp/client-grant.d.ts +74 -0
  61. package/lib/mcp/client-grant.d.ts.map +1 -0
  62. package/lib/mcp/client-grant.js +60 -0
  63. package/lib/mcp/credential-cli.d.ts +38 -0
  64. package/lib/mcp/credential-cli.d.ts.map +1 -0
  65. package/lib/mcp/credential-cli.js +148 -0
  66. package/lib/mcp/credential-store.d.ts +143 -0
  67. package/lib/mcp/credential-store.d.ts.map +1 -0
  68. package/lib/mcp/credential-store.js +454 -0
  69. package/lib/mcp/host-binding.d.ts +25 -0
  70. package/lib/mcp/host-binding.d.ts.map +1 -1
  71. package/lib/mcp/host-binding.js +107 -14
  72. package/lib/mcp/http.d.ts +68 -18
  73. package/lib/mcp/http.d.ts.map +1 -1
  74. package/lib/mcp/http.js +390 -62
  75. package/lib/mcp/oauth.d.ts +149 -0
  76. package/lib/mcp/oauth.d.ts.map +1 -0
  77. package/lib/mcp/oauth.js +396 -0
  78. package/lib/mcp/protocol.d.ts +46 -7
  79. package/lib/mcp/protocol.d.ts.map +1 -1
  80. package/lib/mcp/protocol.js +63 -16
  81. package/lib/mcp/rate-limit.d.ts +68 -0
  82. package/lib/mcp/rate-limit.d.ts.map +1 -0
  83. package/lib/mcp/rate-limit.js +114 -0
  84. package/lib/mcp/sdk-v2.d.ts +17 -0
  85. package/lib/mcp/sdk-v2.d.ts.map +1 -0
  86. package/lib/mcp/sdk-v2.js +181 -0
  87. package/lib/mcp/server.d.ts +11 -1
  88. package/lib/mcp/server.d.ts.map +1 -1
  89. package/lib/mcp/server.js +80 -13
  90. package/lib/mcp/stdio.d.ts.map +1 -1
  91. package/lib/mcp/stdio.js +9 -1
  92. package/lib/mcp/subscriptions.d.ts +43 -1
  93. package/lib/mcp/subscriptions.d.ts.map +1 -1
  94. package/lib/mcp/subscriptions.js +90 -9
  95. package/lib/model-qualification.d.ts +192 -0
  96. package/lib/model-qualification.d.ts.map +1 -0
  97. package/lib/model-qualification.js +178 -0
  98. package/lib/planning.d.ts +246 -0
  99. package/lib/planning.d.ts.map +1 -0
  100. package/lib/planning.js +457 -0
  101. package/lib/prepared-assets.d.ts +178 -0
  102. package/lib/prepared-assets.d.ts.map +1 -0
  103. package/lib/prepared-assets.js +286 -0
  104. package/lib/provider-bridge.d.ts +37 -0
  105. package/lib/provider-bridge.d.ts.map +1 -1
  106. package/lib/provider-bridge.js +189 -12
  107. package/lib/ptc-authority.d.ts +140 -0
  108. package/lib/ptc-authority.d.ts.map +1 -0
  109. package/lib/ptc-authority.js +216 -0
  110. package/lib/ptc-runtime.d.ts +156 -0
  111. package/lib/ptc-runtime.d.ts.map +1 -0
  112. package/lib/ptc-runtime.js +191 -0
  113. package/lib/publication.d.ts +2 -1
  114. package/lib/publication.d.ts.map +1 -1
  115. package/lib/publication.js +13 -5
  116. package/lib/receipt.d.ts.map +1 -1
  117. package/lib/receipt.js +8 -0
  118. package/lib/title-proposal.d.ts +194 -0
  119. package/lib/title-proposal.d.ts.map +1 -0
  120. package/lib/title-proposal.js +244 -0
  121. package/lib/tool-mode-run.d.ts +235 -0
  122. package/lib/tool-mode-run.d.ts.map +1 -0
  123. package/lib/tool-mode-run.js +283 -0
  124. package/lib/types.d.ts +3 -1
  125. package/lib/types.d.ts.map +1 -1
  126. package/lib/update-surface.d.ts +79 -0
  127. package/lib/update-surface.d.ts.map +1 -0
  128. package/lib/update-surface.js +224 -0
  129. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
  130. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
  131. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +85 -0
  132. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
  133. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
  134. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
  135. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
  136. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
  137. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
  138. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
  139. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
  140. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
  141. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
  142. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
  143. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
  144. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
  145. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
  146. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +274 -0
  147. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +26 -0
  148. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
  149. package/node_modules/@try-works/tool-mode-contracts/lib/index.js +48 -0
  150. package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +104 -0
  151. package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
  152. package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +55 -0
  153. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +107 -0
  154. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
  155. package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +246 -0
  156. package/node_modules/@try-works/tool-mode-contracts/package.json +34 -0
  157. package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
  158. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
  159. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
  160. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
  161. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
  162. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
  163. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
  164. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
  165. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
  166. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
  167. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
  168. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
  169. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
  170. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +31 -0
  171. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
  172. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +31 -0
  173. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
  174. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
  175. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +185 -0
  176. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
  177. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
  178. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
  179. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
  180. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
  181. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
  182. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
  183. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
  184. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
  185. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
  186. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
  187. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
  188. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
  189. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
  190. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
  191. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
  192. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
  193. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
  194. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
  195. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
  196. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
  197. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
  198. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
  199. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
  200. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
  201. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
  202. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
  203. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
  204. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
  205. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
  206. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
  207. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
  208. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
  209. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
  210. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
  211. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
  212. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
  213. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
  214. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
  215. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
  216. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
  217. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
  218. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
  219. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
  220. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
  221. package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
  222. package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
  223. package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
  224. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
  225. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
  226. package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
  227. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
  228. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
  229. package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
  230. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
  231. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
  232. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
  233. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
  234. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
  235. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
  236. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
  237. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
  238. package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
  239. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
  240. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
  241. package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
  242. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
  243. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
  244. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
  245. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
  246. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
  247. package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
  248. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
  249. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
  250. package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
  251. package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
  252. package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
  253. package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
  254. package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
  255. package/package.json +50 -17
  256. package/remote/spec.mjs +439 -0
  257. package/remote/typert.host.mjs +14 -0
  258. package/remote/typert.remote-client.mjs +16 -0
  259. package/lib/adapters/tongue.d.ts +0 -31
  260. package/lib/adapters/tongue.d.ts.map +0 -1
  261. package/lib/adapters/tongue.js +0 -93
  262. package/lib/adapters/voz.d.ts +0 -28
  263. package/lib/adapters/voz.d.ts.map +0 -1
  264. package/lib/adapters/voz.js +0 -102
  265. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
  266. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
  267. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
@@ -0,0 +1,1351 @@
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-model-as-tool/extensions-service
50
+ */
51
+ import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
52
+ import { dirname, join, resolve } from 'node:path';
53
+ import { digestInstalledContent, discoverExtension, ExtensionStore, ManagementError, stagedInstallFromDirectory, } from '@try-works/tool-mode-extension-sdk';
54
+ import { createFirstPartyDependencyGraph, resolveFirstPartyDependency } from "./extension-dependencies.js";
55
+ import { createHttpSource } from "./http-acquisition.js";
56
+ /**
57
+ * The readiness values that permit activation (the SDK's callable set).
58
+ *
59
+ * `prepared-cold` is callable: the model loads on first use. Every other value is
60
+ * a concrete blocker this service names in a row reason and in the enable refusal.
61
+ */
62
+ const CALLABLE_READINESS = new Set([
63
+ 'prepared-cold',
64
+ 'loading',
65
+ 'ready',
66
+ 'busy',
67
+ ]);
68
+ /**
69
+ * The order an unmet requirement is surfaced in (row reason and enable preflight).
70
+ *
71
+ * Integrity and platform first (nothing the operator can do in the page will clear
72
+ * them), then what the operator CAN act on: dependencies, the harness model, the
73
+ * executable approval, model assets, activation cleanup; store/source last because
74
+ * an installed row already satisfied them.
75
+ */
76
+ const REQUIREMENT_PRIORITY = [
77
+ 'integrity',
78
+ 'platform',
79
+ 'dependencies',
80
+ 'harness-model',
81
+ 'executable',
82
+ 'assets',
83
+ 'activation',
84
+ 'store',
85
+ 'source',
86
+ ];
87
+ /**
88
+ * Whether one UNMET requirement actually prevents this row from reaching active.
89
+ *
90
+ * A not-yet-installed row is blocked only by facts that would stop the INSTALL or
91
+ * would make the eventual activation impossible (store/source, dependencies,
92
+ * platform, harness model). Readiness, executable approval, integrity and
93
+ * activation state describe installed bytes, so they are not meaningful before an
94
+ * install and do not by themselves make the Install button impossible.
95
+ * @param installed - whether the store holds an installed copy.
96
+ * @param requirement - the requirement to classify.
97
+ * @returns whether the unmet requirement blocks the row.
98
+ */
99
+ function blocksRow(installed, requirement) {
100
+ if (requirement.satisfied)
101
+ return false;
102
+ if (installed)
103
+ return true;
104
+ return requirement.kind === 'store'
105
+ || requirement.kind === 'source'
106
+ || requirement.kind === 'dependencies'
107
+ || requirement.kind === 'platform'
108
+ || requirement.kind === 'harness-model';
109
+ }
110
+ /** Build one requirement value. */
111
+ function requirement(kind, satisfied, reason, action = 'none') {
112
+ return { kind, satisfied, reason, action };
113
+ }
114
+ /** The highest-priority UNMET requirement that blocks this row, or undefined. */
115
+ function firstBlockingRequirement(requirements, installed) {
116
+ for (const kind of REQUIREMENT_PRIORITY) {
117
+ const found = requirements.find(candidate => candidate.kind === kind);
118
+ if (found !== undefined && blocksRow(installed, found))
119
+ return found;
120
+ }
121
+ return undefined;
122
+ }
123
+ /**
124
+ * A named refusal from the extension-management surface.
125
+ *
126
+ * The `code` is stable and machine-readable; the message is operator-facing and
127
+ * names the concrete condition (path, SDK code, or identity) rather than a generic
128
+ * "failed", because the Settings page shows it verbatim.
129
+ */
130
+ export class ExtensionsServiceError extends Error {
131
+ name = 'ExtensionsServiceError';
132
+ code;
133
+ constructor(code, message, options) {
134
+ super(message, options);
135
+ this.code = code;
136
+ }
137
+ }
138
+ /**
139
+ * The directory a first-party checkout for one package identity occupies.
140
+ *
141
+ * A source directory holds PACKAGE DIRECTORIES, not scoped directories: the
142
+ * workspace layout is `packages/extension-laya`, whose package name is
143
+ * `@try-works/extension-laya`. The scope is therefore dropped and the final path
144
+ * segment is used, which is the only mapping that requires no registry lookup.
145
+ * @param extensionId - the extension identity, which equals its package name.
146
+ * @returns the directory name under the configured source directory.
147
+ */
148
+ export function packageDirFor(extensionId) {
149
+ const slash = extensionId.lastIndexOf('/');
150
+ return slash === -1 ? extensionId : extensionId.slice(slash + 1);
151
+ }
152
+ /** A package identity: one optional scope segment, then one name segment. */
153
+ const PACKAGE_ID = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
154
+ /**
155
+ * Refuse an identity that cannot safely become a path component.
156
+ *
157
+ * `packageDirFor` is joined onto the source directory, so an id such as `..` or
158
+ * one carrying a separator would address a directory outside the configured root.
159
+ * @param extensionId - the requested id.
160
+ * @throws ExtensionsServiceError `extension-id-invalid`.
161
+ */
162
+ function assertUsableId(extensionId) {
163
+ if (!PACKAGE_ID.test(extensionId)) {
164
+ throw new ExtensionsServiceError('extension-id-invalid', `${JSON.stringify(extensionId)} is not a package identity; expected a name like @scope/name`);
165
+ }
166
+ }
167
+ /** A message from an arbitrary thrown value. */
168
+ function messageOf(error) {
169
+ return error instanceof Error ? error.message : String(error);
170
+ }
171
+ /** Facts for a row with no readable package directory. */
172
+ const EMPTY_FACTS = {
173
+ packageName: null,
174
+ packageVersion: null,
175
+ packageDir: null,
176
+ tools: [],
177
+ models: [],
178
+ executionKind: undefined,
179
+ platforms: [],
180
+ dependencies: [],
181
+ problem: null,
182
+ };
183
+ /** Read a package.json's `dependencies` as name→range pairs; empty when unreadable. */
184
+ function readDeclaredDependencies(packageDir) {
185
+ if (packageDir === null)
186
+ return [];
187
+ try {
188
+ const parsed = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
189
+ if (typeof parsed !== 'object' || parsed === null)
190
+ return [];
191
+ // SAFETY: the value came from JSON.parse and the object guard above; only
192
+ // string-valued dependency specs are kept, so an unexpected shape yields an
193
+ // empty declaration rather than a trusted one.
194
+ const dependencies = parsed.dependencies;
195
+ if (typeof dependencies !== 'object' || dependencies === null || Array.isArray(dependencies))
196
+ return [];
197
+ const declared = [];
198
+ // SAFETY: the dependency container was checked to be a non-array object; only
199
+ // string entries become declared dependencies, and non-strings are ignored.
200
+ for (const [name, range] of Object.entries(dependencies)) {
201
+ if (typeof range === 'string' && range.length > 0)
202
+ declared.push({ name, range });
203
+ }
204
+ return declared;
205
+ }
206
+ catch {
207
+ return [];
208
+ }
209
+ }
210
+ /**
211
+ * Read one package directory's declared facts without importing or executing it.
212
+ * @param packageDir - the installed copy or first-party checkout.
213
+ * @returns the facts, with `problem` set (and manifest facts empty) when discovery fails.
214
+ */
215
+ function readExtensionFacts(packageDir) {
216
+ const dependencies = readDeclaredDependencies(packageDir);
217
+ try {
218
+ const discovery = discoverExtension(packageDir);
219
+ const manifest = discovery.manifest;
220
+ return {
221
+ packageName: discovery.packageName,
222
+ packageVersion: discovery.packageVersion,
223
+ packageDir,
224
+ tools: manifest.tools.map(tool => tool.name),
225
+ models: manifest.models.map(model => model.id),
226
+ executionKind: manifest.execution.kind,
227
+ platforms: manifest.models.flatMap(model => model.platforms),
228
+ dependencies,
229
+ problem: null,
230
+ };
231
+ }
232
+ catch (error) {
233
+ return {
234
+ ...EMPTY_FACTS,
235
+ packageName: null,
236
+ packageVersion: null,
237
+ packageDir,
238
+ dependencies,
239
+ problem: `the package at ${packageDir} is not a usable extension: ${messageOf(error)}`,
240
+ };
241
+ }
242
+ }
243
+ /** A short human label for one declared platform. */
244
+ function platformLabel(platform) {
245
+ return `${platform.os}-${platform.arch}${platform.accelerator === undefined ? '' : `-${platform.accelerator}`}`;
246
+ }
247
+ /**
248
+ * Whether the host's own platform matches any declared deployment platform.
249
+ * @param platforms - the manifest's declared platforms.
250
+ * @returns whether this host can run the declared deployment.
251
+ */
252
+ function hostPlatformMatches(platforms) {
253
+ return platforms.some(platform => platform.os === process.platform && platform.arch === process.arch);
254
+ }
255
+ /**
256
+ * Every declared deployment whose recorded readiness does not permit activation.
257
+ *
258
+ * A deployment with NO recorded readiness is reported as `needs-preparation`
259
+ * because that is the constant the SDK's lifecycle projects for an undeclared
260
+ * state (`#readinessOf` defaults every declared model to `needs-preparation`);
261
+ * reporting anything else would invent a fact the host does not hold.
262
+ * @param readiness - the persisted readiness map (may be empty).
263
+ * @param declaredModels - the manifest's model deployment ids.
264
+ * @returns one operator-facing line per blocker, most actionable phrasing first.
265
+ */
266
+ function preparationBlockers(readiness, declaredModels) {
267
+ const blockers = [];
268
+ for (const modelId of declaredModels) {
269
+ const value = readiness[modelId] ?? 'needs-preparation';
270
+ if (CALLABLE_READINESS.has(value))
271
+ continue;
272
+ if (value === 'needs-preparation') {
273
+ blockers.push(`${modelId} needs its model assets prepared — run prepare for this extension`);
274
+ }
275
+ else if (value === 'preparing') {
276
+ blockers.push(`${modelId} is still preparing its model assets`);
277
+ }
278
+ else if (value === 'unavailable') {
279
+ blockers.push(`${modelId} is unavailable on this host — its runtime or assets are missing`);
280
+ }
281
+ else if (value === 'failed') {
282
+ blockers.push(`${modelId} failed inspection on this host — repair or reinstall its assets, then prepare again`);
283
+ }
284
+ else {
285
+ blockers.push(`${modelId} is ${value}, which does not permit activation`);
286
+ }
287
+ }
288
+ return blockers;
289
+ }
290
+ /** Project the SDK/coordinator readiness map onto the wire readiness vocabulary. */
291
+ function readinessView(readiness, declaredModels) {
292
+ const view = {};
293
+ for (const modelId of declaredModels) {
294
+ const value = readiness[modelId];
295
+ // SAFETY: a readiness value read from the SDK's state is one of the SDK's
296
+ // ModelReadiness literals, which is the same union as ExtensionModelReadiness;
297
+ // an absent entry is projected as its documented default.
298
+ view[modelId] = value === undefined ? 'needs-preparation' : value;
299
+ }
300
+ return view;
301
+ }
302
+ /**
303
+ * Project the coordinator's plan onto the wire disclosure.
304
+ *
305
+ * The digest and identity fields the provider needs stay host-side; the page gets
306
+ * the bytes, sources, executable flags, totals, terms and revision an operator
307
+ * approves (the same fields the management channel discloses).
308
+ * @param plan - the resolved preparation plan.
309
+ * @returns the wire disclosure.
310
+ */
311
+ function preparationPlanView(plan) {
312
+ return {
313
+ modelId: plan.identity.modelId,
314
+ modelRevision: plan.identity.modelRevision,
315
+ assets: plan.assets.map(asset => ({
316
+ assetId: asset.assetId,
317
+ bytes: asset.bytes,
318
+ source: asset.source,
319
+ executable: asset.executable,
320
+ })),
321
+ totalBytes: plan.totalBytes,
322
+ storageBytes: plan.storageBytes,
323
+ requiresExecutableApproval: plan.requiresExecutableApproval,
324
+ license: { name: plan.license.name, url: plan.license.url },
325
+ };
326
+ }
327
+ /**
328
+ * Whether this process is likely able to create or write inside a directory.
329
+ *
330
+ * `access(W_OK)` is the strongest check available without performing a write, and
331
+ * it can still report success on a Windows ACL that later denies one. That gap is
332
+ * the reason every action names its write failure instead of trusting `writable`.
333
+ * @param directory - the store directory (it may not exist yet).
334
+ * @returns whether the nearest existing ancestor is a writable directory.
335
+ */
336
+ function directoryWritable(directory) {
337
+ try {
338
+ let candidate = resolve(directory);
339
+ while (!existsSync(candidate)) {
340
+ const parent = dirname(candidate);
341
+ if (parent === candidate)
342
+ return false;
343
+ candidate = parent;
344
+ }
345
+ if (!statSync(candidate).isDirectory())
346
+ return false;
347
+ accessSync(candidate, constants.W_OK);
348
+ return true;
349
+ }
350
+ catch {
351
+ return false;
352
+ }
353
+ }
354
+ /**
355
+ * The SDK lifecycle state for one profile, indexed by extension id.
356
+ * @param store - the extension store.
357
+ * @param profile - the lifecycle profile.
358
+ * @returns the states and whether the document could be trusted.
359
+ */
360
+ function readProfileStates(store, profile) {
361
+ const result = store.readProfileState(profile);
362
+ const byId = new Map();
363
+ for (const state of result.value)
364
+ byId.set(state.extensionId, state);
365
+ return { byId, corrupt: result.corrupt, diagnostic: result.diagnostic ?? null };
366
+ }
367
+ /** Read a candidate package.json name for a directory whose discovery failed. */
368
+ function declaredPackageName(directory) {
369
+ try {
370
+ const parsed = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
371
+ if (parsed === null)
372
+ return undefined;
373
+ // SAFETY: the value was parsed from the package's own package.json, and a
374
+ // non-string `name` is rejected below rather than trusted. The null guard above
375
+ // is what keeps the property read total for this boundary.
376
+ const name = parsed.name;
377
+ return typeof name === 'string' && PACKAGE_ID.test(name) ? name : undefined;
378
+ }
379
+ catch {
380
+ return undefined;
381
+ }
382
+ }
383
+ /**
384
+ * Discover every first-party package under a source directory.
385
+ *
386
+ * A directory whose package.json is unreadable is not listed at all: without an
387
+ * identity there is nothing truthful to name. A directory whose package.json names
388
+ * a package but whose manifest is unusable is listed WITH the reason, so the
389
+ * operator can see the broken checkout instead of an unexplained absence.
390
+ * @param sourceDir - the configured source directory.
391
+ * @returns packages in directory-name order.
392
+ */
393
+ function listSourcePackages(sourceDir) {
394
+ let entries;
395
+ try {
396
+ entries = readdirSync(sourceDir, { withFileTypes: true });
397
+ }
398
+ catch {
399
+ return [];
400
+ }
401
+ const packages = [];
402
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
403
+ if (!entry.isDirectory())
404
+ continue;
405
+ const directory = join(sourceDir, entry.name);
406
+ const facts = readExtensionFacts(directory);
407
+ if (facts.packageName !== null && facts.problem === null) {
408
+ packages.push({
409
+ id: facts.packageName,
410
+ version: facts.packageVersion,
411
+ tools: facts.tools,
412
+ facts,
413
+ problem: null,
414
+ });
415
+ continue;
416
+ }
417
+ const id = facts.packageName ?? declaredPackageName(directory);
418
+ if (id === undefined)
419
+ continue;
420
+ packages.push({
421
+ id,
422
+ version: facts.packageVersion,
423
+ tools: facts.tools,
424
+ facts: facts.problem === null
425
+ ? { ...facts, problem: `the package at ${directory} could not be discovered as an extension` }
426
+ : facts,
427
+ problem: facts.problem ?? `the package at ${directory} could not be discovered as an extension`,
428
+ });
429
+ }
430
+ return packages;
431
+ }
432
+ /**
433
+ * Select the current installed record for one identity.
434
+ *
435
+ * The EXT-30 active-version pointer wins when it names an installed version; a
436
+ * store without pointers (the pre-update world) uses the last recorded version,
437
+ * which is the order `recordInstall` appends in.
438
+ * @param records - every installed record.
439
+ * @param activeVersions - the EXT-30 pointer map.
440
+ * @param extensionId - the identity to select.
441
+ * @returns the selected record, or undefined when nothing is installed for the id.
442
+ */
443
+ function selectInstalled(records, activeVersions, extensionId) {
444
+ const versions = records.filter(record => record.extensionId === extensionId);
445
+ const newest = versions[versions.length - 1];
446
+ if (newest === undefined)
447
+ return undefined;
448
+ const pointedVersion = activeVersions[extensionId];
449
+ return pointedVersion === undefined
450
+ ? newest
451
+ : versions.find(candidate => candidate.version === pointedVersion) ?? newest;
452
+ }
453
+ /**
454
+ * The extension-management service: describe, install, enable, disable.
455
+ *
456
+ * Construct one per host and share it between the wire transport and any local
457
+ * surface; it owns the in-process override list and must not be duplicated, or two
458
+ * instances would report different disabled sets.
459
+ */
460
+ export class ExtensionsService {
461
+ #options;
462
+ /**
463
+ * This process's disabled choices, layered over `config.disabledExtensions`.
464
+ *
465
+ * An override lives for the life of the process: a Settings save that rewrites
466
+ * the config list cannot silently undo a toggle the operator just made here, and
467
+ * the override is dropped on restart, when the persisted config is read again.
468
+ */
469
+ #overrides = new Map();
470
+ constructor(options = {}) {
471
+ this.#options = options;
472
+ }
473
+ /** The configured store as an absolute path, or `''` when unconfigured. */
474
+ #storeDir() {
475
+ const configured = this.#options.storeDir ?? '';
476
+ return configured.trim().length === 0 ? '' : resolve(configured);
477
+ }
478
+ /** The configured source directory as an absolute path, or `''` when unconfigured. */
479
+ #sourceDir() {
480
+ const configured = this.#options.sourceDir ?? '';
481
+ return configured.trim().length === 0 ? '' : resolve(configured);
482
+ }
483
+ /** The lifecycle profile this service reads and writes. */
484
+ #profile() {
485
+ return this.#options.profile ?? 'default';
486
+ }
487
+ /** The mounted lifecycle, or undefined when the host mounted none. */
488
+ #lifecycle() {
489
+ return this.#options.lifecycle?.();
490
+ }
491
+ /**
492
+ * Require a configured store.
493
+ * @throws ExtensionsServiceError `extension-store-unconfigured`.
494
+ * @returns the absolute store directory.
495
+ */
496
+ #requireStoreDir() {
497
+ const storeDir = this.#storeDir();
498
+ if (storeDir === '') {
499
+ throw new ExtensionsServiceError('extension-store-unconfigured', 'no extension store is configured (config.extensionStore); install and activation state cannot be read or changed');
500
+ }
501
+ return storeDir;
502
+ }
503
+ /** The disabled set this process reports: configured ids, adjusted by local toggles. */
504
+ #effectiveDisabled() {
505
+ const ids = new Set(this.#options.disabledExtensions?.() ?? []);
506
+ for (const [id, disabled] of this.#overrides) {
507
+ if (disabled)
508
+ ids.add(id);
509
+ else
510
+ ids.delete(id);
511
+ }
512
+ return ids;
513
+ }
514
+ /** Record one toggle in this process. */
515
+ #rememberDisabled(extensionId, disabled) {
516
+ this.#overrides.set(extensionId, disabled);
517
+ }
518
+ /**
519
+ * Hand the effective disabled list to the host's writer, when one exists.
520
+ * @throws ExtensionsServiceError `extension-override-persist-failed` when the
521
+ * writer rejects; the caller reports the partial outcome.
522
+ */
523
+ async #persistDisabled() {
524
+ const persist = this.#options.persistDisabledExtensions;
525
+ if (persist === undefined)
526
+ return;
527
+ const ids = [...this.#effectiveDisabled()].sort();
528
+ try {
529
+ await persist(ids);
530
+ }
531
+ catch (error) {
532
+ 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 });
533
+ }
534
+ }
535
+ /** The content digest, through the test seam when one was supplied. */
536
+ #digest(directory) {
537
+ const digest = this.#options.digestContent ?? digestInstalledContent;
538
+ return digest(directory);
539
+ }
540
+ /**
541
+ * Select the installed version to operate on, refusing to guess over a corrupt store.
542
+ *
543
+ * A corrupt lock or profile document yields no records from the SDK's fail-closed
544
+ * read, and treating that as "not installed" would tell the operator the package
545
+ * is absent when the truth is that the store cannot be read.
546
+ * @param storeDir - the configured store.
547
+ * @param extensionId - the identity to select.
548
+ * @returns the current installed record.
549
+ * @throws ExtensionsServiceError `extension-store-unreadable` or `extension-not-installed`.
550
+ */
551
+ #requireInstalled(storeDir, extensionId) {
552
+ const store = new ExtensionStore(storeDir);
553
+ const lock = store.readLock();
554
+ if (lock.corrupt) {
555
+ throw new ExtensionsServiceError('extension-store-unreadable', `the extension store lock at ${store.lockPath} is unreadable, so ${extensionId} cannot be managed: ${lock.diagnostic ?? 'unknown reason'}`);
556
+ }
557
+ const pointers = store.readProfilePointers(this.#profile());
558
+ if (pointers.corrupt) {
559
+ throw new ExtensionsServiceError('extension-store-unreadable', `the profile activation state for ${this.#profile()} is unreadable, so ${extensionId} cannot be managed: ${pointers.diagnostic ?? 'unknown reason'}`);
560
+ }
561
+ const selection = selectInstalled(lock.value.extensions, pointers.value.activeVersions, extensionId);
562
+ if (selection === undefined) {
563
+ throw new ExtensionsServiceError('extension-not-installed', `${extensionId} has no installed record in ${storeDir}; install it before changing its activation state`);
564
+ }
565
+ return selection;
566
+ }
567
+ /** The directory installer, through the test seam when one was supplied. */
568
+ #install(options) {
569
+ const install = this.#options.installDirectory ?? stagedInstallFromDirectory;
570
+ return install(options);
571
+ }
572
+ /**
573
+ * The dependency graph a first-party source install resolves against.
574
+ *
575
+ * The SDK calls it only when the package declares runtime dependencies; the
576
+ * default graph resolves each one from the host's own installed tree and refuses
577
+ * by name when it cannot. Constructing it is cheap and performs no I/O.
578
+ * @param sourcePackageDir - the checkout being installed.
579
+ * @returns the graph the SDK's directory installer consumes.
580
+ */
581
+ #dependencyGraph(sourcePackageDir) {
582
+ const supplied = this.#options.dependencyGraph;
583
+ if (supplied !== undefined)
584
+ return supplied(sourcePackageDir);
585
+ const root = this.#options.preinstalledDependenciesRoot;
586
+ const base = {
587
+ sourcePackageDir,
588
+ allowPreinstalled: this.#options.allowPreinstalledDependencies === true,
589
+ };
590
+ return (root === undefined
591
+ ? createFirstPartyDependencyGraph(base)
592
+ : createFirstPartyDependencyGraph({ ...base, preinstalledRoot: root })).graph;
593
+ }
594
+ /**
595
+ * Verify an installed copy's content digest.
596
+ * @param storeDir - the configured store.
597
+ * @param record - the installed record, when there is one.
598
+ * @returns the integrity verdict plus the diagnostic text when it is not unchanged.
599
+ */
600
+ #integrityOf(storeDir, record) {
601
+ if (record === undefined || storeDir === '')
602
+ return { integrity: 'unknown', reason: null };
603
+ try {
604
+ const verdict = new ExtensionStore(storeDir).verifyUnchanged(record);
605
+ return {
606
+ integrity: verdict.unchanged ? 'unchanged' : 'changed',
607
+ reason: verdict.unchanged
608
+ ? null
609
+ : `the installed content is not the content that was recorded: ${verdict.diagnostic ?? 'it changed on disk after installation'}`,
610
+ };
611
+ }
612
+ catch (error) {
613
+ return {
614
+ integrity: 'unknown',
615
+ reason: `the installed content could not be digested, so its integrity is unknown: ${messageOf(error)}`,
616
+ };
617
+ }
618
+ }
619
+ /**
620
+ * Compute EVERY prerequisite this deployment checked, satisfied or not.
621
+ *
622
+ * The list is the answer to "why can't I enable this?" BEFORE an enable is
623
+ * attempted: each unmet requirement names the exact fact and (when one exists)
624
+ * the action that clears it. Nothing here executes extension code; the only I/O
625
+ * is the store read the caller already performed, discovery of the package
626
+ * directory, a host-tree dependency walk, and the preparation PLAN (not an
627
+ * acquisition) when an executable approval might be pending.
628
+ * @param input - the row's already-read facts.
629
+ * @returns the requirements in display order (store, source, dependencies,
630
+ * platform, integrity, harness-model, executable, assets, activation).
631
+ */
632
+ #computeRequirements(input) {
633
+ const requirements = [];
634
+ // STORE: configured and readable. Writability is a surface property the page
635
+ // already receives as `writable`; a read-only mount still answers reads.
636
+ if (input.storeDir === '') {
637
+ requirements.push(requirement('store', false, 'the extension store is not configured (config.extensionStore)', 'config'));
638
+ }
639
+ else if (input.lockCorrupt) {
640
+ requirements.push(requirement('store', false, `the extension store lock is unreadable, so no installed extension can be trusted: ${input.lockDiagnostic ?? 'unknown reason'}`, 'config'));
641
+ }
642
+ else if (input.profileStates.corrupt) {
643
+ requirements.push(requirement('store', false, `the profile activation state is unreadable, so the host restores nothing: ${input.profileStates.diagnostic ?? 'unknown reason'}`, 'config'));
644
+ }
645
+ else {
646
+ requirements.push(requirement('store', true, `the extension store at ${input.storeDir} is configured and readable`));
647
+ }
648
+ // SOURCE: the package bytes an install would copy, or the installed copy.
649
+ if (input.installed) {
650
+ requirements.push(requirement('source', true, `the installed copy is the source of record (version ${input.record?.version ?? 'unknown'})`));
651
+ }
652
+ else if (input.sourcePackage === undefined) {
653
+ requirements.push(requirement('source', false, input.sourceDir === ''
654
+ ? 'not installed, and no first-party source directory is configured (config.extensionSourceDir)'
655
+ : `no first-party package with this identity exists under ${input.sourceDir}`, input.sourceDir === '' ? 'config' : 'none'));
656
+ }
657
+ else if (input.sourcePackage.problem !== null) {
658
+ requirements.push(requirement('source', false, input.sourcePackage.problem, 'none'));
659
+ }
660
+ else if (this.#options.allowSourceDirectories !== true) {
661
+ requirements.push(requirement('source', false, 'installing from a source directory is disabled by configuration (extensions.allowSourceDirectories)', 'config'));
662
+ }
663
+ else {
664
+ requirements.push(requirement('source', true, `the first-party package is available at ${join(input.sourceDir, packageDirFor(input.id))}`, 'install'));
665
+ }
666
+ // DEPENDENCIES: declared runtime dependencies must be resolvable/materialized.
667
+ requirements.push(this.#dependencyRequirement(input));
668
+ // PLATFORM: the manifest's declared deployment platforms must include this host.
669
+ if (input.facts.problem !== null) {
670
+ requirements.push(requirement('platform', false, input.facts.problem, 'none'));
671
+ }
672
+ else if (input.facts.platforms.length === 0) {
673
+ requirements.push(requirement('platform', true, 'the extension declares no platform-specific model deployment'));
674
+ }
675
+ else if (hostPlatformMatches(input.facts.platforms)) {
676
+ requirements.push(requirement('platform', true, `the declared deployment supports this host (${process.platform}/${process.arch}: ${input.facts.platforms.map(platformLabel).join(', ')})`));
677
+ }
678
+ else {
679
+ requirements.push(requirement('platform', false, `the extension supports ${input.facts.platforms.map(platformLabel).join(', ')} but this host is ${process.platform}/${process.arch}`, 'none'));
680
+ }
681
+ // INTEGRITY: the installed bytes must still be the recorded bytes.
682
+ if (!input.installed) {
683
+ requirements.push(requirement('integrity', true, 'the extension is not installed, so there are no installed bytes to verify'));
684
+ }
685
+ else if (input.integrity === 'unchanged') {
686
+ requirements.push(requirement('integrity', true, 'the installed content matches the recorded digest'));
687
+ }
688
+ else {
689
+ requirements.push(requirement('integrity', false, input.integrityReason ?? `the installed content's integrity is ${input.integrity}`, 'install'));
690
+ }
691
+ // HARNESS MODEL: only declarative packages are served by the host broker.
692
+ if (input.facts.executionKind === 'declarative') {
693
+ requirements.push(this.#harnessModelRequirement());
694
+ }
695
+ // Readiness drives both the executable gate and the assets requirement.
696
+ const blockers = preparationBlockers(input.state?.readiness ?? {}, input.facts.models);
697
+ // EXECUTABLE: a downloaded engine needs the separate RES-02 approval.
698
+ if (input.facts.models.length === 0) {
699
+ requirements.push(requirement('executable', true, 'the extension declares no model deployment, so no executable asset can be planned'));
700
+ }
701
+ else if (!input.installed) {
702
+ requirements.push(requirement('executable', true, 'not installed yet; the executable-asset approval is checked once the preparation plan can be resolved'));
703
+ }
704
+ else if (blockers.length === 0) {
705
+ requirements.push(requirement('executable', true, 'every declared model deployment is already prepared or callable, so no executable approval is pending'));
706
+ }
707
+ else {
708
+ requirements.push(this.#executableRequirement(input.id));
709
+ }
710
+ // ASSETS: each declared deployment must be in a callable readiness state.
711
+ if (input.facts.models.length === 0) {
712
+ requirements.push(requirement('assets', true, 'the extension declares no model deployment to prepare'));
713
+ }
714
+ else if (!input.installed) {
715
+ requirements.push(requirement('assets', false, `not installed; ${input.facts.models.join(', ')} need${input.facts.models.length === 1 ? 's' : ''} model assets prepared after installation`, 'install'));
716
+ }
717
+ else if (blockers.length > 0) {
718
+ requirements.push(requirement('assets', false, `activation blocked: ${blockers.join('; ')}`, 'prepare'));
719
+ }
720
+ else {
721
+ requirements.push(requirement('assets', true, `every declared model deployment is prepared or callable (${input.facts.models.join(', ')})`));
722
+ }
723
+ // ACTIVATION: a stored fault that a retry cannot clear.
724
+ if (!input.installed) {
725
+ requirements.push(requirement('activation', true, 'not installed; no activation state exists'));
726
+ }
727
+ else if (input.state?.observedActivation === 'quarantined') {
728
+ requirements.push(requirement('activation', false, 'the host quarantined it: some contributions could not be withdrawn, so it is not fully stopped', 'none'));
729
+ }
730
+ else if (input.state?.observedActivation === 'activating' || input.state?.observedActivation === 'deactivating') {
731
+ requirements.push(requirement('activation', false, `a ${input.state.observedActivation} operation was interrupted; the recorded state is not settled`, 'none'));
732
+ }
733
+ else if (input.state?.observedActivation === 'failed') {
734
+ const explained = requirements.some(candidate => !candidate.satisfied);
735
+ requirements.push(requirement('activation', true, explained
736
+ ? 'the last activation attempt failed; the unmet requirement above is the actionable blocker'
737
+ : 'the last activation attempt failed, but no remaining prerequisite blocks a retry, so enable retries activation'));
738
+ }
739
+ else if (input.state === undefined) {
740
+ requirements.push(requirement('activation', true, 'installed but never activated in this profile; no stored activation fault'));
741
+ }
742
+ else {
743
+ requirements.push(requirement('activation', true, `the stored activation state is ${input.state.observedActivation}`));
744
+ }
745
+ return requirements;
746
+ }
747
+ /** The dependencies requirement: materialized in the installed copy, or resolvable in the host tree. */
748
+ #dependencyRequirement(input) {
749
+ if (input.facts.dependencies.length === 0) {
750
+ return requirement('dependencies', true, 'the package declares no runtime dependencies');
751
+ }
752
+ const count = input.facts.dependencies.length;
753
+ const missing = [];
754
+ if (input.installed && input.record !== undefined) {
755
+ for (const dependency of input.facts.dependencies) {
756
+ const installed = join(input.record.installPath, 'node_modules', ...dependency.name.split('/'), 'package.json');
757
+ if (!existsSync(installed))
758
+ missing.push(`${dependency.name} (declared ${dependency.range})`);
759
+ }
760
+ return missing.length === 0
761
+ ? requirement('dependencies', true, `all ${count} declared runtime dependenc${count === 1 ? 'y is' : 'ies are'} materialized inside the extension's own installation`)
762
+ : requirement('dependencies', false, `the installed copy is missing ${missing.length} declared runtime dependenc${missing.length === 1 ? 'y' : 'ies'}: ${missing.join(', ')}`, 'install');
763
+ }
764
+ if (input.facts.packageDir === null) {
765
+ return requirement('dependencies', false, 'the declared runtime dependencies cannot be inspected because the package directory is unknown', 'none');
766
+ }
767
+ for (const dependency of input.facts.dependencies) {
768
+ try {
769
+ resolveFirstPartyDependency({ sourcePackageDir: input.facts.packageDir }, dependency.name, dependency.range);
770
+ }
771
+ catch {
772
+ missing.push(`${dependency.name} (declared ${dependency.range})`);
773
+ }
774
+ }
775
+ return missing.length === 0
776
+ ? requirement('dependencies', true, `all ${count} declared runtime dependenc${count === 1 ? 'y resolves' : 'ies resolve'} in the host's installed tree`)
777
+ : 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');
778
+ }
779
+ /** The harness-model requirement for a declarative package. */
780
+ #harnessModelRequirement() {
781
+ const status = this.#options.harnessProviderStatus?.();
782
+ if (status === undefined) {
783
+ return requirement('harness-model', false, 'the host could not report whether a harness model provider is available (provider status unknown); '
784
+ + 'a declarative extension needs a composed provider, for example config.harnessModel', 'config');
785
+ }
786
+ if (status.available) {
787
+ const identity = [status.provider, status.model].filter((part) => part !== undefined && part.length > 0);
788
+ return requirement('harness-model', true, identity.length === 0
789
+ ? 'a harness model provider is available for declarative extensions'
790
+ : `a harness model provider is available (${identity.join(' / ')})`);
791
+ }
792
+ return requirement('harness-model', false, status.reason
793
+ ?? 'no harness model provider is configured, so declarative extensions cannot be served; set config.harnessModel to a runnable model', status.action ?? 'config');
794
+ }
795
+ /** The executable requirement for an installed, not-yet-callable model deployment. */
796
+ #executableRequirement(extensionId) {
797
+ const surface = this.#options.prepareSurface?.();
798
+ if (surface === undefined) {
799
+ /**
800
+ * No plan exists to approve, so this requirement is NOT what blocks the row:
801
+ * the assets requirement names the missing preparation. Reporting it unmet
802
+ * here would replace the actionable "run prepare" text with a host gap the
803
+ * row cannot fix.
804
+ */
805
+ return requirement('executable', true, 'the prepared-assets surface is not mounted, so there is no preparation plan whose executable assets could be approved; '
806
+ + 'the assets requirement reports the missing preparation');
807
+ }
808
+ let plan;
809
+ try {
810
+ plan = surface.plan(extensionId);
811
+ }
812
+ catch (error) {
813
+ return requirement('executable', true, `the preparation plan could not be inspected, so executable approval is not the blocker: ${messageOf(error)}`);
814
+ }
815
+ if (!plan.requiresExecutableApproval) {
816
+ return requirement('executable', true, 'the preparation plan declares no executable assets');
817
+ }
818
+ return this.#options.allowExecutablePreparation?.() === true
819
+ ? requirement('executable', true, 'the preparation plan includes an executable asset and the operator approved executable preparation (config.allowExecutablePreparation)')
820
+ : requirement('executable', false, 'activation blocked: the preparation plan includes an executable asset, and executable preparation is not approved for the client path '
821
+ + '(config.allowExecutablePreparation is off)', 'config');
822
+ }
823
+ /**
824
+ * Read the current state of the extension surface.
825
+ *
826
+ * Every row is derived from the store, the profile state and the source
827
+ * directory; nothing is cached between calls, so a page that re-reads after an
828
+ * action sees the committed result.
829
+ * @param signal - cancellation.
830
+ * @returns the store/source configuration and one row per known extension.
831
+ */
832
+ async describe(signal) {
833
+ signal?.throwIfAborted();
834
+ const storeDir = this.#storeDir();
835
+ const sourceDir = this.#sourceDir();
836
+ const disabled = this.#effectiveDisabled();
837
+ let lockCorrupt = false;
838
+ let lockDiagnostic = null;
839
+ let installed = [];
840
+ let profileStates = { byId: new Map(), corrupt: false, diagnostic: null };
841
+ let activeVersions = {};
842
+ if (storeDir !== '') {
843
+ const store = new ExtensionStore(storeDir);
844
+ const lock = store.readLock();
845
+ lockCorrupt = lock.corrupt;
846
+ lockDiagnostic = lock.diagnostic ?? null;
847
+ installed = lock.value.extensions;
848
+ profileStates = readProfileStates(store, this.#profile());
849
+ activeVersions = store.readProfilePointers(this.#profile()).value.activeVersions;
850
+ }
851
+ const sourcePackages = new Map();
852
+ if (sourceDir !== '') {
853
+ for (const candidate of listSourcePackages(sourceDir))
854
+ sourcePackages.set(candidate.id, candidate);
855
+ }
856
+ const ids = new Set();
857
+ for (const record of installed)
858
+ ids.add(record.extensionId);
859
+ for (const id of profileStates.byId.keys())
860
+ ids.add(id);
861
+ for (const id of sourcePackages.keys())
862
+ ids.add(id);
863
+ const extensions = [...ids].sort().map(id => this.#describeOne({
864
+ id,
865
+ installed,
866
+ activeVersions,
867
+ profileStates,
868
+ sourcePackage: sourcePackages.get(id),
869
+ disabled: disabled.has(id),
870
+ storeDir,
871
+ sourceDir,
872
+ lockCorrupt,
873
+ lockDiagnostic,
874
+ }));
875
+ const writable = storeDir !== ''
876
+ && !lockCorrupt
877
+ && this.#options.readOnly !== true
878
+ && directoryWritable(storeDir);
879
+ return {
880
+ store: storeDir === '' ? null : storeDir,
881
+ writable,
882
+ sourceDir: sourceDir === '' ? null : sourceDir,
883
+ extensions,
884
+ };
885
+ }
886
+ /** Build one row, keeping the reason precedence in one place. */
887
+ #describeOne(view) {
888
+ const selection = selectInstalled(view.installed, view.activeVersions, view.id);
889
+ const record = selection;
890
+ const state = view.profileStates.byId.get(view.id);
891
+ const installed = record !== undefined;
892
+ const integrity = this.#integrityOf(view.storeDir, record);
893
+ const facts = record !== undefined
894
+ ? readExtensionFacts(record.installPath)
895
+ : view.sourcePackage?.facts ?? EMPTY_FACTS;
896
+ const active = state !== undefined
897
+ && state.observedActivation === 'active'
898
+ && (state.ownerPid === undefined || state.ownerPid === process.pid);
899
+ const stateTools = state?.tools.map(tool => tool.toolName) ?? [];
900
+ const tools = facts.tools.length > 0
901
+ ? facts.tools
902
+ : stateTools.length > 0
903
+ ? stateTools
904
+ : view.sourcePackage?.tools ?? [];
905
+ const requirements = this.#computeRequirements({
906
+ id: view.id,
907
+ installed,
908
+ record,
909
+ state,
910
+ integrity: integrity.integrity,
911
+ integrityReason: integrity.reason,
912
+ storeDir: view.storeDir,
913
+ sourceDir: view.sourceDir,
914
+ lockCorrupt: view.lockCorrupt,
915
+ lockDiagnostic: view.lockDiagnostic,
916
+ profileStates: view.profileStates,
917
+ sourcePackage: view.sourcePackage,
918
+ facts,
919
+ });
920
+ const blocked = requirements.some(candidate => blocksRow(installed, candidate));
921
+ const version = record?.version ?? view.sourcePackage?.version ?? null;
922
+ const reason = this.#reasonFor({
923
+ ...view,
924
+ state,
925
+ integrityReason: integrity.reason,
926
+ active,
927
+ installed,
928
+ requirements,
929
+ });
930
+ return {
931
+ id: view.id,
932
+ package: view.id,
933
+ version,
934
+ installed,
935
+ active,
936
+ disabled: view.disabled,
937
+ integrity: integrity.integrity,
938
+ tools: [...tools],
939
+ reason,
940
+ requirements,
941
+ blocked,
942
+ };
943
+ }
944
+ /** The first condition worth telling the operator about, or null when none is known. */
945
+ #reasonFor(view) {
946
+ if (view.lockCorrupt) {
947
+ return `the extension store lock is unreadable, so no installed extension can be trusted: ${view.lockDiagnostic ?? 'unknown reason'}`;
948
+ }
949
+ if (view.integrityReason !== null)
950
+ return view.integrityReason;
951
+ if (view.profileStates.corrupt) {
952
+ return `the profile activation state is unreadable, so the host restores nothing: ${view.profileStates.diagnostic ?? 'unknown reason'}`;
953
+ }
954
+ if (view.storeDir === '') {
955
+ return 'the extension store is not configured (config.extensionStore)';
956
+ }
957
+ /**
958
+ * The highest-priority UNMET requirement that actually blocks this row is the
959
+ * single summary. The wording is the requirement's own, so the page, the
960
+ * preflight error and the requirement list never disagree.
961
+ */
962
+ const unmet = firstBlockingRequirement(view.requirements, view.installed);
963
+ if (view.state?.observedActivation === 'failed') {
964
+ return unmet?.reason
965
+ ?? 'activation failed earlier in this host; the store records no remaining preparation blocker, so retry enable to see whether it succeeds';
966
+ }
967
+ if (view.state?.observedActivation === 'quarantined') {
968
+ return unmet?.reason
969
+ ?? 'the host quarantined it: some contributions could not be withdrawn, so it is not fully stopped';
970
+ }
971
+ if (view.state?.observedActivation === 'activating' || view.state?.observedActivation === 'deactivating') {
972
+ return unmet?.reason
973
+ ?? `a ${view.state.observedActivation} operation was interrupted; the recorded state is not settled`;
974
+ }
975
+ if (view.disabled) {
976
+ return 'disabled by the operator (config.disabledExtensions); the host does not restore it';
977
+ }
978
+ if (!view.installed) {
979
+ if (view.sourcePackage?.problem !== null && view.sourcePackage?.problem !== undefined)
980
+ return view.sourcePackage.problem;
981
+ if (view.sourceDir === '') {
982
+ return 'not installed, and no first-party source directory is configured (config.extensionSourceDir)';
983
+ }
984
+ if (this.#options.allowSourceDirectories !== true) {
985
+ return 'not installed; installing from a source directory is disabled by configuration (extensions.allowSourceDirectories)';
986
+ }
987
+ return view.sourcePackage === undefined
988
+ ? 'not installed, and no first-party package with this identity exists under the configured source directory'
989
+ : `not installed; the first-party package is available at ${join(view.sourceDir, packageDirFor(view.id))}`;
990
+ }
991
+ if (unmet !== undefined)
992
+ return unmet.reason;
993
+ if (!view.active) {
994
+ if (view.state === undefined)
995
+ return 'installed but never activated in this profile';
996
+ return `installed; this host observes it as ${view.state.observedActivation} (desired ${view.state.desiredActivation})`;
997
+ }
998
+ return null;
999
+ }
1000
+ /**
1001
+ * Install one extension from a local source directory (EXT-06 developer mode).
1002
+ *
1003
+ * Refusals are named and happen BEFORE the copy: an unconfigured store, an
1004
+ * unconfigured/missing source, a source whose package identity differs from the
1005
+ * requested id, a disallowed source-directory install, or content that cannot be
1006
+ * digested. The SDK then copies, validates, verifies and records the install; a
1007
+ * failure there is re-thrown as `extension-install-failed` with the SDK's own
1008
+ * named code in the message.
1009
+ *
1010
+ * INSTALLATION IS NOT ACTIVATION: the result is always `active: false`.
1011
+ * @param extensionId - the package identity to install.
1012
+ * @param source - an explicit source directory, or absent to use the configured one.
1013
+ * @param signal - cancellation.
1014
+ * @returns the installed identity and version.
1015
+ * @throws ExtensionsServiceError for every refusal and failure.
1016
+ */
1017
+ async install(extensionId, source, signal) {
1018
+ signal?.throwIfAborted();
1019
+ assertUsableId(extensionId);
1020
+ const storeDir = this.#requireStoreDir();
1021
+ const sourceDir = source !== undefined && source.trim().length > 0
1022
+ ? resolve(source)
1023
+ : (() => {
1024
+ const configured = this.#sourceDir();
1025
+ if (configured === '') {
1026
+ throw new ExtensionsServiceError('extension-source-unconfigured', `no source was given for ${extensionId} and no first-party source directory is configured (config.extensionSourceDir)`);
1027
+ }
1028
+ return join(configured, packageDirFor(extensionId));
1029
+ })();
1030
+ let sourceIsDirectory = false;
1031
+ try {
1032
+ sourceIsDirectory = statSync(sourceDir).isDirectory();
1033
+ }
1034
+ catch {
1035
+ sourceIsDirectory = false;
1036
+ }
1037
+ if (!sourceIsDirectory) {
1038
+ throw new ExtensionsServiceError('extension-source-missing', `the source directory ${sourceDir} does not exist or is not a directory`);
1039
+ }
1040
+ let discovered;
1041
+ try {
1042
+ discovered = discoverExtension(sourceDir);
1043
+ }
1044
+ catch (error) {
1045
+ throw new ExtensionsServiceError('extension-source-invalid', `the source directory ${sourceDir} is not a usable extension package: ${messageOf(error)}`, { cause: error });
1046
+ }
1047
+ if (discovered.packageName !== extensionId) {
1048
+ throw new ExtensionsServiceError('extension-source-mismatch', `the source directory ${sourceDir} contains ${JSON.stringify(discovered.packageName)}, not ${JSON.stringify(extensionId)}`);
1049
+ }
1050
+ if (this.#options.allowSourceDirectories !== true) {
1051
+ throw new ExtensionsServiceError('extension-source-directories-not-allowed', 'source-directory installs are developer mode and are not allowed by the operator configuration (extensions.allowSourceDirectories)');
1052
+ }
1053
+ // Refuse BEFORE copying when the source cannot be digested: an installation the
1054
+ // store cannot verify later would be bytes nobody can attest to (EXT-12/EXT-33).
1055
+ try {
1056
+ this.#digest(sourceDir);
1057
+ }
1058
+ catch (error) {
1059
+ 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 });
1060
+ }
1061
+ let installed;
1062
+ try {
1063
+ installed = await this.#install({
1064
+ sourceDir,
1065
+ storeDir,
1066
+ allowSourceDirectories: true,
1067
+ /**
1068
+ * The dependency graph is supplied only on this developer-mode path. It is
1069
+ * consulted by the SDK only when the package declares runtime dependencies;
1070
+ * every declared dependency is then resolved from the host's own installed
1071
+ * tree or the install fails with a named DEPENDENCY_UNSATISFIED reason.
1072
+ */
1073
+ dependencies: this.#dependencyGraph(sourceDir),
1074
+ });
1075
+ }
1076
+ catch (error) {
1077
+ if (error instanceof ManagementError && error.code === 'DEPENDENCY_UNSATISFIED') {
1078
+ throw new ExtensionsServiceError('extension-dependency-unsatisfied', `[DEPENDENCY_UNSATISFIED] ${error.message}`, { cause: error });
1079
+ }
1080
+ const named = error instanceof ManagementError ? `[${error.code}] ` : '';
1081
+ throw new ExtensionsServiceError('extension-install-failed', `${named}${messageOf(error)}`, { cause: error });
1082
+ }
1083
+ if (installed.archiveDigest.trim().length === 0) {
1084
+ throw new ExtensionsServiceError('extension-digest-unavailable', `${installed.extensionId}@${installed.version} was installed without a content digest, so it cannot be verified before activation`);
1085
+ }
1086
+ return { id: installed.extensionId, version: installed.version, active: false };
1087
+ }
1088
+ /**
1089
+ * Activate an installed extension and clear its disabled override.
1090
+ *
1091
+ * Activation runs through the mounted lifecycle, which verifies the installed
1092
+ * bytes, publishes the contributions and persists `desiredActivation: 'active'`.
1093
+ * The disabled list is updated only after activation succeeds, so a refused
1094
+ * activation does not leave the operator's override claiming a state change that
1095
+ * did not happen.
1096
+ * @param extensionId - the installed extension.
1097
+ * @param signal - cancellation.
1098
+ * @returns the id with the observed active/disabled state.
1099
+ * @throws ExtensionsServiceError for every refusal.
1100
+ */
1101
+ async enable(extensionId, signal) {
1102
+ signal?.throwIfAborted();
1103
+ assertUsableId(extensionId);
1104
+ const storeDir = this.#requireStoreDir();
1105
+ const record = this.#requireInstalled(storeDir, extensionId);
1106
+ const lifecycle = this.#lifecycle();
1107
+ if (lifecycle === undefined) {
1108
+ throw new ExtensionsServiceError('extension-activation-unavailable', `the extension lifecycle is not mounted, so ${extensionId} cannot be activated by this host`);
1109
+ }
1110
+ const current = lifecycle.stateOf(extensionId);
1111
+ /**
1112
+ * THE PREFLIGHT. Every prerequisite the deployment can check is evaluated
1113
+ * BEFORE the lifecycle is touched: a deployment gap must never surface as the
1114
+ * SDK's generic EXTENSION_FAULT after an activation attempt. The refusal's code
1115
+ * maps the unmet requirement and its message is the requirement's own reason,
1116
+ * so the page, `describe()` and the error cannot disagree.
1117
+ */
1118
+ const preflightIntegrity = this.#integrityOf(storeDir, record);
1119
+ const preflightFacts = readExtensionFacts(record.installPath);
1120
+ const requirements = this.#computeRequirements({
1121
+ id: extensionId,
1122
+ installed: true,
1123
+ record,
1124
+ state: current,
1125
+ integrity: preflightIntegrity.integrity,
1126
+ integrityReason: preflightIntegrity.reason,
1127
+ storeDir,
1128
+ sourceDir: this.#sourceDir(),
1129
+ lockCorrupt: false,
1130
+ lockDiagnostic: null,
1131
+ profileStates: readProfileStates(new ExtensionStore(storeDir), this.#profile()),
1132
+ sourcePackage: undefined,
1133
+ facts: preflightFacts,
1134
+ });
1135
+ const unmet = firstBlockingRequirement(requirements, true);
1136
+ if (unmet !== undefined) {
1137
+ if (unmet.kind === 'assets') {
1138
+ // The established preparation refusal keeps its own code and wording.
1139
+ throw new ExtensionsServiceError('extension-preparation-required', unmet.reason);
1140
+ }
1141
+ if (unmet.kind === 'integrity') {
1142
+ // The activation path named the SDK's integrity code; the preflight keeps
1143
+ // that classification while refusing before the lifecycle call.
1144
+ throw new ExtensionsServiceError('extension-activation-failed', `[INTEGRITY_MISMATCH] ${unmet.reason}`);
1145
+ }
1146
+ throw new ExtensionsServiceError('extension-requirement-unmet', unmet.reason);
1147
+ }
1148
+ const ownedHere = current?.observedActivation === 'active'
1149
+ && (current.ownerPid === undefined || current.ownerPid === process.pid);
1150
+ if (!ownedHere) {
1151
+ /**
1152
+ * The registrar refuses a contribution from an extension outside its
1153
+ * activated set, so the id must be admitted BEFORE the lifecycle publishes.
1154
+ * A failed activation revokes the admission again (the host implementation
1155
+ * reconciles against the lifecycle state).
1156
+ */
1157
+ this.#options.onActivationChange?.(extensionId, true);
1158
+ try {
1159
+ await lifecycle.activate(extensionId, record.version);
1160
+ }
1161
+ catch (error) {
1162
+ this.#options.onActivationChange?.(extensionId, false);
1163
+ const named = error instanceof ManagementError ? `[${error.code}] ` : '';
1164
+ throw new ExtensionsServiceError('extension-activation-failed', `${named}${messageOf(error)}`, { cause: error });
1165
+ }
1166
+ }
1167
+ this.#rememberDisabled(extensionId, false);
1168
+ await this.#persistDisabled();
1169
+ const observed = lifecycle.stateOf(extensionId);
1170
+ return {
1171
+ id: extensionId,
1172
+ active: observed?.observedActivation === 'active'
1173
+ && (observed.ownerPid === undefined || observed.ownerPid === process.pid),
1174
+ disabled: this.#effectiveDisabled().has(extensionId),
1175
+ };
1176
+ }
1177
+ /**
1178
+ * Record the failed activation attempt a fresh extension needs before its
1179
+ * readiness can be written.
1180
+ *
1181
+ * The SDK's `recordReadiness` refuses an extension the profile has never seen,
1182
+ * and a freshly installed extension has no profile state until an activation is
1183
+ * attempted. The probe therefore performs the SAME attempt the operator's failed
1184
+ * Enable performs: every declared deployment defaults to `needs-preparation`, so
1185
+ * the SDK refuses at the readiness assessment, records the state, and publishes
1186
+ * nothing. Any other refusal is a real problem and is reported.
1187
+ * @param lifecycle - the mounted lifecycle.
1188
+ * @param extensionId - the installed extension.
1189
+ * @param version - its installed version.
1190
+ * @throws ExtensionsServiceError when the probe fails for a reason other than the
1191
+ * expected preparation refusal.
1192
+ */
1193
+ async #recordPreparationBlocker(lifecycle, extensionId, version) {
1194
+ try {
1195
+ await lifecycle.activate(extensionId, version);
1196
+ }
1197
+ catch (error) {
1198
+ if (error instanceof ManagementError && error.code === 'EXTENSION_FAULT')
1199
+ return;
1200
+ throw new ExtensionsServiceError('extension-preparation-failed', `the extension's activation record could not be written before preparation: ${messageOf(error)}`, { cause: error });
1201
+ }
1202
+ }
1203
+ /**
1204
+ * Acquire and verify an installed extension's model assets, then record the
1205
+ * resulting readiness.
1206
+ *
1207
+ * This is the management-channel `extensions-prepare-plan` + `extensions-prepare`
1208
+ * pair behind one call, over the SAME host surface: the plan is resolved from the
1209
+ * installed manifest's asset lock, the acquisition runs through the injectable
1210
+ * source (the real ranged HTTP source unless the host injects one), and a
1211
+ * successful acquisition records `prepared-cold` through the lifecycle — which is
1212
+ * what lets a following `enable` activate the extension.
1213
+ *
1214
+ * The RES-02 executable gate is deliberately CLOSED here: the Remote call carries
1215
+ * no executable-approval argument, so a plan containing an executable asset is
1216
+ * returned as `prepared: false` with the registry's own reason rather than
1217
+ * acquired. The pinned EmbeddingGemma assets are not executable.
1218
+ * @param extensionId - the installed extension to prepare.
1219
+ * @param signal - cancellation.
1220
+ * @returns the disclosure that was executed and the resulting readiness.
1221
+ * @throws ExtensionsServiceError for every precondition and plan failure.
1222
+ */
1223
+ async prepare(extensionId, signal) {
1224
+ signal?.throwIfAborted();
1225
+ assertUsableId(extensionId);
1226
+ const storeDir = this.#requireStoreDir();
1227
+ const record = this.#requireInstalled(storeDir, extensionId);
1228
+ const surface = this.#options.prepareSurface?.();
1229
+ if (surface === undefined) {
1230
+ throw new ExtensionsServiceError('extension-preparation-unavailable', `the prepared-assets surface is not mounted, so ${extensionId}'s model assets cannot be prepared by this host`);
1231
+ }
1232
+ const declaredModels = readExtensionFacts(record.installPath).models;
1233
+ if (declaredModels.length === 0) {
1234
+ throw new ExtensionsServiceError('extension-preparation-failed', `${extensionId} declares no model deployment to prepare`);
1235
+ }
1236
+ let plan;
1237
+ try {
1238
+ plan = surface.plan(extensionId);
1239
+ }
1240
+ catch (error) {
1241
+ throw new ExtensionsServiceError('extension-preparation-failed', `the preparation plan for ${extensionId} could not be resolved: ${messageOf(error)}`, { cause: error });
1242
+ }
1243
+ const lifecycle = this.#lifecycle();
1244
+ if (lifecycle !== undefined && lifecycle.stateOf(extensionId) === undefined) {
1245
+ await this.#recordPreparationBlocker(lifecycle, extensionId, record.version);
1246
+ }
1247
+ /**
1248
+ * RES-02: the Remote path has no per-call executable argument, so the approval
1249
+ * is the operator's STANDING configuration value. With it off the registry
1250
+ * returns its exact refusal and NOTHING is acquired; with it on the same
1251
+ * digest-pinned plan is approved as a whole.
1252
+ */
1253
+ const allowExecutable = this.#options.allowExecutablePreparation?.() === true;
1254
+ let outcome;
1255
+ try {
1256
+ outcome = await surface.prepare(extensionId, {
1257
+ source: (this.#options.preparationSource ?? createHttpSource)(),
1258
+ allowExecutable,
1259
+ signal: signal ?? new AbortController().signal,
1260
+ });
1261
+ }
1262
+ catch (error) {
1263
+ throw new ExtensionsServiceError('extension-preparation-failed', `the preparation of ${extensionId} could not be recorded: ${messageOf(error)}`, { cause: error });
1264
+ }
1265
+ const state = lifecycle?.stateOf(extensionId);
1266
+ const readiness = readinessView(state?.readiness ?? {}, declaredModels);
1267
+ if (!outcome.ok) {
1268
+ return {
1269
+ id: extensionId,
1270
+ prepared: false,
1271
+ reason: outcome.reason,
1272
+ plan: preparationPlanView(plan),
1273
+ readiness,
1274
+ groups: [],
1275
+ activationReady: false,
1276
+ };
1277
+ }
1278
+ const integrity = new ExtensionStore(storeDir).verifyUnchanged(record).unchanged;
1279
+ const blockers = preparationBlockers(state?.readiness ?? {}, declaredModels);
1280
+ return {
1281
+ id: extensionId,
1282
+ prepared: true,
1283
+ reason: null,
1284
+ plan: preparationPlanView(plan),
1285
+ readiness,
1286
+ groups: outcome.groups.map(group => group.groupId),
1287
+ activationReady: integrity && blockers.length === 0,
1288
+ };
1289
+ }
1290
+ /**
1291
+ * Deactivate an installed extension and record it as disabled.
1292
+ *
1293
+ * The SDK's deactivation persists `desiredActivation: 'inactive'` BEFORE it
1294
+ * withdraws anything, so the durable answer survives a crash mid-withdrawal. The
1295
+ * disabled override is recorded only after the lifecycle call settles; a
1296
+ * deactivation that throws leaves the operator's list unchanged.
1297
+ * @param extensionId - the installed extension.
1298
+ * @param signal - cancellation.
1299
+ * @returns the id with the observed active/disabled state.
1300
+ * @throws ExtensionsServiceError for every refusal.
1301
+ */
1302
+ async disable(extensionId, signal) {
1303
+ signal?.throwIfAborted();
1304
+ assertUsableId(extensionId);
1305
+ const storeDir = this.#requireStoreDir();
1306
+ // Not installed (or the store is unreadable) is still a refusal for disable:
1307
+ // there is no activation record this host could be switching off.
1308
+ this.#requireInstalled(storeDir, extensionId);
1309
+ const lifecycle = this.#lifecycle();
1310
+ if (lifecycle === undefined) {
1311
+ throw new ExtensionsServiceError('extension-activation-unavailable', `the extension lifecycle is not mounted, so ${extensionId} cannot be deactivated by this host`);
1312
+ }
1313
+ /**
1314
+ * An installed extension that was never activated has no profile state, and
1315
+ * `deactivate` refuses such an id. That is not an error for the operator: the
1316
+ * extension is already inactive, and recording the disabled choice is enough.
1317
+ */
1318
+ if (lifecycle.stateOf(extensionId) !== undefined) {
1319
+ try {
1320
+ await lifecycle.deactivate(extensionId);
1321
+ }
1322
+ catch (error) {
1323
+ if (!(error instanceof ManagementError && error.code === 'EXTENSION_INACTIVE')) {
1324
+ const named = error instanceof ManagementError ? `[${error.code}] ` : '';
1325
+ throw new ExtensionsServiceError('extension-deactivation-failed', `${named}${messageOf(error)}`, { cause: error });
1326
+ }
1327
+ }
1328
+ }
1329
+ this.#rememberDisabled(extensionId, true);
1330
+ await this.#persistDisabled();
1331
+ // The registrar's activated set follows the lifecycle: a deactivated extension
1332
+ // must not stay admitted to publish.
1333
+ this.#options.onActivationChange?.(extensionId, false);
1334
+ const observed = lifecycle.stateOf(extensionId);
1335
+ return {
1336
+ id: extensionId,
1337
+ active: observed?.observedActivation === 'active'
1338
+ && (observed.ownerPid === undefined || observed.ownerPid === process.pid),
1339
+ disabled: this.#effectiveDisabled().has(extensionId),
1340
+ };
1341
+ }
1342
+ }
1343
+ /**
1344
+ * Construct the extension-management service.
1345
+ * @param options - store/source configuration and the host's seams.
1346
+ * @returns a service satisfying {@link ModelToolsRemote}.
1347
+ */
1348
+ export function createExtensionsService(options = {}) {
1349
+ return new ExtensionsService(options);
1350
+ }
1351
+ //# sourceMappingURL=extensions-service.js.map