@lamplitisles/dsh-nanocodex 0.1.0-beta.0

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 (276) hide show
  1. package/LICENSE +65 -0
  2. package/NOTICE +2 -0
  3. package/README.md +182 -0
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/cordis.patch.yml +52 -0
  6. package/dist/index.d.ts +435 -0
  7. package/dist/index.js +3405 -0
  8. package/dist/standard-preset.d.ts +7 -0
  9. package/dist/standard-preset.js +25 -0
  10. package/engine-release.json +14 -0
  11. package/package.json +119 -0
  12. package/presets/standard/agent.cordis.yml +6 -0
  13. package/presets/standard/preset.yml +2 -0
  14. package/vendor/nanocodex/README.md +1344 -0
  15. package/vendor/nanocodex/actions/events.d.mts +10 -0
  16. package/vendor/nanocodex/actions/events.mjs +163 -0
  17. package/vendor/nanocodex/actions/index.d.mts +13 -0
  18. package/vendor/nanocodex/actions/index.mjs +35 -0
  19. package/vendor/nanocodex/actions/session.d.mts +66 -0
  20. package/vendor/nanocodex/actions/session.mjs +59 -0
  21. package/vendor/nanocodex/actions/turn.d.mts +59 -0
  22. package/vendor/nanocodex/actions/turn.mjs +37 -0
  23. package/vendor/nanocodex/actions/voice.d.mts +22 -0
  24. package/vendor/nanocodex/actions/voice.mjs +50 -0
  25. package/vendor/nanocodex/browser/Agent.d.mts +55 -0
  26. package/vendor/nanocodex/browser/Agent.mjs +11 -0
  27. package/vendor/nanocodex/browser/ChatGptSubscription.d.mts +7 -0
  28. package/vendor/nanocodex/browser/ChatGptSubscription.mjs +19 -0
  29. package/vendor/nanocodex/browser/InlineAgent.mjs +266 -0
  30. package/vendor/nanocodex/browser/Transport.d.mts +74 -0
  31. package/vendor/nanocodex/browser/Transport.mjs +61 -0
  32. package/vendor/nanocodex/browser/Voice.d.mts +71 -0
  33. package/vendor/nanocodex/browser/Voice.mjs +291 -0
  34. package/vendor/nanocodex/browser/VoiceSession.mjs +688 -0
  35. package/vendor/nanocodex/browser/WorkerAgent.d.mts +65 -0
  36. package/vendor/nanocodex/browser/WorkerAgent.mjs +1472 -0
  37. package/vendor/nanocodex/browser/agent.worker.mjs +3 -0
  38. package/vendor/nanocodex/browser/config.d.mts +36 -0
  39. package/vendor/nanocodex/browser/config.mjs +439 -0
  40. package/vendor/nanocodex/browser/engine.mjs +13 -0
  41. package/vendor/nanocodex/browser/harness.mjs +55 -0
  42. package/vendor/nanocodex/browser/host.d.mts +83 -0
  43. package/vendor/nanocodex/browser/host.mjs +555 -0
  44. package/vendor/nanocodex/browser/hostManagedWebSocket.d.mts +14 -0
  45. package/vendor/nanocodex/browser/hostManagedWebSocket.mjs +110 -0
  46. package/vendor/nanocodex/browser/index.d.mts +92 -0
  47. package/vendor/nanocodex/browser/index.mjs +23 -0
  48. package/vendor/nanocodex/browser/indexeddb-durability-store.mjs +197 -0
  49. package/vendor/nanocodex/browser/workspace.d.mts +16 -0
  50. package/vendor/nanocodex/browser/workspace.mjs +107 -0
  51. package/vendor/nanocodex/cloud/Client.d.mts +83 -0
  52. package/vendor/nanocodex/cloud/Client.mjs +231 -0
  53. package/vendor/nanocodex/cloud/Decorator.d.mts +31 -0
  54. package/vendor/nanocodex/cloud/Decorator.mjs +37 -0
  55. package/vendor/nanocodex/cloud/Dialog.d.mts +113 -0
  56. package/vendor/nanocodex/cloud/Dialog.mjs +366 -0
  57. package/vendor/nanocodex/cloud/Errors.d.mts +22 -0
  58. package/vendor/nanocodex/cloud/Errors.mjs +39 -0
  59. package/vendor/nanocodex/cloud/Principal.d.mts +26 -0
  60. package/vendor/nanocodex/cloud/Principal.mjs +88 -0
  61. package/vendor/nanocodex/cloud/RemoteProvider.mjs +88 -0
  62. package/vendor/nanocodex/cloud/Transport.d.mts +47 -0
  63. package/vendor/nanocodex/cloud/Transport.mjs +454 -0
  64. package/vendor/nanocodex/cloud/actions/account.d.mts +9 -0
  65. package/vendor/nanocodex/cloud/actions/account.mjs +19 -0
  66. package/vendor/nanocodex/cloud/actions/agent.d.mts +18 -0
  67. package/vendor/nanocodex/cloud/actions/agent.mjs +428 -0
  68. package/vendor/nanocodex/cloud/actions/connection.d.mts +133 -0
  69. package/vendor/nanocodex/cloud/actions/connection.mjs +580 -0
  70. package/vendor/nanocodex/cloud/actions/grant.d.mts +10 -0
  71. package/vendor/nanocodex/cloud/actions/grant.mjs +12 -0
  72. package/vendor/nanocodex/cloud/actions/index.d.mts +7 -0
  73. package/vendor/nanocodex/cloud/actions/index.mjs +7 -0
  74. package/vendor/nanocodex/cloud/actions/machineUsd.d.mts +23 -0
  75. package/vendor/nanocodex/cloud/actions/machineUsd.mjs +37 -0
  76. package/vendor/nanocodex/cloud/actions/model.d.mts +12 -0
  77. package/vendor/nanocodex/cloud/actions/model.mjs +50 -0
  78. package/vendor/nanocodex/cloud/actions/mpp.d.mts +28 -0
  79. package/vendor/nanocodex/cloud/actions/mpp.mjs +39 -0
  80. package/vendor/nanocodex/cloud/index.d.mts +30 -0
  81. package/vendor/nanocodex/cloud/index.mjs +9 -0
  82. package/vendor/nanocodex/cloud/internal.mjs +533 -0
  83. package/vendor/nanocodex/cloud/mercator.mjs +108 -0
  84. package/vendor/nanocodex/cloud/server/HostPrincipal.d.mts +34 -0
  85. package/vendor/nanocodex/cloud/server/HostPrincipal.mjs +202 -0
  86. package/vendor/nanocodex/cloud/server/index.d.mts +1 -0
  87. package/vendor/nanocodex/cloud/server/index.mjs +1 -0
  88. package/vendor/nanocodex/cloud/types.d.mts +211 -0
  89. package/vendor/nanocodex/cloudflare/Agent.d.mts +133 -0
  90. package/vendor/nanocodex/cloudflare/Agent.mjs +811 -0
  91. package/vendor/nanocodex/cloudflare/egress-subject.mjs +19 -0
  92. package/vendor/nanocodex/cloudflare/egress.d.mts +41 -0
  93. package/vendor/nanocodex/cloudflare/egress.mjs +119 -0
  94. package/vendor/nanocodex/cloudflare/event-socket.mjs +316 -0
  95. package/vendor/nanocodex/cloudflare/index.d.mts +7 -0
  96. package/vendor/nanocodex/cloudflare/index.mjs +6 -0
  97. package/vendor/nanocodex/host/Agent.d.mts +55 -0
  98. package/vendor/nanocodex/host/Agent.mjs +1 -0
  99. package/vendor/nanocodex/host/index.d.mts +82 -0
  100. package/vendor/nanocodex/host/index.mjs +10 -0
  101. package/vendor/nanocodex/index.d.mts +81 -0
  102. package/vendor/nanocodex/index.mjs +12 -0
  103. package/vendor/nanocodex/internal.mjs +1275 -0
  104. package/vendor/nanocodex/managed/Agent.d.mts +380 -0
  105. package/vendor/nanocodex/managed/Agent.mjs +1694 -0
  106. package/vendor/nanocodex/managed/ManagedError.d.mts +9 -0
  107. package/vendor/nanocodex/managed/ManagedError.mjs +8 -0
  108. package/vendor/nanocodex/managed/README.md +241 -0
  109. package/vendor/nanocodex/managed/Voice.mjs +202 -0
  110. package/vendor/nanocodex/managed/index.d.mts +20 -0
  111. package/vendor/nanocodex/managed/index.mjs +2 -0
  112. package/vendor/nanocodex/managed/internal.mjs +119 -0
  113. package/vendor/nanocodex/node/Agent.d.mts +50 -0
  114. package/vendor/nanocodex/node/Agent.mjs +203 -0
  115. package/vendor/nanocodex/node/ChatGptSubscription.d.mts +9 -0
  116. package/vendor/nanocodex/node/ChatGptSubscription.mjs +17 -0
  117. package/vendor/nanocodex/node/Transport.d.mts +44 -0
  118. package/vendor/nanocodex/node/Transport.mjs +46 -0
  119. package/vendor/nanocodex/node/host.mjs +731 -0
  120. package/vendor/nanocodex/node/index.d.mts +73 -0
  121. package/vendor/nanocodex/node/index.mjs +17 -0
  122. package/vendor/nanocodex/node/workspace.d.mts +10 -0
  123. package/vendor/nanocodex/node/workspace.mjs +198 -0
  124. package/vendor/nanocodex/package.json +200 -0
  125. package/vendor/nanocodex/pkg-node/nanocodex.d.ts +675 -0
  126. package/vendor/nanocodex/pkg-node/nanocodex.js +2628 -0
  127. package/vendor/nanocodex/pkg-node/nanocodex_bg.wasm.d.ts +105 -0
  128. package/vendor/nanocodex/pkg-node/package.json +1 -0
  129. package/vendor/nanocodex/pkg-web/.nanocodex-bindgen-stamp +6 -0
  130. package/vendor/nanocodex/pkg-web/nanocodex-build.json +1 -0
  131. package/vendor/nanocodex/pkg-web/nanocodex.d.ts +805 -0
  132. package/vendor/nanocodex/pkg-web/nanocodex.js +2714 -0
  133. package/vendor/nanocodex/pkg-web/nanocodex_bg.js +2615 -0
  134. package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm +0 -0
  135. package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm.d.ts +105 -0
  136. package/vendor/nanocodex/pkg-web/nanocodex_worker.js +9 -0
  137. package/vendor/nanocodex/pkg-web/package.json +1 -0
  138. package/vendor/nanocodex/runtime/chatgpt-subscription.mjs +212 -0
  139. package/vendor/nanocodex/runtime/cloudflare-durability-store.d.mts +26 -0
  140. package/vendor/nanocodex/runtime/cloudflare-durability-store.mjs +168 -0
  141. package/vendor/nanocodex/runtime/code-evaluator.worker.mjs +137 -0
  142. package/vendor/nanocodex/runtime/code-runtime.mjs +1 -0
  143. package/vendor/nanocodex/runtime/durability-store.d.mts +85 -0
  144. package/vendor/nanocodex/runtime/durability-store.mjs +631 -0
  145. package/vendor/nanocodex/runtime/durability.mjs +153 -0
  146. package/vendor/nanocodex/runtime/managed-transport.mjs +460 -0
  147. package/vendor/nanocodex/runtime/mcp-runtime.mjs +534 -0
  148. package/vendor/nanocodex/runtime/postgres-durability-store.d.mts +64 -0
  149. package/vendor/nanocodex/runtime/postgres-durability-store.mjs +532 -0
  150. package/vendor/nanocodex/runtime/quickjs-evaluator.d.mts +17 -0
  151. package/vendor/nanocodex/runtime/quickjs-evaluator.mjs +236 -0
  152. package/vendor/nanocodex/runtime/responses-transport.mjs +22 -0
  153. package/vendor/nanocodex/runtime/subagents.d.mts +107 -0
  154. package/vendor/nanocodex/runtime/subagents.mjs +54 -0
  155. package/vendor/nanocodex/runtime/subscription-store.d.mts +8 -0
  156. package/vendor/nanocodex/runtime/subscription-store.mjs +42 -0
  157. package/vendor/nanocodex/runtime/tempo-provider.d.mts +125 -0
  158. package/vendor/nanocodex/runtime/tempo-provider.mjs +264 -0
  159. package/vendor/nanocodex/runtime/tool-configuration.mjs +1 -0
  160. package/vendor/nanocodex/runtime/tool-router.mjs +1 -0
  161. package/vendor/nanocodex/runtime/utf8.mjs +1 -0
  162. package/vendor/nanocodex/runtime/worker-evaluator.mjs +162 -0
  163. package/vendor/nanocodex/runtime/workspace.d.mts +1 -0
  164. package/vendor/nanocodex/runtime/workspace.mjs +1 -0
  165. package/vendor/nanocodex/tools/Tools.d.mts +56 -0
  166. package/vendor/nanocodex/tools/Tools.mjs +136 -0
  167. package/vendor/nanocodex/tools/artifact.d.mts +1 -0
  168. package/vendor/nanocodex/tools/artifact.mjs +1 -0
  169. package/vendor/nanocodex/tools/attachment.mjs +1 -0
  170. package/vendor/nanocodex/tools/bash.d.mts +1 -0
  171. package/vendor/nanocodex/tools/bash.mjs +1 -0
  172. package/vendor/nanocodex/tools/browser/accountInfo.mjs +795 -0
  173. package/vendor/nanocodex/tools/browser/browserBuffer.mjs +18 -0
  174. package/vendor/nanocodex/tools/browser/browserCompiler.mjs +155 -0
  175. package/vendor/nanocodex/tools/browser/browserEgress.mjs +161 -0
  176. package/vendor/nanocodex/tools/browser/browserPython.mjs +133 -0
  177. package/vendor/nanocodex/tools/browser/browserShell.mjs +1075 -0
  178. package/vendor/nanocodex/tools/browser/browserSprintf.mjs +4 -0
  179. package/vendor/nanocodex/tools/browser/browserSsh.mjs +201 -0
  180. package/vendor/nanocodex/tools/browser/browserZlib.mjs +136 -0
  181. package/vendor/nanocodex/tools/browser/compiler.worker.mjs +160 -0
  182. package/vendor/nanocodex/tools/browser/devTunnelsSshBrowser.mjs +13 -0
  183. package/vendor/nanocodex/tools/browser/index.d.mts +209 -0
  184. package/vendor/nanocodex/tools/browser/index.mjs +174 -0
  185. package/vendor/nanocodex/tools/browser/opfsGit.mjs +248 -0
  186. package/vendor/nanocodex/tools/browser/python.worker.mjs +114 -0
  187. package/vendor/nanocodex/tools/browser/threadGit.mjs +197 -0
  188. package/vendor/nanocodex/tools/browser/unsupportedNodeRsa.mjs +5 -0
  189. package/vendor/nanocodex/tools/browser/workspace.mjs +83 -0
  190. package/vendor/nanocodex/tools/dataset.d.mts +1 -0
  191. package/vendor/nanocodex/tools/dataset.mjs +1 -0
  192. package/vendor/nanocodex/tools/datasetContract.mjs +1 -0
  193. package/vendor/nanocodex/tools/datasetEngine.mjs +1 -0
  194. package/vendor/nanocodex/tools/hostedCatalog.d.mts +1 -0
  195. package/vendor/nanocodex/tools/hostedCatalog.mjs +1 -0
  196. package/vendor/nanocodex/tools/index.d.mts +49 -0
  197. package/vendor/nanocodex/tools/index.mjs +19 -0
  198. package/vendor/nanocodex/tools/namedTool.mjs +1 -0
  199. package/vendor/nanocodex/tools/repository-workspace.d.mts +1 -0
  200. package/vendor/nanocodex/tools/repository-workspace.mjs +1 -0
  201. package/vendor/nanocodex/tools/ssh.d.mts +1 -0
  202. package/vendor/nanocodex/tools/ssh.mjs +1 -0
  203. package/vendor/nanocodex/tools/standard.mjs +1 -0
  204. package/vendor/nanocodex/tools/standardDescriptions.mjs +1 -0
  205. package/vendor/nanocodex/types.d.mts +663 -0
  206. package/vendor/nanocodex/wasm.d.mts +4 -0
  207. package/vendor/nanocodex/worker/ChatGptSubscription.d.mts +9 -0
  208. package/vendor/nanocodex/worker/ChatGptSubscription.mjs +29 -0
  209. package/vendor/nanocodex/worker/index.d.mts +18 -0
  210. package/vendor/nanocodex/worker/index.mjs +5 -0
  211. package/vendor/nanocodex-tools/README.md +65 -0
  212. package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.d.ts +7 -0
  213. package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.js +14 -0
  214. package/vendor/nanocodex-tools/dist/hosted/broker-core.d.ts +172 -0
  215. package/vendor/nanocodex-tools/dist/hosted/broker-core.js +1278 -0
  216. package/vendor/nanocodex-tools/dist/hosted/index.d.ts +3 -0
  217. package/vendor/nanocodex-tools/dist/hosted/index.js +3 -0
  218. package/vendor/nanocodex-tools/dist/hosted/protocol.d.ts +120 -0
  219. package/vendor/nanocodex-tools/dist/hosted/protocol.js +415 -0
  220. package/vendor/nanocodex-tools/dist/index.d.ts +19 -0
  221. package/vendor/nanocodex-tools/dist/index.js +14 -0
  222. package/vendor/nanocodex-tools/dist/memory.d.ts +204 -0
  223. package/vendor/nanocodex-tools/dist/memory.js +411 -0
  224. package/vendor/nanocodex-tools/dist/namespace.d.ts +87 -0
  225. package/vendor/nanocodex-tools/dist/namespace.js +338 -0
  226. package/vendor/nanocodex-tools/dist/repository-archive.d.ts +4 -0
  227. package/vendor/nanocodex-tools/dist/repository-archive.js +104 -0
  228. package/vendor/nanocodex-tools/dist/runtime.d.ts +30 -0
  229. package/vendor/nanocodex-tools/dist/runtime.js +37 -0
  230. package/vendor/nanocodex-tools/dist/session.d.ts +167 -0
  231. package/vendor/nanocodex-tools/dist/session.js +286 -0
  232. package/vendor/nanocodex-tools/dist/shell.d.ts +51 -0
  233. package/vendor/nanocodex-tools/dist/shell.js +553 -0
  234. package/vendor/nanocodex-tools/dist/ssh.d.ts +29 -0
  235. package/vendor/nanocodex-tools/dist/ssh.js +66 -0
  236. package/vendor/nanocodex-tools/dist/workspace.d.ts +45 -0
  237. package/vendor/nanocodex-tools/dist/workspace.js +237 -0
  238. package/vendor/nanocodex-tools/package.json +122 -0
  239. package/vendor/nanocodex-tools/runtime/code-runtime.mjs +817 -0
  240. package/vendor/nanocodex-tools/runtime/tool-configuration.mjs +50 -0
  241. package/vendor/nanocodex-tools/runtime/tool-router.mjs +730 -0
  242. package/vendor/nanocodex-tools/runtime/utf8.d.mts +2 -0
  243. package/vendor/nanocodex-tools/runtime/utf8.mjs +25 -0
  244. package/vendor/nanocodex-tools/tools/artifact.d.mts +69 -0
  245. package/vendor/nanocodex-tools/tools/artifact.mjs +270 -0
  246. package/vendor/nanocodex-tools/tools/attachment.mjs +603 -0
  247. package/vendor/nanocodex-tools/tools/bash.d.mts +90 -0
  248. package/vendor/nanocodex-tools/tools/bash.mjs +742 -0
  249. package/vendor/nanocodex-tools/tools/dataset.d.mts +9 -0
  250. package/vendor/nanocodex-tools/tools/dataset.mjs +47 -0
  251. package/vendor/nanocodex-tools/tools/datasetContract.mjs +54 -0
  252. package/vendor/nanocodex-tools/tools/datasetEngine.mjs +1167 -0
  253. package/vendor/nanocodex-tools/tools/execution-contract.d.mts +5 -0
  254. package/vendor/nanocodex-tools/tools/execution-contract.mjs +63 -0
  255. package/vendor/nanocodex-tools/tools/hostedCatalog.d.mts +20 -0
  256. package/vendor/nanocodex-tools/tools/hostedCatalog.mjs +67 -0
  257. package/vendor/nanocodex-tools/tools/hostedMachine.d.mts +10 -0
  258. package/vendor/nanocodex-tools/tools/hostedMachine.mjs +56 -0
  259. package/vendor/nanocodex-tools/tools/just-bash-browser.d.mts +1 -0
  260. package/vendor/nanocodex-tools/tools/just-bash-browser.mjs +1 -0
  261. package/vendor/nanocodex-tools/tools/namedTool.d.mts +3 -0
  262. package/vendor/nanocodex-tools/tools/namedTool.mjs +3 -0
  263. package/vendor/nanocodex-tools/tools/nodeProcess.d.mts +10 -0
  264. package/vendor/nanocodex-tools/tools/nodeProcess.mjs +314 -0
  265. package/vendor/nanocodex-tools/tools/processOutput.mjs +56 -0
  266. package/vendor/nanocodex-tools/tools/repository-workspace.d.mts +55 -0
  267. package/vendor/nanocodex-tools/tools/repository-workspace.mjs +397 -0
  268. package/vendor/nanocodex-tools/tools/ssh.d.mts +56 -0
  269. package/vendor/nanocodex-tools/tools/ssh.mjs +503 -0
  270. package/vendor/nanocodex-tools/tools/standard.mjs +458 -0
  271. package/vendor/nanocodex-tools/tools/standardDescriptions.mjs +3 -0
  272. package/vendor/nanocodex-tools/tools/types.d.mts +83 -0
  273. package/vendor/nanocodex-tools/tools/workspace.d.mts +25 -0
  274. package/vendor/nanocodex-tools/tools/workspace.mjs +282 -0
  275. package/vendor/nanocodex-tools/tools/x.d.mts +30 -0
  276. package/vendor/nanocodex-tools/tools/x.mjs +91 -0
