@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.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 (272) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +20 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir7 } from "node:fs/promises";
5
- import { resolve as resolve4, dirname as dirname2, join as join9 } from "node:path";
4
+ import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir8 } from "node:fs/promises";
5
+ import { resolve as resolve5, dirname as dirname4, join as join9 } from "node:path";
6
6
 
7
7
  // src/agent.ts
8
- import { readFile as readFile8, writeFile as writeFile5, mkdir as mkdir6, rename, unlink as unlink4, stat as stat7, realpath as realpath7 } from "node:fs/promises";
9
- import { join as join8, dirname, resolve as resolve3 } from "node:path";
10
- import { randomUUID as randomUUID6 } from "node:crypto";
8
+ import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir6, rename, unlink as unlink5, stat as stat7, realpath as realpath7 } from "node:fs/promises";
9
+ import { join as join8, dirname as dirname2, resolve as resolve3 } from "node:path";
10
+ import { randomUUID as randomUUID7 } from "node:crypto";
11
11
  import { spawn as spawn4, execFile as execFile2 } from "node:child_process";
12
12
  import { promisify as promisify2 } from "node:util";
13
13
 
14
14
  // src/protocol.ts
15
15
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
16
- var AGENT_VERSION = "0.1.1";
16
+ var AGENT_VERSION = "0.1.2";
17
17
  var AGENT_API_MAJOR = 1;
