@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,504 @@
1
+ /**
2
+ * EXT-30 staged update and rollback (slices 1+2 of the approved design,
3
+ * docs/tool-mode/EXT-30-UPDATE-DESIGN.md).
4
+ *
5
+ * The two properties this module exists to enforce:
6
+ *
7
+ * - **Side-by-side, never in place.** A new version is fetched into
8
+ * `staging/`, validated as DATA (manifest, integrity, schema/license/
9
+ * permission deltas, structural-static self-tests), approved by an operator,
10
+ * and only then promoted to its OWN version directory. The currently active
11
+ * version's bytes are never mutated.
12
+ * - **The active version is a POINTER, committed atomically.** The profile
13
+ * state document carries `activeVersions`/`previousVersions` maps written by
14
+ * the store's single atomic writer under the store lock. Rollback restores
15
+ * the pointer; it never copies bytes over a live install, never re-enables
16
+ * disabled tools, never undoes revocations (those facts live beside the
17
+ * pointer and are not touched).
18
+ *
19
+ * A post-commit rebind failure rolls the pointer back AUTOMATICALLY and names
20
+ * the reason. A corrupted version directory fails closed everywhere.
21
+ *
22
+ * Slice 1's self-tests are STRUCTURAL/STATIC only (manifest, referenced files,
23
+ * schema JSON parses): host-executed self-tests arrive with slice 3, and this
24
+ * module never executes extension code.
25
+ *
26
+ * @module @try-works/tool-mode-extension-sdk/install/update-flow
27
+ */
28
+ import { createHash } from 'node:crypto';
29
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
30
+ import { join, resolve } from 'node:path';
31
+ import { ManagementError } from "./../errors.js";
32
+ import { discoverExtension } from "./../discovery.js";
33
+ import { digestInstalledContent, ExtensionStore } from "./store.js";
34
+ import { installPathFor, DEFAULT_INSTALL_LIMITS } from "./staged.js";
35
+ import { validateArchiveEntries, verifyIntegrity } from "./archive.js";
36
+ /** The current-installed comparison base: the named version, or the highest recorded one. */
37
+ function currentRecord(store, extensionId, currentVersion) {
38
+ const records = store.listInstalled().value.filter(record => record.extensionId === extensionId);
39
+ if (records.length === 0) {
40
+ throw new ManagementError('UPGRADE_FAILED', 'install', `${extensionId} is not installed; an update updates an existing extension`, 'Install the extension first; an update never creates a fresh installation.');
41
+ }
42
+ const record = currentVersion === undefined
43
+ ? records.reduce((latest, candidate) => (candidate.version.localeCompare(latest.version, undefined, { numeric: true }) > 0 ? candidate : latest))
44
+ : records.find(candidate => candidate.version === currentVersion);
45
+ if (record === undefined) {
46
+ throw new ManagementError('UPGRADE_FAILED', 'install', `${extensionId}@${currentVersion} is not installed; the comparison base does not exist`, 'Stage the update against the version that is actually installed.');
47
+ }
48
+ return record;
49
+ }
50
+ /** Parse one schema file for the delta computation; unparseable is a refusal. */
51
+ function schemaShape(text) {
52
+ const parsed = JSON.parse(text);
53
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
54
+ throw new Error('the schema file is not a JSON object');
55
+ }
56
+ const record = parsed;
57
+ const properties = {};
58
+ const raw = record.properties;
59
+ if (raw !== undefined) {
60
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
61
+ throw new Error('the schema properties are not an object');
62
+ for (const [name, declaration] of Object.entries(raw)) {
63
+ const type = declaration?.type;
64
+ properties[name] = typeof type === 'string' ? type : 'unknown';
65
+ }
66
+ }
67
+ const required = Array.isArray(record.required) && record.required.every(entry => typeof entry === 'string')
68
+ ? record.required
69
+ : [];
70
+ return { properties, required };
71
+ }
72
+ /** Compute the schema deltas for one tool between the current and staged manifests. */
73
+ function schemaDeltasFor(current, staged, currentDir, stagedDir) {
74
+ const deltas = [];
75
+ const byContract = new Map();
76
+ for (const tool of current.tools)
77
+ byContract.set(tool.contractId, tool);
78
+ for (const tool of staged.tools) {
79
+ const previous = byContract.get(tool.contractId);
80
+ if (previous === undefined) {
81
+ deltas.push({ toolName: tool.name, file: 'input', breaking: false, detail: 'a new tool; additive' });
82
+ continue;
83
+ }
84
+ for (const file of ['input', 'output']) {
85
+ const currentPath = file === 'input' ? previous.inputSchema : previous.outputSchema;
86
+ const stagedPath = file === 'input' ? tool.inputSchema : tool.outputSchema;
87
+ let currentShape;
88
+ let stagedShape;
89
+ try {
90
+ currentShape = schemaShape(readFileSync(join(currentDir, currentPath), 'utf8'));
91
+ stagedShape = schemaShape(readFileSync(join(stagedDir, stagedPath), 'utf8'));
92
+ }
93
+ catch (error) {
94
+ return {
95
+ deltas: [],
96
+ unparseable: `${tool.name} ${file} schema: ${error instanceof Error ? error.message : String(error)}`,
97
+ };
98
+ }
99
+ const problems = [];
100
+ for (const [name, type] of Object.entries(currentShape.properties)) {
101
+ if (!(name in stagedShape.properties)) {
102
+ problems.push(`removed property ${JSON.stringify(name)}`);
103
+ }
104
+ else if (stagedShape.properties[name] !== type) {
105
+ problems.push(`changed type of ${JSON.stringify(name)} from ${type} to ${stagedShape.properties[name]}`);
106
+ }
107
+ }
108
+ for (const name of stagedShape.required) {
109
+ if (!currentShape.required.includes(name) && name in currentShape.properties) {
110
+ problems.push(`made ${JSON.stringify(name)} required (it was optional)`);
111
+ }
112
+ }
113
+ if (problems.length > 0) {
114
+ deltas.push({ toolName: tool.name, file, breaking: true, detail: problems.join('; ') });
115
+ }
116
+ }
117
+ }
118
+ for (const tool of current.tools) {
119
+ if (!staged.tools.some(candidate => candidate.contractId === tool.contractId)) {
120
+ deltas.push({
121
+ toolName: tool.name, file: 'input', breaking: true,
122
+ detail: `the staged version drops the published contract ${JSON.stringify(tool.contractId)}; pinned compositions would fail`,
123
+ });
124
+ }
125
+ }
126
+ return { deltas };
127
+ }
128
+ /** Render the manifest licence declaration (the delta comparator's stable form). */
129
+ function licenseForm(manifest) {
130
+ return JSON.stringify({
131
+ license: manifest.license,
132
+ layers: manifest.licenses?.map(layer => ({
133
+ layer: layer.layer, name: layer.name, url: layer.url,
134
+ openSource: layer.openSource, redistribution: layer.redistribution,
135
+ })),
136
+ });
137
+ }
138
+ /** List permission EXPANSIONS (staged vs current). Reductions need no approval. */
139
+ function permissionDeltasFor(current, staged) {
140
+ const deltas = [];
141
+ const previous = new Set(current.permissions.networkOrigins);
142
+ for (const origin of staged.permissions.networkOrigins) {
143
+ if (!previous.has(origin))
144
+ deltas.push(`new network origin ${JSON.stringify(origin)}`);
145
+ }
146
+ if (staged.permissions.inputFiles && !current.permissions.inputFiles)
147
+ deltas.push('inputFiles: false -> true');
148
+ if (staged.permissions.storage !== 'none' && current.permissions.storage === 'none')
149
+ deltas.push('storage: none -> extension-private');
150
+ const previousSecrets = new Set(current.permissions.secrets);
151
+ for (const secret of staged.permissions.secrets) {
152
+ if (!previousSecrets.has(secret))
153
+ deltas.push(`new declared secret ${JSON.stringify(secret)}`);
154
+ }
155
+ if (staged.permissions.nativeCode && !current.permissions.nativeCode)
156
+ deltas.push('nativeCode: false -> true');
157
+ return deltas;
158
+ }
159
+ const stageMetadataPath = (stagedDir) => join(stagedDir, 'update-stage.json');
160
+ /**
161
+ * Stage an update (EXT-30 slice 1).
162
+ *
163
+ * Fetch → integrity → entry validation → extract into `staging/` → manifest
164
+ * discovery → schema/licence/permission deltas against the current version →
165
+ * structural-static self-tests. A BREAKING schema delta or an unparseable
166
+ * schema REFUSES the stage (the staged directory is removed); a licence or
167
+ * permission delta only marks the stage as requiring approval. The installed
168
+ * version is never touched.
169
+ *
170
+ * @param options - the source, store, injected archive source, and limits.
171
+ * @returns the staged update and its validation report.
172
+ * @throws ManagementError for every rejection path.
173
+ */
174
+ export async function stageUpdate(options) {
175
+ const limits = options.limits ?? DEFAULT_INSTALL_LIMITS;
176
+ const store = new ExtensionStore(options.storeDir);
177
+ const stagedBase = join(resolve(options.storeDir), 'staging');
178
+ mkdirSync(stagedBase, { recursive: true });
179
+ const attempt = join(stagedBase, `attempt-${process.pid}-${Date.now().toString(36)}`);
180
+ const extracted = join(attempt, 'package');
181
+ mkdirSync(extracted, { recursive: true });
182
+ try {
183
+ const bytes = await options.archive.fetch(options.source, {
184
+ maxBytes: limits.maxArchiveBytes,
185
+ timeoutMs: limits.fetchTimeoutMs,
186
+ });
187
+ if (bytes.byteLength > limits.maxArchiveBytes) {
188
+ throw new ManagementError('INVALID_EXTENSION', 'install', `the fetched archive is ${bytes.byteLength} bytes, over the ${limits.maxArchiveBytes} limit`, 'Ask the publisher to reduce the package, or raise the operator limit deliberately.');
189
+ }
190
+ // Integrity: verified against the declared digest when one is given; the
191
+ // computed digest is always recorded for the admission-time re-check.
192
+ const archiveDigest = options.expectedSha512 === undefined
193
+ ? createHash('sha512').update(bytes).digest('hex')
194
+ : verifyIntegrity(bytes, options.expectedSha512);
195
+ validateArchiveEntries(options.archive.list(bytes), limits.archive);
196
+ options.archive.extract(bytes, extracted, '');
197
+ const discovery = discoverExtension(extracted);
198
+ const current = currentRecord(store, discovery.packageName, options.currentVersion);
199
+ const currentDiscovery = discoverExtension(current.installPath);
200
+ const schema = schemaDeltasFor(currentDiscovery.manifest, discovery.manifest, current.installPath, extracted);
201
+ if (schema.unparseable !== undefined) {
202
+ throw new ManagementError('UPGRADE_FAILED', 'install', `the staged package carries an unparseable schema (${schema.unparseable}); a manifest pointing at unparseable `
203
+ + 'schemas is refused rather than activated', 'Fix the schema files and republish.');
204
+ }
205
+ const breaking = schema.deltas.filter(delta => delta.breaking);
206
+ if (breaking.length > 0) {
207
+ throw new ManagementError('UPGRADE_FAILED', 'install', `the update changes published contracts incompatibly (${breaking.map(delta => `${delta.toolName} ${delta.file}: ${delta.detail}`).join('; ')})`
208
+ + '; breaking contract changes require a reviewed migration (EXT-30)', 'Publish a compatible version, or conduct the reviewed migration the contract change needs.');
209
+ }
210
+ const currentLicense = licenseForm(currentDiscovery.manifest);
211
+ const stagedLicense = licenseForm(discovery.manifest);
212
+ const permissionDeltas = permissionDeltasFor(currentDiscovery.manifest, discovery.manifest);
213
+ const report = {
214
+ manifestOk: true,
215
+ comparedAgainst: current.version,
216
+ schemaDeltas: schema.deltas,
217
+ ...currentLicense === stagedLicense ? {} : { licenseDelta: 'the manifest licence declaration changed; operator approval required' },
218
+ permissionDeltas,
219
+ selfTests: {
220
+ kind: 'structural-static',
221
+ passed: true,
222
+ detail: 'structural/static checks only (manifest, referenced files, schema JSON parses); '
223
+ + 'host-executed self-tests arrive with slice 3',
224
+ },
225
+ requiresApproval: currentLicense !== stagedLicense || permissionDeltas.length > 0,
226
+ };
227
+ const contentDigest = digestInstalledContent(extracted);
228
+ // The attempt directory is renamed to the CANONICAL staged location, so the
229
+ // staged bytes have a stable identity that a commit in ANOTHER process can
230
+ // find and re-verify. The metadata record lives BESIDE the package dir, so
231
+ // admission moves only the package and the record stays out of the install.
232
+ const canonical = stagedPathFor(options.storeDir, discovery.packageName, discovery.packageVersion);
233
+ renameSync(attempt, canonical);
234
+ const metadata = {
235
+ extensionId: discovery.packageName,
236
+ stagedVersion: discovery.packageVersion,
237
+ stagedPath: join(canonical, 'package'),
238
+ currentVersion: current.version,
239
+ archiveDigest,
240
+ contentDigest,
241
+ report,
242
+ stagedAt: new Date().toISOString(),
243
+ };
244
+ writeFileSync(stageMetadataPath(canonical), `${JSON.stringify(metadata, null, 2)}\n`);
245
+ return {
246
+ extensionId: discovery.packageName,
247
+ stagedVersion: discovery.packageVersion,
248
+ stagedPath: join(canonical, 'package'),
249
+ report,
250
+ };
251
+ }
252
+ catch (error) {
253
+ // A refused stage leaves NO partial visibility: the staged extraction is
254
+ // removed on every rejection path. The installed version was never touched.
255
+ rmSync(attempt, { recursive: true, force: true });
256
+ throw error;
257
+ }
258
+ }
259
+ /** Read and verify the staged-update metadata (fail-closed corruption). */
260
+ function readStageMetadata(stagedPath) {
261
+ const path = stageMetadataPath(stagedPath);
262
+ if (!existsSync(path)) {
263
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the staged update at ${stagedPath} has no metadata record; it cannot be trusted`, 'Stage the update again; a staged directory without its record is never committed.');
264
+ }
265
+ let parsed;
266
+ try {
267
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
268
+ }
269
+ catch (error) {
270
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the staged update metadata is unreadable: ${error instanceof Error ? error.message : String(error)}`, 'Stage the update again; a corrupt record fails closed.');
271
+ }
272
+ const candidate = parsed;
273
+ if (typeof candidate.extensionId !== 'string' || typeof candidate.stagedVersion !== 'string'
274
+ || typeof candidate.stagedPath !== 'string' || typeof candidate.archiveDigest !== 'string'
275
+ || typeof candidate.contentDigest !== 'string' || typeof candidate.report !== 'object') {
276
+ throw new ManagementError('EXTENSION_FAULT', 'install', 'the staged update metadata is malformed', 'Stage the update again; a malformed record fails closed.');
277
+ }
278
+ return candidate;
279
+ }
280
+ /** Locate the staged path for one extension/version (id sanitized for the path). */
281
+ function stagedPathFor(storeDir, extensionId, version) {
282
+ const safe = extensionId.replaceAll('/', '__');
283
+ return join(resolve(storeDir), 'staging', `${safe}--${version}.staged`);
284
+ }
285
+ /**
286
+ * Record ONE operator approval decision for a staged update (EXT-30).
287
+ *
288
+ * The record is retained for the store's lifetime and is write-once per staged
289
+ * version (see {@link ExtensionStore.recordApproval}).
290
+ * @param options - the store, the staged identity, and the decision.
291
+ */
292
+ export function recordApproval(options) {
293
+ new ExtensionStore(options.storeDir).recordApproval(options.extensionId, options.stagedVersion, options.decision);
294
+ }
295
+ /**
296
+ * Commit a staged, approved update (EXT-30, two-phase seam).
297
+ *
298
+ * Under the store lock, in order: verify the approval → verify the staged
299
+ * bytes (digest re-check, fail-closed) → verify the CURRENT active version's
300
+ * directory (fail-closed) → admit the staged version to its OWN version
301
+ * directory (never the live one) → **quiescePrevious** (the host stops
302
+ * old-generation admissions BEFORE the pointer moves; a failure aborts the
303
+ * commit with the pointer unchanged) → commit the pointer atomically via the
304
+ * profile-state writer → **rebindNext** (the host republishes the new
305
+ * generation; a failure rolls the pointer back AUTOMATICALLY and names the
306
+ * reason).
307
+ *
308
+ * REBIND-REVERT CONTRACT: the automatic rollback after a rebindNext failure
309
+ * runs NO further hooks — quiescePrevious is not re-run and no restore hook
310
+ * fires. A host whose rebind needs its own revert must supply it INSIDE its
311
+ * all-or-nothing rebindNext wrapper; the SDK never re-enters host hooks.
312
+ *
313
+ * NO-SEAM REFUSAL: a commit with no rebindNext REFUSES by default (the durable
314
+ * pointer must not advance while nothing republishes the surface). Pass
315
+ * `pointerOnly: true` as the explicit, documented deployment opt-out.
316
+ *
317
+ * @param options - the store, profile, staged identity, and the two-phase hooks.
318
+ * @returns the committed pointer and the previous one.
319
+ * @throws ManagementError for every rejection path.
320
+ */
321
+ export async function commitUpdate(options) {
322
+ const quiescePrevious = options.hooks?.quiescePrevious;
323
+ const rebindNext = options.hooks?.rebindNext;
324
+ const store = new ExtensionStore(options.storeDir);
325
+ return await store.exclusive(async () => {
326
+ const approval = store.readApproval(options.extensionId, options.stagedVersion);
327
+ if (approval.corrupt) {
328
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the approval record is unreadable: ${approval.diagnostic ?? 'unknown reason'}`, 'Inspect the record deliberately; an unreadable approval never proceeds.');
329
+ }
330
+ if (approval.value === undefined || !approval.value.approved) {
331
+ throw new ManagementError('UPGRADE_FAILED', 'install', `${options.extensionId}@${options.stagedVersion} has no APPROVED record; the commit is refused`, 'Record an explicit operator approval first; a commit never invents consent.');
332
+ }
333
+ // NO-SEAM REFUSAL (fail closed): an approved commit with no rebindNext must
334
+ // not move the durable pointer while nothing republishes the surface.
335
+ if (rebindNext === undefined && options.pointerOnly !== true) {
336
+ throw new ManagementError('UPGRADE_FAILED', 'install', `${options.extensionId}@${options.stagedVersion} cannot commit without a rebind seam: nothing would republish `
337
+ + 'the new surface after the pointer moved', 'Provide hooks.rebindNext (with hooks.quiescePrevious for the two-phase ordering), or pass pointerOnly: true '
338
+ + 'for a deliberate deployment-tooling pointer commit.');
339
+ }
340
+ const stagedPath = stagedPathFor(options.storeDir, options.extensionId, options.stagedVersion);
341
+ const metadata = readStageMetadata(stagedPath);
342
+ if (!existsSync(metadata.stagedPath) || digestInstalledContent(metadata.stagedPath) !== metadata.contentDigest) {
343
+ throw new ManagementError('EXTENSION_FAULT', 'install', 'the staged bytes changed or disappeared after staging; the commit is refused', 'Stage the update again; a commit never proceeds over unverified bytes.');
344
+ }
345
+ const pointers = store.readProfilePointers(options.profile);
346
+ if (pointers.corrupt) {
347
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the profile pointers are unreadable: ${pointers.diagnostic ?? 'unknown reason'}`, 'Repair the profile state deliberately.');
348
+ }
349
+ const previous = pointers.value.activeVersions[options.extensionId];
350
+ if (previous === undefined) {
351
+ throw new ManagementError('UPGRADE_FAILED', 'install', `${options.extensionId} has no recorded active version; an update commits over an existing pointer`, 'Activate the installed version first, or use the install flow for a fresh extension.');
352
+ }
353
+ if (previous === options.stagedVersion) {
354
+ throw new ManagementError('UPGRADE_FAILED', 'install', `${options.extensionId} is already active at ${options.stagedVersion}`, 'Stage a different version.');
355
+ }
356
+ // Fail closed: the CURRENT active version's directory must still verify.
357
+ const currentRecordEntry = store.listInstalled().value.find(record => record.extensionId === options.extensionId && record.version === previous);
358
+ if (currentRecordEntry === undefined) {
359
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the active version ${previous} of ${options.extensionId} is not recorded; the commit is refused`, 'Inspect the store lock; an update never commits over an unrecorded version.');
360
+ }
361
+ const currentCheck = store.verifyUnchanged(currentRecordEntry);
362
+ if (!currentCheck.unchanged) {
363
+ throw new ManagementError('EXTENSION_FAULT', 'install', `cannot commit: the active version changed on disk (${currentCheck.diagnostic ?? 'unknown reason'})`, 'Restore the active version or reinstall it; a commit never overwrites a changed live directory.');
364
+ }
365
+ // Admission: promote the staged bytes to their OWN version directory.
366
+ const destination = installPathFor(options.storeDir, options.extensionId, options.stagedVersion);
367
+ const alreadyAdmitted = store.listInstalled().value.some(record => record.extensionId === options.extensionId && record.version === options.stagedVersion);
368
+ if (!alreadyAdmitted) {
369
+ if (existsSync(destination)) {
370
+ throw new ManagementError('EXTENSION_FAULT', 'install', `${options.extensionId}@${options.stagedVersion} exists on disk without a lock record; the commit is refused`, 'Inspect the store; a directory without its record is never promoted over.');
371
+ }
372
+ mkdirSync(join(destination, '..'), { recursive: true });
373
+ renameSync(metadata.stagedPath, destination);
374
+ writeFileSync(join(destination, '.tool-mode-install.json'), `${JSON.stringify({
375
+ extensionId: options.extensionId,
376
+ version: options.stagedVersion,
377
+ archiveDigest: metadata.archiveDigest,
378
+ source: { kind: 'update-stage', requested: 'staged update (EXT-30)' },
379
+ installedAt: new Date().toISOString(),
380
+ }, null, 2)}\n`);
381
+ store.recordInstall({
382
+ extensionId: options.extensionId,
383
+ version: options.stagedVersion,
384
+ installPath: destination,
385
+ archiveDigest: metadata.archiveDigest,
386
+ contentDigest: digestInstalledContent(destination),
387
+ source: { kind: 'update-stage', requested: 'staged update (EXT-30)' },
388
+ installedAt: new Date().toISOString(),
389
+ });
390
+ }
391
+ // The staged directory is spent either way: the admitted copy is the
392
+ // installation, and the stage metadata must not linger as a second source
393
+ // of truth.
394
+ rmSync(stagedPath, { recursive: true, force: true });
395
+ const extensions = store.readProfileState(options.profile);
396
+ if (extensions.corrupt) {
397
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the profile state is unreadable: ${extensions.diagnostic ?? 'unknown reason'}`, 'Repair the profile state deliberately.');
398
+ }
399
+ const priorPrevious = pointers.value.previousVersions[options.extensionId];
400
+ const nextActive = { ...pointers.value.activeVersions, [options.extensionId]: options.stagedVersion };
401
+ const nextPrevious = { ...pointers.value.previousVersions, [options.extensionId]: previous };
402
+ // PHASE 1 — quiescePrevious: the host stops old-generation admissions
403
+ // BEFORE the pointer moves. A failure ABORTS the commit: the pointer is
404
+ // unchanged and rebindNext never runs.
405
+ if (quiescePrevious !== undefined) {
406
+ try {
407
+ await quiescePrevious(previous);
408
+ }
409
+ catch (error) {
410
+ const reason = error instanceof Error ? error.message : String(error);
411
+ throw new ManagementError('UPGRADE_FAILED', 'install', `the quiesce of ${previous} failed before the pointer commit; the commit is aborted and the pointer is unchanged: ${reason}`, 'The previous version stays active. Diagnose the quiesce failure, then retry the commit.');
412
+ }
413
+ }
414
+ store.writeProfileState(options.profile, extensions.value, undefined, nextActive, nextPrevious);
415
+ // PHASE 2 — rebindNext: the host republishes the new generation. A failure
416
+ // rolls the pointer back AUTOMATICALLY and runs NO further hooks (the
417
+ // rebind-revert contract: the host's own wrapper owns any revert it needs).
418
+ if (rebindNext !== undefined) {
419
+ try {
420
+ await rebindNext(options.stagedVersion);
421
+ }
422
+ catch (error) {
423
+ // AUTOMATIC ROLLBACK: the pointer returns to the previous version and
424
+ // the reason is reported — the durable answer is never "half updated".
425
+ const reason = error instanceof Error ? error.message : String(error);
426
+ const rolledActive = { ...nextActive, [options.extensionId]: previous };
427
+ const rolledPrevious = {
428
+ ...nextPrevious,
429
+ [options.extensionId]: priorPrevious ?? previous,
430
+ };
431
+ store.writeProfileState(options.profile, extensions.value, undefined, rolledActive, rolledPrevious);
432
+ throw new ManagementError('UPGRADE_FAILED', 'install', `the rebind failed after the pointer commit; the pointer was rolled back to ${previous}: ${reason}`, 'The previous version stays active. Diagnose the rebind failure, then retry the commit or stage a new version.');
433
+ }
434
+ }
435
+ return { extensionId: options.extensionId, activeVersion: options.stagedVersion, previousVersion: previous };
436
+ });
437
+ }
438
+ /**
439
+ * Roll back one extension to its previous active version (EXT-30 slice 2).
440
+ *
441
+ * POINTER RESTORE ONLY: no bytes are copied over any live install, disabled
442
+ * tools stay disabled, revocations stay revoked (those facts live in the
443
+ * profile state beside the pointer). The previous version's directory is
444
+ * verified first; a corrupt previous version refuses the rollback (quarantine,
445
+ * the current pointer stays).
446
+ *
447
+ * The optional `rebindRestored` hook (the host's republish of the restored
448
+ * generation) runs AFTER the pointer restore. Its failure is REPORTED on the
449
+ * result as `hookFailure` — the restore stands and no second rollback runs.
450
+ *
451
+ * @param options - the store, profile, extension, and the optional restore hook.
452
+ * @returns the restored pointer.
453
+ */
454
+ export async function rollback(options) {
455
+ const store = new ExtensionStore(options.storeDir);
456
+ return await store.exclusive(async () => {
457
+ const pointers = store.readProfilePointers(options.profile);
458
+ if (pointers.corrupt) {
459
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the profile pointers are unreadable: ${pointers.diagnostic ?? 'unknown reason'}`, 'Repair the profile state deliberately.');
460
+ }
461
+ const active = pointers.value.activeVersions[options.extensionId];
462
+ const previous = pointers.value.previousVersions[options.extensionId];
463
+ if (active === undefined) {
464
+ throw new ManagementError('UPGRADE_FAILED', 'install', `${options.extensionId} has no recorded active version to roll back from`, 'Activate an installed version first.');
465
+ }
466
+ if (previous === undefined) {
467
+ throw new ManagementError('UPGRADE_FAILED', 'install', `${options.extensionId} has no previous version to roll back to`, 'There is no retained working version; nothing is restored.');
468
+ }
469
+ const record = store.listInstalled().value.find(candidate => candidate.extensionId === options.extensionId && candidate.version === previous);
470
+ if (record === undefined) {
471
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the rollback target ${previous} is not recorded; refusing`, 'Inspect the store lock; an unrecorded version is never restored.');
472
+ }
473
+ const check = store.verifyUnchanged(record);
474
+ if (!check.unchanged) {
475
+ throw new ManagementError('EXTENSION_FAULT', 'install', `cannot roll back: the previous version ${previous} changed on disk (${check.diagnostic ?? 'unknown reason'}); the extension stays quarantined on its current pointer`, 'Restore or reinstall the previous version deliberately; a rollback never points at unverified bytes.');
476
+ }
477
+ const extensions = store.readProfileState(options.profile);
478
+ if (extensions.corrupt) {
479
+ throw new ManagementError('EXTENSION_FAULT', 'install', `the profile state is unreadable: ${extensions.diagnostic ?? 'unknown reason'}`, 'Repair the profile state deliberately.');
480
+ }
481
+ const nextActive = { ...pointers.value.activeVersions, [options.extensionId]: previous };
482
+ const nextPrevious = { ...pointers.value.previousVersions, [options.extensionId]: active };
483
+ store.writeProfileState(options.profile, extensions.value, undefined, nextActive, nextPrevious);
484
+ // The POST-RESTORE observer runs after the pointer restore has committed.
485
+ // Its failure is REPORTED, never thrown: the restore must not be rolled
486
+ // back again over a hook error (no double rollback).
487
+ let hookFailure;
488
+ if (options.rebindRestored !== undefined) {
489
+ try {
490
+ await options.rebindRestored(previous);
491
+ }
492
+ catch (error) {
493
+ hookFailure = error instanceof Error ? error.message : String(error);
494
+ }
495
+ }
496
+ return {
497
+ extensionId: options.extensionId,
498
+ activeVersion: previous,
499
+ previousVersion: active,
500
+ ...hookFailure === undefined ? {} : { hookFailure },
501
+ };
502
+ });
503
+ }
504
+ //# sourceMappingURL=update-flow.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A real, zip-backed {@link ArchiveSource} over `adm-zip`.
3
+ *
4
+ * It lives here rather than in the SDK's public surface because it pulls in an
5
+ * archive library; the SDK itself declares only the interface, so an author
6
+ * embedding it does not inherit that dependency. Tests use this to exercise the
7
+ * installer against genuine archives rather than fabricated entry lists, which
8
+ * matters because the defenses are about how a real archive's metadata can lie.
9
+ *
10
+ * `adm-zip` is a devDependency for exactly this reason.
11
+ *
12
+ * @module @try-works/tool-mode-extension-sdk/install/zip-source
13
+ */
14
+ import type { ArchiveSource, InstallSource } from './staged.ts';
15
+ /** Build an {@link ArchiveSource} backed by `adm-zip`. */
16
+ export declare function createZipArchiveSource(fetchBytes?: (source: InstallSource) => Promise<Uint8Array>): ArchiveSource;
17
+ //# sourceMappingURL=zip-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-source.d.ts","sourceRoot":"","sources":["../../src/install/zip-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAoB/D,0DAA0D;AAC1D,wBAAgB,sBAAsB,CAAC,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAC,UAAU,CAAC,GAAG,aAAa,CAsCjH"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * A real, zip-backed {@link ArchiveSource} over `adm-zip`.
3
+ *
4
+ * It lives here rather than in the SDK's public surface because it pulls in an
5
+ * archive library; the SDK itself declares only the interface, so an author
6
+ * embedding it does not inherit that dependency. Tests use this to exercise the
7
+ * installer against genuine archives rather than fabricated entry lists, which
8
+ * matters because the defenses are about how a real archive's metadata can lie.
9
+ *
10
+ * `adm-zip` is a devDependency for exactly this reason.
11
+ *
12
+ * @module @try-works/tool-mode-extension-sdk/install/zip-source
13
+ */
14
+ import { mkdirSync, writeFileSync } from 'node:fs';
15
+ import { dirname, join, resolve, sep } from 'node:path';
16
+ import AdmZip from 'adm-zip';
17
+ /** The external-attributes bit that marks a POSIX symlink in a zip entry. */
18
+ const SYMLINK_MODE = 0xa000;
19
+ /** The external-attributes bit that marks a POSIX directory. */
20
+ const DIRECTORY_MODE = 0x4000;
21
+ /** Classify one zip entry from its recorded attributes. */
22
+ function kindOf(entry) {
23
+ const mode = (entry.attr >>> 16) & 0xffff;
24
+ if (entry.isDirectory)
25
+ return 'directory';
26
+ if ((mode & 0xf000) === SYMLINK_MODE)
27
+ return 'symlink';
28
+ if ((mode & 0xf000) === DIRECTORY_MODE)
29
+ return 'directory';
30
+ // Zip has no portable hardlink marker; `adm-zip` cannot report one, so a
31
+ // hardlink-shaped entry appears as a file here. The entry-list validator still
32
+ // refuses the shapes it can see, and the positive-list extractor below refuses
33
+ // to write anything that is not a plain entry.
34
+ return 'file';
35
+ }
36
+ /** Build an {@link ArchiveSource} backed by `adm-zip`. */
37
+ export function createZipArchiveSource(fetchBytes) {
38
+ return {
39
+ async fetch(source) {
40
+ if (fetchBytes === undefined) {
41
+ // No network stack in the SDK or its tests: a caller that wants a remote
42
+ // archive supplies the fetcher, and a test supplies bytes directly.
43
+ throw new Error(`no fetcher configured for source ${source.requested}`);
44
+ }
45
+ return fetchBytes(source);
46
+ },
47
+ list(bytes) {
48
+ const zip = new AdmZip(Buffer.from(bytes));
49
+ return zip.getEntries().map((entry) => ({
50
+ path: entry.entryName,
51
+ size: entry.header.size,
52
+ kind: kindOf(entry),
53
+ compressedSize: entry.header.compressedSize,
54
+ }));
55
+ },
56
+ extract(bytes, destination, root) {
57
+ const zip = new AdmZip(Buffer.from(bytes));
58
+ const prefix = root.length > 0 ? `${root}/` : '';
59
+ const target = resolve(destination);
60
+ for (const entry of zip.getEntries()) {
61
+ if (entry.isDirectory)
62
+ continue;
63
+ if (kindOf(entry) !== 'file')
64
+ continue;
65
+ const name = entry.entryName.startsWith(prefix) ? entry.entryName.slice(prefix.length) : entry.entryName;
66
+ if (name.length === 0)
67
+ continue;
68
+ // Confinement is re-asserted at the write, not only in the validator: the
69
+ // extraction is the step that touches the filesystem, so it is the step
70
+ // that must not be able to escape even if validation were bypassed.
71
+ const out = resolve(join(target, name));
72
+ if (out !== target && !out.startsWith(target + sep))
73
+ continue;
74
+ mkdirSync(dirname(out), { recursive: true });
75
+ writeFileSync(out, entry.getData());
76
+ }
77
+ },
78
+ };
79
+ }
80
+ //# sourceMappingURL=zip-source.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Loading an extension through its public entry (EXT-17, EXT-19).
3
+ *
4
+ * The host reaches an extension ONLY through `./tool-mode` and the interfaces in
5
+ * `runtime.ts`. It never imports a file inside the package, never reaches into an
6
+ * adapter registry the host owns, and never special-cases a model name. That is
7
+ * what makes BASE-09's "no private built-in adapter path" true rather than
8
+ * aspirational: a first-party package and a third-party package are loaded by the
9
+ * same function, so there is no second route that only first-party code can take.
10
+ *
11
+ * Loading is split from activation deliberately. `loadExtensionModule` validates
12
+ * the SHAPE of the export; `activateExtension` calls `create()`. EXT-19 requires
13
+ * `create()` to be lightweight, and keeping the two calls separate is what lets a
14
+ * host reject a malformed package without ever running its code.
15
+ *
16
+ * @module @try-works/tool-mode-extension-sdk/load
17
+ */
18
+ import { type ExtensionContextV1, type ExtensionInstanceV1, type ToolModeExtensionV1 } from './runtime.ts';
19
+ import type { ExtensionDiscovery } from './discovery.ts';
20
+ /** The public entry subpath every module extension declares. */
21
+ export declare const EXTENSION_ENTRY = "./tool-mode";
22
+ /**
23
+ * Resolve the absolute path of an installed extension's public entry.
24
+ *
25
+ * The path comes from the package's own `exports` map rather than from a
26
+ * convention, so the host reads the same declaration a Node resolver would and a
27
+ * package that points its entry elsewhere keeps working.
28
+ * @param discovery - the package discovery result.
29
+ * @returns the absolute entry path.
30
+ * @throws ManagementError when the package declares no importable `./tool-mode` entry.
31
+ */
32
+ export declare function resolveExtensionEntry(discovery: ExtensionDiscovery): string;
33
+ /**
34
+ * Import an extension's public entry and validate its shape.
35
+ *
36
+ * Importing runs the package's module top level, which is why EXT-19 constrains it
37
+ * to be lightweight, and why the shape is checked BEFORE `create()` is called.
38
+ * @param entryPath - absolute path of the module to import.
39
+ * @returns the validated extension.
40
+ * @throws ManagementError when the import fails or the export is malformed.
41
+ */
42
+ export declare function loadExtensionModule(entryPath: string): Promise<ToolModeExtensionV1>;
43
+ /**
44
+ * Activate a loaded extension.
45
+ *
46
+ * @param extension - the validated extension.
47
+ * @param context - the host-issued context.
48
+ * @returns the activated instance.
49
+ * @throws ManagementError when `create()` fails or returns no usable instance.
50
+ */
51
+ export declare function activateExtension(extension: ToolModeExtensionV1, context: ExtensionContextV1): Promise<ExtensionInstanceV1>;
52
+ //# sourceMappingURL=load.d.ts.map