@@ -0,0 +1,1278 @@
1
+ import { HOSTED_TOOL_CALL_TIMEOUT_MS, HOSTED_TOOLS_LEASE_MS, MAX_HOSTED_TOOL_OUTPUT_BYTES, HostedToolsProtocolError, parseHostedToolsHostFrame, parseHostedToolsManagedFrame, } from "./protocol.js";
2
+ import { hostedToolCatalogDigest } from "../../tools/hostedCatalog.mjs";
3
+ import { EXEC_COMMAND_PARAMETERS, EXECUTION_OUTPUT_SCHEMA, MACHINE_PREVIEW_PARAMETERS, PREVIEW_OUTPUT_SCHEMA, WRITE_STDIN_PARAMETERS, } from "../../tools/execution-contract.mjs";
4
+ const SOCKET_TAG = "hosted-tools";
5
+ const INVALID_CONNECT_GRANT_ID = "invalid-connect-grant";
6
+ const DEFAULT_MAX_IN_FLIGHT = 64;
7
+ const OPEN = 1;
8
+ const MAX_RETAINED_RECEIPTS = 512;
9
+ const MAX_CALLS_PER_GENERATION = 512;
10
+ const REVOKED_ROUTE_LEASE_EXPIRES_AT = -1;
11
+ const TOOL_RESULT = Symbol.for("nanocodex.toolResult");
12
+ const LEGACY_ROUTE_ID = "$legacy";
13
+ export const HOSTED_MACHINE_TOOL_NAMES = Object.freeze([
14
+ "exec_command",
15
+ "write_stdin",
16
+ "preview",
17
+ ]);
18
+ const MACHINE_TOOL_NAMES = new Set(HOSTED_MACHINE_TOOL_NAMES);
19
+ const encoder = new TextEncoder();
20
+ /**
21
+ * Marks the one case where an attached source is known to be absent before a
22
+ * durable admission. The unified ToolRouter may then select the exact
23
+ * same-name cloud contract. It must never infer this from an outcome message:
24
+ * every other unavailable, cancellation, timeout, and ambiguous outcome is
25
+ * pinned to the attached source and is final.
26
+ */
27
+ export const HOSTED_TOOLS_PRE_ADMISSION_UNAVAILABLE = Symbol.for("nanocodex.tool.preDispatchUnavailable");
28
+ /** Owns one reverse-tool attachment over an injected socket and durable ledger. */
29
+ export class HostedToolsBrokerCore {
30
+ context;
31
+ #provider;
32
+ #pending = new Map();
33
+ #now;
34
+ #randomUUID;
35
+ #maxInFlight;
36
+ #maxCallsPerGeneration;
37
+ #persistence;
38
+ #onCatalogChanged;
39
+ #entryAllowed;
40
+ #renewLeasedAttachment;
41
+ #catalogValidator;
42
+ #nextCandidateGeneration;
43
+ constructor(context, options) {
44
+ this.context = context;
45
+ this.#now = options.now ?? Date.now;
46
+ this.#randomUUID = options.randomUUID ?? (() => crypto.randomUUID());
47
+ this.#maxInFlight = options.maxInFlight ?? DEFAULT_MAX_IN_FLIGHT;
48
+ if (!Number.isSafeInteger(this.#maxInFlight) || this.#maxInFlight < 1
49
+ || this.#maxInFlight > DEFAULT_MAX_IN_FLIGHT) {
50
+ throw new TypeError(`maxInFlight must be an integer from 1 through ${DEFAULT_MAX_IN_FLIGHT}`);
51
+ }
52
+ this.#maxCallsPerGeneration = options.maxCallsPerGeneration ?? MAX_CALLS_PER_GENERATION;
53
+ if (!Number.isSafeInteger(this.#maxCallsPerGeneration) || this.#maxCallsPerGeneration < 1
54
+ || this.#maxCallsPerGeneration > MAX_CALLS_PER_GENERATION) {
55
+ throw new TypeError(`maxCallsPerGeneration must be an integer from 1 through ${MAX_CALLS_PER_GENERATION}`);
56
+ }
57
+ this.#persistence = options.persistence;
58
+ this.#onCatalogChanged = options.onCatalogChanged;
59
+ this.#entryAllowed = options.entryAllowed ?? (() => true);
60
+ this.#renewLeasedAttachment = options.renewLeasedAttachment;
61
+ const now = this.#now();
62
+ const sockets = this.context.sockets();
63
+ const retainHosts = options.resumeRetainedSockets === true && sockets.length > 0;
64
+ const retained = this.#persistence.initialize(now);
65
+ this.#nextCandidateGeneration = Math.max(0, ...this.#persistence.states().map((state) => state.generation));
66
+ for (const socket of sockets) {
67
+ const generation = this.#attachment(socket)?.generation;
68
+ if (generation !== undefined)
69
+ this.#nextCandidateGeneration = Math.max(this.#nextCandidateGeneration, generation);
70
+ }
71
+ for (const state of retained) {
72
+ if (!state.lease_id)
73
+ continue;
74
+ const resumed = retainHosts
75
+ && state.lease_expires_at > now
76
+ && this.#routingSocketForState(state) !== undefined;
77
+ if (resumed)
78
+ continue;
79
+ this.#persistence.clearHost(state.lease_id, state.generation);
80
+ for (const socket of sockets) {
81
+ const attachment = this.#attachment(socket);
82
+ if (attachment?.routeId !== state.route_id
83
+ || attachment.leaseId !== state.lease_id
84
+ || attachment.generation !== state.generation)
85
+ continue;
86
+ closeSocket(socket, 1012, "Hosted Tools owner restarted");
87
+ }
88
+ }
89
+ this.#provider = Object.freeze({
90
+ // ToolRouter owns the one aggregate tool_search. This provider exposes
91
+ // only the current attached definitions, which stay deferred and can be
92
+ // overlaid onto exact cloud contracts by that router.
93
+ definitions: () => {
94
+ return this.#publicCatalogBindings()
95
+ .filter((binding) => this.#entryAllowed(binding.entry, binding.connectGrantId, binding.appToolCatalogDigest))
96
+ .map((binding) => Object.freeze({
97
+ ...binding.entry.definition,
98
+ defer_loading: true,
99
+ }));
100
+ },
101
+ resolve: (name) => {
102
+ const prepared = this.#resolve(name);
103
+ if (!prepared || !this.#entryAllowed(prepared.entry, prepared.connectGrantId, prepared.appToolCatalogDigest))
104
+ return undefined;
105
+ return this.#codeTool(name, prepared);
106
+ },
107
+ setCatalogValidator: (validator) => {
108
+ this.#catalogValidator = validator;
109
+ },
110
+ });
111
+ }
112
+ owns(socket) { return this.handles(socket); }
113
+ async message(socket, message) {
114
+ await this.webSocketMessage(socket, message);
115
+ }
116
+ close(socket, reason) {
117
+ if (this.handles(socket))
118
+ this.#retire(socket, reason);
119
+ }
120
+ shutdown(reason) {
121
+ const sockets = this.context.sockets();
122
+ for (const socket of sockets)
123
+ this.#fence(socket, reason);
124
+ for (const state of this.#persistence.states())
125
+ this.#retireState(state, reason);
126
+ }
127
+ /** Fences one attachment only when its exact durable route is still current. */
128
+ revokeRoute(routeId, reason) {
129
+ const state = this.#persistence.state(routeId) ?? emptyState(routeId);
130
+ const active = state.lease_id !== null;
131
+ if (active) {
132
+ const socket = this.#socketForState(state);
133
+ if (socket)
134
+ this.#fence(socket, reason);
135
+ else
136
+ this.#retireState(state, reason);
137
+ }
138
+ // Revocation can race ahead of catalog publication in another Durable
139
+ // Object. Retain an exact route tombstone so a previously validated socket
140
+ // cannot publish after its control-plane fence has completed.
141
+ const retired = this.#persistence.state(routeId) ?? state;
142
+ this.#persistence.replaceHost({
143
+ ...retired,
144
+ host_id: null,
145
+ lease_id: null,
146
+ lease_expires_at: REVOKED_ROUTE_LEASE_EXPIRES_AT,
147
+ catalog_json: null,
148
+ });
149
+ return active;
150
+ }
151
+ isReady() { return this.#definitions().length > 0; }
152
+ hasPendingCalls() { return this.#pending.size > 0; }
153
+ provider() { return this.#provider; }
154
+ /** Resolves one canonical machine primitive against its exact admitted attachment generation. */
155
+ machineTool(machineId, name, context) {
156
+ if (!MACHINE_TOOL_NAMES.has(name))
157
+ return undefined;
158
+ const binding = this.#catalogBindings().find((candidate) => (candidate.machine?.id === machineId && candidate.wireName === name));
159
+ if (!binding)
160
+ return undefined;
161
+ const prepared = this.#preparedTool(binding);
162
+ if (!this.#entryAllowed(prepared.entry, prepared.connectGrantId, prepared.appToolCatalogDigest, context))
163
+ return undefined;
164
+ return this.#codeTool(name, prepared);
165
+ }
166
+ /** Resolves one canonical machine primitive only on the named durable route. */
167
+ machineToolOnRoute(routeId, machineId, name, context) {
168
+ if (!MACHINE_TOOL_NAMES.has(name))
169
+ return undefined;
170
+ const binding = this.#catalogBindings(undefined, true).find((candidate) => (candidate.routeId === routeId
171
+ && candidate.machine?.id === machineId
172
+ && candidate.wireName === name));
173
+ if (!binding)
174
+ return undefined;
175
+ const prepared = this.#preparedTool(binding);
176
+ if (!this.#entryAllowed(prepared.entry, prepared.connectGrantId, prepared.appToolCatalogDigest, context))
177
+ return undefined;
178
+ return this.#codeTool(name, prepared);
179
+ }
180
+ /** Returns one live machine only when its exact durable route still owns it. */
181
+ machineOnRoute(routeId, machineId) {
182
+ const state = this.#persistence.state(routeId);
183
+ if (state === undefined)
184
+ return undefined;
185
+ const socket = this.#liveRoutingSocketForState(state);
186
+ if (socket === undefined)
187
+ return undefined;
188
+ const attachment = this.#attachment(socket);
189
+ if (attachment?.connectGrantId !== undefined)
190
+ return undefined;
191
+ return attachment?.machines?.find(({ id }) => id === machineId);
192
+ }
193
+ /** Returns the live, non-secret user-machine snapshot for the account-owned host. */
194
+ machines() {
195
+ const machines = [];
196
+ const ids = new Set();
197
+ for (const state of this.#sortedStates()) {
198
+ const socket = this.#liveRoutingSocketForState(state);
199
+ if (socket === undefined)
200
+ continue;
201
+ const attachment = this.#attachment(socket);
202
+ if (attachment?.connectGrantId !== undefined)
203
+ continue;
204
+ for (const machine of attachment?.machines ?? []) {
205
+ if (ids.has(machine.id))
206
+ return [];
207
+ ids.add(machine.id);
208
+ machines.push({ routeId: state.route_id, machine });
209
+ }
210
+ }
211
+ return machines
212
+ .sort((left, right) => left.machine.id.localeCompare(right.machine.id)
213
+ || left.routeId.localeCompare(right.routeId))
214
+ .map(({ machine }) => machine);
215
+ }
216
+ accept(socket, sessionId, allowedMcpIds, appToolCatalogDigest, connectGrantId, leasedAttachment) {
217
+ if (allowedMcpIds !== undefined && !isConnectGrantId(connectGrantId)) {
218
+ throw new TypeError("Connect Hosted Tools requires an exact grant ID");
219
+ }
220
+ if (leasedAttachment !== undefined && (connectGrantId !== undefined
221
+ || leasedAttachment.expectedAttachmentId.length === 0
222
+ || leasedAttachment.fixedRouteId.length === 0
223
+ || leasedAttachment.renewalToken.length === 0
224
+ || leasedAttachment.renewalToken.length > 2_048
225
+ || !Number.isSafeInteger(leasedAttachment.maximumLeaseExpiresAt))) {
226
+ throw new TypeError("leased Hosted Tools requires one complete ordinary-route policy");
227
+ }
228
+ this.context.writeAttachment(socket, {
229
+ kind: SOCKET_TAG,
230
+ sessionId,
231
+ ...(allowedMcpIds === undefined ? {} : { allowedMcpIds: [...allowedMcpIds] }),
232
+ ...(appToolCatalogDigest === undefined ? {} : { appToolCatalogDigest }),
233
+ ...(connectGrantId === undefined ? {} : { connectGrantId }),
234
+ ...(leasedAttachment === undefined ? {} : {
235
+ expectedAttachmentId: leasedAttachment.expectedAttachmentId,
236
+ maximumLeaseExpiresAt: leasedAttachment.maximumLeaseExpiresAt,
237
+ fixedRouteId: leasedAttachment.fixedRouteId,
238
+ renewalToken: leasedAttachment.renewalToken,
239
+ }),
240
+ });
241
+ this.context.accept(socket);
242
+ }
243
+ handles(socket) {
244
+ return this.#attachment(socket)?.kind === SOCKET_TAG;
245
+ }
246
+ async webSocketMessage(socket, message) {
247
+ if (!this.handles(socket))
248
+ return;
249
+ if (typeof message !== "string") {
250
+ this.#fence(socket, "Hosted Tools requires bounded text frames", 1003);
251
+ return;
252
+ }
253
+ let frame;
254
+ try {
255
+ frame = parseHostedToolsHostFrame(message);
256
+ await this.#dispatchHostFrame(socket, frame);
257
+ }
258
+ catch (error) {
259
+ const protocol = error instanceof HostedToolsProtocolError
260
+ ? error
261
+ : new HostedToolsProtocolError("broker_failure", errorMessage(error));
262
+ this.#fence(socket, `${protocol.code}: ${protocol.message}`);
263
+ }
264
+ }
265
+ webSocketClose(socket, code, reason) {
266
+ if (!this.handles(socket))
267
+ return;
268
+ this.#retire(socket, reason || `peer closed with code ${code}`);
269
+ closeSocket(socket, code, reason || "Hosted Tools peer closed");
270
+ }
271
+ webSocketError(socket) {
272
+ if (!this.handles(socket))
273
+ return;
274
+ this.#retire(socket, "WebSocket failed");
275
+ closeSocket(socket, 1011, "Hosted Tools WebSocket failed");
276
+ }
277
+ /** May be called by an owning alarm; normal reads and call timers also expire leases lazily. */
278
+ expire() {
279
+ for (const state of this.#persistence.states()) {
280
+ if (!state.lease_id || state.lease_expires_at > this.#now())
281
+ continue;
282
+ const socket = this.#socketForState(state);
283
+ if (socket)
284
+ this.#fence(socket, "Hosted Tools lease expired");
285
+ else
286
+ this.#retireState(state, "Hosted Tools lease expired");
287
+ }
288
+ }
289
+ cancel(callId) {
290
+ const pending = this.#pending.get(callId);
291
+ if (!pending)
292
+ return false;
293
+ const row = this.#persistence.call(callId);
294
+ if (!row || row.state !== "dispatched")
295
+ return false;
296
+ const state = this.#stateForLease(row.lease_id, row.generation);
297
+ const socket = this.#socketForState(state);
298
+ if (!socket
299
+ || !state
300
+ || state.lease_expires_at <= this.#now()) {
301
+ this.#finishAmbiguous(row, "Hosted Tools cancellation lost its pinned attachment");
302
+ return false;
303
+ }
304
+ const cancelRequested = this.#persistence.markCancelRequested(callId, this.#now());
305
+ if (!cancelRequested || cancelRequested.state !== "dispatched"
306
+ || cancelRequested.cancel_requested !== 1)
307
+ return false;
308
+ try {
309
+ this.#send(socket, {
310
+ type: "cancel",
311
+ call_id: row.call_id,
312
+ });
313
+ return true;
314
+ }
315
+ catch {
316
+ this.#retire(socket, "cancellation delivery failed");
317
+ closeSocket(socket, 1011, "Hosted Tools cancellation delivery failed");
318
+ return false;
319
+ }
320
+ }
321
+ async #dispatchHostFrame(socket, frame) {
322
+ if (frame.type === "catalog")
323
+ await this.#publishCatalog(socket, frame);
324
+ else if (frame.type === "ping")
325
+ await this.#heartbeat(socket, frame);
326
+ else if (frame.type === "drain")
327
+ this.#drain(socket);
328
+ else
329
+ this.#completeResult(socket, frame);
330
+ }
331
+ #activeAttachment(socket) {
332
+ const attachment = this.#attachment(socket);
333
+ const state = attachment?.routeId === undefined
334
+ ? undefined
335
+ : this.#persistence.state(attachment.routeId);
336
+ if (!attachment?.active || !attachment.leaseId || attachment.generation === undefined || !state
337
+ || state.lease_id !== attachment.leaseId
338
+ || state.generation !== attachment.generation
339
+ || state.lease_expires_at <= this.#now()) {
340
+ throw new HostedToolsProtocolError("stale_socket", "socket no longer owns the tool attachment");
341
+ }
342
+ return attachment;
343
+ }
344
+ async #heartbeat(socket, frame) {
345
+ let attachment = this.#activeAttachment(socket);
346
+ if (attachment.renewalToken !== undefined) {
347
+ const renewal = {
348
+ expectedAttachmentId: attachment.expectedAttachmentId,
349
+ fixedRouteId: attachment.fixedRouteId,
350
+ renewalToken: attachment.renewalToken,
351
+ };
352
+ let maximumLeaseExpiresAt;
353
+ try {
354
+ maximumLeaseExpiresAt = await this.#renewLeasedAttachment?.(renewal);
355
+ }
356
+ catch {
357
+ this.#retire(socket, "leased Hosted Tools validation unavailable");
358
+ closeSocket(socket, 1011, "leased Hosted Tools validation unavailable");
359
+ return;
360
+ }
361
+ if (!Number.isSafeInteger(maximumLeaseExpiresAt)
362
+ || Number(maximumLeaseExpiresAt) <= this.#now()) {
363
+ this.revokeRoute(renewal.fixedRouteId, "leased Hosted Tools validation failed");
364
+ return;
365
+ }
366
+ const renewedExpiry = Number(maximumLeaseExpiresAt);
367
+ const renewed = this.#activeAttachment(socket);
368
+ if (renewed.routeId !== attachment.routeId
369
+ || renewed.leaseId !== attachment.leaseId
370
+ || renewed.generation !== attachment.generation
371
+ || renewed.fixedRouteId !== renewal.fixedRouteId
372
+ || renewed.expectedAttachmentId !== renewal.expectedAttachmentId
373
+ || renewed.renewalToken !== renewal.renewalToken) {
374
+ throw new HostedToolsProtocolError("stale_socket", "socket changed while its leased attachment was being validated");
375
+ }
376
+ attachment = { ...renewed, maximumLeaseExpiresAt: renewedExpiry };
377
+ this.context.writeAttachment(socket, attachment);
378
+ }
379
+ const expiresAt = Math.min(this.#now() + HOSTED_TOOLS_LEASE_MS, attachment.maximumLeaseExpiresAt ?? Number.MAX_SAFE_INTEGER);
380
+ const state = this.#persistence.state(attachment.routeId);
381
+ this.#persistence.replaceHost({ ...state, lease_expires_at: expiresAt });
382
+ this.#send(socket, {
383
+ type: "pong",
384
+ nonce: frame.nonce,
385
+ });
386
+ }
387
+ async #publishCatalog(socket, frame) {
388
+ const initial = this.#attachment(socket);
389
+ if (!initial || initial.leaseId || initial.generation !== undefined || initial.active) {
390
+ throw new HostedToolsProtocolError("catalog_immutable", "one immutable catalog is allowed per socket");
391
+ }
392
+ if (this.#nextCandidateGeneration >= Number.MAX_SAFE_INTEGER) {
393
+ throw new HostedToolsProtocolError("generation_exhausted", "Hosted Tools generation is exhausted");
394
+ }
395
+ const routeId = initial.fixedRouteId ?? scopedRouteId(initial.connectGrantId, frame.attachment_id);
396
+ let maximumLeaseExpiresAt = initial.maximumLeaseExpiresAt;
397
+ if (initial.renewalToken !== undefined) {
398
+ const renewal = {
399
+ expectedAttachmentId: initial.expectedAttachmentId,
400
+ fixedRouteId: routeId,
401
+ renewalToken: initial.renewalToken,
402
+ };
403
+ try {
404
+ const renewed = await this.#renewLeasedAttachment?.(renewal);
405
+ if (!Number.isSafeInteger(renewed) || Number(renewed) <= this.#now()) {
406
+ this.revokeRoute(routeId, "leased Hosted Tools validation failed before admission");
407
+ throw new HostedToolsProtocolError("route_revoked", "leased Hosted Tools validation failed before admission");
408
+ }
409
+ maximumLeaseExpiresAt = Number(renewed);
410
+ }
411
+ catch (error) {
412
+ if (error instanceof HostedToolsProtocolError)
413
+ throw error;
414
+ throw new HostedToolsProtocolError("lease_validation_unavailable", "leased Hosted Tools validation was unavailable before admission");
415
+ }
416
+ }
417
+ let state = this.#persistence.state(routeId) ?? emptyState(routeId);
418
+ if (state.lease_id === null && state.lease_expires_at === REVOKED_ROUTE_LEASE_EXPIRES_AT) {
419
+ throw new HostedToolsProtocolError("route_revoked", "tool attachment route was revoked before admission");
420
+ }
421
+ if (state.lease_id && state.lease_expires_at <= this.#now()) {
422
+ const expiredSocket = this.#socketForState(state);
423
+ if (expiredSocket)
424
+ this.#fence(expiredSocket, "Hosted Tools lease expired");
425
+ else
426
+ this.#retireState(state, "Hosted Tools lease expired");
427
+ state = this.#persistence.state(routeId) ?? emptyState(routeId);
428
+ }
429
+ const activeSocket = this.#socketForState(state);
430
+ const activeGrantId = activeSocket === undefined
431
+ ? undefined
432
+ : this.#activeConnectGrantId(state);
433
+ if (activeSocket !== undefined && activeGrantId !== initial.connectGrantId) {
434
+ throw new HostedToolsProtocolError("grant_conflict", "another Connect grant already owns this agent's tool host");
435
+ }
436
+ const generation = ++this.#nextCandidateGeneration;
437
+ const leaseId = this.#randomUUID();
438
+ const expiresAt = Math.min(this.#now() + HOSTED_TOOLS_LEASE_MS, maximumLeaseExpiresAt ?? Number.MAX_SAFE_INTEGER);
439
+ const candidate = {
440
+ ...initial,
441
+ ...(maximumLeaseExpiresAt === undefined ? {} : { maximumLeaseExpiresAt }),
442
+ routeId,
443
+ leaseId,
444
+ generation,
445
+ };
446
+ this.context.writeAttachment(socket, candidate);
447
+ const catalogJson = JSON.stringify(frame.tools);
448
+ const machine = frame.machines?.[0];
449
+ const candidateEntries = frame.tools
450
+ .filter((entry) => !reservedMachineEntry(entry, machine))
451
+ .map((entry) => exposedEntry(entry, machine));
452
+ const candidateDefinitions = candidateEntries.map((entry) => Object.freeze({
453
+ ...entry,
454
+ definition: Object.freeze({
455
+ ...entry.definition,
456
+ defer_loading: true,
457
+ }),
458
+ }));
459
+ try {
460
+ if (initial.connectGrantId !== undefined && (frame.machines?.length ?? 0) > 0) {
461
+ throw new Error("Connect tool hosts cannot publish account machine metadata");
462
+ }
463
+ if ((frame.machines?.length ?? 0) > 0
464
+ && (frame.machines?.length !== 1 || frame.attachment_id !== frame.machines[0]?.id)) {
465
+ throw new Error("an account machine route requires one machine whose id equals attachment_id");
466
+ }
467
+ if (initial.expectedAttachmentId !== undefined
468
+ && (frame.attachment_id !== initial.expectedAttachmentId
469
+ || frame.machines?.length !== 1
470
+ || frame.machines[0]?.id !== initial.expectedAttachmentId)) {
471
+ throw new Error("leased tool attachment must publish its exact assigned machine ID");
472
+ }
473
+ if (initial.expectedAttachmentId !== undefined) {
474
+ const extra = frame.tools.find((entry) => !MACHINE_TOOL_NAMES.has(entry.definition.name));
475
+ if (extra !== undefined) {
476
+ throw new Error("leased tool attachments may publish only canonical machine primitives");
477
+ }
478
+ }
479
+ if (machine !== undefined)
480
+ validateMachineToolContracts(frame.tools);
481
+ if (initial.allowedMcpIds !== undefined) {
482
+ if (!isConnectGrantId(initial.connectGrantId)) {
483
+ throw new Error("Connect tool host is missing its exact grant binding");
484
+ }
485
+ const allowed = new Set(initial.allowedMcpIds);
486
+ const forbiddenMcp = frame.tools.find((entry) => {
487
+ const match = /^mcp:([A-Za-z0-9_-]{43})$/.exec(entry.provider);
488
+ return entry.provider.startsWith("mcp:")
489
+ && (match === null || !allowed.has(match[1]));
490
+ });
491
+ if (forbiddenMcp) {
492
+ throw new Error(`tool ${forbiddenMcp.provider}:${forbiddenMcp.remote_name} is not authorized by the Connect grant`);
493
+ }
494
+ const appTools = frame.tools.filter((entry) => !entry.provider.startsWith("mcp:"));
495
+ const candidateDigest = appTools.length === 0
496
+ ? undefined
497
+ : await hostedToolCatalogDigest(appTools);
498
+ if (candidateDigest !== initial.appToolCatalogDigest) {
499
+ throw new Error("the app-local tool catalog does not match the signed Connect grant");
500
+ }
501
+ }
502
+ const otherBindings = this.#publicCatalogBindings(routeId, true);
503
+ const exposedNames = new Set(otherBindings.map((binding) => binding.entry.definition.name));
504
+ const duplicateTool = candidateEntries.find((entry) => exposedNames.has(entry.definition.name));
505
+ if (duplicateTool) {
506
+ throw new Error(`tool name ${duplicateTool.definition.name} is already exposed by another attachment`);
507
+ }
508
+ if (initial.connectGrantId === undefined) {
509
+ const machineIds = new Set(this.#machineIds(routeId));
510
+ const duplicateMachine = frame.machines?.find((machine) => machineIds.has(machine.id));
511
+ if (duplicateMachine) {
512
+ throw new Error(`machine ID ${duplicateMachine.id} is already published by another attachment`);
513
+ }
514
+ }
515
+ const validator = this.#catalogValidator;
516
+ const aggregateDefinitions = [
517
+ ...otherBindings.map((binding) => Object.freeze({
518
+ ...binding.entry,
519
+ definition: Object.freeze({
520
+ ...binding.entry.definition,
521
+ defer_loading: true,
522
+ }),
523
+ })),
524
+ ...candidateDefinitions,
525
+ ].sort((left, right) => left.definition.name.localeCompare(right.definition.name));
526
+ if (validator !== undefined && validator(aggregateDefinitions) !== true) {
527
+ throw new Error("ToolRouter rejected the candidate catalog");
528
+ }
529
+ }
530
+ catch (error) {
531
+ throw new HostedToolsProtocolError("catalog_contract_mismatch", `candidate catalog is incompatible with the managed tool route: ${errorMessage(error)}`);
532
+ }
533
+ const now = this.#now();
534
+ const replaced = state.lease_id ? state : undefined;
535
+ // A failed ready send must leave the previous attachment routable.
536
+ this.#send(socket, { type: "ready" });
537
+ this.#persistence.transaction(() => {
538
+ if (state.lease_id) {
539
+ this.#persistence.markGenerationAmbiguous(state.lease_id, state.generation, JSON.stringify(hostedToolsAmbiguous("Hosted Tools call became ambiguous when its host was replaced")), now);
540
+ }
541
+ this.#persistence.replaceHost({
542
+ route_id: routeId,
543
+ generation,
544
+ host_id: candidate.sessionId,
545
+ lease_id: leaseId,
546
+ lease_expires_at: expiresAt,
547
+ catalog_json: catalogJson,
548
+ });
549
+ });
550
+ if (replaced?.lease_id) {
551
+ const outcome = hostedToolsAmbiguous("Hosted Tools call became ambiguous when its host was replaced");
552
+ this.#resolveGeneration(replaced.lease_id, replaced.generation, outcome);
553
+ for (const existing of this.context.sockets()) {
554
+ if (existing === socket)
555
+ continue;
556
+ const old = this.#attachment(existing);
557
+ if (!old?.active || old.leaseId !== replaced.lease_id || old.generation !== replaced.generation)
558
+ continue;
559
+ closeSocket(existing, 1008, "Hosted Tools attachment replaced");
560
+ }
561
+ }
562
+ this.context.writeAttachment(socket, {
563
+ ...candidate,
564
+ active: true,
565
+ ...(frame.machines === undefined ? {} : { machines: frame.machines }),
566
+ });
567
+ this.#notifyCatalogChanged();
568
+ }
569
+ #drain(socket) {
570
+ const attachment = this.#activeAttachment(socket);
571
+ if (attachment.draining) {
572
+ throw new HostedToolsProtocolError("already_draining", "socket is already draining");
573
+ }
574
+ const state = this.#persistence.state(attachment.routeId);
575
+ // Visibility is removed before the peer is told that draining began.
576
+ this.#persistence.clearCatalog(state.lease_id, state.generation);
577
+ this.context.writeAttachment(socket, { ...attachment, draining: true });
578
+ this.#notifyCatalogChanged();
579
+ this.#send(socket, { type: "draining" });
580
+ }
581
+ #completeResult(socket, frame) {
582
+ const attachment = this.#activeAttachment(socket);
583
+ const row = this.#persistence.call(frame.call_id);
584
+ const stored = JSON.stringify(frame.outcome);
585
+ if (!row
586
+ || row.lease_id !== attachment.leaseId
587
+ || row.generation !== attachment.generation) {
588
+ throw new HostedToolsProtocolError("unknown_call", "result does not match an admitted pinned call");
589
+ }
590
+ if (row.state === "ambiguous") {
591
+ const receiptJson = JSON.stringify({ type: "result", outcome: frame.outcome });
592
+ const recorded = this.#persistence.recordLateReceipt(row.call_id, receiptJson, this.#now());
593
+ if (!recorded || recorded.receipt_json !== receiptJson) {
594
+ throw new HostedToolsProtocolError("result_conflict", "late terminal receipt conflicts with retained proof");
595
+ }
596
+ this.#ackResult(socket, frame);
597
+ return;
598
+ }
599
+ if (row.state !== "dispatched") {
600
+ if (row.result_json === stored && row.state === outcomeState(frame.outcome)) {
601
+ this.#ackResult(socket, frame);
602
+ return;
603
+ }
604
+ throw new HostedToolsProtocolError("result_conflict", "terminal call result cannot be changed");
605
+ }
606
+ if (this.#now() >= row.deadline_at) {
607
+ this.#finishAmbiguous(row, "Hosted Tools call result arrived after its durable deadline");
608
+ const receiptJson = JSON.stringify({ type: "result", outcome: frame.outcome });
609
+ const recorded = this.#persistence.recordLateReceipt(row.call_id, receiptJson, this.#now());
610
+ if (!recorded || recorded.receipt_json !== receiptJson) {
611
+ throw new HostedToolsProtocolError("result_conflict", "late terminal receipt conflicts with retained proof");
612
+ }
613
+ this.#ackResult(socket, frame);
614
+ return;
615
+ }
616
+ if (frame.outcome.status === "completed"
617
+ && encoder.encode(JSON.stringify(frame.outcome.output)).byteLength > row.output_byte_budget) {
618
+ throw new HostedToolsProtocolError("output_budget_exceeded", "completed output exceeds the byte budget pinned to the call");
619
+ }
620
+ const completed = this.#persistence.transitionCall(row.call_id, ["dispatched"], outcomeState(frame.outcome), stored, this.#now());
621
+ if (!completed || completed.result_json !== stored) {
622
+ throw new HostedToolsProtocolError("result_conflict", "call result lost durable ownership");
623
+ }
624
+ const pending = this.#takePending(row.call_id);
625
+ this.#pruneReceipts();
626
+ this.#ackResult(socket, frame);
627
+ pending?.resolve(frame.outcome);
628
+ }
629
+ #ackResult(socket, frame) {
630
+ try {
631
+ this.#send(socket, {
632
+ type: "ack",
633
+ call_id: frame.call_id,
634
+ });
635
+ }
636
+ catch {
637
+ this.#retire(socket, "result acknowledgement delivery failed");
638
+ closeSocket(socket, 1011, "Hosted Tools result acknowledgement failed");
639
+ }
640
+ }
641
+ #definitions() {
642
+ return this.#publicCatalogBindings().map((binding) => binding.entry);
643
+ }
644
+ #resolve(name) {
645
+ const binding = this.#publicCatalogBindings()
646
+ .find((candidate) => candidate.entry.definition.name === name);
647
+ if (!binding)
648
+ return undefined;
649
+ return this.#preparedTool(binding);
650
+ }
651
+ #preparedTool(binding) {
652
+ return Object.freeze({
653
+ routeToken: JSON.stringify([
654
+ binding.routeId,
655
+ binding.generation,
656
+ binding.leaseId,
657
+ binding.wireName,
658
+ ]),
659
+ ...(binding.connectGrantId === undefined ? {} : { connectGrantId: binding.connectGrantId }),
660
+ ...(binding.appToolCatalogDigest === undefined
661
+ ? {}
662
+ : { appToolCatalogDigest: binding.appToolCatalogDigest }),
663
+ canonicalName: binding.wireName,
664
+ ...(binding.machine === undefined ? {} : { machine: binding.machine }),
665
+ entry: binding.entry,
666
+ invoke: (request) => this.#invoke(binding, request),
667
+ });
668
+ }
669
+ #codeTool(name, prepared) {
670
+ return Object.freeze({
671
+ name,
672
+ parallelSafe: prepared.entry.parallel_safe,
673
+ routeToken: prepared.routeToken,
674
+ provider: prepared.entry.provider,
675
+ remoteName: prepared.entry.remote_name,
676
+ summary: prepared.entry.summary,
677
+ timeoutMs: prepared.entry.timeout_ms,
678
+ handler: async (input, context) => {
679
+ if (!this.#entryAllowed(prepared.entry, prepared.connectGrantId, prepared.appToolCatalogDigest, context)) {
680
+ return toolResult("Hosted tool is outside the active grant", {
681
+ status: "unavailable",
682
+ message: "Hosted tool is outside the active grant",
683
+ }, false, null);
684
+ }
685
+ const outcome = await prepared.invoke({
686
+ sessionId: context.sessionId,
687
+ callId: context.callId,
688
+ model: context.model ?? "unknown",
689
+ input: input,
690
+ outputTokenBudget: 10_000,
691
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
692
+ });
693
+ if (outcome.status === "completed") {
694
+ return wireToolResult(outcome.output, prepared.canonicalName, prepared.machine);
695
+ }
696
+ const result = toolResult(outcome.message, outcome, false, null);
697
+ return outcome[HOSTED_TOOLS_PRE_ADMISSION_UNAVAILABLE] === true
698
+ ? Object.freeze({
699
+ ...result,
700
+ [HOSTED_TOOLS_PRE_ADMISSION_UNAVAILABLE]: true,
701
+ })
702
+ : result;
703
+ },
704
+ });
705
+ }
706
+ #invoke(binding, request) {
707
+ const retained = this.#persistence.callBySource(request.sessionId, request.callId);
708
+ const leaseId = binding.leaseId;
709
+ const now = this.#now();
710
+ const deadlineAt = request.deadlineAt === undefined && retained
711
+ ? retained.deadline_at
712
+ : Math.min(request.deadlineAt ?? Number.MAX_SAFE_INTEGER, now + Math.min(binding.entry.timeout_ms, HOSTED_TOOL_CALL_TIMEOUT_MS));
713
+ const outputByteBudget = request.outputByteBudget ?? MAX_HOSTED_TOOL_OUTPUT_BYTES;
714
+ const transportCallId = retained?.call_id ?? this.#randomUUID();
715
+ const hostId = retained?.host_id ?? binding.hostId;
716
+ const pinnedLeaseId = retained?.lease_id ?? binding.leaseId;
717
+ const generation = retained?.generation ?? binding.generation;
718
+ let call;
719
+ try {
720
+ call = parseHostedToolsManagedFrame(JSON.stringify({
721
+ type: "call",
722
+ session_id: request.sessionId,
723
+ call_id: transportCallId,
724
+ model: request.model,
725
+ name: binding.wireName,
726
+ input: request.input,
727
+ output_token_budget: request.outputTokenBudget,
728
+ output_byte_budget: outputByteBudget,
729
+ deadline_at: deadlineAt,
730
+ }));
731
+ }
732
+ catch (error) {
733
+ return Promise.resolve(hostedToolsUnavailable(`Hosted Tools call was invalid before dispatch: ${errorMessage(error)}`));
734
+ }
735
+ const inputJson = JSON.stringify(call.input);
736
+ const proposed = {
737
+ call_id: call.call_id,
738
+ session_id: call.session_id,
739
+ source_call_id: request.callId,
740
+ host_id: hostId,
741
+ lease_id: pinnedLeaseId,
742
+ generation,
743
+ model: call.model,
744
+ name: call.name,
745
+ input_json: inputJson,
746
+ output_token_budget: call.output_token_budget,
747
+ output_byte_budget: call.output_byte_budget,
748
+ deadline_at: call.deadline_at,
749
+ cancel_requested: 0,
750
+ state: "admitted",
751
+ result_json: null,
752
+ receipt_json: null,
753
+ };
754
+ if (retained)
755
+ return this.#repeatedCall(retained, proposed);
756
+ const existing = this.#persistence.call(call.call_id);
757
+ if (existing)
758
+ return this.#repeatedCall(existing, proposed);
759
+ if (!this.#attachmentIsPresent(binding, now)) {
760
+ return Promise.resolve(preAdmissionUnavailable("Hosted Tools attachment was absent before durable admission"));
761
+ }
762
+ if (this.#persistence.generationCallCount(leaseId, binding.generation)
763
+ >= this.#maxCallsPerGeneration) {
764
+ const state = this.#persistence.state(binding.routeId);
765
+ const socket = state?.lease_id === leaseId && state.generation === binding.generation
766
+ ? this.#socketForState(state)
767
+ : undefined;
768
+ if (socket)
769
+ this.#fence(socket, "Hosted Tools generation exhausted its durable call ledger");
770
+ else if (state?.lease_id === leaseId && state.generation === binding.generation) {
771
+ this.#retireState(state, "Hosted Tools generation exhausted its durable call ledger");
772
+ }
773
+ return Promise.resolve(hostedToolsUnavailable("Hosted Tools generation reached its durable call limit"));
774
+ }
775
+ try {
776
+ this.#persistence.insertCall(proposed, now);
777
+ }
778
+ catch {
779
+ const recovered = this.#persistence.callBySource(request.sessionId, request.callId)
780
+ ?? this.#persistence.call(call.call_id);
781
+ if (recovered)
782
+ return this.#repeatedCall(recovered, proposed);
783
+ return Promise.resolve(hostedToolsAmbiguous("Hosted Tools admission may have persisted; replay is unsafe"));
784
+ }
785
+ if (request.signal?.aborted) {
786
+ return Promise.resolve(this.#finishBeforeDispatch(proposed, "cancelled", {
787
+ status: "cancelled",
788
+ message: "Hosted Tools call was cancelled before dispatch",
789
+ }));
790
+ }
791
+ const current = this.#persistence.state(binding.routeId);
792
+ const dispatchNow = this.#now();
793
+ const socket = this.#routingSocketForState(current);
794
+ if (!socket
795
+ || !current
796
+ || current.host_id !== binding.hostId
797
+ || current.lease_id !== leaseId
798
+ || current.generation !== binding.generation
799
+ || current.lease_expires_at <= dispatchNow
800
+ || deadlineAt <= dispatchNow) {
801
+ return Promise.resolve(this.#finishBeforeDispatch(proposed, "unavailable", hostedToolsUnavailable("Hosted Tools binding became unavailable before dispatch")));
802
+ }
803
+ if (this.#persistence.activeCallCount(leaseId, binding.generation) > this.#maxInFlight) {
804
+ return Promise.resolve(this.#finishBeforeDispatch(proposed, "unavailable", hostedToolsUnavailable("Hosted Tools host is at its bounded in-flight limit")));
805
+ }
806
+ const dispatched = this.#persistence.transitionCall(call.call_id, ["admitted"], "dispatched", "", dispatchNow);
807
+ if (!dispatched || dispatched.state !== "dispatched") {
808
+ return Promise.resolve(hostedToolsAmbiguous("Hosted Tools call lost durable dispatch ownership"));
809
+ }
810
+ let resolve;
811
+ const promise = new Promise((completed) => { resolve = completed; });
812
+ const pending = {
813
+ leaseId,
814
+ generation: binding.generation,
815
+ deadlineAt,
816
+ promise,
817
+ resolve,
818
+ };
819
+ this.#pending.set(call.call_id, pending);
820
+ if (request.signal) {
821
+ const cancel = () => { this.cancel(call.call_id); };
822
+ request.signal.addEventListener("abort", cancel, { once: true });
823
+ pending.removeAbort = () => request.signal?.removeEventListener("abort", cancel);
824
+ }
825
+ this.#armExpiry(call.call_id, pending, Math.min(current.lease_expires_at, deadlineAt));
826
+ try {
827
+ this.#send(socket, call);
828
+ }
829
+ catch {
830
+ this.#retire(socket, "call delivery failed");
831
+ closeSocket(socket, 1011, "Hosted Tools call delivery failed");
832
+ }
833
+ return promise;
834
+ }
835
+ #attachmentIsPresent(binding, now) {
836
+ const current = this.#persistence.state(binding.routeId);
837
+ return current !== undefined
838
+ && current.host_id === binding.hostId
839
+ && current.lease_id === binding.leaseId
840
+ && current.generation === binding.generation
841
+ && current.lease_expires_at > now
842
+ && this.#routingSocketForState(current) !== undefined;
843
+ }
844
+ #repeatedCall(existing, proposed) {
845
+ if (!sameImmutableCall(existing, proposed)) {
846
+ const state = this.#stateForLease(existing.lease_id, existing.generation);
847
+ const socket = this.#socketForState(state);
848
+ if (socket)
849
+ this.#fence(socket, "call ID was reused with different immutable fields");
850
+ return Promise.resolve(hostedToolsAmbiguous("Hosted Tools call ID conflicts with retained durable state"));
851
+ }
852
+ if (existing.result_json)
853
+ return Promise.resolve(JSON.parse(existing.result_json));
854
+ const pending = this.#pending.get(existing.call_id);
855
+ if (existing.state === "dispatched" && pending)
856
+ return pending.promise;
857
+ return Promise.resolve(existing.state === "admitted"
858
+ ? hostedToolsUnavailable("Hosted Tools call was admitted but never dispatched")
859
+ : hostedToolsAmbiguous("Hosted Tools call has no retained terminal receipt"));
860
+ }
861
+ #finishBeforeDispatch(row, state, outcome) {
862
+ this.#persistence.transitionCall(row.call_id, ["admitted"], state, JSON.stringify(outcome), this.#now());
863
+ this.#pruneReceipts();
864
+ return outcome;
865
+ }
866
+ #armExpiry(callId, pending, at) {
867
+ if (pending.timeout !== undefined)
868
+ clearTimeout(pending.timeout);
869
+ pending.timeout = setTimeout(() => {
870
+ const current = this.#pending.get(callId);
871
+ if (current !== pending)
872
+ return;
873
+ const state = this.#stateForLease(pending.leaseId, pending.generation);
874
+ const now = this.#now();
875
+ if (state?.lease_id === pending.leaseId
876
+ && state.generation === pending.generation
877
+ && state.lease_expires_at > now
878
+ && pending.deadlineAt > now) {
879
+ this.#armExpiry(callId, pending, Math.min(state.lease_expires_at, pending.deadlineAt));
880
+ return;
881
+ }
882
+ if (state?.lease_id === pending.leaseId
883
+ && state.generation === pending.generation
884
+ && state.lease_expires_at <= now) {
885
+ const socket = this.#socketForState(state);
886
+ if (socket)
887
+ this.#fence(socket, "Hosted Tools lease expired during a call");
888
+ else
889
+ this.#retireState(state, "Hosted Tools lease expired during a call");
890
+ return;
891
+ }
892
+ const row = this.#persistence.call(callId);
893
+ if (row) {
894
+ this.cancel(callId);
895
+ this.#finishAmbiguous(row, "Hosted Tools call deadline expired after dispatch");
896
+ }
897
+ }, Math.max(1, at - this.#now()));
898
+ }
899
+ #finishAmbiguous(row, message) {
900
+ const outcome = hostedToolsAmbiguous(message);
901
+ this.#persistence.transitionCall(row.call_id, ["dispatched"], "ambiguous", JSON.stringify(outcome), this.#now());
902
+ this.#pruneReceipts();
903
+ this.#takePending(row.call_id)?.resolve(outcome);
904
+ }
905
+ #retire(socket, reason) {
906
+ const attachment = this.#attachment(socket);
907
+ if (!attachment?.leaseId || attachment.generation === undefined)
908
+ return;
909
+ const state = attachment.routeId === undefined
910
+ ? undefined
911
+ : this.#persistence.state(attachment.routeId);
912
+ if (state)
913
+ this.#retireState(state, reason, attachment.leaseId, attachment.generation);
914
+ }
915
+ #retireState(state, reason, leaseId = state.lease_id ?? undefined, generation = state.generation) {
916
+ if (!leaseId)
917
+ return;
918
+ const outcome = hostedToolsAmbiguous(`Hosted Tools outcome is ambiguous after transport loss: ${reason}`);
919
+ this.#persistence.transaction(() => {
920
+ this.#persistence.markGenerationAmbiguous(leaseId, generation, JSON.stringify(outcome), this.#now());
921
+ this.#persistence.clearHost(leaseId, generation);
922
+ });
923
+ this.#pruneReceipts();
924
+ this.#resolveGeneration(leaseId, generation, outcome);
925
+ this.#notifyCatalogChanged();
926
+ }
927
+ #resolveGeneration(leaseId, generation, outcome) {
928
+ for (const [callId, pending] of this.#pending) {
929
+ if (pending.leaseId !== leaseId || pending.generation !== generation)
930
+ continue;
931
+ this.#takePending(callId)?.resolve(outcome);
932
+ }
933
+ }
934
+ #pruneReceipts() {
935
+ this.#persistence.pruneReceipts(MAX_RETAINED_RECEIPTS);
936
+ }
937
+ #takePending(callId) {
938
+ const pending = this.#pending.get(callId);
939
+ if (!pending)
940
+ return undefined;
941
+ if (pending.timeout !== undefined)
942
+ clearTimeout(pending.timeout);
943
+ pending.removeAbort?.();
944
+ this.#pending.delete(callId);
945
+ return pending;
946
+ }
947
+ #fence(socket, reason, code = 1008) {
948
+ const attachment = this.#attachment(socket);
949
+ if (attachment?.leaseId && attachment.generation !== undefined) {
950
+ this.#retire(socket, reason);
951
+ }
952
+ closeSocket(socket, code, boundedReason(reason));
953
+ }
954
+ #socketForState(state) {
955
+ if (!state)
956
+ return undefined;
957
+ if (!state.host_id || !state.lease_id)
958
+ return undefined;
959
+ return this.context.sockets().find((socket) => {
960
+ const attachment = this.#attachment(socket);
961
+ return socket.readyState === OPEN
962
+ && attachment?.leaseId === state.lease_id
963
+ && attachment.generation === state.generation
964
+ && attachment.routeId === state.route_id
965
+ && attachment.active === true;
966
+ });
967
+ }
968
+ #routingSocketForState(state) {
969
+ if (!state)
970
+ return undefined;
971
+ if (!state.catalog_json)
972
+ return undefined;
973
+ const socket = this.#socketForState(state);
974
+ return socket && this.#attachment(socket)?.draining !== true ? socket : undefined;
975
+ }
976
+ #liveRoutingSocketForState(state) {
977
+ if (state.lease_id && state.lease_expires_at <= this.#now()) {
978
+ const socket = this.#socketForState(state);
979
+ if (socket)
980
+ this.#fence(socket, "Hosted Tools lease expired");
981
+ else
982
+ this.#retireState(state, "Hosted Tools lease expired");
983
+ return undefined;
984
+ }
985
+ return this.#routingSocketForState(state);
986
+ }
987
+ #activeConnectGrantId(state) {
988
+ const socket = this.#socketForState(state);
989
+ if (socket === undefined)
990
+ return undefined;
991
+ const attachment = this.#attachment(socket);
992
+ if (attachment?.connectGrantId === undefined)
993
+ return undefined;
994
+ return isConnectGrantId(attachment.connectGrantId)
995
+ ? attachment.connectGrantId
996
+ : INVALID_CONNECT_GRANT_ID;
997
+ }
998
+ #activeAppToolCatalogDigest(state) {
999
+ const socket = this.#socketForState(state);
1000
+ return socket === undefined ? undefined : this.#attachment(socket)?.appToolCatalogDigest;
1001
+ }
1002
+ #sortedStates() {
1003
+ return [...this.#persistence.states()].sort((left, right) => left.route_id.localeCompare(right.route_id));
1004
+ }
1005
+ #stateForLease(leaseId, generation) {
1006
+ return this.#persistence.states().find((state) => state.lease_id === leaseId
1007
+ && state.generation === generation);
1008
+ }
1009
+ #catalogBindings(excludeRouteId, includeAmbiguous = false) {
1010
+ const bindings = [];
1011
+ for (const state of this.#sortedStates()) {
1012
+ if (state.route_id === excludeRouteId || !state.host_id || !state.lease_id || !state.catalog_json)
1013
+ continue;
1014
+ const socket = this.#liveRoutingSocketForState(state);
1015
+ if (!socket)
1016
+ continue;
1017
+ const attachment = this.#attachment(socket);
1018
+ const connectGrantId = this.#activeConnectGrantId(state);
1019
+ const appToolCatalogDigest = this.#activeAppToolCatalogDigest(state);
1020
+ let entries;
1021
+ try {
1022
+ entries = JSON.parse(state.catalog_json);
1023
+ }
1024
+ catch {
1025
+ continue;
1026
+ }
1027
+ for (const entry of entries) {
1028
+ const machine = attachment?.machines?.[0];
1029
+ bindings.push(Object.freeze({
1030
+ routeId: state.route_id,
1031
+ hostId: state.host_id,
1032
+ leaseId: state.lease_id,
1033
+ generation: state.generation,
1034
+ wireName: entry.definition.name,
1035
+ ...(machine === undefined ? {} : { machine }),
1036
+ entry: exposedEntry(entry, machine),
1037
+ ...(connectGrantId === undefined ? {} : { connectGrantId }),
1038
+ ...(appToolCatalogDigest === undefined ? {} : { appToolCatalogDigest }),
1039
+ }));
1040
+ }
1041
+ }
1042
+ bindings.sort((left, right) => left.routeId.localeCompare(right.routeId)
1043
+ || left.entry.definition.name.localeCompare(right.entry.definition.name));
1044
+ if (includeAmbiguous)
1045
+ return bindings;
1046
+ const counts = new Map();
1047
+ for (const binding of bindings) {
1048
+ const name = binding.entry.definition.name;
1049
+ counts.set(name, (counts.get(name) ?? 0) + 1);
1050
+ }
1051
+ return bindings.filter((binding) => counts.get(binding.entry.definition.name) === 1);
1052
+ }
1053
+ #publicCatalogBindings(excludeRouteId, includeAmbiguous = false) {
1054
+ return this.#catalogBindings(excludeRouteId, includeAmbiguous)
1055
+ .filter((binding) => !reservedMachineBinding(binding));
1056
+ }
1057
+ #machineIds(excludeRouteId) {
1058
+ const ids = [];
1059
+ for (const state of this.#sortedStates()) {
1060
+ if (state.route_id === excludeRouteId)
1061
+ continue;
1062
+ const socket = this.#liveRoutingSocketForState(state);
1063
+ if (!socket)
1064
+ continue;
1065
+ const attachment = this.#attachment(socket);
1066
+ if (attachment?.connectGrantId !== undefined)
1067
+ continue;
1068
+ for (const machine of attachment?.machines ?? [])
1069
+ ids.push(machine.id);
1070
+ }
1071
+ return ids;
1072
+ }
1073
+ #attachment(socket) {
1074
+ const value = this.context.readAttachment(socket);
1075
+ return value?.kind === SOCKET_TAG ? value : undefined;
1076
+ }
1077
+ #send(socket, frame) {
1078
+ socket.send(JSON.stringify(frame));
1079
+ }
1080
+ #notifyCatalogChanged() {
1081
+ this.#onCatalogChanged?.(this.#definitions());
1082
+ }
1083
+ }
1084
+ function emptyState(routeId) {
1085
+ return {
1086
+ route_id: routeId,
1087
+ generation: 0,
1088
+ host_id: null,
1089
+ lease_id: null,
1090
+ lease_expires_at: 0,
1091
+ catalog_json: null,
1092
+ };
1093
+ }
1094
+ function scopedRouteId(connectGrantId, attachmentId) {
1095
+ return `${connectGrantId === undefined ? "user" : "connect"}:${attachmentId ?? LEGACY_ROUTE_ID}`;
1096
+ }
1097
+ function reservedMachineEntry(entry, machine) {
1098
+ return machine !== undefined && MACHINE_TOOL_NAMES.has(entry.definition.name);
1099
+ }
1100
+ function reservedMachineBinding(binding) {
1101
+ return binding.machine !== undefined && MACHINE_TOOL_NAMES.has(binding.wireName);
1102
+ }
1103
+ function validateMachineToolContracts(entries) {
1104
+ for (const entry of entries) {
1105
+ const name = entry.definition.name;
1106
+ if (!MACHINE_TOOL_NAMES.has(name))
1107
+ continue;
1108
+ if (entry.definition.type !== "function") {
1109
+ throw new Error(`machine tool ${name} must use its canonical function schema`);
1110
+ }
1111
+ switch (name) {
1112
+ case "exec_command":
1113
+ validateCanonicalObjectSchema(name, entry.definition.parameters, EXEC_COMMAND_PARAMETERS);
1114
+ validateCanonicalObjectSchema(`${name} output`, entry.definition.output_schema, EXECUTION_OUTPUT_SCHEMA);
1115
+ break;
1116
+ case "write_stdin":
1117
+ validateCanonicalObjectSchema(name, entry.definition.parameters, WRITE_STDIN_PARAMETERS);
1118
+ validateCanonicalObjectSchema(`${name} output`, entry.definition.output_schema, EXECUTION_OUTPUT_SCHEMA);
1119
+ break;
1120
+ case "preview":
1121
+ validateCanonicalObjectSchema(name, entry.definition.parameters, MACHINE_PREVIEW_PARAMETERS);
1122
+ validateCanonicalObjectSchema(`${name} output`, entry.definition.output_schema, PREVIEW_OUTPUT_SCHEMA);
1123
+ break;
1124
+ }
1125
+ }
1126
+ }
1127
+ function validateCanonicalObjectSchema(label, schema, canonical) {
1128
+ const properties = objectRecord(canonical.properties);
1129
+ const required = canonical.required;
1130
+ if (properties === undefined || !Array.isArray(required)) {
1131
+ throw new Error(`invalid canonical machine tool schema for ${label}`);
1132
+ }
1133
+ const propertyTypes = Object.fromEntries(Object.entries(properties).map(([name, property]) => {
1134
+ const type = objectRecord(property)?.type;
1135
+ if (typeof type !== "string")
1136
+ throw new Error(`invalid canonical type for ${label}.${name}`);
1137
+ return [name, type];
1138
+ }));
1139
+ validateObjectSchema(label, schema, required, propertyTypes);
1140
+ }
1141
+ function validateObjectSchema(label, schema, required, propertyTypes) {
1142
+ const properties = objectRecord(schema?.properties);
1143
+ const actualRequired = schema?.required;
1144
+ if (schema?.type !== "object"
1145
+ || schema?.additionalProperties !== false
1146
+ || properties === undefined
1147
+ || !Array.isArray(actualRequired)
1148
+ || actualRequired.some((value) => typeof value !== "string")
1149
+ || !sameStrings(actualRequired, required)
1150
+ || !sameStrings(Object.keys(properties), Object.keys(propertyTypes))) {
1151
+ throw new Error(`machine tool ${label} must use its canonical object schema`);
1152
+ }
1153
+ for (const [property, type] of Object.entries(propertyTypes)) {
1154
+ if (objectRecord(properties[property])?.type !== type) {
1155
+ throw new Error(`machine tool ${label}.${property} must use canonical type ${type}`);
1156
+ }
1157
+ }
1158
+ }
1159
+ function objectRecord(value) {
1160
+ return value !== null && typeof value === "object" && !Array.isArray(value)
1161
+ ? value
1162
+ : undefined;
1163
+ }
1164
+ function sameStrings(left, right) {
1165
+ return left.length === right.length && left.every((value) => right.includes(value));
1166
+ }
1167
+ function exposedEntry(entry, machine) {
1168
+ if (machine === undefined)
1169
+ return entry;
1170
+ const routeName = `user:${machine.id}:${entry.definition.name}`;
1171
+ const candidate = `user_${machine.id}_${entry.definition.name}`;
1172
+ const safeCandidate = candidate.replace(/[^A-Za-z0-9_-]/g, "_");
1173
+ const exposedName = candidate === safeCandidate && candidate.length <= 128
1174
+ ? candidate
1175
+ : `${safeCandidate.slice(0, 111)}_${stableHash(routeName)}`;
1176
+ const definition = {
1177
+ ...entry.definition,
1178
+ name: exposedName,
1179
+ description: `Routes to ${routeName}. ${entry.definition.description}`,
1180
+ };
1181
+ return Object.freeze({
1182
+ ...entry,
1183
+ definition: Object.freeze(definition),
1184
+ });
1185
+ }
1186
+ function stableHash(value) {
1187
+ let hash = 0xcbf29ce484222325n;
1188
+ for (const byte of encoder.encode(value)) {
1189
+ hash ^= BigInt(byte);
1190
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
1191
+ }
1192
+ return hash.toString(16).padStart(16, "0");
1193
+ }
1194
+ function isConnectGrantId(value) {
1195
+ return typeof value === "string" && /^0x[0-9a-f]{64}$/.test(value);
1196
+ }
1197
+ function wireToolResult(output, canonicalName, machine) {
1198
+ return toolResult(output.output, output.structured_result, output.success, toolExecutionMetadata(output.metadata, canonicalName, machine));
1199
+ }
1200
+ function toolExecutionMetadata(metadata, canonicalName, machine) {
1201
+ if (machine === undefined)
1202
+ return metadata;
1203
+ const execution = {
1204
+ machine_id: machine.id,
1205
+ machine_name: machine.name,
1206
+ tool_name: canonicalName,
1207
+ };
1208
+ if (metadata === null || metadata === undefined)
1209
+ return Object.freeze(execution);
1210
+ if (typeof metadata === "object" && !Array.isArray(metadata)) {
1211
+ return Object.freeze({ ...metadata, ...execution });
1212
+ }
1213
+ return Object.freeze({ ...execution, provider_metadata: metadata });
1214
+ }
1215
+ function toolResult(output, structuredResult, success, metadata) {
1216
+ return Object.freeze({
1217
+ [TOOL_RESULT]: true,
1218
+ metadata,
1219
+ output,
1220
+ structuredResult,
1221
+ success,
1222
+ value: structuredResult ?? output,
1223
+ });
1224
+ }
1225
+ function sameImmutableCall(left, right) {
1226
+ return left.call_id === right.call_id
1227
+ && left.session_id === right.session_id
1228
+ && left.source_call_id === right.source_call_id
1229
+ && left.host_id === right.host_id
1230
+ && left.lease_id === right.lease_id
1231
+ && left.generation === right.generation
1232
+ && left.model === right.model
1233
+ && left.name === right.name
1234
+ && left.input_json === right.input_json
1235
+ && left.output_token_budget === right.output_token_budget
1236
+ && left.output_byte_budget === right.output_byte_budget
1237
+ && left.deadline_at === right.deadline_at;
1238
+ }
1239
+ function outcomeState(outcome) {
1240
+ return outcome.status;
1241
+ }
1242
+ export function hostedToolsUnavailable(message) {
1243
+ return { status: "unavailable", message: boundedReason(message) };
1244
+ }
1245
+ function preAdmissionUnavailable(message) {
1246
+ return Object.freeze({
1247
+ ...hostedToolsUnavailable(message),
1248
+ [HOSTED_TOOLS_PRE_ADMISSION_UNAVAILABLE]: true,
1249
+ });
1250
+ }
1251
+ export function hostedToolsAmbiguous(message) {
1252
+ return { status: "ambiguous", message: boundedReason(message) };
1253
+ }
1254
+ function boundedReason(message) {
1255
+ if (encoder.encode(message).byteLength <= 2 * 1024)
1256
+ return message;
1257
+ return "Hosted Tools protocol failure exceeded the bounded reason limit";
1258
+ }
1259
+ function errorMessage(error) {
1260
+ return error instanceof Error ? error.message : String(error);
1261
+ }
1262
+ function closeSocket(socket, code, reason) {
1263
+ try {
1264
+ socket.close(code, websocketCloseReason(reason));
1265
+ }
1266
+ catch { /* The socket is already closed or never reached an open state. */ }
1267
+ }
1268
+ function websocketCloseReason(reason) {
1269
+ if (encoder.encode(reason).byteLength <= 123)
1270
+ return reason;
1271
+ let bounded = "";
1272
+ for (const scalar of reason) {
1273
+ if (encoder.encode(bounded + scalar).byteLength > 123)
1274
+ break;
1275
+ bounded += scalar;
1276
+ }
1277
+ return bounded;
1278
+ }