18
18
  var AgentError = class extends Error {
19
19
  constructor(code, status = 400) {
@@ -86,7 +86,1042 @@ var isMutating = (name) => CATALOG.find((x) => x.name === name)?.mutating ?? fal
86
86
  import { mkdirSync, openSync, readFileSync, writeFileSync, closeSync, unlinkSync, chmodSync, fstatSync, lstatSync } from "node:fs";
87
87
  import { join } from "node:path";
88
88
  import { createRequire } from "node:module";
89
- import { randomUUID } from "node:crypto";
89
+ import { randomUUID as randomUUID2 } from "node:crypto";
90
+
91
+ // src/secure-pairing.ts
92
+ import { randomInt, randomUUID, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
93
+
94
+ // ../modules/openmaus-protocol/dist/protocol.mjs
95
+ import { createCipheriv, createDecipheriv, createECDH, hkdfSync, randomBytes as randomBytes2 } from "node:crypto";
96
+ import { createHash as createHash2, createHmac, createPrivateKey, createPublicKey, sign, timingSafeEqual as timingSafeEqual2, verify } from "node:crypto";
97
+ import { Duplex } from "node:stream";
98
+ var B64U = /^[A-Za-z0-9_-]*$/;
99
+ var b64u = (bytes) => Buffer.from(bytes).toString("base64url");
100
+ function fromB64u(text) {
101
+ if (!B64U.test(text) || text.length % 4 === 1) return null;
102
+ const bytes = Buffer.from(text, "base64url");
103
+ return bytes.toString("base64url") === text ? bytes : null;
104
+ }
105
+ var sha256 = (bytes) => createHash2("sha256").update(bytes).digest();
106
+ var hmacSha256 = (key2, message) => createHmac("sha256", key2).update(message).digest();
107
+ var KEY_ID_BYTES = 16;
108
+ var PSK_LABEL = Buffer.from("omb-e2e psk v1", "utf8");
109
+ var ADMISSION_LABEL = Buffer.from("omb-relay admission v1", "utf8");
110
+ function credentialHashFromHex(hex) {
111
+ return /^[0-9a-f]{64}$/.test(hex) ? Buffer.from(hex, "hex") : null;
112
+ }
113
+ var keyId = (transportPublicKey) => sha256(transportPublicKey).subarray(0, KEY_ID_BYTES);
114
+ var psk = (credHash, transportKeyId) => hmacSha256(credHash, Buffer.concat([PSK_LABEL, transportKeyId]));
115
+ var RELAY_TOKEN_TTL_S = 12 * 60 * 60;
116
+ var PROTOCOL_NAME = "Noise_NKpsk0_P256_AESGCM_SHA256";
117
+ var PUBLIC_KEY_BYTES = 65;
118
+ var TAG_BYTES = 16;
119
+ var MAX_RECORD_BYTES = 65535;
120
+ var MAX_RECORD_PLAINTEXT = MAX_RECORD_BYTES - TAG_BYTES;
121
+ var MAX_HANDSHAKE_PAYLOAD = 512;
122
+ var MAX_HANDSHAKE_MESSAGE = PUBLIC_KEY_BYTES + MAX_HANDSHAKE_PAYLOAD + TAG_BYTES;
123
+ var CONN_BYTES = 16;
124
+ var MAX_APP_CHARS = 40;
125
+ var NONCE_LIMIT = 2 ** 32;
126
+ var HANDSHAKE_DEADLINE_MS = 1e4;
127
+ var TRANSPORT_DIRECT = 2;
128
+ var CLOSE = {
129
+ normal: 1e3,
130
+ goingAway: 1001,
131
+ protocol: 4e3,
132
+ rejected: 4001,
133
+ deadline: 4002
134
+ };
135
+ var NoiseError = class extends Error {
136
+ reason;
137
+ constructor(reason) {
138
+ super(`e2e: ${reason}`);
139
+ this.reason = reason;
140
+ }
141
+ };
142
+ function prologue(transport) {
143
+ return Buffer.concat([Buffer.from("omb-e2e/1", "utf8"), Buffer.from([0, transport])]);
144
+ }
145
+ var ecdh = (privateKey) => {
146
+ const curve = createECDH("prime256v1");
147
+ if (privateKey) curve.setPrivateKey(privateKey);
148
+ else curve.generateKeys();
149
+ return curve;
150
+ };
151
+ function keyPairFromPrivate(privateKey) {
152
+ if (privateKey.length !== 32) throw new NoiseError("bad_private_key");
153
+ return { privateKey: Buffer.from(privateKey), publicKey: ecdh(privateKey).getPublicKey(null, "uncompressed") };
154
+ }
155
+ function generateKeyPair() {
156
+ const curve = ecdh();
157
+ const raw = curve.getPrivateKey();
158
+ return { privateKey: Buffer.concat([Buffer.alloc(32 - raw.length), raw]), publicKey: curve.getPublicKey(null, "uncompressed") };
159
+ }
160
+ function dh(privateKey, publicKey) {
161
+ if (publicKey.length !== PUBLIC_KEY_BYTES || publicKey[0] !== 4) throw new NoiseError("bad_point");
162
+ try {
163
+ return ecdh(privateKey).computeSecret(publicKey);
164
+ } catch {
165
+ throw new NoiseError("bad_point");
166
+ }
167
+ }
168
+ var CHECK_SCALAR = Buffer.alloc(32, 1);
169
+ function isPublicKey(bytes) {
170
+ try {
171
+ dh(CHECK_SCALAR, bytes);
172
+ return true;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ function hkdf(chainingKey, ikm, outputs) {
178
+ const out = Buffer.from(hkdfSync("sha256", ikm, chainingKey, Buffer.alloc(0), 32 * outputs));
179
+ return Array.from({ length: outputs }, (_, index) => out.subarray(index * 32, index * 32 + 32));
180
+ }
181
+ function nonceBytes(n) {
182
+ const nonce = Buffer.alloc(12);
183
+ nonce.writeBigUInt64BE(BigInt(n), 4);
184
+ return nonce;
185
+ }
186
+ function aeadSeal(key2, n, ad, plaintext) {
187
+ const cipher = createCipheriv("aes-256-gcm", key2, nonceBytes(n), { authTagLength: TAG_BYTES });
188
+ cipher.setAAD(ad);
189
+ return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
190
+ }
191
+ function aeadOpen(key2, n, ad, ciphertext) {
192
+ if (ciphertext.length < TAG_BYTES) throw new NoiseError("short_ciphertext");
193
+ const decipher = createDecipheriv("aes-256-gcm", key2, nonceBytes(n), { authTagLength: TAG_BYTES });
194
+ decipher.setAAD(ad);
195
+ decipher.setAuthTag(ciphertext.subarray(ciphertext.length - TAG_BYTES));
196
+ try {
197
+ return Buffer.concat([decipher.update(ciphertext.subarray(0, ciphertext.length - TAG_BYTES)), decipher.final()]);
198
+ } catch {
199
+ throw new NoiseError("aead");
200
+ }
201
+ }
202
+ var CipherState = class {
203
+ #key;
204
+ #n;
205
+ /** `n` other than 0 is for tests of the counter limit. */
206
+ constructor(key2 = null, n = 0) {
207
+ this.#key = key2;
208
+ this.#n = n;
209
+ }
210
+ get hasKey() {
211
+ return this.#key !== null;
212
+ }
213
+ get nonce() {
214
+ return this.#n;
215
+ }
216
+ /** Test-only view of the key, for the vectors. */
217
+ get keyBytes() {
218
+ return this.#key;
219
+ }
220
+ encryptWithAd(ad, plaintext) {
221
+ if (!this.#key) return Buffer.from(plaintext);
222
+ if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
223
+ const out = aeadSeal(this.#key, this.#n, ad, plaintext);
224
+ this.#n += 1;
225
+ return out;
226
+ }
227
+ /** The counter moves only on success; any failure ends the session anyway. */
228
+ decryptWithAd(ad, ciphertext) {
229
+ if (!this.#key) return Buffer.from(ciphertext);
230
+ if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
231
+ const out = aeadOpen(this.#key, this.#n, ad, ciphertext);
232
+ this.#n += 1;
233
+ return out;
234
+ }
235
+ };
236
+ var SymmetricState = class _SymmetricState {
237
+ ck;
238
+ h;
239
+ cipher = new CipherState();
240
+ trace;
241
+ constructor(h, ck, trace) {
242
+ this.h = h;
243
+ this.ck = ck;
244
+ this.trace = trace;
245
+ }
246
+ static initialize(trace = null) {
247
+ const name = Buffer.from(PROTOCOL_NAME, "utf8");
248
+ const h = name.length <= 32 ? Buffer.concat([name, Buffer.alloc(32 - name.length)]) : sha256(name);
249
+ return new _SymmetricState(h, Buffer.from(h), trace);
250
+ }
251
+ clone() {
252
+ const copy = new _SymmetricState(Buffer.from(this.h), Buffer.from(this.ck), this.trace ? [...this.trace] : null);
253
+ copy.cipher = this.cipher;
254
+ return copy;
255
+ }
256
+ mark(step) {
257
+ this.trace?.push({ step, h: this.h.toString("hex"), ck: this.ck.toString("hex"), k: this.cipher.keyBytes?.toString("hex") ?? null });
258
+ }
259
+ mixHash(data) {
260
+ this.h = sha256(Buffer.concat([this.h, data]));
261
+ }
262
+ mixKey(ikm) {
263
+ const [ck, key2] = hkdf(this.ck, ikm, 2);
264
+ this.ck = ck;
265
+ this.cipher = new CipherState(key2);
266
+ }
267
+ mixKeyAndHash(ikm) {
268
+ const [ck, tempH, key2] = hkdf(this.ck, ikm, 3);
269
+ this.ck = ck;
270
+ this.mixHash(tempH);
271
+ this.cipher = new CipherState(key2);
272
+ }
273
+ encryptAndHash(plaintext) {
274
+ const out = this.cipher.encryptWithAd(this.h, plaintext);
275
+ this.mixHash(out);
276
+ return out;
277
+ }
278
+ decryptAndHash(ciphertext) {
279
+ const out = this.cipher.decryptWithAd(this.h, ciphertext);
280
+ this.mixHash(ciphertext);
281
+ return out;
282
+ }
283
+ /** k_pc then k_cp. */
284
+ split() {
285
+ const [first, second] = hkdf(this.ck, Buffer.alloc(0), 2);
286
+ return [new CipherState(first), new CipherState(second)];
287
+ }
288
+ };
289
+ function initialized(transport, responderStatic, trace) {
290
+ const state = SymmetricState.initialize(trace);
291
+ state.mark("init");
292
+ state.mixHash(prologue(transport));
293
+ state.mark("prologue");
294
+ state.mixHash(responderStatic);
295
+ state.mark("s");
296
+ return state;
297
+ }
298
+ var RecordCipher = class {
299
+ #state;
300
+ constructor(state) {
301
+ this.#state = state;
302
+ }
303
+ get nonce() {
304
+ return this.#state.nonce;
305
+ }
306
+ seal(plaintext) {
307
+ if (plaintext.length > MAX_RECORD_PLAINTEXT) throw new NoiseError("record_too_large");
308
+ return this.#state.encryptWithAd(Buffer.alloc(0), plaintext);
309
+ }
310
+ open(record) {
311
+ if (record.length > MAX_RECORD_BYTES) throw new NoiseError("record_too_large");
312
+ return this.#state.decryptWithAd(Buffer.alloc(0), record);
313
+ }
314
+ };
315
+ function sessionKeys(state, side) {
316
+ const [pc, cp] = state.split();
317
+ const kPc = pc.keyBytes;
318
+ const kCp = cp.keyBytes;
319
+ return side === "phone" ? { send: new RecordCipher(pc), receive: new RecordCipher(cp), handshakeHash: state.h, kPc, kCp } : { send: new RecordCipher(cp), receive: new RecordCipher(pc), handshakeHash: state.h, kPc, kCp };
320
+ }
321
+ function encodePayload1(conn, app) {
322
+ return Buffer.from(JSON.stringify({ v: 1, conn: b64u(conn), app }), "utf8");
323
+ }
324
+ function encodePayload2(conn) {
325
+ return Buffer.from(JSON.stringify({ v: 1, conn }), "utf8");
326
+ }
327
+ function jsonObject(bytes) {
328
+ try {
329
+ const value = JSON.parse(Buffer.from(bytes).toString("utf8"));
330
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
331
+ } catch {
332
+ return null;
333
+ }
334
+ }
335
+ var isConn = (value) => typeof value === "string" && fromB64u(value)?.length === CONN_BYTES;
336
+ function parsePayload1(bytes) {
337
+ if (bytes.length > MAX_HANDSHAKE_PAYLOAD) return null;
338
+ const value = jsonObject(bytes);
339
+ if (!value || value.v !== 1 || !isConn(value.conn)) return null;
340
+ if (value.app !== void 0 && (typeof value.app !== "string" || value.app.length > MAX_APP_CHARS)) return null;
341
+ return { v: 1, conn: value.conn, ...typeof value.app === "string" ? { app: value.app } : {} };
342
+ }
343
+ function checkPayload2(bytes, conn) {
344
+ const value = bytes.length <= MAX_HANDSHAKE_PAYLOAD ? jsonObject(bytes) : null;
345
+ if (!value || value.v !== 1) return "version";
346
+ return value.conn === conn ? "ok" : "conn";
347
+ }
348
+ function phoneHello(options, payload1, conn) {
349
+ if (!isPublicKey(options.remoteStatic)) throw new NoiseError("bad_point");
350
+ if (options.psk.length !== 32) throw new NoiseError("bad_psk");
351
+ if (payload1.length > MAX_HANDSHAKE_PAYLOAD) throw new NoiseError("payload_too_large");
352
+ const state = initialized(options.transport, options.remoteStatic, options.trace ?? null);
353
+ state.mixKeyAndHash(options.psk);
354
+ state.mark("msg1.psk");
355
+ const e = options.ephemeral ?? generateKeyPair();
356
+ state.mixHash(e.publicKey);
357
+ state.mixKey(e.publicKey);
358
+ state.mark("msg1.e");
359
+ state.mixKey(dh(e.privateKey, options.remoteStatic));
360
+ state.mark("msg1.es");
361
+ const message1 = Buffer.concat([e.publicKey, state.encryptAndHash(payload1)]);
362
+ state.mark("msg1.payload");
363
+ let done = false;
364
+ return {
365
+ message1,
366
+ finish(message2) {
367
+ if (done) throw new NoiseError("finished");
368
+ done = true;
369
+ if (message2.length < PUBLIC_KEY_BYTES + TAG_BYTES || message2.length > MAX_HANDSHAKE_MESSAGE) throw new NoiseError("message2_size");
370
+ const re = message2.subarray(0, PUBLIC_KEY_BYTES);
371
+ const ee = dh(e.privateKey, re);
372
+ state.mixHash(re);
373
+ state.mixKey(re);
374
+ state.mark("msg2.e");
375
+ state.mixKey(ee);
376
+ state.mark("msg2.ee");
377
+ const payload2 = state.decryptAndHash(message2.subarray(PUBLIC_KEY_BYTES));
378
+ state.mark("msg2.payload");
379
+ const check = checkPayload2(payload2, conn);
380
+ if (check !== "ok") throw new NoiseError(`payload2_${check}`);
381
+ return sessionKeys(state, "phone");
382
+ }
383
+ };
384
+ }
385
+ function computerAccept(options, message1) {
386
+ if (message1.length < PUBLIC_KEY_BYTES + TAG_BYTES || message1.length > MAX_HANDSHAKE_MESSAGE) return null;
387
+ const re = message1.subarray(0, PUBLIC_KEY_BYTES);
388
+ let es;
389
+ try {
390
+ es = dh(options.staticKey.privateKey, re);
391
+ } catch {
392
+ return null;
393
+ }
394
+ const base = initialized(options.transport, options.staticKey.publicKey, options.trace ?? null);
395
+ let match = null;
396
+ for (const candidate of options.candidates) {
397
+ if (candidate.psk.length !== 32) continue;
398
+ const state2 = base.clone();
399
+ state2.mixKeyAndHash(candidate.psk);
400
+ state2.mark("msg1.psk");
401
+ state2.mixHash(re);
402
+ state2.mixKey(re);
403
+ state2.mark("msg1.e");
404
+ state2.mixKey(es);
405
+ state2.mark("msg1.es");
406
+ try {
407
+ const payload = state2.decryptAndHash(message1.subarray(PUBLIC_KEY_BYTES));
408
+ state2.mark("msg1.payload");
409
+ match ??= { id: candidate.id, state: state2, payload };
410
+ } catch {
411
+ }
412
+ }
413
+ if (!match) return null;
414
+ const payload1 = parsePayload1(match.payload);
415
+ if (!payload1) return null;
416
+ const { state } = match;
417
+ const e = options.ephemeral ?? generateKeyPair();
418
+ state.mixHash(e.publicKey);
419
+ state.mixKey(e.publicKey);
420
+ state.mark("msg2.e");
421
+ state.mixKey(dh(e.privateKey, re));
422
+ state.mark("msg2.ee");
423
+ const payload2 = options.payload2 ? options.payload2(payload1) : encodePayload2(payload1.conn);
424
+ const message2 = Buffer.concat([e.publicKey, state.encryptAndHash(payload2)]);
425
+ state.mark("msg2.payload");
426
+ if (options.trace && state.trace) options.trace.splice(0, options.trace.length, ...state.trace);
427
+ return { id: match.id, payload1, message2, keys: sessionKeys(state, "computer") };
428
+ }
429
+ var newConn = () => randomBytes2(CONN_BYTES);
430
+ var VERSION = 0;
431
+ var TYPE = { data: 0, windowUpdate: 1, ping: 2, goAway: 3 };
432
+ var FLAG = { syn: 1, ack: 2, fin: 4, rst: 8 };
433
+ var KNOWN_FLAGS = 15;
434
+ var GO_AWAY = { normal: 0, protocol: 1, internal: 2, removed: 3 };
435
+ var HEADER_BYTES = 12;
436
+ var MAX_DATA_PAYLOAD = 65507;
437
+ var INITIAL_WINDOW = 256 * 1024;
438
+ var STREAM_WINDOW = 2 * 1024 * 1024;
439
+ var OPEN_WINDOW_DELTA = STREAM_WINDOW - INITIAL_WINDOW;
440
+ var WINDOW_UPDATE_AFTER = 1024 * 1024;
441
+ var MAX_STREAMS = 64;
442
+ var MAX_STREAM_ID = 2 ** 31 - 1;
443
+ var PING_INTERVAL_MS = 2e4;
444
+ var PING_TIMEOUT_MS = 1e4;
445
+ var YamuxError = class extends Error {
446
+ reason;
447
+ constructor(reason) {
448
+ super(`yamux: ${reason}`);
449
+ this.reason = reason;
450
+ }
451
+ };
452
+ function encodeFrame(frame) {
453
+ const payload = frame.type === TYPE.data ? frame.data ?? new Uint8Array(0) : new Uint8Array(0);
454
+ if (frame.type === TYPE.data && payload.length !== frame.length) throw new YamuxError("data_length");
455
+ if (payload.length > MAX_DATA_PAYLOAD) throw new YamuxError("data_too_large");
456
+ const out = Buffer.alloc(HEADER_BYTES + payload.length);
457
+ out.writeUInt8(VERSION, 0);
458
+ out.writeUInt8(frame.type, 1);
459
+ out.writeUInt16BE(frame.flags, 2);
460
+ out.writeUInt32BE(frame.streamId, 4);
461
+ out.writeUInt32BE(frame.length, 8);
462
+ out.set(payload, HEADER_BYTES);
463
+ return out;
464
+ }
465
+ function decodeFrame(bytes) {
466
+ if (bytes.length < HEADER_BYTES) throw new YamuxError("short_frame");
467
+ const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
468
+ if (view.readUInt8(0) !== VERSION) throw new YamuxError("version");
469
+ const type = view.readUInt8(1);
470
+ if (type > TYPE.goAway) throw new YamuxError("type");
471
+ const flags = view.readUInt16BE(2);
472
+ if ((flags & ~KNOWN_FLAGS) !== 0) throw new YamuxError("flags");
473
+ const streamId = view.readUInt32BE(4);
474
+ const length = view.readUInt32BE(8);
475
+ if (type === TYPE.data) {
476
+ if (length > MAX_DATA_PAYLOAD) throw new YamuxError("data_too_large");
477
+ if (bytes.length !== HEADER_BYTES + length) throw new YamuxError("data_length");
478
+ if (streamId === 0) throw new YamuxError("stream_zero");
479
+ return { type, flags, streamId, length, data: view.subarray(HEADER_BYTES) };
480
+ }
481
+ if (bytes.length !== HEADER_BYTES) throw new YamuxError("trailing_bytes");
482
+ if (type === TYPE.windowUpdate) {
483
+ if (streamId === 0) throw new YamuxError("stream_zero");
484
+ } else {
485
+ if (streamId !== 0) throw new YamuxError("session_frame_stream");
486
+ if (type === TYPE.ping && flags !== FLAG.syn && flags !== FLAG.ack) throw new YamuxError("ping_flags");
487
+ if (type === TYPE.goAway && flags !== 0) throw new YamuxError("goaway_flags");
488
+ }
489
+ return { type, flags, streamId, length };
490
+ }
491
+ var openFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.syn, streamId, length: OPEN_WINDOW_DELTA });
492
+ var acceptFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.ack, streamId, length: OPEN_WINDOW_DELTA });
493
+ var resetFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.rst, streamId, length: 0 });
494
+ var finFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.fin, streamId, length: 0 });
495
+ var windowFrame = (streamId, delta) => ({ type: TYPE.windowUpdate, flags: 0, streamId, length: delta });
496
+ var dataFrame = (streamId, data, flags = 0) => ({ type: TYPE.data, flags, streamId, length: data.length, data });
497
+ var pingFrame = (value, flags = FLAG.syn) => ({ type: TYPE.ping, flags, streamId: 0, length: value >>> 0 });
498
+ var goAwayFrame = (code) => ({ type: TYPE.goAway, flags: 0, streamId: 0, length: code });
499
+ var Mux = class {
500
+ role;
501
+ #events;
502
+ #streams = /* @__PURE__ */ new Map();
503
+ /** The highest ID the phone has opened. */
504
+ #highestId = 0;
505
+ #peerGoneAway = false;
506
+ #localGoneAway = false;
507
+ constructor(role, events) {
508
+ this.role = role;
509
+ this.#events = events;
510
+ }
511
+ get activeStreams() {
512
+ return this.#streams.size;
513
+ }
514
+ get peerGoneAway() {
515
+ return this.#peerGoneAway;
516
+ }
517
+ has(streamId) {
518
+ return this.#streams.has(streamId);
519
+ }
520
+ sendWindow(streamId) {
521
+ return this.#streams.get(streamId)?.sendWindow ?? 0;
522
+ }
523
+ /** The phone side. Data may follow at once, within the first 256 KiB. */
524
+ open() {
525
+ if (this.role !== "phone") throw new YamuxError("computer_open");
526
+ if (this.#localGoneAway || this.#peerGoneAway) throw new YamuxError("gone_away");
527
+ if (this.#streams.size >= MAX_STREAMS) throw new YamuxError("too_many_streams");
528
+ const streamId = this.#highestId === 0 ? 1 : this.#highestId + 2;
529
+ if (streamId > MAX_STREAM_ID) throw new YamuxError("ids_exhausted");
530
+ this.#highestId = streamId;
531
+ this.#streams.set(streamId, {
532
+ established: false,
533
+ receiveWindow: INITIAL_WINDOW + OPEN_WINDOW_DELTA,
534
+ sendWindow: INITIAL_WINDOW,
535
+ uncredited: 0,
536
+ localFin: false,
537
+ remoteFin: false
538
+ });
539
+ this.#events.send(openFrame(streamId));
540
+ return streamId;
541
+ }
542
+ /** The computer side, once the origin socket is connected. */
543
+ accept(streamId) {
544
+ const stream = this.#streams.get(streamId);
545
+ if (!stream || stream.established || this.role !== "computer") return;
546
+ stream.established = true;
547
+ stream.receiveWindow += OPEN_WINDOW_DELTA;
548
+ this.#events.send(acceptFrame(streamId));
549
+ }
550
+ /** Sends up to the window and the frame cap; returns the bytes taken. */
551
+ write(streamId, data) {
552
+ const stream = this.#streams.get(streamId);
553
+ if (!stream || stream.localFin || this.role === "computer" && !stream.established) return 0;
554
+ const size = Math.min(data.length, stream.sendWindow, MAX_DATA_PAYLOAD);
555
+ if (size === 0) return 0;
556
+ stream.sendWindow -= size;
557
+ this.#events.send(dataFrame(streamId, data.subarray(0, size)));
558
+ return size;
559
+ }
560
+ finish(streamId) {
561
+ const stream = this.#streams.get(streamId);
562
+ if (!stream || stream.localFin) return;
563
+ stream.localFin = true;
564
+ this.#events.send(finFrame(streamId));
565
+ this.#closeIfDone(streamId, stream);
566
+ }
567
+ reset(streamId) {
568
+ if (!this.#streams.delete(streamId)) return;
569
+ this.#events.send(resetFrame(streamId));
570
+ this.#events.reset(streamId);
571
+ }
572
+ /** The receiver handed `bytes` on (to the origin socket, or the HTTP stack). Grants them back in 1 MiB steps. */
573
+ consumed(streamId, bytes) {
574
+ const stream = this.#streams.get(streamId);
575
+ if (!stream || stream.remoteFin) return;
576
+ stream.uncredited += bytes;
577
+ if (stream.uncredited < WINDOW_UPDATE_AFTER) return;
578
+ const delta = stream.uncredited;
579
+ stream.uncredited = 0;
580
+ stream.receiveWindow += delta;
581
+ this.#events.send(windowFrame(streamId, delta));
582
+ }
583
+ ping(value) {
584
+ this.#events.send(pingFrame(value));
585
+ }
586
+ goAway(code = GO_AWAY.normal) {
587
+ this.#localGoneAway = true;
588
+ this.#events.send(goAwayFrame(code));
589
+ }
590
+ /** Sends GoAway(1); the caller closes with 4000. */
591
+ protocolError() {
592
+ this.goAway(GO_AWAY.protocol);
593
+ this.closeAll();
594
+ }
595
+ closeAll() {
596
+ const ids = [...this.#streams.keys()];
597
+ this.#streams.clear();
598
+ for (const id2 of ids) this.#events.reset(id2);
599
+ }
600
+ receive(frame) {
601
+ if (frame.type === TYPE.ping) {
602
+ if (frame.flags === FLAG.syn) this.#events.send(pingFrame(frame.length, FLAG.ack));
603
+ else this.#events.pong?.(frame.length);
604
+ return;
605
+ }
606
+ if (frame.type === TYPE.goAway) {
607
+ this.#peerGoneAway = true;
608
+ this.#events.goAway?.(frame.length);
609
+ return;
610
+ }
611
+ if (frame.flags & FLAG.syn) {
612
+ this.#receiveOpen(frame);
613
+ return;
614
+ }
615
+ if (frame.flags & FLAG.ack) {
616
+ this.#receiveAck(frame);
617
+ return;
618
+ }
619
+ const stream = this.#known(frame.streamId);
620
+ if (!stream) return;
621
+ this.#receiveOnStream(frame.streamId, stream, frame);
622
+ }
623
+ /** A stream that ended is ignored: an RST and the peer's last frames cross. An ID never opened is an error. */
624
+ #known(streamId) {
625
+ if (streamId % 2 === 0 || streamId > this.#highestId) throw new YamuxError("unknown_stream");
626
+ return this.#streams.get(streamId) ?? null;
627
+ }
628
+ #receiveOpen(frame) {
629
+ if (this.role === "phone") throw new YamuxError("computer_opened_stream");
630
+ if (frame.type !== TYPE.windowUpdate || frame.flags !== FLAG.syn) throw new YamuxError("open_flags");
631
+ if (frame.streamId % 2 === 0) throw new YamuxError("even_stream");
632
+ if (frame.streamId <= this.#highestId) throw new YamuxError("stream_reused");
633
+ this.#highestId = frame.streamId;
634
+ if (this.#localGoneAway || this.#streams.size >= MAX_STREAMS) {
635
+ this.#events.send(resetFrame(frame.streamId));
636
+ return;
637
+ }
638
+ this.#streams.set(frame.streamId, {
639
+ established: false,
640
+ receiveWindow: INITIAL_WINDOW,
641
+ sendWindow: INITIAL_WINDOW + frame.length,
642
+ uncredited: 0,
643
+ localFin: false,
644
+ remoteFin: false
645
+ });
646
+ this.#events.opened?.(frame.streamId);
647
+ }
648
+ #receiveAck(frame) {
649
+ if (this.role === "computer") throw new YamuxError("phone_ack");
650
+ if (frame.type !== TYPE.windowUpdate || (frame.flags & ~(FLAG.ack | FLAG.fin | FLAG.rst)) !== 0) throw new YamuxError("ack_flags");
651
+ const stream = this.#known(frame.streamId);
652
+ if (!stream) return;
653
+ if (stream.established) throw new YamuxError("duplicate_ack");
654
+ stream.established = true;
655
+ this.#events.accepted?.(frame.streamId);
656
+ this.#receiveOnStream(frame.streamId, stream, { ...frame, flags: frame.flags & ~FLAG.ack });
657
+ }
658
+ #receiveOnStream(streamId, stream, frame) {
659
+ if (frame.flags & FLAG.rst) {
660
+ this.#streams.delete(streamId);
661
+ this.#events.reset(streamId);
662
+ return;
663
+ }
664
+ if (frame.type === TYPE.data) {
665
+ if (stream.remoteFin && (frame.length > 0 || frame.flags & FLAG.fin)) throw new YamuxError("data_after_fin");
666
+ if (frame.length > stream.receiveWindow) throw new YamuxError("window_exceeded");
667
+ stream.receiveWindow -= frame.length;
668
+ if (frame.length > 0) this.#events.data(streamId, frame.data);
669
+ } else if (frame.length > 0) {
670
+ stream.sendWindow += frame.length;
671
+ this.#events.writable(streamId);
672
+ }
673
+ if (frame.flags & FLAG.fin) {
674
+ if (stream.remoteFin) throw new YamuxError("data_after_fin");
675
+ stream.remoteFin = true;
676
+ this.#events.ended(streamId);
677
+ this.#closeIfDone(streamId, stream);
678
+ }
679
+ }
680
+ #closeIfDone(streamId, stream) {
681
+ if (stream.localFin && stream.remoteFin) this.#streams.delete(streamId);
682
+ }
683
+ };
684
+ var SEND_BUFFER_HIGH = 1024 * 1024;
685
+ var SessionClosed = class extends Error {
686
+ code;
687
+ constructor(code) {
688
+ super(`e2e session closed (${code})`);
689
+ this.code = code;
690
+ }
691
+ };
692
+ var noop = () => {
693
+ };
694
+ var MuxStream = class extends Duplex {
695
+ streamId;
696
+ #session;
697
+ #pending = null;
698
+ #pendingDone = null;
699
+ #ungranted = 0;
700
+ constructor(session, streamId) {
701
+ super({ allowHalfOpen: true, readableHighWaterMark: 256 * 1024 });
702
+ this.#session = session;
703
+ this.streamId = streamId;
704
+ this.on("error", noop);
705
+ }
706
+ _write(chunk, _encoding, done) {
707
+ this.#pending = chunk;
708
+ this.#pendingDone = done;
709
+ this.flush();
710
+ }
711
+ _final(done) {
712
+ this.#session.mux?.finish(this.streamId);
713
+ done();
714
+ }
715
+ _read() {
716
+ if (this.#ungranted === 0) return;
717
+ const bytes = this.#ungranted;
718
+ this.#ungranted = 0;
719
+ this.#session.mux?.consumed(this.streamId, bytes);
720
+ }
721
+ _destroy(error2, done) {
722
+ const mux = this.#session.mux;
723
+ if (mux?.has(this.streamId)) mux.reset(this.streamId);
724
+ this.#session.forget(this.streamId);
725
+ const pendingDone = this.#pendingDone;
726
+ this.#pending = null;
727
+ this.#pendingDone = null;
728
+ pendingDone?.(error2 ?? new Error("stream reset"));
729
+ done(error2);
730
+ }
731
+ /** Granted back as the reader takes it, so a slow reader slows the sender. */
732
+ deliver(data) {
733
+ if (this.push(data)) this.#session.mux?.consumed(this.streamId, data.length);
734
+ else this.#ungranted += data.length;
735
+ }
736
+ remoteEnded() {
737
+ this.push(null);
738
+ }
739
+ remoteReset() {
740
+ if (!this.destroyed) this.destroy(new Error("stream reset"));
741
+ }
742
+ flush() {
743
+ const mux = this.#session.mux;
744
+ while (this.#pending && this.#pending.length > 0) {
745
+ if (!mux || !mux.has(this.streamId)) return void this.destroy(new Error("stream reset"));
746
+ if (this.#session.congested) return this.#session.whenDrained(() => this.flush());
747
+ const taken = mux.write(this.streamId, this.#pending);
748
+ if (taken === 0) return;
749
+ this.#pending = this.#pending.subarray(taken);
750
+ }
751
+ const done = this.#pendingDone;
752
+ this.#pending = null;
753
+ this.#pendingDone = null;
754
+ done?.();
755
+ }
756
+ };
757
+ var E2ESession = class _E2ESession {
758
+ role;
759
+ #transport;
760
+ #streams = /* @__PURE__ */ new Map();
761
+ #timers;
762
+ #state = "handshake";
763
+ #keys = null;
764
+ #mux = null;
765
+ #accept = null;
766
+ #deadline = null;
767
+ #pingTimer = null;
768
+ #pongTimer = null;
769
+ #awaitingPong = null;
770
+ #pingCounter = 0;
771
+ #drainWaiters = [];
772
+ #settleReady;
773
+ #settleClosed;
774
+ #connectOrigin = null;
775
+ #removed = false;
776
+ /** The computer side: which device (or pairing window) this is. */
777
+ peer = null;
778
+ ready;
779
+ closed;
780
+ constructor(role, transport, timers) {
781
+ this.role = role;
782
+ this.#transport = transport;
783
+ this.#timers = {
784
+ pingIntervalMs: timers.pingIntervalMs ?? PING_INTERVAL_MS,
785
+ pingTimeoutMs: timers.pingTimeoutMs ?? PING_TIMEOUT_MS,
786
+ handshakeDeadlineMs: timers.handshakeDeadlineMs ?? HANDSHAKE_DEADLINE_MS
787
+ };
788
+ this.ready = new Promise((resolve6, reject) => {
789
+ this.#settleReady = (error2) => error2 ? reject(error2) : resolve6();
790
+ });
791
+ this.ready.catch(noop);
792
+ this.closed = new Promise((resolve6) => {
793
+ this.#settleClosed = resolve6;
794
+ });
795
+ this.#deadline = setTimeout(() => this.close(CLOSE.deadline), this.#timers.handshakeDeadlineMs);
796
+ this.#deadline.unref?.();
797
+ }
798
+ /** Waits for message 1. Anything wrong with it closes 4001 and nothing else is sent. */
799
+ static computer(options) {
800
+ const session = new _E2ESession("computer", options.transport, options);
801
+ session.#connectOrigin = options.connectOrigin;
802
+ session.#accept = (message) => {
803
+ const accepted = computerAccept({ staticKey: options.staticKey, transport: options.transportByte, candidates: options.candidates() }, message);
804
+ if (!accepted) return session.close(CLOSE.rejected);
805
+ options.transport.send(accepted.message2);
806
+ session.peer = accepted.id;
807
+ session.#start(accepted.keys);
808
+ options.opened?.(accepted.id, accepted.payload1);
809
+ };
810
+ return session;
811
+ }
812
+ /** Sends message 1 now. `ready` rejects with a NoiseError when the computer can't be verified: a hard stop. */
813
+ static phone(options) {
814
+ const session = new _E2ESession("phone", options.transport, options);
815
+ const conn = newConn();
816
+ const hello = phoneHello(
817
+ { remoteStatic: options.remoteStatic, psk: options.psk, transport: options.transportByte, ephemeral: options.ephemeral },
818
+ encodePayload1(conn, options.app),
819
+ b64u(conn)
820
+ );
821
+ session.#accept = (message) => {
822
+ let keys;
823
+ try {
824
+ keys = hello.finish(message);
825
+ } catch (error2) {
826
+ session.#settleReady(error2);
827
+ return session.close(CLOSE.protocol);
828
+ }
829
+ session.#start(keys);
830
+ };
831
+ options.transport.send(hello.message1);
832
+ return session;
833
+ }
834
+ get mux() {
835
+ return this.#mux;
836
+ }
837
+ /** The phone side: the computer said, inside the encryption, that this device was removed. Only this, or a 4001 on
838
+ * a direct route, means "pair again"; a bare 4001 through the relay may be the relay's own and means "keep trying". */
839
+ get removed() {
840
+ return this.#removed;
841
+ }
842
+ get state() {
843
+ return this.#state;
844
+ }
845
+ /** Records each way so far, for diagnostics. */
846
+ get records() {
847
+ return { sent: this.#keys?.send.nonce ?? 0, received: this.#keys?.receive.nonce ?? 0 };
848
+ }
849
+ get congested() {
850
+ return this.#transport.bufferedAmount > SEND_BUFFER_HIGH;
851
+ }
852
+ get activeStreams() {
853
+ return this.#streams.size;
854
+ }
855
+ whenDrained(listener) {
856
+ this.#drainWaiters.push(listener);
857
+ if (this.#drainWaiters.length > 1) return;
858
+ this.#transport.onceDrained(() => {
859
+ const waiters = this.#drainWaiters;
860
+ this.#drainWaiters = [];
861
+ for (const waiter of waiters) waiter();
862
+ });
863
+ }
864
+ forget(streamId) {
865
+ this.#streams.delete(streamId);
866
+ }
867
+ /** Every message the transport receives, in order. */
868
+ receive(message, binary) {
869
+ if (this.#state === "closed") return;
870
+ if (this.#state === "handshake") {
871
+ const accept = this.#accept;
872
+ this.#accept = null;
873
+ if (!binary || !accept) return this.close(this.role === "computer" ? CLOSE.rejected : CLOSE.protocol);
874
+ return accept(Buffer.from(message));
875
+ }
876
+ if (!binary) return this.close(CLOSE.protocol);
877
+ let plaintext;
878
+ try {
879
+ plaintext = this.#keys.receive.open(message);
880
+ } catch {
881
+ return this.close(CLOSE.protocol);
882
+ }
883
+ try {
884
+ this.#mux.receive(decodeFrame(plaintext));
885
+ } catch (error2) {
886
+ if (!(error2 instanceof YamuxError)) throw error2;
887
+ this.#mux?.protocolError();
888
+ this.close(CLOSE.protocol);
889
+ }
890
+ }
891
+ /** The phone side. */
892
+ openStream() {
893
+ if (this.#state !== "open" || !this.#mux) throw new SessionClosed(CLOSE.normal);
894
+ const streamId = this.#mux.open();
895
+ const stream = new MuxStream(this, streamId);
896
+ this.#streams.set(streamId, stream);
897
+ return stream;
898
+ }
899
+ /** The computer side: the device was removed. The phone hears it authenticated, then the 4001 close. */
900
+ remove() {
901
+ if (this.#state === "open") this.#mux?.goAway(GO_AWAY.removed);
902
+ this.close(CLOSE.rejected);
903
+ }
904
+ /** A planned close (quit, sleep, restart): GoAway first, so the peer knows it isn't an error. */
905
+ shutdown() {
906
+ if (this.#state === "open") this.#mux?.goAway(GO_AWAY.normal);
907
+ this.close(CLOSE.normal);
908
+ }
909
+ close(code, reason) {
910
+ if (this.#state === "closed") return;
911
+ this.#transport.close(code, reason);
912
+ this.#end(code);
913
+ }
914
+ /** The transport closed under us. */
915
+ transportClosed(code) {
916
+ this.#end(code);
917
+ }
918
+ #end(code) {
919
+ if (this.#state === "closed") return;
920
+ const wasHandshake = this.#state === "handshake";
921
+ this.#state = "closed";
922
+ for (const timer of [this.#deadline, this.#pongTimer]) if (timer) clearTimeout(timer);
923
+ if (this.#pingTimer) clearInterval(this.#pingTimer);
924
+ this.#mux?.closeAll();
925
+ for (const stream of this.#streams.values()) stream.remoteReset();
926
+ this.#streams.clear();
927
+ const waiters = this.#drainWaiters;
928
+ this.#drainWaiters = [];
929
+ for (const waiter of waiters) waiter();
930
+ if (wasHandshake) this.#settleReady(new SessionClosed(code));
931
+ this.#settleClosed(code);
932
+ }
933
+ #start(keys) {
934
+ if (this.#deadline) clearTimeout(this.#deadline);
935
+ this.#keys = keys;
936
+ this.#state = "open";
937
+ this.#mux = new Mux(this.role, {
938
+ send: (frame) => this.#send(frame),
939
+ opened: (streamId) => this.#openOrigin(streamId),
940
+ data: (streamId, data) => this.#streams.get(streamId)?.deliver(Buffer.from(data.buffer, data.byteOffset, data.byteLength)),
941
+ ended: (streamId) => this.#streams.get(streamId)?.remoteEnded(),
942
+ reset: (streamId) => {
943
+ const stream = this.#streams.get(streamId);
944
+ this.#streams.delete(streamId);
945
+ stream?.remoteReset();
946
+ },
947
+ writable: (streamId) => this.#streams.get(streamId)?.flush(),
948
+ goAway: (code) => {
949
+ if (code === GO_AWAY.removed && this.role === "phone") this.#removed = true;
950
+ else if (code !== GO_AWAY.normal) this.close(CLOSE.protocol);
951
+ },
952
+ pong: (value) => {
953
+ if (value !== this.#awaitingPong) return;
954
+ this.#awaitingPong = null;
955
+ if (this.#pongTimer) clearTimeout(this.#pongTimer);
956
+ }
957
+ });
958
+ this.#pingTimer = setInterval(() => this.#ping(), this.#timers.pingIntervalMs);
959
+ this.#pingTimer.unref?.();
960
+ this.#settleReady();
961
+ }
962
+ #ping() {
963
+ if (this.#awaitingPong !== null || !this.#mux) return;
964
+ this.#pingCounter = this.#pingCounter + 1 >>> 0;
965
+ this.#awaitingPong = this.#pingCounter;
966
+ this.#mux.ping(this.#pingCounter);
967
+ this.#pongTimer = setTimeout(() => this.close(CLOSE.goingAway), this.#timers.pingTimeoutMs);
968
+ this.#pongTimer.unref?.();
969
+ }
970
+ #send(frame) {
971
+ if (this.#state !== "open" || !this.#keys) return;
972
+ let record;
973
+ try {
974
+ record = this.#keys.send.seal(encodeFrame(frame));
975
+ } catch (error2) {
976
+ if (error2 instanceof NoiseError) return this.close(CLOSE.normal);
977
+ throw error2;
978
+ }
979
+ this.#transport.send(record);
980
+ if (this.#keys.send.nonce >= NONCE_LIMIT - 1024) queueMicrotask(() => this.close(CLOSE.normal));
981
+ }
982
+ #openOrigin(streamId) {
983
+ const stream = new MuxStream(this, streamId);
984
+ this.#streams.set(streamId, stream);
985
+ const connect = this.#connectOrigin;
986
+ if (!connect) return void stream.destroy();
987
+ connect().then(
988
+ (socket) => {
989
+ if (stream.destroyed || this.#state !== "open") return void socket.destroy();
990
+ this.#mux?.accept(streamId);
991
+ socket.on("error", () => stream.destroy());
992
+ socket.on("close", () => {
993
+ if (!socket.readableEnded) stream.destroy();
994
+ });
995
+ stream.on("close", () => socket.destroy());
996
+ stream.pipe(socket);
997
+ socket.pipe(stream);
998
+ },
999
+ () => stream.destroy()
1000
+ );
1001
+ }
1002
+ };
1003
+ var DIRECT_PROTOCOL = "omb-e2e.v1";
1004
+
1005
+ // src/secure-pairing.ts
1006
+ var PAIRING_TTL_MS = 12e4;
1007
+ var PAIRING_ATTEMPTS = 5;
1008
+ var MAX_PAIRED_CLIENTS = 128;
1009
+ var MAX_INVITES = 20;
1010
+ var digestEqual = (a, b) => /^[0-9a-f]{64}$/.test(a) && /^[0-9a-f]{64}$/.test(b) && timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
1011
+ var nameOf = (value) => String(value ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 60) || "DSH Host";
1012
+ var SecurePairing = class {
1013
+ constructor(db, machineId, commitClient, now = Date.now) {
1014
+ this.db = db;
1015
+ this.machineId = machineId;
1016
+ this.commitClient = commitClient;
1017
+ this.now = now;
1018
+ db.exec(`CREATE TABLE IF NOT EXISTS pairing_invites (
1019
+ id TEXT PRIMARY KEY, client_id TEXT NOT NULL, token_digest TEXT NOT NULL, code_digest TEXT NOT NULL,
1020
+ expires_at INTEGER NOT NULL, attempts_left INTEGER NOT NULL, ceiling TEXT NOT NULL, name TEXT NOT NULL,
1021
+ consumed_request TEXT, consumed_at INTEGER
1022
+ ); CREATE INDEX IF NOT EXISTS pairing_invite_expiry ON pairing_invites(expires_at);`);
1023
+ const generated = generateKeyPair();
1024
+ db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("e2eTransportPrivateKey", b64u(generated.privateKey));
1025
+ const stored = db.prepare("SELECT value FROM meta WHERE key=?").get("e2eTransportPrivateKey")?.value;
1026
+ if (typeof stored !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(stored)) fail("PAIRING_IDENTITY_INVALID");
1027
+ try {
1028
+ this.transportKey = keyPairFromPrivate(Buffer.from(stored, "base64url"));
1029
+ } catch {
1030
+ fail("PAIRING_IDENTITY_INVALID");
1031
+ }
1032
+ }
1033
+ db;
1034
+ machineId;
1035
+ commitClient;
1036
+ now;
1037
+ replay = /* @__PURE__ */ new Map();
1038
+ transportKey;
1039
+ identity() {
1040
+ return { kind: "noise-v1", publicKey: b64u(this.transportKey.publicKey), fingerprint: sha(this.transportKey.publicKey) };
1041
+ }
1042
+ create(clientId, ceiling, options) {
1043
+ identifier(clientId);
1044
+ let endpoint;
1045
+ try {
1046
+ endpoint = new URL(options.endpoint);
1047
+ } catch {
1048
+ fail("PAIRING_ENDPOINT_INVALID");
1049
+ }
1050
+ if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.href.length > 1024) fail("PAIRING_ENDPOINT_INVALID");
1051
+ const now = this.now(), token = `dsh_pair_${secret()}`, code = String(randomInt(1e6)).padStart(6, "0"), id2 = `invite-${randomUUID()}`;
1052
+ return this.db.transaction(() => {
1053
+ this.db.prepare("DELETE FROM pairing_invites WHERE expires_at<=?").run(now - PAIRING_TTL_MS);
1054
+ if (this.db.prepare("SELECT 1 FROM clients WHERE id=? AND revoked=0").get(clientId)) fail("CLIENT_ALREADY_PAIRED", 409);
1055
+ if (this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
1056
+ this.db.prepare("UPDATE pairing_invites SET attempts_left=0 WHERE client_id=? AND consumed_request IS NULL").run(clientId);
1057
+ if (this.db.prepare("SELECT count(*) AS n FROM pairing_invites WHERE expires_at>? AND attempts_left>0 AND consumed_request IS NULL").get(now).n >= MAX_INVITES) fail("PAIRING_WINDOW_LIMIT", 429);
1058
+ this.db.prepare("INSERT INTO pairing_invites VALUES (?,?,?,?,?,?,?,?,NULL,NULL)").run(id2, clientId, sha(token), sha(code), now + PAIRING_TTL_MS, PAIRING_ATTEMPTS, canonical(ceiling), nameOf(options.name));
1059
+ return {
1060
+ format: "dsh-pairing-invite",
1061
+ version: 2,
1062
+ inviteId: id2,
1063
+ machineId: this.machineId,
1064
+ clientId,
1065
+ name: nameOf(options.name),
1066
+ endpoint: endpoint.origin,
1067
+ expiresAt: now + PAIRING_TTL_MS,
1068
+ token,
1069
+ code,
1070
+ transport: this.identity(),
1071
+ ceiling: structuredClone(ceiling)
1072
+ };
1073
+ }).immediate();
1074
+ }
1075
+ candidates() {
1076
+ const now = this.now(), transportId = keyId(this.transportKey.publicKey);
1077
+ for (const [id2, row] of this.replay) if (row.expiresAt <= now) this.replay.delete(id2);
1078
+ const invites = this.db.prepare("SELECT id,token_digest FROM pairing_invites WHERE expires_at>? AND attempts_left>0").all(now).map((row) => ({ id: { kind: "invite", id: row.id, digest: row.token_digest }, hash: row.token_digest }));
1079
+ const clients = this.db.prepare("SELECT id,digest,revision FROM clients WHERE revoked=0").all().map((row) => ({ id: { kind: "client", id: row.id, digest: row.digest, revision: row.revision }, hash: row.digest }));
1080
+ if (clients.length > MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
1081
+ return [...invites, ...clients].map((row) => ({ id: row.id, psk: psk(credentialHashFromHex(row.hash), transportId) }));
1082
+ }
1083
+ validPeer(peer) {
1084
+ if (peer.kind === "client") {
1085
+ const client = this.db.prepare("SELECT digest,revision,revoked FROM clients WHERE id=?").get(peer.id);
1086
+ return Boolean(client && !client.revoked && client.revision === peer.revision && digestEqual(client.digest, peer.digest));
1087
+ }
1088
+ const invite = this.db.prepare("SELECT token_digest,expires_at,attempts_left FROM pairing_invites WHERE id=?").get(peer.id);
1089
+ return Boolean(invite && invite.expires_at > this.now() && invite.attempts_left > 0 && digestEqual(invite.token_digest, peer.digest));
1090
+ }
1091
+ redeem(peer, input) {
1092
+ if (peer.kind !== "invite" || !input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key2) => !["requestId", "code"].includes(key2)) || typeof input.requestId !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(input.requestId) || typeof input.code !== "string" || !/^\d{6}$/.test(input.code)) fail("PAIRING_REQUEST_INVALID");
1093
+ const now = this.now();
1094
+ const decision = this.db.transaction(() => {
1095
+ const row = this.db.prepare("SELECT * FROM pairing_invites WHERE id=?").get(peer.id);
1096
+ if (!row || !digestEqual(row.token_digest, peer.digest) || row.expires_at <= now || row.attempts_left <= 0) return { error: "PAIRING_WINDOW_CLOSED" };
1097
+ if (!digestEqual(row.code_digest, sha(input.code))) {
1098
+ this.db.prepare("UPDATE pairing_invites SET attempts_left=attempts_left-1 WHERE id=?").run(peer.id);
1099
+ return { error: "PAIRING_CODE_INVALID" };
1100
+ }
1101
+ if (row.consumed_request !== null) {
1102
+ const cached = this.replay.get(row.id), client = this.db.prepare("SELECT digest,revoked FROM clients WHERE id=?").get(row.client_id);
1103
+ if (row.consumed_request !== input.requestId || !cached || cached.expiresAt <= now || cached.proof !== peer.digest || cached.requestId !== input.requestId) return { error: "PAIRING_ALREADY_REDEEMED" };
1104
+ if (!client || client.revoked || !digestEqual(client.digest, sha(cached.result.token))) return { error: "PAIRING_DEVICE_CHANGED" };
1105
+ return { result: structuredClone(cached.result), ceiling: JSON.parse(row.ceiling), recovered: true };
1106
+ }
1107
+ const result = this.commitClient(row.client_id, JSON.parse(row.ceiling));
1108
+ this.db.prepare("UPDATE pairing_invites SET consumed_request=?,consumed_at=? WHERE id=?").run(input.requestId, now, row.id);
1109
+ this.replay.set(row.id, { proof: peer.digest, requestId: input.requestId, expiresAt: row.expires_at, result: structuredClone(result) });
1110
+ return { result, ceiling: JSON.parse(row.ceiling), recovered: false };
1111
+ }).immediate();
1112
+ if (decision.error) fail(decision.error, 403);
1113
+ return { ...decision.result, transport: this.identity(), ceiling: decision.ceiling, recovered: decision.recovered };
1114
+ }
1115
+ invalidate(clientId) {
1116
+ this.db.prepare("UPDATE pairing_invites SET attempts_left=0 WHERE client_id=?").run(clientId);
1117
+ for (const [id2, row] of this.replay) if (row.result.clientId === clientId) this.replay.delete(id2);
1118
+ }
1119
+ close() {
1120
+ this.replay.clear();
1121
+ }
1122
+ };
1123
+
1124
+ // src/store.ts
90
1125
  var Store = class {
91
1126
  constructor(dataDir, Database, options = {}) {
92
1127
  this.dataDir = dataDir;
@@ -131,7 +1166,7 @@ var Store = class {
131
1166
  unlinkSync(this.lockPath);
132
1167
  this.lockFd = openSync(this.lockPath, "wx", 384);
133
1168
  }
134
- writeFileSync(this.lockFd, JSON.stringify({ pid: process.pid, instanceId: randomUUID() }));
1169
+ writeFileSync(this.lockFd, JSON.stringify({ pid: process.pid, instanceId: randomUUID2() }));
135
1170
  }
136
1171
  this.db = new Driver(join(dataDir, "agent.sqlite"));
137
1172
  this.db.pragma("journal_mode = WAL");
@@ -145,8 +1180,9 @@ var Store = class {
145
1180
  CREATE TABLE IF NOT EXISTS artifacts(id TEXT PRIMARY KEY,context TEXT NOT NULL,hash TEXT NOT NULL,size INTEGER NOT NULL,media TEXT NOT NULL);
146
1181
  CREATE TABLE IF NOT EXISTS worktrees(id TEXT PRIMARY KEY,context TEXT NOT NULL,root TEXT NOT NULL,path TEXT NOT NULL,head TEXT NOT NULL,source TEXT NOT NULL);
147
1182
  CREATE TABLE IF NOT EXISTS audit(id INTEGER PRIMARY KEY,time INTEGER NOT NULL,code TEXT NOT NULL,actor TEXT,operation TEXT);`);
148
- this.db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("machineId", `machine-${randomUUID()}`);
1183
+ this.db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("machineId", `machine-${randomUUID2()}`);
149
1184
  this.machineId = this.db.prepare("SELECT value FROM meta WHERE key=?").get("machineId").value;
1185
+ this.pairing = new SecurePairing(this.db, this.machineId, (id2, ceiling) => this.commitClient(id2, ceiling));
150
1186
  if (!options.admin) this.db.prepare("UPDATE operations SET status='unknown',error='OUTCOME_UNKNOWN' WHERE status IN ('admitted','running','cancel_pending')").run();
151
1187
  if (!options.admin) this.db.prepare("DELETE FROM grants").run();
152
1188
  } catch (e) {
@@ -160,6 +1196,7 @@ var Store = class {
160
1196
  }
161
1197
  }
162
1198
  dataDir;
1199
+ pairing;
163
1200
  db;
164
1201
  machineId;
165
1202
  lockFd;
@@ -196,7 +1233,7 @@ var Store = class {
196
1233
  }
197
1234
  }
198
1235
  }
199
- pair(id2, ceiling) {
1236
+ approvedCeiling(id2, ceiling) {
200
1237
  identifier(id2);
201
1238
  if (!ceiling || ceiling.hostManagedBots !== void 0 && typeof ceiling.hostManagedBots !== "boolean" || !Array.isArray(ceiling.botIds ?? []) || !Array.isArray(ceiling.roots) || !ceiling.roots.length || !Array.isArray(ceiling.tools) || !ceiling.tools.length || ceiling.tools.some((x) => !TOOL_NAMES.includes(x))) fail("INVALID_CEILING");
202
1239
  const managed = ceiling.hostManagedBots === true, botIds = ceiling.botIds ?? [];
@@ -205,9 +1242,25 @@ var Store = class {
205
1242
  botIds.forEach(identifier);
206
1243
  ceiling.roots.forEach(identifier);
207
1244
  const saved = { botIds, roots: ceiling.roots, tools: ceiling.tools, ...managed ? { hostManagedBots: true } : {} };
208
- const token = secret();
209
- this.db.prepare("INSERT INTO clients VALUES (?,?,1,0,?)").run(id2, sha(token), canonical(saved));
210
- return { clientId: id2, token, machineId: this.machineId, secretRef: `remote-agent/${this.machineId}/${id2}`, hostManagedBots: managed };
1245
+ return saved;
1246
+ }
1247
+ commitClient(id2, ceiling) {
1248
+ return this.db.transaction(() => {
1249
+ const previous = this.db.prepare("SELECT revoked FROM clients WHERE id=?").get(id2);
1250
+ if (previous && !previous.revoked) fail("CLIENT_ALREADY_PAIRED", 409);
1251
+ if (this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
1252
+ const token = secret();
1253
+ if (previous) this.db.prepare("UPDATE clients SET digest=?,revision=revision+1,revoked=0,ceiling=? WHERE id=? AND revoked=1").run(sha(token), canonical(ceiling), id2);
1254
+ else this.db.prepare("INSERT INTO clients VALUES (?,?,1,0,?)").run(id2, sha(token), canonical(ceiling));
1255
+ return { clientId: id2, token, machineId: this.machineId, secretRef: `remote-agent/${this.machineId}/${id2}`, hostManagedBots: ceiling.hostManagedBots === true };
1256
+ }).immediate();
1257
+ }
1258
+ pair(id2, ceiling) {
1259
+ return this.commitClient(id2, this.approvedCeiling(id2, ceiling));
1260
+ }
1261
+ invite(id2, ceiling, options) {
1262
+ if (!this.admin) fail("ADMIN_REQUIRED", 403);
1263
+ return this.pairing.create(id2, this.approvedCeiling(id2, ceiling), options);
211
1264
  }
212
1265
  client(token) {
213
1266
  if (typeof token !== "string" || token.length > 256) fail("AUTH_INVALID", 401);
@@ -217,12 +1270,20 @@ var Store = class {
217
1270
  return row;
218
1271
  }
219
1272
  rotate(id2) {
220
- const token = secret();
221
- if (!this.db.prepare("UPDATE clients SET digest=?,revision=revision+1,revoked=0 WHERE id=?").run(sha(token), identifier(id2)).changes) fail("AUTH_INVALID", 401);
222
- return { clientId: id2, token };
1273
+ return this.db.transaction(() => {
1274
+ identifier(id2);
1275
+ const row = this.db.prepare("SELECT revoked FROM clients WHERE id=?").get(id2);
1276
+ if (!row) fail("AUTH_INVALID", 401);
1277
+ if (row.revoked && this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
1278
+ const token = secret();
1279
+ this.db.prepare("UPDATE clients SET digest=?,revision=revision+1,revoked=0 WHERE id=?").run(sha(token), id2);
1280
+ this.pairing.invalidate(id2);
1281
+ return { clientId: id2, token };
1282
+ }).immediate();
223
1283
  }
224
1284
  revoke(id2) {
225
1285
  this.db.prepare("UPDATE clients SET revoked=1,revision=revision+1 WHERE id=?").run(identifier(id2));
1286
+ this.pairing.invalidate(id2);
226
1287
  }
227
1288
  currentClient(client) {
228
1289
  const row = this.db.prepare("SELECT * FROM clients WHERE id=?").get(client?.id);
@@ -238,7 +1299,7 @@ var Store = class {
238
1299
  if (!Number.isSafeInteger(ttl) || ttl < 1e3 || ttl > 3e5) fail("INVALID_ARGUMENT");
239
1300
  this.db.prepare("DELETE FROM grants WHERE json_extract(context,'$.expiresAt')<=?").run(Date.now());
240
1301
  if (this.db.prepare("SELECT count(*) AS count FROM grants").get().count >= 4096) fail("GRANT_LIMIT", 429);
241
- const context = { clientId: client.id, botId: request.botId, taskId: request.taskId, attemptId: request.attemptId, roots: request.roots, tools: request.tools, expiresAt: Date.now() + ttl, revision: client.revision, grantId: randomUUID() };
1302
+ const context = { clientId: client.id, botId: request.botId, taskId: request.taskId, attemptId: request.attemptId, roots: request.roots, tools: request.tools, expiresAt: Date.now() + ttl, revision: client.revision, grantId: randomUUID2() };
242
1303
  const token = secret();
243
1304
  this.db.prepare("INSERT INTO grants VALUES (?,?)").run(sha(token), canonical(context));
244
1305
  return { grantToken: token, machineId: this.machineId, agentApiMajor: 1, ...context };
@@ -303,6 +1364,7 @@ var Store = class {
303
1364
  close() {
304
1365
  if (this.closed) return;
305
1366
  this.closed = true;
1367
+ this.pairing?.close();
306
1368
  try {
307
1369
  this.db.close();
308
1370
  } finally {
@@ -466,7 +1528,7 @@ var Roots = class {
466
1528
  };
467
1529
 
468
1530
  // src/browser.ts
469
- import { randomUUID as randomUUID2, createHash as createHash2 } from "node:crypto";
1531
+ import { randomUUID as randomUUID3, createHash as createHash3 } from "node:crypto";
470
1532
  import { access, mkdir, readFile, stat as stat2, unlink } from "node:fs/promises";
471
1533
  import { join as join2 } from "node:path";
472
1534
  var BrowserTools = class {
@@ -515,7 +1577,7 @@ var BrowserTools = class {
515
1577
  if (a.profileId !== void 0) {
516
1578
  const root = this.profileRoots[a.profileId];
517
1579
  if (!root) fail("BROWSER_PROFILE_DENIED", 403);
518
- const owner = JSON.stringify([c.clientId, c.botId, a.profileId]), key2 = createHash2("sha256").update(owner).digest("hex");
1580
+ const owner = JSON.stringify([c.clientId, c.botId, a.profileId]), key2 = createHash3("sha256").update(owner).digest("hex");
519
1581
  try {
520
1582
  context = this.profiles.get(key2);
521
1583
  if (!context) {
@@ -547,9 +1609,9 @@ var BrowserTools = class {
547
1609
  });
548
1610
  const page = await context.newPage();
549
1611
  page.on("popup", (p) => void p.close());
550
- const id2 = `browser-${randomUUID2()}`, downloads = /* @__PURE__ */ new Map();
1612
+ const id2 = `browser-${randomUUID3()}`, downloads = /* @__PURE__ */ new Map();
551
1613
  page.on("download", (download2) => {
552
- const downloadId2 = `download-${randomUUID2()}`;
1614
+ const downloadId2 = `download-${randomUUID3()}`;
553
1615
  downloads.set(downloadId2, { download: download2, path: download2.path(), suggestedFilename: download2.suggestedFilename() });
554
1616
  });
555
1617
  this.sessions.set(id2, { context, page, owner: JSON.stringify([c.clientId, c.botId, c.taskId, c.attemptId]), revision: null, persistent, profileId, downloads });
@@ -598,7 +1660,7 @@ var BrowserTools = class {
598
1660
  return { completed: true, url: session.page.url(), result: value ?? null };
599
1661
  }
600
1662
  if (name === "browser_snapshot") {
601
- session.revision = `snapshot-${randomUUID2()}`;
1663
+ session.revision = `snapshot-${randomUUID3()}`;
602
1664
  const nodes = await session.page.locator('a,button,input,textarea,select,[role="button"]').evaluateAll((elements) => elements.slice(0, 300).map((el, i) => {
603
1665
  el.setAttribute("data-dsh-agent-ref", String(i));
604
1666
  return { ref: String(i), tag: el.tagName.toLowerCase(), text: (el.innerText || el.getAttribute("aria-label") || "").slice(0, 300) };
@@ -642,7 +1704,7 @@ var BrowserTools = class {
642
1704
  // src/desktop.ts
643
1705
  import net from "node:net";
644
1706
  import { readFile as readFile2 } from "node:fs/promises";
645
- import { randomUUID as randomUUID3 } from "node:crypto";
1707
+ import { randomUUID as randomUUID4 } from "node:crypto";
646
1708
  var error = (code, possiblyApplied = false, stopConfirmed = false) => Object.assign(new AgentError(code, 409), { possiblyApplied, stopConfirmed });
647
1709
  var DesktopTools = class {
648
1710
  constructor(config, options = {}) {
@@ -651,7 +1713,7 @@ var DesktopTools = class {
651
1713
  }
652
1714
  config;
653
1715
  options;
654
- nonce = randomUUID3();
1716
+ nonce = randomUUID4();
655
1717
  owners = /* @__PURE__ */ new Map();
656
1718
  closing = new AbortController();
657
1719
  active = /* @__PURE__ */ new Set();
@@ -674,7 +1736,7 @@ var DesktopTools = class {
674
1736
  async rpc(control2, packet, beforeWrite = () => {
675
1737
  }) {
676
1738
  const name = this.config.pipeName + (control2 ? "-control" : "");
677
- return new Promise((resolve5, reject) => {
1739
+ return new Promise((resolve6, reject) => {
678
1740
  const socket = (this.options.connect ?? net.createConnection)(`\\\\.\\pipe\\${name}`), chunks = [];
679
1741
  let bytes = 0, sent = false, settled = false;
680
1742
  const finish = (value, e) => {
@@ -683,7 +1745,7 @@ var DesktopTools = class {
683
1745
  clearTimeout(timer);
684
1746
  socket.destroy();
685
1747
  if (e) reject(e);
686
- else resolve5(value);
1748
+ else resolve6(value);
687
1749
  };
688
1750
  const timer = setTimeout(() => finish(null, error(sent ? "OUTCOME_UNKNOWN" : "DESKTOP_UNAVAILABLE", !control2 && sent)), this.options.timeoutMs ?? 1e4);
689
1751
  socket.once("connect", () => {
@@ -826,7 +1888,7 @@ var DesktopTools = class {
826
1888
  import net2 from "node:net";
827
1889
  import { readFile as readFile3 } from "node:fs/promises";
828
1890
  import { EventEmitter } from "node:events";
829
- import { randomUUID as randomUUID4 } from "node:crypto";
1891
+ import { randomUUID as randomUUID5 } from "node:crypto";
830
1892
  var inputTools = ["desktop_click", "desktop_type", "desktop_key", "desktop_scroll"];
831
1893
  var unknown = () => Object.assign(new AgentError("OUTCOME_UNKNOWN", 409), { possiblyApplied: true, stopConfirmed: false });
832
1894
  var safeOrigin = (value) => {
@@ -942,13 +2004,13 @@ var NativeDesktopPipe = class _NativeDesktopPipe extends EventEmitter {
942
2004
  if (this.ended) return Promise.reject(unknown());
943
2005
  if (this.pending.size >= 64) return Promise.reject(new AgentError("DESKTOP_INPUT_LIMIT", 429));
944
2006
  const id2 = ++this.nextId;
945
- return new Promise((resolve5, reject) => {
2007
+ return new Promise((resolve6, reject) => {
946
2008
  const timer = setTimeout(() => {
947
2009
  this.pending.delete(id2);
948
2010
  reject(unknown());
949
2011
  this.finish(unknown());
950
2012
  }, 6500);
951
- this.pending.set(id2, { resolve: resolve5, reject, timer });
2013
+ this.pending.set(id2, { resolve: resolve6, reject, timer });
952
2014
  try {
953
2015
  this.write({ ...message, id: id2 });
954
2016
  } catch (e) {
@@ -996,7 +2058,7 @@ var DesktopSessions = class {
996
2058
  forceKeyframes = /* @__PURE__ */ new Set();
997
2059
  flights = /* @__PURE__ */ new WeakMap();
998
2060
  sessions = /* @__PURE__ */ new Map();
999
- nonce = randomUUID4();
2061
+ nonce = randomUUID5();
1000
2062
  closing = false;
1001
2063
  timer;
1002
2064
  sockets = /* @__PURE__ */ new Set();
@@ -1034,7 +2096,7 @@ var DesktopSessions = class {
1034
2096
  if (mode === "control" && !controlAllowed) fail("SCOPE_DENIED", 403);
1035
2097
  await this.sweep();
1036
2098
  if (this.opening + [...this.sessions.values()].filter((s) => !s.closed).length >= 4 || this.sessions.size >= 256) fail("DESKTOP_SESSION_LIMIT", 429);
1037
- const id2 = `desktop-${randomUUID4()}`, token = secret();
2099
+ const id2 = `desktop-${randomUUID5()}`, token = secret();
1038
2100
  this.opening++;
1039
2101
  let native;
1040
2102
  try {
@@ -1097,6 +2159,13 @@ var DesktopSessions = class {
1097
2159
  const row = this.own(c, request?.sessionId);
1098
2160
  return this.stop(row, "SESSION_CLOSED");
1099
2161
  }
2162
+ async closeClientSession(clientId, request) {
2163
+ if (!request || typeof request !== "object" || Array.isArray(request) || Object.keys(request).some((key2) => !["sessionId", "botId", "taskId", "attemptId"].includes(key2))) fail("INVALID_ARGUMENT");
2164
+ for (const key2 of ["sessionId", "botId", "taskId", "attemptId"]) identifier(request[key2]);
2165
+ const row = this.sessions.get(request.sessionId);
2166
+ if (!row || row.context.clientId !== clientId || row.context.botId !== request.botId || row.context.taskId !== request.taskId || row.context.attemptId !== request.attemptId) fail("SCOPE_DENIED", 403);
2167
+ return this.stop(row, "SESSION_CLOSED");
2168
+ }
1100
2169
  hasOrigin(origin) {
1101
2170
  return [...this.sessions.values()].some((row) => !row.closed && row.origin === origin && row.context.expiresAt > Date.now());
1102
2171
  }
@@ -1212,7 +2281,7 @@ var DesktopSessions = class {
1212
2281
  fail("INVALID_ARGUMENT");
1213
2282
  }
1214
2283
  /** Called only after the HTTP server validates Host/network/TLS on upgrade. */
1215
- accept(socket, origin, allowedOrigins = []) {
2284
+ accept(socket, origin, allowedOrigins = [], peerClientId) {
1216
2285
  this.sockets.add(socket);
1217
2286
  let row, authenticating = false, alive2 = true;
1218
2287
  const authTimer = setTimeout(() => socket.close(1008, "Desktop authentication required"), this.options.authTimeoutMs ?? 5e3);
@@ -1278,6 +2347,7 @@ var DesktopSessions = class {
1278
2347
  try {
1279
2348
  const candidate = this.sessions.get(message.sessionId);
1280
2349
  if (message.type !== "auth" || typeof message.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(message.token) || !candidate || !matches(message.token, candidate.digest)) fail("AUTH_INVALID", 401);
2350
+ if (peerClientId !== void 0 && candidate.context.clientId !== peerClientId) fail("AUTH_INVALID", 401);
1281
2351
  if (candidate.closed || candidate.used) fail("DESKTOP_SESSION_CLOSED", 409);
1282
2352
  this.check(candidate.context);
1283
2353
  if (origin !== void 0 && (candidate.origin ? origin !== candidate.origin : !allowedOrigins.includes(origin))) fail("ORIGIN_DENIED", 403);
@@ -1381,7 +2451,7 @@ var DesktopSessions = class {
1381
2451
  }
1382
2452
  stop(row, reason) {
1383
2453
  this.forceKeyframes.delete(row);
1384
- if (row.stop) return row.stop;
2454
+ if (row.stop) return row.stopped ? Promise.resolve({ stopConfirmed: true }) : row.stop;
1385
2455
  row.closed = true;
1386
2456
  row.closedAt = Date.now();
1387
2457
  clearTimeout(row.frameTimer);
@@ -1432,7 +2502,7 @@ import { execFile, spawn } from "node:child_process";
1432
2502
  import { promisify } from "node:util";
1433
2503
  import { mkdir as mkdir2, open as open2, readFile as readFile4, lstat as lstat2 } from "node:fs/promises";
1434
2504
  import { isAbsolute, join as join3 } from "node:path";
1435
- import { randomUUID as randomUUID5 } from "node:crypto";
2505
+ import { randomUUID as randomUUID6 } from "node:crypto";
1436
2506
 
1437
2507
  // src/container-supervisor.mjs
1438
2508
  async function runContainerSupervisor() {
@@ -1596,9 +2666,9 @@ var ContainerCommands = class {
1596
2666
  child.kill("SIGKILL");
1597
2667
  }, 15e3);
1598
2668
  try {
1599
- const code = await new Promise((resolve5, reject) => {
2669
+ const code = await new Promise((resolve6, reject) => {
1600
2670
  child.once("error", reject);
1601
- child.once("close", resolve5);
2671
+ child.once("close", resolve6);
1602
2672
  });
1603
2673
  if (failed || code !== 0) fail("CONTAINER_OUTPUT_UNAVAILABLE", 503);
1604
2674
  return { stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) };
@@ -1727,7 +2797,7 @@ var ContainerCommands = class {
1727
2797
  }
1728
2798
  }
1729
2799
  async run({ id: id2, c, rootId, workspace, workspaceIdentity, executable, argv, timeout, validate, validateWorkspace, shouldCancel, artifact }) {
1730
- const name = "dsh-worker-" + sha(canonical({ machine: this.store.machineId, id: id2 })).slice(0, 40), nonce = randomUUID5(), digest2 = sha(canonical({ rootId, workspace, workspaceIdentity, executable, argv, image: this.config.image }));
2800
+ const name = "dsh-worker-" + sha(canonical({ machine: this.store.machineId, id: id2 })).slice(0, 40), nonce = randomUUID6(), digest2 = sha(canonical({ rootId, workspace, workspaceIdentity, executable, argv, image: this.config.image }));
1731
2801
  const deadlineAt = Math.min(c.expiresAt, Date.now() + timeout), supervisorFile = await this.supervisor();
1732
2802
  const args2 = containerArguments({ name, nonce, machineId: this.store.machineId, workspace, workspaceIdentity, config: this.config, executable, argv, deadlineAt, supervisorFile });
1733
2803
  this.store.db.prepare("INSERT INTO container_workers(operation_id,name,nonce,context,root_id,workspace,input_digest,phase,cid,reason,workspace_identity) VALUES (?,?,?,?,?,?,?,'prepared',NULL,NULL,?)").run(id2, name, nonce, canonical(c), rootId, workspace, digest2, workspaceIdentity);
@@ -1789,11 +2859,11 @@ var ContainerCommands = class {
1789
2859
  }
1790
2860
  this.store.db.prepare("UPDATE container_workers SET phase='starting' WHERE operation_id=?").run(id2);
1791
2861
  child = spawn(this.config.dockerExecutable, ["container", "start", "-a", cid], { env: this.dockerEnv, detached: true, stdio: ["ignore", "ignore", "ignore"], windowsHide: true });
1792
- await new Promise((resolve5, reject) => {
2862
+ await new Promise((resolve6, reject) => {
1793
2863
  child.once("error", reject);
1794
2864
  child.once("close", () => {
1795
2865
  closed2 = true;
1796
- resolve5();
2866
+ resolve6();
1797
2867
  });
1798
2868
  });
1799
2869
  await cancelling;
@@ -2185,7 +3255,7 @@ function reconcileRootsGeneration(db, generation) {
2185
3255
  import { constants as constants3 } from "node:fs";
2186
3256
  import { open as open4, lstat as lstat4, stat as stat4, realpath as realpath3, link, unlink as unlink3 } from "node:fs/promises";
2187
3257
  import path3 from "node:path";
2188
- import { randomBytes as randomBytes2 } from "node:crypto";
3258
+ import { randomBytes as randomBytes3 } from "node:crypto";
2189
3259
  var PINNED_WRITE_MAX_BYTES = 1048576;
2190
3260
  var PINNED_WRITE_MAX_LIMIT_BYTES = 16 * 1024 * 1024;
2191
3261
  var PINNED_WRITE_MAX_PATH_BYTES = 4096;
@@ -2424,7 +3494,7 @@ async function pinnedCreateNewFile(request) {
2424
3494
  try {
2425
3495
  await pinned.verifyHostPath(directoryPath);
2426
3496
  if (await pinned.lstatChild(targetName)) failWrite("TARGET_EXISTS", 409, { target: targetPath });
2427
- tempName = `${PINNED_WRITE_TEMP_PREFIX}${randomBytes2(16).toString("hex")}`;
3497
+ tempName = `${PINNED_WRITE_TEMP_PREFIX}${randomBytes3(16).toString("hex")}`;
2428
3498
  const temp = await open4(pinned.childPath(tempName), TEMP_FLAGS, 384).catch((e) => {
2429
3499
  const code = fsCode(e);
2430
3500
  if (code === "EEXIST") failWrite("PINNED_WRITE_FAILED", 500, { reason: "temp-name-collision" });
@@ -2513,15 +3583,15 @@ async function pinnedCreateNewFile(request) {
2513
3583
  }
2514
3584
 
2515
3585
  // src/cli-task.ts
2516
- import { readFile as readFile7, stat as stat6, writeFile as writeFile4 } from "node:fs/promises";
3586
+ import { readFile as readFile8, stat as stat6, writeFile as writeFile4 } from "node:fs/promises";
2517
3587
  import { isAbsolute as isAbsolute6, join as join7, relative, resolve as resolve2 } from "node:path";
2518
3588
 
2519
3589
  // ../packages/cli-runtime/src/agent-codex-runtime.mjs
2520
- import { randomBytes as randomBytes4 } from "node:crypto";
3590
+ import { randomBytes as randomBytes6 } from "node:crypto";
2521
3591
 
2522
3592
  // ../packages/cli-runtime/src/codex-session-core.mjs
2523
- import { createHash as createHash3 } from "node:crypto";
2524
- import { readFile as readFile6, realpath as realpath6, stat as stat5, mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
3593
+ import { createHash as createHash6 } from "node:crypto";
3594
+ import { readFile as readFile7, realpath as realpath6, stat as stat5, mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
2525
3595
  import { spawn as spawn3 } from "node:child_process";
2526
3596
  import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
2527
3597
 
@@ -2529,11 +3599,128 @@ import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
2529
3599
  import { spawn as spawn2 } from "node:child_process";
2530
3600
  import { EventEmitter as EventEmitter2 } from "node:events";
2531
3601
  import { StringDecoder } from "node:string_decoder";
2532
- import { access as access2, realpath as realpath4 } from "node:fs/promises";
2533
- import { isAbsolute as isAbsolute3, join as join4 } from "node:path";
3602
+ import { access as access2, lstat as lstat5, readFile as readFile6, readdir as readdir2, realpath as realpath4, rmdir, unlink as unlink4 } from "node:fs/promises";
3603
+ import { lstatSync as lstatSync2, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
3604
+ import { randomBytes as randomBytes4 } from "node:crypto";
3605
+ import { tmpdir } from "node:os";
3606
+ import { basename, dirname, isAbsolute as isAbsolute3, join as join4 } from "node:path";
2534
3607
  import { fileURLToPath } from "node:url";
3608
+
3609
+ // ../packages/cli-runtime/src/native-cli-output.mjs
3610
+ import { createHash as createHash4 } from "node:crypto";
3611
+ var FRAME = 262144;
3612
+ var RAW_FRAME = 18 * 1024 * 1024;
3613
+ var IMAGE = 6 * 1024 * 1024;
3614
+ var fail2 = () => {
3615
+ throw new Error("CLI_FRAME_LIMIT");
3616
+ };
3617
+ function receipt(frame, rawBytes) {
3618
+ const results = frame?.type === "user" && frame.message?.content;
3619
+ const toolResult = Array.isArray(results) && results.length > 0 && results.every((block) => block?.type === "tool_result" && typeof block.tool_use_id === "string");
3620
+ if (!toolResult) {
3621
+ if (rawBytes > FRAME) fail2();
3622
+ return frame;
3623
+ }
3624
+ let imageBytes = 0, images = 0, nodes = 0;
3625
+ const visit = (value, depth = 0) => {
3626
+ if (++nodes > 3e4 || depth > 24) fail2();
3627
+ if (!value || typeof value !== "object") return value;
3628
+ if (value.type === "image") {
3629
+ const source = value.source;
3630
+ const data = source?.type === "base64" ? source.data : value.data;
3631
+ const mediaType = source?.type === "base64" ? source.media_type : value.mimeType;
3632
+ if (typeof data !== "string" || data.length > IMAGE / 3 * 4 || !["image/png", "image/jpeg", "image/webp"].includes(mediaType)) fail2();
3633
+ const bytes = Buffer.from(data, "base64");
3634
+ if (!bytes.length || bytes.length > IMAGE || bytes.toString("base64") !== data || ++images > 32 || (imageBytes += bytes.length) > IMAGE * 2) fail2();
3635
+ return { type: "image", mediaType, bytes: bytes.length, sha256: createHash4("sha256").update(bytes).digest("hex") };
3636
+ }
3637
+ if (Array.isArray(value)) return value.map((item) => visit(item, depth + 1));
3638
+ return Object.fromEntries(Object.entries(value).map(([key2, item]) => [key2, visit(item, depth + 1)]));
3639
+ };
3640
+ const result = visit(frame);
3641
+ if (rawBytes > FRAME && !images || Buffer.byteLength(JSON.stringify(result)) > FRAME) fail2();
3642
+ return result;
3643
+ }
3644
+ var NativeCliOutput = class {
3645
+ #buffer = "";
3646
+ #bytes = 0;
3647
+ push(text) {
3648
+ this.#buffer += text;
3649
+ const frames = [];
3650
+ let end;
3651
+ while ((end = this.#buffer.indexOf("\n")) >= 0) {
3652
+ const line = this.#buffer.slice(0, end);
3653
+ this.#buffer = this.#buffer.slice(end + 1);
3654
+ const rawBytes = Buffer.byteLength(line);
3655
+ if (rawBytes > RAW_FRAME) fail2();
3656
+ if (!line.trim()) continue;
3657
+ const frame = receipt(JSON.parse(line), rawBytes);
3658
+ this.#bytes += Buffer.byteLength(JSON.stringify(frame));
3659
+ if (this.#bytes > 8 * 1024 * 1024) fail2();
3660
+ frames.push(frame);
3661
+ }
3662
+ if (Buffer.byteLength(this.#buffer) > RAW_FRAME) fail2();
3663
+ return frames;
3664
+ }
3665
+ };
3666
+
3667
+ // ../packages/cli-runtime/src/native-process.mjs
2535
3668
  var HERE = fileURLToPath(new URL(".", import.meta.url));
2536
3669
  var MAX_FRAME = 262144;
3670
+ var COMPILER_ROOT = "DSH_NATIVE_COMPILER_ROOT";
3671
+ var COMPILER_SCRATCH = "DSH_NATIVE_COMPILER_SCRATCH";
3672
+ var inheritedCompilerRoot = process.platform === "win32" ? process.env[COMPILER_ROOT] : void 0;
3673
+ var hostTemp = process.platform === "win32" ? tmpdir() : null;
3674
+ var compilerRoot = null;
3675
+ var compilerFailure = () => Object.assign(new Error("NATIVE_COMPILER_SCRATCH_UNAVAILABLE"), { code: "NATIVE_COMPILER_SCRATCH_UNAVAILABLE" });
3676
+ var sameDirectory = (info, expected) => info.isDirectory() && !info.isSymbolicLink() && info.dev === expected.dev && info.ino === expected.ino;
3677
+ var rootStamp = (path4) => `dsh-native-compiler-v1:${basename(path4)}
3678
+ `;
3679
+ function compilerDirectory() {
3680
+ try {
3681
+ if (!compilerRoot) {
3682
+ if (inheritedCompilerRoot !== void 0) {
3683
+ if (!isAbsolute3(inheritedCompilerRoot) || inheritedCompilerRoot.length > 140 || !/^dshc-[a-f0-9]{16}$/.test(basename(inheritedCompilerRoot))) throw compilerFailure();
3684
+ const info2 = lstatSync2(inheritedCompilerRoot, { bigint: true });
3685
+ if (!info2.isDirectory() || info2.isSymbolicLink() || realpathSync(inheritedCompilerRoot) !== inheritedCompilerRoot || readFileSync2(join4(inheritedCompilerRoot, ".dsh-root"), "utf8") !== rootStamp(inheritedCompilerRoot)) throw compilerFailure();
3686
+ compilerRoot = { path: inheritedCompilerRoot, info: info2 };
3687
+ } else {
3688
+ const parent = realpathSync(hostTemp);
3689
+ const path5 = join4(parent, `dshc-${randomBytes4(8).toString("hex")}`);
3690
+ if (path5.length > 140) throw compilerFailure();
3691
+ mkdirSync2(path5);
3692
+ writeFileSync2(join4(path5, ".dsh-root"), rootStamp(path5), { flag: "wx" });
3693
+ compilerRoot = { path: path5, info: lstatSync2(path5, { bigint: true }) };
3694
+ }
3695
+ }
3696
+ if (!sameDirectory(lstatSync2(compilerRoot.path, { bigint: true }), compilerRoot.info) || realpathSync(compilerRoot.path) !== compilerRoot.path) throw compilerFailure();
3697
+ const path4 = mkdtempSync(join4(compilerRoot.path, "c-"));
3698
+ const info = lstatSync2(path4, { bigint: true }), owner = randomBytes4(16).toString("hex");
3699
+ writeFileSync2(join4(path4, ".dsh-owner"), owner, { flag: "wx" });
3700
+ const root = compilerRoot;
3701
+ const cleanup = async () => {
3702
+ try {
3703
+ if (dirname(path4) !== root.path || !/^c-[A-Za-z0-9]{6}$/.test(basename(path4))) return false;
3704
+ if (!sameDirectory(await lstat5(root.path, { bigint: true }), root.info) || await realpath4(root.path) !== root.path) return false;
3705
+ if (!sameDirectory(await lstat5(path4, { bigint: true }), info) || await realpath4(path4) !== path4 || await readFile6(join4(path4, ".dsh-owner"), "utf8") !== owner) return false;
3706
+ const names = await readdir2(path4);
3707
+ if (names.length > 128) return false;
3708
+ for (const name of names) {
3709
+ const entry = await lstat5(join4(path4, name));
3710
+ if (!entry.isFile() || entry.isSymbolicLink()) return false;
3711
+ }
3712
+ for (const name of names) await unlink4(join4(path4, name));
3713
+ await rmdir(path4);
3714
+ return true;
3715
+ } catch {
3716
+ return false;
3717
+ }
3718
+ };
3719
+ return { root: root.path, path: path4, cleanup };
3720
+ } catch {
3721
+ throw compilerFailure();
3722
+ }
3723
+ }
2537
3724
  function privateEnvironment(home, source = process.env) {
2538
3725
  const env2 = {
2539
3726
  HOME: home,
@@ -2553,6 +3740,7 @@ function privateEnvironment(home, source = process.env) {
2553
3740
  };
2554
3741
  for (const key2 of ["SystemRoot", "windir", "ComSpec", "LANG", "LC_ALL"]) if (source[key2]) env2[key2] = source[key2];
2555
3742
  env2.PATH = process.platform === "win32" ? join4(source.SystemRoot ?? "C:\\Windows", "System32") : "/usr/bin:/bin";
3743
+ if (process.platform === "win32" && (compilerRoot?.path ?? inheritedCompilerRoot)) env2[COMPILER_ROOT] = compilerRoot?.path ?? inheritedCompilerRoot;
2556
3744
  return env2;
2557
3745
  }
2558
3746
  async function resolveNativeHelper({ python = "/usr/bin/python3" } = {}) {
@@ -2568,27 +3756,28 @@ async function resolveNativeHelper({ python = "/usr/bin/python3" } = {}) {
2568
3756
  function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironment(home) } = {}) {
2569
3757
  const events = new EventEmitter2();
2570
3758
  const args2 = helper.platform === "win32" ? [...helper.prefix, "-Mode", mode] : [...helper.prefix, mode];
2571
- const child = spawn2(helper.executable, args2, { cwd: home, env: env2, windowsHide: true, shell: false, stdio: ["pipe", "pipe", "pipe"] });
2572
- let tail = "", buffer = "", bytes = 0, closed2 = false;
3759
+ const compiler = helper.platform === "win32" ? compilerDirectory() : null;
3760
+ const helperEnv = compiler ? Object.fromEntries(Object.entries(env2).filter(([key2]) => ![COMPILER_ROOT, COMPILER_SCRATCH].includes(key2.toUpperCase()))) : env2;
3761
+ if (compiler) {
3762
+ helperEnv[COMPILER_ROOT] = compiler.root;
3763
+ helperEnv[COMPILER_SCRATCH] = compiler.path;
3764
+ }
3765
+ const child = spawn2(helper.executable, args2, { cwd: home, env: helperEnv, windowsHide: true, shell: false, stdio: ["pipe", "pipe", "pipe"] });
3766
+ let tail = "", buffer = "", bytes = 0, closed2 = false, treeStopped = false;
2573
3767
  const decoder = new StringDecoder("utf8"), cliDecoder = new StringDecoder("utf8");
2574
- let cliBuffer = "";
3768
+ const cliOutput = new NativeCliOutput();
2575
3769
  const frame = (value) => {
3770
+ if (mode === "supervise" && value.control === "stopped" && value.treeStopped === true) treeStopped = true;
2576
3771
  if (mode === "supervise" && value.type === "cli-output") {
2577
3772
  if (typeof value.data !== "string" || value.data.length > MAX_FRAME * 2) throw new Error("INVALID_CLI_FRAME");
2578
- cliBuffer += cliDecoder.write(Buffer.from(value.data, "base64"));
2579
- if (Buffer.byteLength(cliBuffer) > MAX_FRAME) throw new Error("CLI_FRAME_LIMIT");
2580
- let end;
2581
- while ((end = cliBuffer.indexOf("\n")) >= 0) {
2582
- const line = cliBuffer.slice(0, end);
2583
- cliBuffer = cliBuffer.slice(end + 1);
2584
- if (line.trim()) events.emit("message", JSON.parse(line));
2585
- }
3773
+ for (const message of cliOutput.push(cliDecoder.write(Buffer.from(value.data, "base64")))) events.emit("message", message);
2586
3774
  } else events.emit(value.control ? "control" : "message", value);
2587
3775
  };
2588
- const exit = new Promise((resolve5) => child.once("close", (code, signal) => {
3776
+ const exit = new Promise((resolve6) => child.once("close", async (code, signal) => {
2589
3777
  closed2 = true;
3778
+ if (compiler && code === 0 && signal === null && (mode !== "supervise" || treeStopped)) await compiler.cleanup();
2590
3779
  events.emit("closed", { code, signal });
2591
- resolve5({ code, signal });
3780
+ resolve6({ code, signal });
2592
3781
  }));
2593
3782
  child.on("error", (error2) => events.emit("failure", error2));
2594
3783
  child.stdin.on("error", () => {
@@ -2599,7 +3788,8 @@ function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironmen
2599
3788
  child.stdout.on("data", (chunk) => {
2600
3789
  bytes += chunk.length;
2601
3790
  buffer += decoder.write(chunk);
2602
- if (bytes > 8 * 1024 * 1024 || Buffer.byteLength(buffer) > MAX_FRAME) {
3791
+ const boundedStream = mode === "client" || mode === "server" || mode === "supervise";
3792
+ if (!boundedStream && bytes > 8 * 1024 * 1024 || Buffer.byteLength(buffer) > MAX_FRAME) {
2603
3793
  events.emit("failure", new Error("OUTPUT_LIMIT"));
2604
3794
  child.stdin.end();
2605
3795
  return;
@@ -2617,17 +3807,17 @@ function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironmen
2617
3807
  }
2618
3808
  }
2619
3809
  });
2620
- const send = (value) => new Promise((resolve5, reject) => {
3810
+ const send = (value) => new Promise((resolve6, reject) => {
2621
3811
  const wire = JSON.stringify(value) + "\n";
2622
3812
  if (closed2 || Buffer.byteLength(wire) > MAX_FRAME) {
2623
3813
  reject(new Error(closed2 ? "NATIVE_CLOSED" : "INPUT_LIMIT"));
2624
3814
  return;
2625
3815
  }
2626
- child.stdin.write(wire, (error2) => error2 ? reject(error2) : resolve5());
3816
+ child.stdin.write(wire, (error2) => error2 ? reject(error2) : resolve6());
2627
3817
  });
2628
3818
  if (spec) void send(spec).catch((error2) => events.emit("failure", error2));
2629
3819
  function waitControl(name, timeoutMs = 15e3) {
2630
- return new Promise((resolve5, reject) => {
3820
+ return new Promise((resolve6, reject) => {
2631
3821
  const clean = () => {
2632
3822
  clearTimeout(timer);
2633
3823
  events.off("control", control2);
@@ -2637,7 +3827,7 @@ function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironmen
2637
3827
  const control2 = (value) => {
2638
3828
  if (value.control === name) {
2639
3829
  clean();
2640
- resolve5(value);
3830
+ resolve6(value);
2641
3831
  }
2642
3832
  };
2643
3833
  const ended = () => {
@@ -2669,8 +3859,8 @@ async function capabilityProbe({ home, python } = {}) {
2669
3859
  try {
2670
3860
  const helper = await resolveNativeHelper({ python });
2671
3861
  proc = nativeProcess(helper, "probe", null, { home });
2672
- const value = await new Promise((resolve5, reject) => {
2673
- proc.events.once("message", resolve5);
3862
+ const value = await new Promise((resolve6, reject) => {
3863
+ proc.events.once("message", resolve6);
2674
3864
  proc.events.once("failure", reject);
2675
3865
  proc.events.once("closed", () => reject(new Error("NATIVE_PROBE_FAILED")));
2676
3866
  });
@@ -2685,8 +3875,8 @@ async function capabilityProbe({ home, python } = {}) {
2685
3875
  async function protectProfile(helper, home) {
2686
3876
  if (helper.platform !== "win32") return;
2687
3877
  const proc = nativeProcess(helper, "protect", { path: home }, { home });
2688
- const protectedValue = await new Promise((resolve5, reject) => {
2689
- proc.events.once("message", resolve5);
3878
+ const protectedValue = await new Promise((resolve6, reject) => {
3879
+ proc.events.once("message", resolve6);
2690
3880
  proc.events.once("failure", reject);
2691
3881
  proc.events.once("closed", () => reject(new Error("PROFILE_ACL_FAILED")));
2692
3882
  });
@@ -2696,9 +3886,9 @@ async function protectProfile(helper, home) {
2696
3886
  }
2697
3887
 
2698
3888
  // ../packages/capability-broker/src/cli-bootstrap.mjs
2699
- import { generateKeyPairSync, sign, randomBytes as randomBytes3 } from "node:crypto";
2700
- import { mkdir as mkdir4, writeFile as writeFile2, mkdtemp, chmod, lstat as lstat5, realpath as realpath5, rmdir } from "node:fs/promises";
2701
- import { tmpdir } from "node:os";
3889
+ import { generateKeyPairSync, sign as sign2, randomBytes as randomBytes5 } from "node:crypto";
3890
+ import { mkdir as mkdir4, writeFile as writeFile2, mkdtemp, chmod, lstat as lstat6, realpath as realpath5, rmdir as rmdir2 } from "node:fs/promises";
3891
+ import { tmpdir as tmpdir2 } from "node:os";
2702
3892
  import { isAbsolute as isAbsolute4, join as join5 } from "node:path";
2703
3893
  import { performance as performance2 } from "node:perf_hooks";
2704
3894
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -2722,14 +3912,65 @@ var PolicyError = class extends Error {
2722
3912
  }
2723
3913
  };
2724
3914
 
3915
+ // ../packages/capability-broker/src/cli-bridge-response.mjs
3916
+ import { createHash as createHash5 } from "node:crypto";
3917
+ var CHUNK_BYTES = 128 * 1024;
3918
+ var MAX_RESPONSE = 9 * 1024 * 1024;
3919
+ var MAX_BUFFERED = 16 * 1024 * 1024;
3920
+ var fail3 = () => {
3921
+ throw new Error("INVALID_BRIDGE_RESPONSE");
3922
+ };
3923
+ var sha2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
3924
+ function* bridgeResponseFrames(id2, value) {
3925
+ let bytes = Buffer.from(JSON.stringify({ type: "response", id: id2, ...value }));
3926
+ if (bytes.length > MAX_RESPONSE) bytes = Buffer.from(JSON.stringify({ type: "response", id: id2, ok: false, code: "OUTPUT_LIMIT" }));
3927
+ if (bytes.length <= CHUNK_BYTES) {
3928
+ yield JSON.parse(bytes.toString("utf8"));
3929
+ return;
3930
+ }
3931
+ yield { type: "response-start", id: id2, bytes: bytes.length };
3932
+ for (let offset = 0, index = 0; offset < bytes.length; offset += CHUNK_BYTES, index++) {
3933
+ yield { type: "response-chunk", id: id2, index, data: bytes.subarray(offset, offset + CHUNK_BYTES).toString("base64") };
3934
+ }
3935
+ yield { type: "response-end", id: id2, sha256: sha2(bytes) };
3936
+ }
3937
+ function normalizeBridgeMcpResult(value) {
3938
+ if (!value || !Array.isArray(value.content) || value.content.length > 32 || Object.keys(value).some((key2) => !["content", "isError"].includes(key2)) || value.isError !== void 0 && typeof value.isError !== "boolean") fail3();
3939
+ for (const block of value.content) {
3940
+ if (block?.type === "text") {
3941
+ if (Object.keys(block).some((key2) => !["type", "text"].includes(key2)) || typeof block.text !== "string") fail3();
3942
+ } else if (block?.type === "image") {
3943
+ if (Object.keys(block).some((key2) => !["type", "data", "mimeType"].includes(key2)) || !["image/png", "image/jpeg", "image/webp"].includes(block.mimeType) || typeof block.data !== "string" || block.data.length > 8 * 1024 * 1024) fail3();
3944
+ const bytes = Buffer.from(block.data, "base64");
3945
+ if (!bytes.length || bytes.toString("base64") !== block.data || bytes.length > 6 * 1024 * 1024) fail3();
3946
+ } else fail3();
3947
+ }
3948
+ return value;
3949
+ }
3950
+
2725
3951
  // ../packages/capability-broker/src/cli-bootstrap.mjs
2726
3952
  var BRIDGE = fileURLToPath2(new URL("./cli-stdio-bridge.mjs", import.meta.url));
2727
3953
  var MAX_BYTES = 262144;
2728
- async function createCliBootstrap({ authority, subject, binding, tools, handlers, home, ttlMs = 3e4, helper }) {
3954
+ var REQUEST_KEYS = ["type", "id", "op", "name", "args"];
3955
+ var capabilityOf = (handler) => {
3956
+ const value = handler.capability ?? handler.definition.name;
3957
+ if (typeof value !== "string" || !value || Buffer.byteLength(value) > 256) throw new TypeError("INVALID_HANDLER_CAPABILITY");
3958
+ return value;
3959
+ };
3960
+ async function createCliBootstrap({ authority, subject, binding, tools, handlers, home, ttlMs = 3e4, helper, contextFor = null }) {
2729
3961
  if (!isAbsolute4(home)) throw new TypeError("An isolated absolute home is required");
3962
+ if (contextFor !== null && typeof contextFor !== "function") throw new TypeError("INVALID_HOST_CONTEXT_CALLBACK");
2730
3963
  const ownedBinding = Object.freeze({ ...binding });
2731
3964
  const registry = new Map(handlers.map((handler) => [handler.definition.name, handler]));
2732
3965
  if (registry.size !== handlers.length || tools.some((name) => !registry.has(name))) throw new TypeError("Unknown or duplicate bridge handler");
3966
+ const selected = new Map(tools.map((name) => [name, { handler: registry.get(name), capability: capabilityOf(registry.get(name)) }]));
3967
+ const leaseTools = [...new Set([...selected.values()].map((entry) => entry.capability))];
3968
+ const byCapability = /* @__PURE__ */ new Map();
3969
+ for (const entry of selected.values()) {
3970
+ const sharing = byCapability.get(entry.capability);
3971
+ if (sharing) sharing.push(entry.handler);
3972
+ else byCapability.set(entry.capability, [entry.handler]);
3973
+ }
2733
3974
  if (!Number.isSafeInteger(ttlMs) || ttlMs < 1e3 || ttlMs > 72e5 || typeof authority.renew !== "function") throw new TypeError("BOUNDED_RENEWABLE_LEASE_REQUIRED");
2734
3975
  const leaseTtlMs = Math.min(3e4, ttlMs);
2735
3976
  const startedAt = performance2.now();
@@ -2738,45 +3979,62 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2738
3979
  const publicSpki = publicKey.export({ type: "spki", format: "der" }).toString("base64url");
2739
3980
  await mkdir4(home, { recursive: true, mode: 448 });
2740
3981
  await mkdir4(join5(home, "tmp"), { recursive: true, mode: 448 });
2741
- const descriptorPath2 = join5(home, `bridge-${randomBytes3(12).toString("hex")}.json`);
3982
+ const descriptorPath2 = join5(home, `bridge-${randomBytes5(12).toString("hex")}.json`);
2742
3983
  let socketDirectory;
2743
- let endpoint = process.platform === "win32" ? `dsh-cli-${randomBytes3(18).toString("hex")}` : join5(home, `p-${randomBytes3(6).toString("hex")}.sock`);
3984
+ let endpoint = process.platform === "win32" ? `dsh-cli-${randomBytes5(18).toString("hex")}` : join5(home, `p-${randomBytes5(6).toString("hex")}.sock`);
2744
3985
  if (process.platform === "linux" && Buffer.byteLength(endpoint) > 103) {
2745
- let parent = await realpath5(tmpdir());
3986
+ let parent = await realpath5(tmpdir2());
2746
3987
  if (Buffer.byteLength(join5(parent, "dsh-cli-XXXXXX", "p.sock")) > 103) parent = await realpath5("/tmp");
2747
3988
  socketDirectory = await mkdtemp(join5(parent, "dsh-cli-"));
2748
3989
  await chmod(socketDirectory, 448);
2749
- const stat8 = await lstat5(socketDirectory);
3990
+ const stat8 = await lstat6(socketDirectory);
2750
3991
  if (!stat8.isDirectory() || stat8.isSymbolicLink() || stat8.uid !== process.getuid() || (stat8.mode & 63) !== 0) {
2751
- await rmdir(socketDirectory);
3992
+ await rmdir2(socketDirectory);
2752
3993
  throw new Error("PRIVATE_SOCKET_DIRECTORY_REQUIRED");
2753
3994
  }
2754
3995
  endpoint = join5(socketDirectory, "p.sock");
2755
3996
  if (Buffer.byteLength(endpoint) > 103) {
2756
- await rmdir(socketDirectory);
3997
+ await rmdir2(socketDirectory);
2757
3998
  throw new Error("PRIVATE_SOCKET_PATH_TOO_LONG");
2758
3999
  }
2759
4000
  }
2760
4001
  let lease;
2761
4002
  try {
2762
- lease = authority.issue(subject, { binding: ownedBinding, tools, ttlMs: leaseTtlMs });
4003
+ lease = authority.issue(subject, { binding: ownedBinding, tools: leaseTools, ttlMs: leaseTtlMs });
2763
4004
  } catch (error2) {
2764
- if (socketDirectory) await rmdir(socketDirectory);
4005
+ if (socketDirectory) await rmdir2(socketDirectory);
2765
4006
  throw error2;
2766
4007
  }
2767
4008
  let relay, closed2 = false, bound = false, connected = false, closing;
2768
- let accepted, rejected = 0, requests = 0, replies = 0;
4009
+ let accepted, rejected = 0, requests = 0, replies = 0, highestRequestId = 0;
2769
4010
  const active = /* @__PURE__ */ new Map();
2770
4011
  const release = () => {
2771
4012
  authority.release(lease.token);
2772
4013
  for (const controller of active.values()) controller.abort(new Error("BRIDGE_CLOSED"));
2773
4014
  };
2774
- const reply = (id2, value) => {
4015
+ const reply = async (id2, value, signal) => {
2775
4016
  if (!relay || relay.closed) return;
2776
4017
  replies++;
2777
- const wire = { type: "response", id: id2, ...value };
2778
- void relay.send(Buffer.byteLength(JSON.stringify(wire)) > MAX_BYTES ? { type: "response", id: id2, ok: false, code: "OUTPUT_LIMIT" } : wire).catch(() => {
2779
- });
4018
+ for (const frame of bridgeResponseFrames(id2, value)) {
4019
+ signal?.throwIfAborted();
4020
+ await relay.send(frame);
4021
+ }
4022
+ };
4023
+ const hostContext = (op, name, id2) => {
4024
+ if (!contextFor) return null;
4025
+ let value;
4026
+ try {
4027
+ value = contextFor({ op, name, id: id2 });
4028
+ } catch (error2) {
4029
+ throw error2 instanceof PolicyError ? error2 : new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-context" });
4030
+ }
4031
+ if (!value || typeof value !== "object" || typeof value.then === "function") throw new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-context" });
4032
+ return value;
4033
+ };
4034
+ const requestSignal = (controller, context) => {
4035
+ const signal = context?.signal ?? null;
4036
+ if (signal !== null && !(signal instanceof AbortSignal)) throw new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-signal" });
4037
+ return signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
2780
4038
  };
2781
4039
  async function request(message) {
2782
4040
  requests++;
@@ -2787,28 +4045,45 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2787
4045
  }
2788
4046
  if (message.type !== "request") return;
2789
4047
  const id2 = message.id;
4048
+ if (id2 <= highestRequestId) {
4049
+ void reply(id2, { ok: false, code: "DUPLICATE_REQUEST" }).catch(() => {
4050
+ });
4051
+ return;
4052
+ }
4053
+ highestRequestId = id2;
2790
4054
  try {
2791
4055
  if (closed2) throw new PolicyError("BRIDGE_CLOSED");
2792
- if (Buffer.byteLength(JSON.stringify(message)) > MAX_BYTES || Object.keys(message).some((key2) => !["type", "id", "op", "name", "args"].includes(key2))) throw new PolicyError("INVALID_REQUEST");
2793
- if (active.has(id2) || active.size >= 16) throw new PolicyError("BUSY");
4056
+ if (Buffer.byteLength(JSON.stringify(message)) > MAX_BYTES || Object.keys(message).some((key2) => !REQUEST_KEYS.includes(key2))) throw new PolicyError("INVALID_REQUEST");
4057
+ if (active.size >= 16) throw new PolicyError("BUSY");
2794
4058
  if (message.op === "list") {
2795
- reply(id2, { ok: true, value: authority.list(lease.token, ownedBinding).map((name) => registry.get(name).definition) });
4059
+ const context2 = hostContext("list", null, id2);
4060
+ requestSignal(new AbortController(), context2).throwIfAborted();
4061
+ await reply(id2, { ok: true, value: authority.list(lease.token, ownedBinding).flatMap((capability) => byCapability.get(capability) ?? []).map((handler) => handler.definition) });
2796
4062
  return;
2797
4063
  }
2798
4064
  if (message.op !== "call") throw new PolicyError("INVALID_REQUEST");
4065
+ const entry = selected.get(message.name);
4066
+ if (!entry) throw new PolicyError("TOOL_DENIED");
4067
+ const context = hostContext("call", message.name, id2);
2799
4068
  const controller = new AbortController();
4069
+ const signal = requestSignal(controller, context);
2800
4070
  active.set(id2, controller);
2801
4071
  try {
2802
- const value = await authority.run(lease.token, ownedBinding, message.name, ({ signal }) => {
2803
- const handler = registry.get(message.name);
2804
- return handler.execute(handler.parse(message.args), { signal, binding: ownedBinding });
2805
- }, { signal: controller.signal });
2806
- reply(id2, { ok: true, value });
4072
+ signal.throwIfAborted();
4073
+ const value = await authority.run(lease.token, ownedBinding, entry.capability, ({ signal: authoritySignal }) => {
4074
+ authoritySignal.throwIfAborted();
4075
+ return entry.handler.execute(entry.handler.parse(message.args), { signal: authoritySignal, binding: ownedBinding, context });
4076
+ }, { signal });
4077
+ signal.throwIfAborted();
4078
+ const mcpResult = entry.handler.render ? normalizeBridgeMcpResult(await entry.handler.render(value)) : null;
4079
+ signal.throwIfAborted();
4080
+ await reply(id2, { ok: true, ...mcpResult ? { mcpResult } : { value } }, signal);
2807
4081
  } finally {
2808
4082
  active.delete(id2);
2809
4083
  }
2810
4084
  } catch (error2) {
2811
- reply(id2, { ok: false, code: error2 instanceof PolicyError ? error2.code : "HANDLER_REFUSED" });
4085
+ await reply(id2, { ok: false, code: error2 instanceof PolicyError ? error2.code : "HANDLER_REFUSED" }).catch(() => {
4086
+ });
2812
4087
  }
2813
4088
  }
2814
4089
  const close = () => {
@@ -2820,8 +4095,8 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2820
4095
  closing = (async () => {
2821
4096
  if (relay) {
2822
4097
  relay.end();
2823
- const stopped = await Promise.race([relay.exit.then(() => true), new Promise((resolve5) => {
2824
- const t = setTimeout(() => resolve5(false), 3e3);
4098
+ const stopped = await Promise.race([relay.exit.then(() => true), new Promise((resolve6) => {
4099
+ const t = setTimeout(() => resolve6(false), 3e3);
2825
4100
  t.unref();
2826
4101
  })]);
2827
4102
  if (!stopped) {
@@ -2829,7 +4104,7 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2829
4104
  await relay.exit;
2830
4105
  }
2831
4106
  }
2832
- if (socketDirectory) await rmdir(socketDirectory).catch(() => {
4107
+ if (socketDirectory) await rmdir2(socketDirectory).catch(() => {
2833
4108
  });
2834
4109
  })();
2835
4110
  return closing;
@@ -2890,7 +4165,7 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
2890
4165
  hostPid: process.pid,
2891
4166
  binding: ownedBinding
2892
4167
  });
2893
- const descriptor = JSON.stringify({ payload, signature: sign(null, Buffer.from(payload), privateKey).toString("base64url") });
4168
+ const descriptor = JSON.stringify({ payload, signature: sign2(null, Buffer.from(payload), privateKey).toString("base64url") });
2894
4169
  await writeFile2(descriptorPath2, descriptor, { flag: "wx", mode: 384 });
2895
4170
  };
2896
4171
  return Object.freeze({
@@ -3026,15 +4301,15 @@ function assertEffectiveProfile(result, { requestedModel, bridge, home, tools, s
3026
4301
  }
3027
4302
 
3028
4303
  // ../packages/cli-runtime/src/codex-session-core.mjs
3029
- var digest = (value) => createHash3("sha256").update(value).digest("hex");
4304
+ var digest = (value) => createHash6("sha256").update(value).digest("hex");
3030
4305
  var id = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
3031
4306
  var validText = (value) => typeof value === "string" && value.trim() && !value.includes("\0") && Buffer.byteLength(value) <= 64e3;
3032
- async function pinCodexExecutable({ executable, sha256, version }, home) {
3033
- if (!isAbsolute5(executable) || !/^[a-f0-9]{64}$/.test(sha256) || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new TypeError("PINNED_NATIVE_ENTRY_REQUIRED");
4307
+ async function pinCodexExecutable({ executable, sha256: sha2562, version }, home) {
4308
+ if (!isAbsolute5(executable) || !/^[a-f0-9]{64}$/.test(sha2562) || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new TypeError("PINNED_NATIVE_ENTRY_REQUIRED");
3034
4309
  const path4 = await realpath6(executable);
3035
4310
  if (!(await stat5(path4)).isFile() || process.platform === "win32" && !path4.toLowerCase().endsWith(".exe")) throw new Error("NATIVE_ENTRY_REQUIRED");
3036
- if (digest(await readFile6(path4)) !== sha256) throw new Error("CLI_ENTRY_CHANGED");
3037
- const reported = await new Promise((resolve5, reject) => {
4311
+ if (digest(await readFile7(path4)) !== sha2562) throw new Error("CLI_ENTRY_CHANGED");
4312
+ const reported = await new Promise((resolve6, reject) => {
3038
4313
  const child = spawn3(path4, ["--version"], { cwd: home, env: privateEnvironment(home), windowsHide: true, shell: false, stdio: ["ignore", "pipe", "pipe"] });
3039
4314
  let output = "", error2 = "";
3040
4315
  const timer = setTimeout(() => {
@@ -3051,12 +4326,12 @@ async function pinCodexExecutable({ executable, sha256, version }, home) {
3051
4326
  child.once("error", reject);
3052
4327
  child.once("close", (code) => {
3053
4328
  clearTimeout(timer);
3054
- if (code === 0) resolve5(output.trim());
4329
+ if (code === 0) resolve6(output.trim());
3055
4330
  else reject(new Error(`CLI_VERSION_FAILED: ${error2.slice(0, 128)}`));
3056
4331
  });
3057
4332
  });
3058
4333
  if (reported !== `codex-cli ${version}`) throw new Error("CLI_VERSION_MISMATCH");
3059
- return Object.freeze({ path: path4, sha256, version });
4334
+ return Object.freeze({ path: path4, sha256: sha2562, version });
3060
4335
  }
3061
4336
  async function createCodexSessionCore({
3062
4337
  authority,
@@ -3101,6 +4376,7 @@ async function createCodexSessionCore({
3101
4376
  await protectProfile(probe.helper, home);
3102
4377
  const entry = await pinCodexExecutable(suppliedEntry, home);
3103
4378
  const bridge = await createCliBootstrap({ authority, subject, binding, tools, handlers, home: join6(home, "b"), ttlMs: deadlineMs, helper: probe.helper });
4379
+ const compilerRoot2 = privateEnvironment(home).DSH_NATIVE_COMPILER_ROOT;
3104
4380
  const config = [
3105
4381
  `model = ${JSON.stringify(requestedModel)}`,
3106
4382
  'model_provider = "openai"',
@@ -3130,15 +4406,16 @@ async function createCodexSessionCore({
3130
4406
  `HOME = ${JSON.stringify(home)}`,
3131
4407
  `USERPROFILE = ${JSON.stringify(home)}`,
3132
4408
  `TEMP = ${JSON.stringify(join6(home, "tmp"))}`,
3133
- `TMP = ${JSON.stringify(join6(home, "tmp"))}`
4409
+ `TMP = ${JSON.stringify(join6(home, "tmp"))}`,
4410
+ ...compilerRoot2 ? [`DSH_NATIVE_COMPILER_ROOT = ${JSON.stringify(compilerRoot2)}`] : []
3134
4411
  ].join("\n") + "\n";
3135
4412
  await writeFile3(join6(home, "config.toml"), config, { flag: "wx", mode: 384 });
3136
4413
  if (provisionCredentials) await provisionCredentials(home);
3137
- if (digest(await readFile6(join6(home, "config.toml"))) !== digest(Buffer.from(config))) {
4414
+ if (digest(await readFile7(join6(home, "config.toml"))) !== digest(Buffer.from(config))) {
3138
4415
  await bridge.close();
3139
4416
  throw new Error("CLI_PROFILE_CHANGED_DURING_PROVISIONING");
3140
4417
  }
3141
- if (digest(await readFile6(entry.path)) !== entry.sha256) {
4418
+ if (digest(await readFile7(entry.path)) !== entry.sha256) {
3142
4419
  await bridge.close();
3143
4420
  throw new Error("CLI_ENTRY_CHANGED");
3144
4421
  }
@@ -3152,7 +4429,7 @@ async function createCodexSessionCore({
3152
4429
  for (const value of pending.values()) value.reject(error2);
3153
4430
  pending.clear();
3154
4431
  };
3155
- const rpc = (method, params, timeoutMs = 3e4) => new Promise((resolve5, reject) => {
4432
+ const rpc = (method, params, timeoutMs = 3e4) => new Promise((resolve6, reject) => {
3156
4433
  if (closed2 || fatal) {
3157
4434
  reject(fatal ?? new Error("CLI_CLOSED"));
3158
4435
  return;
@@ -3166,7 +4443,7 @@ async function createCodexSessionCore({
3166
4443
  clearTimeout(timer2);
3167
4444
  fn(value);
3168
4445
  };
3169
- pending.set(id2, { resolve: settle(resolve5), reject: settle(reject) });
4446
+ pending.set(id2, { resolve: settle(resolve6), reject: settle(reject) });
3170
4447
  void supervisor.send({ id: id2, method, params }).catch((error2) => {
3171
4448
  const value = pending.get(id2);
3172
4449
  pending.delete(id2);
@@ -3182,11 +4459,11 @@ async function createCodexSessionCore({
3182
4459
  stopPromise = (async () => {
3183
4460
  await bridge.close();
3184
4461
  if (!fatal && !signal?.aborted && ["completed", "failed"].includes(evidence.snapshot().terminal) && !stopEvidence?.treeStopped) {
3185
- await new Promise((resolve5) => {
4462
+ await new Promise((resolve6) => {
3186
4463
  const finish = () => {
3187
4464
  clearTimeout(grace);
3188
4465
  signal?.removeEventListener("abort", finish);
3189
- resolve5();
4466
+ resolve6();
3190
4467
  };
3191
4468
  const grace = setTimeout(finish, 1e3);
3192
4469
  supervisor.exit.then(finish);
@@ -3195,8 +4472,8 @@ async function createCodexSessionCore({
3195
4472
  });
3196
4473
  }
3197
4474
  supervisor.end();
3198
- const result = await Promise.race([supervisor.exit, new Promise((resolve5) => {
3199
- const t = setTimeout(() => resolve5(null), 8e3);
4475
+ const result = await Promise.race([supervisor.exit, new Promise((resolve6) => {
4476
+ const t = setTimeout(() => resolve6(null), 8e3);
3200
4477
  t.unref();
3201
4478
  })]);
3202
4479
  if (!result) {
@@ -3330,11 +4607,11 @@ async function createCodexSessionCore({
3330
4607
  activeTurn = true;
3331
4608
  requestCount++;
3332
4609
  try {
3333
- const completion = new Promise((resolve5, reject) => {
4610
+ const completion = new Promise((resolve6, reject) => {
3334
4611
  const listener = (_message, update) => {
3335
4612
  if (update.completed) {
3336
4613
  notifications.delete(listener);
3337
- resolve5(evidence.snapshot());
4614
+ resolve6(evidence.snapshot());
3338
4615
  }
3339
4616
  };
3340
4617
  notifications.add(listener);
@@ -3408,7 +4685,7 @@ function agentAuthority(context, validate, requestedModel, catalogProvider, cata
3408
4685
  issue(_subject, { binding, tools, ttlMs }) {
3409
4686
  validate(context);
3410
4687
  if (binding?.location !== "remote" || !Array.isArray(tools) || tools.length || !Number.isSafeInteger(ttlMs) || ttlMs < 1 || ttlMs > 3e5) throw new Error("AGENT_CLI_LEASE_DENIED");
3411
- const token = randomBytes4(32).toString("base64url");
4688
+ const token = randomBytes6(32).toString("base64url");
3412
4689
  leases.set(token, { binding: bindingKey(binding), tools: new Set(tools), expiresAt: Math.min(Date.now() + ttlMs, context.expiresAt) });
3413
4690
  return { token };
3414
4691
  },
@@ -3521,7 +4798,7 @@ var CliTasks = class {
3521
4798
  }
3522
4799
  async provision(profile, c, home) {
3523
4800
  const reference = profile.bots[c.botId].credentialRef;
3524
- const bytes = await readFile7(reference);
4801
+ const bytes = await readFile8(reference);
3525
4802
  if (bytes.length > 65536) fail("CLI_CREDENTIAL_PROVISION_FAILED");
3526
4803
  await writeFile4(join7(home, "auth.json"), bytes, { flag: "wx", mode: 384 });
3527
4804
  }
@@ -3617,19 +4894,19 @@ var CliTasks = class {
3617
4894
  if (identity) await identity.pin.close();
3618
4895
  hooks.clearProcess();
3619
4896
  }
3620
- const base = session?.snapshot(), receipt = identity && base ? this.remoteReceipt(identity, base) : null, stopConfirmed = stop?.treeStopped === true;
3621
- if (receipt && stopConfirmed && controller.signal.aborted && !["completed", "failed"].includes(receipt.terminal)) receipt.terminal = "interrupted";
3622
- const terminal2 = receipt?.terminal === "completed" ? "succeeded" : receipt?.terminal === "failed" ? "failed" : receipt?.terminal === "interrupted" ? "cancelled" : "unknown";
4897
+ const base = session?.snapshot(), receipt2 = identity && base ? this.remoteReceipt(identity, base) : null, stopConfirmed = stop?.treeStopped === true;
4898
+ if (receipt2 && stopConfirmed && controller.signal.aborted && !["completed", "failed"].includes(receipt2.terminal)) receipt2.terminal = "interrupted";
4899
+ const terminal2 = receipt2?.terminal === "completed" ? "succeeded" : receipt2?.terminal === "failed" ? "failed" : receipt2?.terminal === "interrupted" ? "cancelled" : "unknown";
3623
4900
  const status = !stopConfirmed ? "unknown" : error2 && terminal2 === "succeeded" ? "unknown" : terminal2;
3624
4901
  const code = status === "unknown" ? "OUTCOME_UNKNOWN" : status === "cancelled" ? "CANCELLED" : status === "failed" ? error2?.code ?? "CLI_TASK_FAILED" : null;
3625
4902
  const result = {
3626
4903
  kind: "remote-cli",
3627
4904
  schemaVersion: 1,
3628
- receipt,
4905
+ receipt: receipt2,
3629
4906
  stopConfirmed,
3630
4907
  stopEvidence: stop ?? { treeStopped: false, status: "unconfirmed" },
3631
4908
  exitCode: Number.isSafeInteger(stop?.exitCode) ? stop.exitCode : null,
3632
- ...receipt?.cliTurnRequestsDispatched === 0 ? { notStarted: true, possiblyApplied: false } : {}
4909
+ ...receipt2?.cliTurnRequestsDispatched === 0 ? { notStarted: true, possiblyApplied: false } : {}
3633
4910
  };
3634
4911
  return { status, result, error: code };
3635
4912
  }
@@ -3854,7 +5131,7 @@ var Agent = class {
3854
5131
  if (data.length > 8 * 1024 * 1024) fail("ARTIFACT_LIMIT", 413);
3855
5132
  const usage = await this.evictArtifacts(data.length);
3856
5133
  if (usage + data.length > ARTIFACT_QUOTA_BYTES) fail("ARTIFACT_QUOTA", 507);
3857
- const id2 = `artifact-${randomUUID6()}`;
5134
+ const id2 = `artifact-${randomUUID7()}`;
3858
5135
  await mkdir6(join8(this.config.dataDir, "artifacts"), { recursive: true, mode: 448 });
3859
5136
  await writeFile5(join8(this.config.dataDir, "artifacts", id2), data, { flag: "wx", mode: 384 });
3860
5137
  this.store.db.prepare("INSERT INTO artifacts VALUES (?,?,?,?,?)").run(id2, canonical(c), sha(data), data.length, media);
@@ -3868,7 +5145,7 @@ var Agent = class {
3868
5145
  if (usage + needed <= ARTIFACT_QUOTA_BYTES) break;
3869
5146
  this.store.db.prepare("DELETE FROM artifacts WHERE id=?").run(row.id);
3870
5147
  usage -= row.size;
3871
- await unlink4(join8(this.config.dataDir, "artifacts", row.id)).catch(() => {
5148
+ await unlink5(join8(this.config.dataDir, "artifacts", row.id)).catch(() => {
3872
5149
  });
3873
5150
  }
3874
5151
  return usage;
@@ -3891,7 +5168,7 @@ var Agent = class {
3891
5168
  if (!row) fail("ARTIFACT_NOT_FOUND", 404);
3892
5169
  this.store.own(row.context, c);
3893
5170
  if (name === "artifact_stat") return { artifactId: row.id, sha256: row.hash, size: row.size, mediaType: row.media };
3894
- const offset = integer(a.offset, 0, Number.MAX_SAFE_INTEGER, 0), length = integer(a.length, 1, 1048576, 65536), data = await readFile8(join8(this.config.dataDir, "artifacts", row.id));
5171
+ const offset = integer(a.offset, 0, Number.MAX_SAFE_INTEGER, 0), length = integer(a.length, 1, 1048576, 65536), data = await readFile9(join8(this.config.dataDir, "artifacts", row.id));
3895
5172
  if (sha(data) !== row.hash) fail("ARTIFACT_CORRUPT");
3896
5173
  return { encoding: "base64", content: data.subarray(offset, offset + length).toString("base64"), eof: offset + length >= row.size };
3897
5174
  }
@@ -3977,9 +5254,9 @@ var Agent = class {
3977
5254
  if (pin) await pin.verify();
3978
5255
  }
3979
5256
  async legacyWrite(a, c, signal, target) {
3980
- const temp = join8(dirname(target), `.dsh-write-${randomUUID6()}`);
5257
+ const temp = join8(dirname2(target), `.dsh-write-${randomUUID7()}`);
3981
5258
  try {
3982
- const parent = await stat7(dirname(target));
5259
+ const parent = await stat7(dirname2(target));
3983
5260
  let previous = null;
3984
5261
  try {
3985
5262
  previous = (await this.roots.read(a.rootId, a.relativePath, 0, 1048577)).data;
@@ -3993,7 +5270,7 @@ var Agent = class {
3993
5270
  if (signal?.aborted) throw Object.assign(new AgentError("WRITE_CANCELLED", 409), { possiblyApplied: false });
3994
5271
  this.store.validate(c);
3995
5272
  await this.roots.resolve(a.rootId, a.relativePath, true);
3996
- const now = await stat7(dirname(target));
5273
+ const now = await stat7(dirname2(target));
3997
5274
  if (now.ino !== parent.ino || now.dev !== parent.dev) fail("FILE_CHANGED", 409);
3998
5275
  let current = null;
3999
5276
  try {
@@ -4006,7 +5283,7 @@ var Agent = class {
4006
5283
  await rename(temp, target);
4007
5284
  return { sha256: sha(data), size: data.length };
4008
5285
  } finally {
4009
- await unlink4(temp).catch(() => {
5286
+ await unlink5(temp).catch(() => {
4010
5287
  });
4011
5288
  }
4012
5289
  }
@@ -4094,9 +5371,9 @@ var Agent = class {
4094
5371
  this.processes.set(id2, { child, context: c, cancel });
4095
5372
  const timer = setTimeout(() => void cancel("PROCESS_TIMEOUT"), timeout);
4096
5373
  try {
4097
- const exit = await new Promise((resolve5, reject) => {
5374
+ const exit = await new Promise((resolve6, reject) => {
4098
5375
  child.once("error", reject);
4099
- child.once("close", (code, signal) => resolve5({ exitCode: code, signal }));
5376
+ child.once("close", (code, signal) => resolve6({ exitCode: code, signal }));
4100
5377
  });
4101
5378
  await cancelPromise;
4102
5379
  const out = await this.artifact(Buffer.concat(stdout), c, "text/plain"), err = await this.artifact(Buffer.concat(stderr), c, "text/plain");
@@ -4116,7 +5393,7 @@ var Agent = class {
4116
5393
  await this.roots.assertPublicTree(source);
4117
5394
  if (await this.git(source, ["status", "--porcelain"])) fail("WORKSPACE_DIRTY", 409);
4118
5395
  const head = await this.git(source, ["rev-parse", "HEAD"]);
4119
- const id2 = `worktree-${randomUUID6()}`, relative2 = a.worktreeRelativePath;
5396
+ const id2 = `worktree-${randomUUID7()}`, relative2 = a.worktreeRelativePath;
4120
5397
  if (typeof relative2 !== "string") fail("INVALID_ARGUMENT");
4121
5398
  const target = await this.roots.resolve(a.rootId, relative2, true);
4122
5399
  if (target === source || this.roots.inside(source, target)) fail("INVALID_WORKTREE_PATH");
@@ -4137,7 +5414,7 @@ var Agent = class {
4137
5414
  const patch = await this.git(row.path, ["diff", "--binary", row.head]);
4138
5415
  if (name === "worktree_diff") return { sourceHead: row.head, ...await this.artifact(Buffer.from(patch + "\n"), c, "text/x-diff") };
4139
5416
  if (await this.git(row.path, ["rev-parse", "HEAD"]) !== row.head || await this.git(row.source, ["rev-parse", "HEAD"]) !== row.head || await this.git(row.source, ["status", "--porcelain"])) return { status: "integration-pending", reason: "SOURCE_CHANGED", worktreeId: a.worktreeId };
4140
- const patchFile = join8(this.config.dataDir, `patch-${randomUUID6()}`);
5417
+ const patchFile = join8(this.config.dataDir, `patch-${randomUUID7()}`);
4141
5418
  await writeFile5(patchFile, patch + "\n", { mode: 384 });
4142
5419
  try {
4143
5420
  if (!patch) return { status: "no-changes" };
@@ -4149,7 +5426,7 @@ var Agent = class {
4149
5426
  if (e instanceof AgentError) throw e;
4150
5427
  return { status: "integration-pending", reason: "PATCH_CONFLICT", worktreeId: a.worktreeId };
4151
5428
  } finally {
4152
- await unlink4(patchFile);
5429
+ await unlink5(patchFile);
4153
5430
  }
4154
5431
  }
4155
5432
  async close() {
@@ -4167,9 +5444,9 @@ var Agent = class {
4167
5444
  };
4168
5445
 
4169
5446
  // src/transport.ts
4170
- import http from "node:http";
5447
+ import http2 from "node:http";
4171
5448
  import https from "node:https";
4172
- import { readFileSync as readFileSync2 } from "node:fs";
5449
+ import { readFileSync as readFileSync3 } from "node:fs";
4173
5450
  import { networkInterfaces } from "node:os";
4174
5451
  import { isIP } from "node:net";
4175
5452
  import { Server, WebStandardStreamableHTTPServerTransport, SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/server";
@@ -4240,7 +5517,133 @@ var AuthThrottle = class {
4240
5517
  };
4241
5518
 
4242
5519
  // src/transport.ts
5520
+ import { WebSocketServer as WebSocketServer2 } from "ws";
5521
+
5522
+ // src/e2e-server.ts
5523
+ import http from "node:http";
5524
+ import { duplexPair } from "node:stream";
4243
5525
  import { WebSocketServer } from "ws";
5526
+ function secureTransport(pairing, handler, upgrade) {
5527
+ const sockets = new WebSocketServer({
5528
+ noServer: true,
5529
+ maxPayload: 65535,
5530
+ perMessageDeflate: false,
5531
+ handleProtocols: (protocols) => protocols.has(DIRECT_PROTOCOL) ? DIRECT_PROTOCOL : false
5532
+ });
5533
+ const sessions = /* @__PURE__ */ new Set(), attempts = /* @__PURE__ */ new Map();
5534
+ let windowStart = 0, windowCount = 0, closing = false;
5535
+ const sweep = setInterval(() => {
5536
+ for (const session of sessions) if (session.peer && !pairing.validPeer(session.peer)) session.remove();
5537
+ }, 500);
5538
+ sweep.unref();
5539
+ return {
5540
+ accept(req, socket, head) {
5541
+ if (closing || sockets.clients.size >= 16) fail("E2E_CONNECTION_LIMIT", 429);
5542
+ if (req.headers.origin !== void 0 || req.headers["sec-websocket-protocol"] !== DIRECT_PROTOCOL) fail("E2E_CLIENT_REQUIRED", 403);
5543
+ const now = Date.now(), source = req.socket.remoteAddress ?? "unknown";
5544
+ if (now - windowStart >= 6e4 || now < windowStart) {
5545
+ windowStart = now;
5546
+ windowCount = 0;
5547
+ }
5548
+ const previous = attempts.get(source), row = previous && now - previous.start < 6e4 && now >= previous.start ? previous : { start: now, count: 0 };
5549
+ if (++row.count > 30 || ++windowCount > 120) fail("E2E_CONNECTION_RATE_LIMIT", 429);
5550
+ attempts.delete(source);
5551
+ attempts.set(source, row);
5552
+ while (attempts.size > 256) attempts.delete(attempts.keys().next().value);
5553
+ sockets.handleUpgrade(req, socket, head, (ws) => {
5554
+ sockets.emit("connection", ws, req);
5555
+ const streams = /* @__PURE__ */ new Set();
5556
+ let session;
5557
+ const origin = http.createServer((request, response) => {
5558
+ if (!session.peer || !pairing.validPeer(session.peer)) {
5559
+ response.writeHead(403);
5560
+ response.end();
5561
+ return;
5562
+ }
5563
+ handler(request, response, session.peer);
5564
+ });
5565
+ origin.requestTimeout = 3e4;
5566
+ origin.headersTimeout = 1e4;
5567
+ origin.on("upgrade", (request, connection, bytes) => {
5568
+ if (!session.peer || !pairing.validPeer(session.peer)) return connection.destroy();
5569
+ upgrade(request, connection, bytes, session.peer);
5570
+ });
5571
+ const transport = {
5572
+ send: (message) => {
5573
+ if (ws.readyState === 1) ws.send(message);
5574
+ },
5575
+ close: (code) => {
5576
+ if (ws.readyState < 2) ws.close(code === 1e3 || code >= 3e3 && code <= 4999 ? code : 1e3);
5577
+ },
5578
+ get bufferedAmount() {
5579
+ return ws.bufferedAmount;
5580
+ },
5581
+ onceDrained(listener) {
5582
+ const check = () => {
5583
+ if (ws.readyState !== 1 || ws.bufferedAmount <= SEND_BUFFER_HIGH / 4) listener();
5584
+ else {
5585
+ const timer = setTimeout(check, 5);
5586
+ timer.unref();
5587
+ }
5588
+ };
5589
+ check();
5590
+ }
5591
+ };
5592
+ session = E2ESession.computer({
5593
+ transport,
5594
+ transportByte: TRANSPORT_DIRECT,
5595
+ staticKey: pairing.transportKey,
5596
+ candidates: () => pairing.candidates(),
5597
+ async connectOrigin() {
5598
+ if (!session.peer || !pairing.validPeer(session.peer)) throw new Error("Peer removed");
5599
+ const [client, server] = duplexPair();
5600
+ Object.assign(server, { setTimeout() {
5601
+ return server;
5602
+ }, setNoDelay() {
5603
+ return server;
5604
+ }, setKeepAlive() {
5605
+ return server;
5606
+ }, remoteAddress: source });
5607
+ client.on("error", () => {
5608
+ });
5609
+ server.on("error", () => {
5610
+ });
5611
+ client.once("close", () => server.destroy());
5612
+ server.once("close", () => client.destroy());
5613
+ streams.add(server);
5614
+ server.once("close", () => streams.delete(server));
5615
+ origin.emit("connection", server);
5616
+ return client;
5617
+ }
5618
+ });
5619
+ sessions.add(session);
5620
+ ws.on("message", (data, binary) => {
5621
+ try {
5622
+ session.receive(Buffer.from(data), binary);
5623
+ } catch {
5624
+ session.close(4e3);
5625
+ }
5626
+ });
5627
+ ws.on("error", () => session.close(4e3));
5628
+ ws.once("close", (code) => session.transportClosed(code));
5629
+ void session.closed.then(() => {
5630
+ sessions.delete(session);
5631
+ for (const stream of streams) stream.destroy();
5632
+ origin.close();
5633
+ });
5634
+ });
5635
+ },
5636
+ async close() {
5637
+ closing = true;
5638
+ clearInterval(sweep);
5639
+ for (const session of sessions) session.shutdown();
5640
+ for (const socket of sockets.clients) socket.terminate();
5641
+ await new Promise((resolve6) => sockets.close(() => resolve6()));
5642
+ }
5643
+ };
5644
+ }
5645
+
5646
+ // src/transport.ts
4244
5647
  var loopback = (host) => ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(host);
4245
5648
  function validateNetwork(c) {
4246
5649
  const host = c.host ?? "127.0.0.1", mode = c.mode ?? "loopback";
@@ -4273,22 +5676,23 @@ function validateNetwork(c) {
4273
5676
  }
4274
5677
  if (mode === "cloudflare-tunnel" && (!loopback(host) || !c.protectedTunnel)) fail("TUNNEL_BACKEND_MUST_BE_LOOPBACK");
4275
5678
  if (mode === "public-tls" && !c.tls && (!loopback(host) || !c.trustedProxies?.length)) fail("TLS_REQUIRED");
4276
- if (!c.tls && !loopback(host) && !c.protectedTunnel) fail("TLS_REQUIRED");
4277
- if (!["loopback", "wireguard", "tailscale", "cloudflare-tunnel", "public-tls"].includes(mode)) fail("INVALID_NETWORK_MODE");
5679
+ if (!c.tls && !loopback(host) && !c.protectedTunnel && mode !== "e2e") fail("TLS_REQUIRED");
5680
+ if (!["loopback", "wireguard", "tailscale", "cloudflare-tunnel", "public-tls", "e2e"].includes(mode)) fail("INVALID_NETWORK_MODE");
4278
5681
  }
4279
5682
  async function serve(agent, c = {}) {
4280
5683
  c = structuredClone(c);
4281
5684
  validateNetwork(c);
4282
5685
  const throttle = new AuthThrottle(c.authLimits), host = c.host ?? "127.0.0.1";
4283
- const requestBoundary = (req, upgrade = false) => {
5686
+ if (c.mode === "e2e" && !agent.store.pairing) fail("PAIRING_SERVICE_REQUIRED");
5687
+ const requestBoundary = (req, upgrade = false, encrypted2 = false) => {
4284
5688
  const address = server.address(), defaultHost = `${host.includes(":") ? `[${host}]` : host}:${address.port}`;
4285
5689
  if (!(c.allowedHosts ?? [defaultHost]).includes(req.headers.host ?? "")) fail("HOST_DENIED", 403);
4286
5690
  const origin = req.headers.origin;
4287
5691
  if (origin !== void 0 && !(c.allowedOrigins ?? []).includes(origin) && !(upgrade && agent.desktopSessions.hasOrigin(origin))) fail("ORIGIN_DENIED", 403);
4288
- if (c.mode === "public-tls" && !c.tls && (!(c.trustedProxies ?? []).includes(req.socket.remoteAddress ?? "unknown") || req.headers["x-forwarded-proto"] !== "https")) fail("TLS_REQUIRED", 403);
5692
+ if (!encrypted2 && c.mode === "public-tls" && !c.tls && (!(c.trustedProxies ?? []).includes(req.socket.remoteAddress ?? "unknown") || req.headers["x-forwarded-proto"] !== "https")) fail("TLS_REQUIRED", 403);
4289
5693
  return defaultHost;
4290
5694
  };
4291
- const handler = async (req, res) => {
5695
+ const handler = async (req, res, peer) => {
4292
5696
  let context;
4293
5697
  const source = req.socket.remoteAddress ?? "unknown";
4294
5698
  const json = (status, value) => {
@@ -4296,7 +5700,27 @@ async function serve(agent, c = {}) {
4296
5700
  res.end(JSON.stringify(value));
4297
5701
  };
4298
5702
  try {
4299
- const defaultHost = requestBoundary(req);
5703
+ const defaultHost = requestBoundary(req, false, !!peer);
5704
+ if (c.mode === "e2e" && !peer) fail("E2E_REQUIRED", 426);
5705
+ const url = new URL(req.url ?? "/", `http://${defaultHost}`);
5706
+ if (peer && !agent.store.pairing.validPeer(peer)) fail("AUTH_INVALID", 401);
5707
+ if (url.pathname === "/pair/v2/redeem") {
5708
+ if (!peer || peer.kind !== "invite" || req.method !== "POST" || url.search) fail("PAIRING_CHANNEL_REQUIRED", 403);
5709
+ let body2 = Buffer.alloc(0);
5710
+ for await (const chunk of req) {
5711
+ body2 = Buffer.concat([body2, chunk]);
5712
+ if (body2.length > 2048) fail("REQUEST_TOO_LARGE", 413);
5713
+ }
5714
+ let input;
5715
+ try {
5716
+ input = JSON.parse(body2.toString("utf8"));
5717
+ } catch {
5718
+ fail("INVALID_JSON");
5719
+ }
5720
+ json(200, agent.store.pairing.redeem(peer, input));
5721
+ return;
5722
+ }
5723
+ if (peer?.kind === "invite") fail("PAIRING_CHANNEL_ONLY", 403);
4300
5724
  const reject = (decision) => {
4301
5725
  if (decision.audit) agent.store.audit(decision.code);
4302
5726
  if (decision.retryAfterMs) res.setHeader("retry-after", String(Math.max(1, Math.ceil(decision.retryAfterMs / 1e3))));
@@ -4306,12 +5730,13 @@ async function serve(agent, c = {}) {
4306
5730
  if (!/^Bearer [A-Za-z0-9_-]{43}$/.test(auth)) {
4307
5731
  reject(throttle.failure(source));
4308
5732
  }
4309
- const url = new URL(req.url ?? "/", `http://${defaultHost}`);
4310
5733
  let client;
4311
5734
  const authenticate = () => {
4312
5735
  try {
5736
+ if (peer && !agent.store.pairing.validPeer(peer)) fail("AUTH_INVALID", 401);
4313
5737
  if (url.pathname.startsWith("/control/v1/")) client = agent.store.client(auth.slice(7));
4314
5738
  else context = agent.store.grant(auth.slice(7));
5739
+ if (peer && (client?.id ?? context?.clientId) !== peer.id) fail("AUTH_INVALID", 401);
4315
5740
  } catch {
4316
5741
  reject(throttle.failure(source));
4317
5742
  }
@@ -4345,6 +5770,10 @@ async function serve(agent, c = {}) {
4345
5770
  json(200, result);
4346
5771
  return;
4347
5772
  }
5773
+ if (url.pathname === "/control/v1/desktop/close") {
5774
+ json(200, await agent.desktopSessions.closeClientSession(client.id, parsed));
5775
+ return;
5776
+ }
4348
5777
  if (url.pathname === "/desktop/v1/session") {
4349
5778
  json(200, await agent.desktopSessions.open(context, parsed));
4350
5779
  return;
@@ -4389,20 +5818,23 @@ async function serve(agent, c = {}) {
4389
5818
  json(e instanceof AgentError ? e.status : 500, { error: { code: e instanceof AgentError ? e.code : "AGENT_INTERNAL" } });
4390
5819
  }
4391
5820
  };
4392
- const tls = c.tls ? { key: readFileSync2(c.tls.keyFile), cert: readFileSync2(c.tls.certFile), ca: c.tls.caFile ? readFileSync2(c.tls.caFile) : void 0, requestCert: !!c.tls.requireClientCertificate, rejectUnauthorized: !!c.tls.requireClientCertificate } : null;
4393
- const server = tls ? https.createServer(tls, handler) : http.createServer(handler);
5821
+ const tls = c.tls ? { key: readFileSync3(c.tls.keyFile), cert: readFileSync3(c.tls.certFile), ca: c.tls.caFile ? readFileSync3(c.tls.caFile) : void 0, requestCert: !!c.tls.requireClientCertificate, rejectUnauthorized: !!c.tls.requireClientCertificate } : null;
5822
+ const publicHandler = (req, res) => void handler(req, res);
5823
+ const server = tls ? https.createServer(tls, publicHandler) : http2.createServer(publicHandler);
4394
5824
  server.requestTimeout = 3e4;
4395
5825
  server.headersTimeout = 1e4;
4396
5826
  server.maxConnections = 128;
4397
- const websocket = new WebSocketServer({ noServer: true, clientTracking: true, maxPayload: 65536, perMessageDeflate: false });
4398
- server.on("upgrade", (req, socket, head) => {
5827
+ const websocket = new WebSocketServer2({ noServer: true, clientTracking: true, maxPayload: 65536, perMessageDeflate: false });
5828
+ const desktopUpgrade = (req, socket, head, peer) => {
4399
5829
  try {
4400
- const defaultHost = requestBoundary(req, true), url = new URL(req.url ?? "/", `http://${defaultHost}`);
5830
+ const defaultHost = requestBoundary(req, true, !!peer), url = new URL(req.url ?? "/", `http://${defaultHost}`);
5831
+ if (c.mode === "e2e" && !peer) fail("E2E_REQUIRED", 426);
5832
+ if (peer && (peer.kind !== "client" || !agent.store.pairing.validPeer(peer))) fail("AUTH_INVALID", 401);
4401
5833
  if (url.pathname !== "/desktop/v1/socket" || url.search) fail("NOT_FOUND", 404);
4402
5834
  if (websocket.clients.size >= 16) fail("DESKTOP_CONNECTION_LIMIT", 429);
4403
5835
  websocket.handleUpgrade(req, socket, head, (ws) => {
4404
5836
  websocket.emit("connection", ws, req);
4405
- agent.desktopSessions.accept(ws, req.headers.origin, c.allowedOrigins ?? []);
5837
+ agent.desktopSessions.accept(ws, req.headers.origin, c.allowedOrigins ?? [], peer?.id);
4406
5838
  });
4407
5839
  } catch (e) {
4408
5840
  const status = e instanceof AgentError ? e.status : 500;
@@ -4410,35 +5842,118 @@ async function serve(agent, c = {}) {
4410
5842
  Connection: close\r
4411
5843
  Content-Length: 0\r
4412
5844
  \r
5845
+ `);
5846
+ }
5847
+ };
5848
+ const encrypted = agent.store.pairing ? secureTransport(agent.store.pairing, (req, res, peer) => void handler(req, res, peer), desktopUpgrade) : null;
5849
+ server.on("upgrade", (req, socket, head) => {
5850
+ try {
5851
+ const defaultHost = requestBoundary(req, true), url = new URL(req.url ?? "/", `http://${defaultHost}`);
5852
+ if (url.pathname === "/v1/e2e" && !url.search) {
5853
+ if (!encrypted) fail("NOT_FOUND", 404);
5854
+ encrypted.accept(req, socket, head);
5855
+ return;
5856
+ }
5857
+ desktopUpgrade(req, socket, head);
5858
+ } catch (e) {
5859
+ const status = e instanceof AgentError ? e.status : 500;
5860
+ socket.end(`HTTP/1.1 ${status} Rejected\r
5861
+ Connection: close\r
5862
+ Content-Length: 0\r
5863
+ \r
4413
5864
  `);
4414
5865
  }
4415
5866
  });
4416
- await new Promise((resolve5, reject) => {
4417
- server.once("error", reject);
4418
- server.listen(c.port ?? 9701, host, () => resolve5());
4419
- });
5867
+ try {
5868
+ await new Promise((resolve6, reject) => {
5869
+ server.once("error", reject);
5870
+ server.listen(c.port ?? 9701, host, () => resolve6());
5871
+ });
5872
+ } catch (error2) {
5873
+ await encrypted?.close();
5874
+ throw error2;
5875
+ }
4420
5876
  return { server, address: server.address(), supportedMcpVersions: SUPPORTED_PROTOCOL_VERSIONS, close: async () => {
4421
5877
  await agent.desktopSessions.close();
4422
5878
  for (const client of websocket.clients) client.terminate();
4423
- await new Promise((resolve5) => websocket.close(() => resolve5()));
4424
- await new Promise((resolve5, reject) => server.close((e) => e ? reject(e) : resolve5()));
5879
+ await new Promise((resolve6) => websocket.close(() => resolve6()));
5880
+ await encrypted?.close();
5881
+ await new Promise((resolve6, reject) => server.close((e) => e ? reject(e) : resolve6()));
4425
5882
  } };
4426
5883
  }
4427
5884
 
5885
+ // src/pairing-page.ts
5886
+ import QRCode from "qrcode";
5887
+ import { randomBytes as randomBytes7 } from "node:crypto";
5888
+ import { mkdir as mkdir7, writeFile as writeFile6, lstat as lstat8, rename as rename2, rm } from "node:fs/promises";
5889
+ import { dirname as dirname3, resolve as resolve4 } from "node:path";
5890
+ var escape = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]);
5891
+ async function pairingPage(invite) {
5892
+ const { code, ...scannable } = invite;
5893
+ const payload = JSON.stringify(scannable), nonce = randomBytes7(18).toString("base64"), fingerprint = invite.transport.fingerprint.match(/.{1,8}/g).join(" ");
5894
+ let qr = "";
5895
+ try {
5896
+ qr = await QRCode.toString(payload, { type: "svg", errorCorrectionLevel: "M", margin: 4, width: 320 });
5897
+ } catch {
5898
+ }
5899
+ const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none';style-src 'unsafe-inline';script-src 'nonce-${nonce}';base-uri 'none';form-action 'none'"><title>\u5B89\u5168\u914D\u5BF9 \xB7 DSH \u7535\u8111</title><style>
5900
+ :root{font-family:system-ui,"Microsoft YaHei",sans-serif;color:#25342f;background:#f5f7f6;color-scheme:light}*{box-sizing:border-box}body{margin:0;padding:24px}.card{max-width:760px;margin:32px auto;padding:32px;background:white;border:1px solid #dfe7e2;border-radius:22px;box-shadow:0 16px 50px #142d2110}h1{margin:8px 0;font-size:25px}p{line-height:1.65;color:#5f6e65}.badge{font-size:12px;color:#317453}.grid{display:grid;grid-template-columns:300px minmax(0,1fr);gap:28px;align-items:center}.qr svg{display:block;width:100%;height:auto}.code{letter-spacing:.18em;font-size:36px;color:#244f3d;font-weight:650}.timer{font-weight:600;color:#317453}.expired{color:#a83a39}.fingerprint{display:block;font-size:12px;line-height:1.8;overflow-wrap:anywhere}textarea{width:100%;min-height:94px;padding:12px;border:1px solid #dfe7e2;border-radius:10px;font:11px monospace;resize:vertical;background:#f8faf8;color:#45584d}button{border:0;border-radius:9px;background:#2d654a;color:white;font:inherit;padding:10px 18px;cursor:pointer}button:disabled{opacity:.5}details{margin-top:20px}summary{cursor:pointer}.footer{font-size:12px;margin-bottom:0}@media(max-width:620px){body{padding:12px}.card{margin:8px auto;padding:22px}.grid{grid-template-columns:1fr;gap:8px}.qr{max-width:300px;margin:auto}}
5901
+ </style></head><body><main class="card"><span class="badge">\u4F60\u7684\u7535\u8111 \xB7 \u7AEF\u5230\u7AEF\u52A0\u5BC6</span><h1>\u628A\u8FD9\u53F0\u7535\u8111\u8FDE\u63A5\u5230 Bot</h1><p>\u5728\u5DF2\u767B\u5F55\u7684\u63D2\u4EF6\u4E2D\u6253\u5F00\u201C\u5B89\u5168\u914D\u5BF9\u7535\u8111\u201D\uFF0C\u626B\u63CF\u6216\u7C98\u8D34\u4E0B\u65B9\u9080\u8BF7\u7801\uFF0C\u518D\u6838\u5BF9\u76EE\u6807\u7535\u8111\u7684\u786E\u8BA4\u7801\u3002</p><div class="grid"><div class="qr">${qr || "<p>\u8FD9\u4EFD\u9080\u8BF7\u5305\u542B\u8F83\u5927\u7684\u6388\u6743\u8303\u56F4\uFF0C\u8BF7\u590D\u5236\u4E0B\u65B9\u9080\u8BF7\u7801\u3002</p>"}</div><section><strong>${escape(invite.name)}</strong><p>${escape(invite.endpoint)}</p><div aria-label="\u516D\u4F4D\u786E\u8BA4\u7801" class="code">${escape(code)}</div><p id="timer" class="timer" role="timer">\u9080\u8BF7\u7801\u6709\u6548\u671F\u4E3A\u4E24\u5206\u949F</p><strong>\u8BBE\u5907\u6307\u7EB9</strong><code class="fingerprint">${escape(fingerprint)}</code></section></div><p>\u5171\u4EAB\u76EE\u5F55\uFF1A${escape(invite.ceiling.roots.join("\u3001"))}\uFF1B\u5141\u8BB8 ${invite.ceiling.tools.length} \u9879\u80FD\u529B\u3002\u8BF7\u5728\u63A5\u6536\u7AEF\u6838\u5BF9\u540C\u4E00\u8303\u56F4\u3002</p><label for="invite">\u8BBE\u5907\u9080\u8BF7\u7801\uFF08\u4E0D\u542B\u786E\u8BA4\u7801\uFF09</label><textarea id="invite" readonly spellcheck="false">${escape(payload)}</textarea><p><button id="copy" type="button">\u590D\u5236\u9080\u8BF7\u7801</button> <span id="feedback" role="status"></span></p><details><summary>\u67E5\u770B\u80FD\u529B\u8303\u56F4</summary><p>${escape(invite.ceiling.tools.join("\u3001"))}</p></details><p class="footer">\u6B64\u9875\u9762\u5728\u672C\u673A\u751F\u6210\uFF0C\u4E0D\u52A0\u8F7D\u8FDC\u7A0B\u8D44\u6E90\u3002\u9080\u8BF7\u7801\u8FC7\u671F\u540E\u8FD0\u884C pair-device.cmd \u91CD\u65B0\u751F\u6210\uFF1B\u5DF2\u914D\u5BF9\u7684\u8BBE\u5907\u9700\u5728\u76EE\u6807\u7535\u8111\u6838\u5BF9\u540E\u7BA1\u7406\u3002</p></main><script nonce="${nonce}">
5902
+ const expiresAt=${invite.expiresAt};const timer=document.getElementById('timer');const update=()=>{const remaining=Math.max(0,Math.ceil((expiresAt-Date.now())/1000));timer.textContent=remaining?'\u9080\u8BF7\u7801\u5269\u4F59 '+remaining+' \u79D2':'\u9080\u8BF7\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210';timer.classList.toggle('expired',!remaining);document.getElementById('copy').disabled=!remaining};update();setInterval(update,1000);
5903
+ document.getElementById('copy').onclick=async()=>{const field=document.getElementById('invite');try{await navigator.clipboard.writeText(field.value);document.getElementById('feedback').textContent='\u5DF2\u590D\u5236'}catch{field.focus();field.select();document.getElementById('feedback').textContent='\u8BF7\u6309 Ctrl+C \u590D\u5236\u9009\u4E2D\u7684\u9080\u8BF7\u7801'}};
5904
+ </script></body></html>`;
5905
+ return { html, qr: !!qr, payload };
5906
+ }
5907
+ async function writePairingPage(invite, destination) {
5908
+ if (!destination) fail("PAIRING_OUTPUT_REQUIRED");
5909
+ const page = resolve4(destination), json = `${page}.private.json`;
5910
+ for (const path4 of [page, json]) {
5911
+ const old = await lstat8(path4).catch((error2) => {
5912
+ if (error2.code === "ENOENT") return null;
5913
+ throw error2;
5914
+ });
5915
+ if (old && (!old.isFile() || old.isSymbolicLink() || old.nlink !== 1)) fail("PAIRING_OUTPUT_INVALID");
5916
+ }
5917
+ await mkdir7(dirname3(page), { recursive: true, mode: 448 });
5918
+ const result = await pairingPage(invite);
5919
+ for (const [path4, text] of [[json, JSON.stringify(JSON.parse(result.payload), null, 2) + "\n"], [page, result.html]]) {
5920
+ const temporary = `${path4}.${randomBytes7(8).toString("hex")}.tmp`;
5921
+ try {
5922
+ await writeFile6(temporary, text, { flag: "wx", mode: 384 });
5923
+ await rename2(temporary, path4);
5924
+ } finally {
5925
+ await rm(temporary, { force: true });
5926
+ }
5927
+ }
5928
+ return { machineId: invite.machineId, clientId: invite.clientId, inviteId: invite.inviteId, expiresAt: invite.expiresAt, transport: invite.transport, pairingFile: json, pageFile: page, qr: result.qr };
5929
+ }
5930
+
4428
5931
  // src/cli.ts
4429
5932
  var [command, configPath, ...args] = process.argv.slice(2);
4430
5933
  try {
4431
- if (!configPath) fail("USAGE: dsh-remote-agent init|serve|pair|rotate|revoke <config.json> [clientId ceiling.json]");
4432
- const file = resolve4(configPath);
5934
+ if (!configPath) fail("USAGE: dsh-remote-agent init|serve|identity|devices|invite|invite-page|pair|rotate|revoke <config.json> [clientId ceiling.json endpoint page.html]");
5935
+ const file = resolve5(configPath);
4433
5936
  if (command === "init") {
4434
- const root = resolve4(args[0] ?? ".");
4435
- const config = { dataDir: join9(dirname2(file), "data"), roots: { project: root }, isolation: "application", network: { mode: "loopback", host: "127.0.0.1", port: 9701 }, executables: { node: process.execPath }, browserOrigins: [] };
4436
- await mkdir7(dirname2(file), { recursive: true, mode: 448 });
4437
- await writeFile6(file, JSON.stringify(config, null, 2) + "\n", { flag: "wx", mode: 384 });
5937
+ const root = resolve5(args[0] ?? ".");
5938
+ const config = { dataDir: join9(dirname4(file), "data"), roots: { project: root }, isolation: "application", network: { mode: "loopback", host: "127.0.0.1", port: 9701 }, executables: { node: process.execPath }, browserOrigins: [] };
5939
+ await mkdir8(dirname4(file), { recursive: true, mode: 448 });
5940
+ await writeFile7(file, JSON.stringify(config, null, 2) + "\n", { flag: "wx", mode: 384 });
4438
5941
  console.log(JSON.stringify({ configured: file, next: "Run pair with an explicit Bot/tool/root ceiling, then serve." }));
4439
5942
  } else {
4440
- const config = JSON.parse(await readFile9(file, "utf8"));
5943
+ const config = JSON.parse(await readFile10(file, "utf8"));
4441
5944
  if (command === "serve") {
5945
+ let invitation;
5946
+ if (args.length) {
5947
+ if (args[0] !== "--invite" || args.length !== 4) fail("USAGE: serve <config.json> --invite <clientId> <ceiling.json> <endpoint>");
5948
+ const local = new Store(config.dataDir, void 0, { admin: true });
5949
+ try {
5950
+ const ceiling = JSON.parse(await readFile10(resolve5(args[2]), "utf8"));
5951
+ for (const root of ceiling.roots ?? []) if (!config.roots[root]) fail("UNKNOWN_ROOT");
5952
+ invitation = local.invite(args[1], ceiling, { endpoint: args[3] });
5953
+ } finally {
5954
+ local.close();
5955
+ }
5956
+ }
4442
5957
  const agent = new Agent(config);
4443
5958
  let handle;
4444
5959
  try {
@@ -4448,6 +5963,7 @@ try {
4448
5963
  throw e;
4449
5964
  }
4450
5965
  console.log(JSON.stringify({ machineId: agent.store.machineId, listening: handle.address, protocols: handle.supportedMcpVersions }));
5966
+ if (invitation) console.log(JSON.stringify(invitation));
4451
5967
  let stopping = false;
4452
5968
  const stop = async () => {
4453
5969
  if (stopping) return;
@@ -4461,11 +5977,15 @@ try {
4461
5977
  const store = new Store(config.dataDir, void 0, { admin: true });
4462
5978
  try {
4463
5979
  let result;
4464
- if (command === "pair") {
4465
- const ceiling = JSON.parse(await readFile9(resolve4(args[1]), "utf8"));
5980
+ if (["pair", "invite", "invite-page"].includes(command)) {
5981
+ if (!args[0] || !args[1] || command === "invite-page" && !args[3]) fail("PAIRING_ARGUMENTS_REQUIRED");
5982
+ const ceiling = JSON.parse(await readFile10(resolve5(args[1]), "utf8"));
4466
5983
  for (const root of ceiling.roots ?? []) if (!config.roots[root]) fail("UNKNOWN_ROOT");
4467
- result = store.pair(args[0], ceiling);
4468
- } else if (command === "rotate") result = store.rotate(args[0]);
5984
+ result = command === "pair" ? store.pair(args[0], ceiling) : store.invite(args[0], ceiling, { endpoint: args[2] });
5985
+ if (command === "invite-page") result = await writePairingPage(result, args[3]);
5986
+ } else if (command === "identity") result = { machineId: store.machineId, transport: store.pairing.identity() };
5987
+ else if (command === "devices") result = { machineId: store.machineId, devices: store.db.prepare("SELECT id,revision,revoked,ceiling FROM clients ORDER BY id").all().map((row) => ({ clientId: row.id, revision: row.revision, revoked: !!row.revoked, ceiling: JSON.parse(row.ceiling) })) };
5988
+ else if (command === "rotate") result = store.rotate(args[0]);
4469
5989
  else if (command === "revoke") {
4470
5990
  store.revoke(args[0]);
4471
5991
  result = { revoked: true };