@try-works/dsh-model-as-tool 0.1.3 → 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 (257) 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/standard.d.ts +38 -0
  130. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
  131. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
  132. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
  133. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
  134. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
  135. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
  136. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
  137. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
  138. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -1
  139. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +69 -55
  140. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +4 -1
  141. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -1
  142. package/node_modules/@try-works/tool-mode-contracts/lib/index.js +26 -2
  143. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +28 -2
  144. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -1
  145. package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +93 -10
  146. package/node_modules/@try-works/tool-mode-contracts/package.json +0 -1
  147. package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
  148. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
  149. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
  150. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
  151. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
  152. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
  153. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
  154. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
  155. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
  156. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
  157. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
  158. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
  159. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
  160. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +31 -0
  161. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
  162. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +31 -0
  163. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
  164. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
  165. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +185 -0
  166. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
  167. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
  168. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
  169. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
  170. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
  171. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
  172. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
  173. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
  174. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
  175. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
  176. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
  177. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
  178. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
  179. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
  180. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
  181. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
  182. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
  183. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
  184. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
  185. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
  186. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
  187. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
  188. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
  189. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
  190. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
  191. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
  192. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
  193. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
  194. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
  195. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
  196. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
  197. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
  198. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
  199. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
  200. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
  201. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
  202. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
  203. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
  204. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
  205. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
  206. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
  207. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
  208. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
  209. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
  210. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
  211. package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
  212. package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
  213. package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
  214. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
  215. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
  216. package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
  217. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
  218. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
  219. package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
  220. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
  221. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
  222. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
  223. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
  224. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
  225. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
  226. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
  227. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
  228. package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
  229. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
  230. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
  231. package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
  232. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
  233. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
  234. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
  235. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
  236. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
  237. package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
  238. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
  239. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
  240. package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
  241. package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
  242. package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
  243. package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
  244. package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
  245. package/package.json +43 -15
  246. package/remote/spec.mjs +439 -0
  247. package/remote/typert.host.mjs +14 -0
  248. package/remote/typert.remote-client.mjs +16 -0
  249. package/lib/adapters/tongue.d.ts +0 -31
  250. package/lib/adapters/tongue.d.ts.map +0 -1
  251. package/lib/adapters/tongue.js +0 -93
  252. package/lib/adapters/voz.d.ts +0 -28
  253. package/lib/adapters/voz.d.ts.map +0 -1
  254. package/lib/adapters/voz.js +0 -102
  255. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
  256. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
  257. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The REAL headless `serve` composition entry (RUN-03, task-74).
3
+ *
4
+ * Composes the host services the plugin needs WITHOUT the DSH CLI context:
5
+ * config load (file/env), the extension-store-less operator surface, the model
6
+ * catalog with capability resolution, the standard-contract bindings with the
7
+ * SAME selection rule the plugin applies (providers override, else the first
8
+ * eligible implementation), the MCP host over those bindings plus `model_tools`
9
+ * and the `tool_mode_run` contract, and a bounded graceful shutdown on
10
+ * SIGINT/SIGTERM.
11
+ *
12
+ * Two boundaries are deliberate and named, not faked:
13
+ * - `tool_mode_run` is REGISTERED (its contract appears on tools/list) but its
14
+ * execution refuses: a headless process composes no DSH hosted-code runtime
15
+ * (the plugin's ProgramRunner needs the DSH PtcHostContext), and pretending
16
+ * otherwise would claim a confinement the deployment does not have.
17
+ * - Extension-routed models resolve through the same bridge as the plugin; a
18
+ * model the headless deployment cannot run simply has no tool, exactly like
19
+ * the plugin.
20
+ *
21
+ * @module @try-works/dsh-model-as-tool/headless
22
+ */
23
+ /** The headless config document (the same Config surface, parsed loosely). */
24
+ interface HeadlessConfig {
25
+ readonly catalogFile?: string;
26
+ readonly allowNpm?: boolean;
27
+ readonly providers?: Record<string, string>;
28
+ readonly grantId?: string;
29
+ readonly recordReceipts?: boolean;
30
+ readonly approvedRoots?: string[];
31
+ readonly cli?: {
32
+ enabled?: boolean;
33
+ command?: string;
34
+ };
35
+ readonly remote?: {
36
+ endpoint?: string;
37
+ token?: string;
38
+ };
39
+ readonly registerControlTool?: boolean;
40
+ readonly mcpV3?: {
41
+ host?: string;
42
+ port?: number;
43
+ path?: string;
44
+ };
45
+ readonly repairAttempts?: number;
46
+ readonly harnessExecutorV3?: {
47
+ defaultRepairAttempts?: number;
48
+ };
49
+ readonly [key: string]: unknown;
50
+ }
51
+ /** A named headless-configuration error. */
52
+ export declare class HeadlessConfigError extends Error {
53
+ readonly name = "HeadlessConfigError";
54
+ }
55
+ /** Read and validate the headless config document. */
56
+ export declare function readHeadlessConfig(configPath: string | undefined, env: NodeJS.ProcessEnv): {
57
+ config: HeadlessConfig;
58
+ path: string;
59
+ };
60
+ /** One started headless serve, with its shutdown. */
61
+ export interface HeadlessServe {
62
+ /** The URL clients should use. */
63
+ readonly url: string;
64
+ /** The host/port actually bound. */
65
+ readonly host: string;
66
+ readonly port: number;
67
+ /** Close the server and settle owned resources (bounded). */
68
+ close(): Promise<void>;
69
+ }
70
+ /**
71
+ * Start the headless serve composition (RUN-03).
72
+ *
73
+ * @param configPath - the config file path; falls back to DSH_TOOL_MODE_CONFIG
74
+ * then ./tool-mode.json.
75
+ * @returns the bound endpoint and a shutdown handle.
76
+ * @throws HeadlessConfigError for a missing/invalid configuration.
77
+ */
78
+ export declare function startHeadlessServe(configPath?: string): Promise<HeadlessServe>;
79
+ /**
80
+ * Run the headless serve as a PROCESS (the launcher entry): prints the bound
81
+ * endpoint and waits for SIGINT/SIGTERM, then exits 0 after a bounded shutdown.
82
+ *
83
+ * @param options - the config path and the JSON-mode flag.
84
+ * @param io - output sinks (injected for tests).
85
+ * @returns the process exit code.
86
+ */
87
+ export declare function runHeadlessServe(options: {
88
+ readonly configPath?: string;
89
+ readonly json?: boolean;
90
+ }, io: {
91
+ readonly out: (line: string) => void;
92
+ readonly err: (line: string) => void;
93
+ }, signals?: {
94
+ once(name: 'SIGINT' | 'SIGTERM', handler: () => void): void;
95
+ }): Promise<number>;
96
+ export {};
97
+ //# sourceMappingURL=headless.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless.d.ts","sourceRoot":"","sources":["../src/headless.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA0BH,8EAA8E;AAC9E,UAAU,cAAc;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAA;IACjC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACvD,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAA;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,iBAAiB,CAAC,EAAE;QAAE,qBAAqB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/D,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAChC;AAED,4CAA4C;AAC5C,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,SAAkB,IAAI,yBAAwB;CAC/C;AAED,sDAAsD;AACtD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG;IAC1F,MAAM,EAAE,cAAc,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;CACb,CAoCA;AAqDD,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6DAA6D;IAC7D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAkJpF;AAwBD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,EAClE,EAAE,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EAClF,OAAO,GAAE;IAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;CAAY,GACjF,OAAO,CAAC,MAAM,CAAC,CA6BjB"}
@@ -0,0 +1,349 @@
1
+ /**
2
+ * The REAL headless `serve` composition entry (RUN-03, task-74).
3
+ *
4
+ * Composes the host services the plugin needs WITHOUT the DSH CLI context:
5
+ * config load (file/env), the extension-store-less operator surface, the model
6
+ * catalog with capability resolution, the standard-contract bindings with the
7
+ * SAME selection rule the plugin applies (providers override, else the first
8
+ * eligible implementation), the MCP host over those bindings plus `model_tools`
9
+ * and the `tool_mode_run` contract, and a bounded graceful shutdown on
10
+ * SIGINT/SIGTERM.
11
+ *
12
+ * Two boundaries are deliberate and named, not faked:
13
+ * - `tool_mode_run` is REGISTERED (its contract appears on tools/list) but its
14
+ * execution refuses: a headless process composes no DSH hosted-code runtime
15
+ * (the plugin's ProgramRunner needs the DSH PtcHostContext), and pretending
16
+ * otherwise would claim a confinement the deployment does not have.
17
+ * - Extension-routed models resolve through the same bridge as the plugin; a
18
+ * model the headless deployment cannot run simply has no tool, exactly like
19
+ * the plugin.
20
+ *
21
+ * @module @try-works/dsh-model-as-tool/headless
22
+ */
23
+ import { readFileSync } from 'node:fs';
24
+ import { resolve } from 'node:path';
25
+ import { STANDARD_CONTRACTS } from '@try-works/tool-mode-contracts';
26
+ import { assertValidContract, assertValidInput, assertValidOutput, crossFieldRules, } from '@try-works/tool-mode-contracts';
27
+ import { createGrant, authorizeTool, createInvocationAuthority } from "./authority.js";
28
+ import { buildMeta, buildReceipt } from "./bindings.js";
29
+ import { DEFAULT_CATALOG_FILE, loadCatalog } from "./catalog.js";
30
+ import { resolveCapability } from "./capability.js";
31
+ import { toContractPayload, toNativeArgs } from "./contract-payload.js";
32
+ import { createAdapter } from "./adapters/index.js";
33
+ import { ProviderBridge } from "./provider-bridge.js";
34
+ import { assertConfigV3DisabledFlags, describeModels, repairCeilingOf } from "./index.js";
35
+ import { TOOL_MODE_RUN_CONTRACT } from "./tool-mode-run.js";
36
+ import { createMcpHost } from "./mcp/host-binding.js";
37
+ import { startHttpServer } from "./mcp/http.js";
38
+ import { commandExists } from "./transport.js";
39
+ /** A named headless-configuration error. */
40
+ export class HeadlessConfigError extends Error {
41
+ name = 'HeadlessConfigError';
42
+ }
43
+ /** Read and validate the headless config document. */
44
+ export function readHeadlessConfig(configPath, env) {
45
+ const target = configPath ?? env.DSH_TOOL_MODE_CONFIG ?? './tool-mode.json';
46
+ let text;
47
+ try {
48
+ text = readFileSync(target, 'utf8');
49
+ }
50
+ catch (error) {
51
+ throw new HeadlessConfigError(`the config file ${target} could not be read: ${error instanceof Error ? error.message : String(error)}. `
52
+ + 'serve requires a config (--config <path> or DSH_TOOL_MODE_CONFIG).');
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(text);
57
+ }
58
+ catch (error) {
59
+ throw new HeadlessConfigError(`the config file ${target} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
62
+ throw new HeadlessConfigError(`the config file ${target} is not a JSON object`);
63
+ }
64
+ const config = parsed;
65
+ // The same fixed-false gate the plugin applies: unsupported values REFUSE.
66
+ try {
67
+ assertConfigV3DisabledFlags(config);
68
+ }
69
+ catch (error) {
70
+ throw new HeadlessConfigError(error instanceof Error ? error.message : String(error));
71
+ }
72
+ // The CODE-11 repair ceiling validates here too: the flat `repairAttempts`
73
+ // key drives, the v3 nested spelling is its equivalent, and an out-of-range
74
+ // value refuses at config load rather than silently clamping.
75
+ try {
76
+ repairCeilingOf(config);
77
+ }
78
+ catch (error) {
79
+ throw new HeadlessConfigError(error instanceof Error ? error.message : String(error));
80
+ }
81
+ return { config, path: resolve(target) };
82
+ }
83
+ /** Resolve the contract bindings exactly like the plugin's selection rule. */
84
+ function resolveBindings(config) {
85
+ const entries = loadCatalog(config.catalogFile ?? DEFAULT_CATALOG_FILE);
86
+ const cliCommand = config.cli?.command ?? 'desertant';
87
+ const cliAvailable = (config.cli?.enabled ?? true) && commandExists(cliCommand);
88
+ const remoteEndpoint = config.remote?.endpoint;
89
+ const transportOptions = {
90
+ cliCommand,
91
+ ...remoteEndpoint !== undefined ? { endpoint: remoteEndpoint } : {},
92
+ ...config.remote?.token !== undefined ? { token: config.remote.token } : {},
93
+ };
94
+ const resolved = entries.map(entry => ({
95
+ entry,
96
+ verdict: resolveCapability(entry, {
97
+ cliAvailable,
98
+ remoteEndpoint,
99
+ ...config.allowNpm !== undefined ? { allowNpm: config.allowNpm } : {},
100
+ }),
101
+ }));
102
+ const grant = createGrant({
103
+ grantId: config.grantId ?? 'local-interactive',
104
+ allowedTools: [...STANDARD_CONTRACTS.map(contract => contract.toolName), 'model_tools', 'tool_mode_run'],
105
+ approvedRoots: config.approvedRoots ?? [],
106
+ hostedCode: false,
107
+ recordingPolicy: config.recordReceipts === false ? 'none' : 'receipts',
108
+ });
109
+ const bridge = new ProviderBridge({
110
+ grant,
111
+ activationGeneration: 1,
112
+ log: () => { },
113
+ });
114
+ const bindings = new Map();
115
+ for (const contract of STANDARD_CONTRACTS) {
116
+ const eligible = resolved.filter(item => item.entry.contract === contract.contract && item.verdict.runnable);
117
+ if (eligible.length === 0)
118
+ continue;
119
+ const override = config.providers?.[contract.contract];
120
+ const chosen = eligible.find(item => item.entry.id === override) ?? eligible[0];
121
+ if (chosen === undefined)
122
+ continue;
123
+ const adapter = createAdapter(chosen.entry, { route: chosen.verdict.route, ...transportOptions }, undefined, bridge);
124
+ if (adapter === undefined)
125
+ continue;
126
+ bindings.set(contract.contract, {
127
+ contract,
128
+ implementation: chosen.entry,
129
+ adapter,
130
+ route: chosen.verdict.route,
131
+ alternatives: eligible.filter(item => item.entry.id !== chosen.entry.id).map(item => item.entry.id),
132
+ });
133
+ }
134
+ return bindings;
135
+ }
136
+ /**
137
+ * Start the headless serve composition (RUN-03).
138
+ *
139
+ * @param configPath - the config file path; falls back to DSH_TOOL_MODE_CONFIG
140
+ * then ./tool-mode.json.
141
+ * @returns the bound endpoint and a shutdown handle.
142
+ * @throws HeadlessConfigError for a missing/invalid configuration.
143
+ */
144
+ export async function startHeadlessServe(configPath) {
145
+ const { config } = readHeadlessConfig(configPath, process.env);
146
+ const bindings = resolveBindings(config);
147
+ // The MCP surface is NEVER unauthenticated (SEC-01): the headless serve
148
+ // requires a credential exactly like the stdio relay does.
149
+ const token = process.env.DSH_TOOL_MODE_MCP_TOKEN;
150
+ if (token === undefined || token.length === 0) {
151
+ throw new HeadlessConfigError('serve requires a credential (DSH_TOOL_MODE_MCP_TOKEN); the MCP surface is never unauthenticated (SEC-01)');
152
+ }
153
+ const models = (() => {
154
+ const entries = loadCatalog(config.catalogFile ?? DEFAULT_CATALOG_FILE);
155
+ const cliCommand = config.cli?.command ?? 'desertant';
156
+ const cliAvailable = (config.cli?.enabled ?? true) && commandExists(cliCommand);
157
+ const remoteEndpoint = config.remote?.endpoint;
158
+ return entries.map(entry => ({
159
+ entry,
160
+ verdict: resolveCapability(entry, {
161
+ cliAvailable,
162
+ remoteEndpoint,
163
+ ...config.allowNpm !== undefined ? { allowNpm: config.allowNpm } : {},
164
+ }),
165
+ }));
166
+ })();
167
+ let receiptCounter = 0;
168
+ const host = {
169
+ extension: '@try-works/dsh-model-as-tool',
170
+ extensionVersion: '0.1.1',
171
+ activationGeneration: 1,
172
+ nextReceiptId: () => `rcpt_${(++receiptCounter).toString(36).padStart(6, '0')}`,
173
+ grant: createGrant({
174
+ grantId: config.grantId ?? 'local-interactive',
175
+ allowedTools: [...STANDARD_CONTRACTS.map(contract => contract.toolName), 'model_tools', 'tool_mode_run'],
176
+ approvedRoots: config.approvedRoots ?? [],
177
+ hostedCode: false,
178
+ recordingPolicy: config.recordReceipts === false ? 'none' : 'receipts',
179
+ }),
180
+ callerKind: 'interactive',
181
+ };
182
+ const published = [];
183
+ for (const binding of bindings.values()) {
184
+ /**
185
+ * Registration is what compiles a contract's validators. The plugin path gets
186
+ * this from registerContractTool; the headless surface builds the published
187
+ * list directly, so it must register each contract here or every real call
188
+ * fails with "input was validated before the contract was registered".
189
+ */
190
+ assertValidContract(binding.contract);
191
+ published.push({ toolName: binding.contract.toolName, contract: binding.contract });
192
+ }
193
+ if (config.registerControlTool !== false) {
194
+ published.push({ toolName: 'model_tools', contract: modelToolsContract() });
195
+ }
196
+ published.push({ toolName: 'tool_mode_run', contract: TOOL_MODE_RUN_CONTRACT });
197
+ const mcpHost = createMcpHost({
198
+ serverInfo: { name: '@try-works/dsh-model-as-tool', version: '0.1.1' },
199
+ surface: {
200
+ get published() {
201
+ return published;
202
+ },
203
+ async execute(request) {
204
+ if (request.caller !== undefined) {
205
+ try {
206
+ authorizeTool({
207
+ grantId: request.caller.grantId,
208
+ allowedTools: request.caller.allowedTools,
209
+ approvedRoots: [],
210
+ remoteDestinations: [],
211
+ hostedCode: false,
212
+ recordingPolicy: request.caller.recordingPolicy,
213
+ limits: request.caller.limits,
214
+ }, request.name);
215
+ }
216
+ catch (error) {
217
+ return { isError: true, message: error instanceof Error ? error.message : String(error) };
218
+ }
219
+ }
220
+ if (request.name === 'model_tools') {
221
+ return { isError: false, value: { models: describeModels(models) } };
222
+ }
223
+ if (request.name === 'tool_mode_run') {
224
+ return {
225
+ isError: true,
226
+ message: 'tool_mode_run is registered but refused in the headless serve: this process composes no DSH hosted-code '
227
+ + 'runtime (the ProgramRunner needs the DSH PtcHostContext), and the refusal is reported rather than a '
228
+ + 'confinement the deployment does not have being claimed (CODE-10)',
229
+ };
230
+ }
231
+ const binding = [...bindings.values()].find(candidate => candidate.contract.toolName === request.name);
232
+ if (binding === undefined) {
233
+ return { isError: true, message: `unknown tool ${JSON.stringify(request.name)}` };
234
+ }
235
+ const { contract, adapter } = binding;
236
+ assertValidInput(contract, request.arguments);
237
+ for (const rule of crossFieldRules(contract)) {
238
+ const violation = rule(request.arguments);
239
+ if (violation !== undefined) {
240
+ return { isError: true, message: `${contract.toolName}: ${violation}` };
241
+ }
242
+ }
243
+ authorizeTool(host.grant, contract.toolName);
244
+ const nativeArgs = toNativeArgs(contract.contract, request.arguments);
245
+ const native = await adapter.invoke({
246
+ args: nativeArgs,
247
+ signal: request.signal,
248
+ authority: createInvocationAuthority(host.grant, host.callerKind),
249
+ });
250
+ const payload = toContractPayload(contract.contract, native, request.arguments);
251
+ const meta = buildMeta(binding, host);
252
+ const value = { ...payload, meta };
253
+ assertValidOutput(contract, value);
254
+ host.recordReceipt?.(buildReceipt(binding, host, meta));
255
+ return { isError: false, value };
256
+ },
257
+ },
258
+ });
259
+ const endpoint = await startHttpServer({
260
+ path: config.mcpV3?.path ?? '/mcp',
261
+ host: config.mcpV3?.host ?? '127.0.0.1',
262
+ port: config.mcpV3?.port ?? 3765,
263
+ maxBodyBytes: 16 * 1024 * 1024,
264
+ allowedOrigins: [],
265
+ auth: { scheme: 'static-token', token, clientId: 'headless-serve' },
266
+ }, mcpHost);
267
+ let closing = false;
268
+ return {
269
+ url: endpoint.url,
270
+ host: endpoint.host,
271
+ port: endpoint.port,
272
+ async close() {
273
+ if (closing)
274
+ return;
275
+ closing = true;
276
+ await Promise.race([
277
+ endpoint.close(),
278
+ new Promise(resolve => setTimeout(resolve, 3000)),
279
+ ]);
280
+ },
281
+ };
282
+ }
283
+ /** The `model_tools` control contract, rendered for the headless MCP surface. */
284
+ function modelToolsContract() {
285
+ return {
286
+ contract: 'model.tools',
287
+ version: '1.0.0',
288
+ toolName: 'model_tools',
289
+ description: 'Inspect the models this headless deployment can run.',
290
+ inputSchema: { type: 'object', additionalProperties: false, properties: {} },
291
+ outputSchema: {
292
+ type: 'object',
293
+ additionalProperties: false,
294
+ required: ['models'],
295
+ properties: {
296
+ models: { type: 'array', items: { type: 'object', additionalProperties: true } },
297
+ },
298
+ },
299
+ semantics: { scoreKind: 'none' },
300
+ limits: {},
301
+ unsupported: ['Changing the catalog: this is an inspection surface.'],
302
+ };
303
+ }
304
+ /**
305
+ * Run the headless serve as a PROCESS (the launcher entry): prints the bound
306
+ * endpoint and waits for SIGINT/SIGTERM, then exits 0 after a bounded shutdown.
307
+ *
308
+ * @param options - the config path and the JSON-mode flag.
309
+ * @param io - output sinks (injected for tests).
310
+ * @returns the process exit code.
311
+ */
312
+ export async function runHeadlessServe(options, io, signals = process) {
313
+ try {
314
+ const serve = await startHeadlessServe(options.configPath);
315
+ if (options.json === true) {
316
+ io.out(JSON.stringify({ ok: true, listening: true, url: serve.url }));
317
+ }
318
+ else {
319
+ io.out(`dsh-tool-mode serve: listening on ${serve.url}`);
320
+ }
321
+ return await new Promise(resolve => {
322
+ let shuttingDown = false;
323
+ const shutdown = () => {
324
+ if (shuttingDown)
325
+ return;
326
+ shuttingDown = true;
327
+ void serve.close().finally(() => resolve(0));
328
+ };
329
+ signals.once('SIGINT', shutdown);
330
+ signals.once('SIGTERM', shutdown);
331
+ });
332
+ }
333
+ catch (error) {
334
+ if (error instanceof HeadlessConfigError) {
335
+ if (options.json === true)
336
+ io.out(JSON.stringify({ ok: false, error: error.message }));
337
+ else
338
+ io.err(`dsh-tool-mode serve: ${error.message}`);
339
+ return 2;
340
+ }
341
+ const message = error instanceof Error ? error.message : String(error);
342
+ if (options.json === true)
343
+ io.out(JSON.stringify({ ok: false, error: message }));
344
+ else
345
+ io.err(`dsh-tool-mode serve: ${message}`);
346
+ return 1;
347
+ }
348
+ }
349
+ //# sourceMappingURL=headless.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The HTTP acquisition source for pinned asset downloads (RES-03).
3
+ *
4
+ * `acquireAsset` is source-agnostic and its tests inject fakes, which is right
5
+ * for the failure paths — but a source nobody wires means the host-driven
6
+ * acquisition path NDL-01 names was never exercised against a real URL. This
7
+ * module is the real source: fetch with byte ranges, bounded redirect
8
+ * handling, and no writes outside the injected store.
9
+ *
10
+ * @module @try-works/dsh-model-as-tool/http-acquisition
11
+ */
12
+ import type { AcquisitionSource } from './asset-acquisition.ts';
13
+ /**
14
+ * Create an HTTP acquisition source.
15
+ *
16
+ * Uses the platform fetch, which supports HTTPS and HTTP on Node 18+. Ranges
17
+ * are requested per chunk; a server that ignores the Range header is detected
18
+ * by the returned byte count and the transfer fails rather than resuming from
19
+ * wrong offsets.
20
+ * @param options - redirect budget and optional fetch override (for tests).
21
+ * @returns the source.
22
+ */
23
+ export declare function createHttpSource(options?: {
24
+ readonly maxRedirects?: number;
25
+ readonly fetchImpl?: typeof fetch;
26
+ }): AcquisitionSource;
27
+ //# sourceMappingURL=http-acquisition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-acquisition.d.ts","sourceRoot":"","sources":["../src/http-acquisition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAS/D;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE;IACxC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAC7B,GAAG,iBAAiB,CAgEzB"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The HTTP acquisition source for pinned asset downloads (RES-03).
3
+ *
4
+ * `acquireAsset` is source-agnostic and its tests inject fakes, which is right
5
+ * for the failure paths — but a source nobody wires means the host-driven
6
+ * acquisition path NDL-01 names was never exercised against a real URL. This
7
+ * module is the real source: fetch with byte ranges, bounded redirect
8
+ * handling, and no writes outside the injected store.
9
+ *
10
+ * @module @try-works/dsh-model-as-tool/http-acquisition
11
+ */
12
+ /** Maximum redirects followed, so a download cannot bounce forever. */
13
+ const MAX_REDIRECTS = 5;
14
+ /** Maximum response size a single range fetch admits, as a guard above the chunk size. */
15
+ const MAX_CHUNK_BYTES = 16 * 1024 * 1024;
16
+ /**
17
+ * Create an HTTP acquisition source.
18
+ *
19
+ * Uses the platform fetch, which supports HTTPS and HTTP on Node 18+. Ranges
20
+ * are requested per chunk; a server that ignores the Range header is detected
21
+ * by the returned byte count and the transfer fails rather than resuming from
22
+ * wrong offsets.
23
+ * @param options - redirect budget and optional fetch override (for tests).
24
+ * @returns the source.
25
+ */
26
+ export function createHttpSource(options = {}) {
27
+ const maxRedirects = options.maxRedirects ?? MAX_REDIRECTS;
28
+ const fetchImpl = options.fetchImpl ?? fetch;
29
+ return {
30
+ supportsRanges: true,
31
+ async fetchRange(asset, from, to, signal) {
32
+ if (to - from + 1 > MAX_CHUNK_BYTES) {
33
+ throw new Error(`a range fetch of ${to - from + 1} bytes exceeds the ${MAX_CHUNK_BYTES} cap`);
34
+ }
35
+ let url = asset.source;
36
+ for (let redirect = 0; redirect <= maxRedirects; redirect += 1) {
37
+ const response = await fetchImpl(url, {
38
+ headers: { range: `bytes=${from}-${to}` },
39
+ signal,
40
+ // Redirects are followed MANUALLY so each hop is counted: a redirect
41
+ // loop is an error, not a hang.
42
+ redirect: 'manual',
43
+ });
44
+ if (response.status >= 300 && response.status < 400) {
45
+ const location = response.headers.get('location');
46
+ if (location === null) {
47
+ throw new Error(`${asset.assetId}: a redirect carried no location`);
48
+ }
49
+ url = new URL(location, url).href;
50
+ continue;
51
+ }
52
+ if (!response.ok) {
53
+ throw new Error(`${asset.assetId}: HTTP ${response.status} fetching ${asset.source}`);
54
+ }
55
+ // RFC 9110 §14.2 permits a server to IGNORE the Range header entirely and
56
+ // answer 200 with the full body. That is a legal response, so it is
57
+ // handled rather than rejected: the wanted window is sliced out of the
58
+ // full body at ITS absolute offset, and the caller's offset math stays
59
+ // correct whether the server honored the Range or not. The budget admits
60
+ // exactly the bytes up to the window's end — the server chose to send
61
+ // them, and the operator already approved the asset's declared size.
62
+ const wanted = to - from + 1;
63
+ if (response.status === 200) {
64
+ // The budget is the operator-approved asset size: a 200 IS the full
65
+ // asset, which the approval already disclosed; anything beyond it is a
66
+ // server that cannot be trusted.
67
+ const body = await readBoundedBody(response, asset.bytes, `${asset.assetId}: full-body`);
68
+ if (body.byteLength < from + wanted) {
69
+ throw new Error(`${asset.assetId}: a 200 response carried ${body.byteLength} bytes, fewer than the ${from + wanted} the requested window requires`);
70
+ }
71
+ return body.subarray(from, from + wanted);
72
+ }
73
+ if (response.status === 206) {
74
+ const body = await readBoundedBody(response, wanted, `${asset.assetId}: range body`);
75
+ if (body.byteLength !== wanted) {
76
+ // A 206 must carry exactly the range the Content-Range describes; a
77
+ // short body means the server truncated, and trusting it would corrupt
78
+ // the resume offsets.
79
+ throw new Error(`${asset.assetId}: requested ${wanted} bytes at ${from}, received ${body.byteLength}`);
80
+ }
81
+ return body;
82
+ }
83
+ throw new Error(`${asset.assetId}: unexpected HTTP ${response.status}`);
84
+ }
85
+ throw new Error(`${asset.assetId}: more than ${maxRedirects} redirects`);
86
+ },
87
+ };
88
+ }
89
+ /**
90
+ * Read a response body under a HARD byte budget.
91
+ *
92
+ * The budget bounds MEMORY before any byte is buffered: the reader is streamed
93
+ * chunk by chunk and refuses the moment the running total would exceed the cap,
94
+ * so a Range-ignoring server cannot push an entire multi-GB asset into a single
95
+ * buffer (the round-48 review's finding 2).
96
+ * @param response - the response to read.
97
+ * @param budget - the maximum bytes admitted.
98
+ * @param label - what is being read, for the error.
99
+ * @returns the buffered bytes.
100
+ */
101
+ async function readBoundedBody(response, budget, label) {
102
+ if (response.body === null) {
103
+ // No stream (rare: opaque or empty responses). Read with a guard anyway.
104
+ const buffer = new Uint8Array(await response.arrayBuffer());
105
+ if (buffer.byteLength > budget)
106
+ throw new Error(`${label} exceeded the ${budget} byte budget`);
107
+ return buffer;
108
+ }
109
+ const reader = response.body.getReader();
110
+ const chunks = [];
111
+ let total = 0;
112
+ try {
113
+ for (;;) {
114
+ const { done, value } = await reader.read();
115
+ if (done)
116
+ break;
117
+ total += value.byteLength;
118
+ if (total > budget) {
119
+ await reader.cancel();
120
+ throw new Error(`${label} exceeded the ${budget} byte budget`);
121
+ }
122
+ chunks.push(value);
123
+ }
124
+ }
125
+ finally {
126
+ reader.releaseLock();
127
+ }
128
+ const out = new Uint8Array(total);
129
+ let offset = 0;
130
+ for (const chunk of chunks) {
131
+ out.set(chunk, offset);
132
+ offset += chunk.byteLength;
133
+ }
134
+ return out;
135
+ }
136
+ //# sourceMappingURL=http-acquisition.js.map