@di-code/coding-agent 0.1.7 → 0.1.8

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 (333) hide show
  1. package/README.md +104 -46
  2. package/compositions/base.yml +54 -0
  3. package/compositions/interactive.yml +14 -0
  4. package/compositions/json.yml +6 -0
  5. package/compositions/print.yml +5 -0
  6. package/compositions/rpc.yml +12 -0
  7. package/compositions/webui.yml +9 -0
  8. package/dist/cli.d.ts +22 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +97 -3
  11. package/dist/cli.js.map +1 -1
  12. package/dist/composition-loader-entry.d.ts +11 -0
  13. package/dist/composition-loader-entry.d.ts.map +1 -0
  14. package/dist/composition-loader-entry.js +12 -0
  15. package/dist/composition-loader-entry.js.map +1 -0
  16. package/dist/compositions.d.ts +32 -0
  17. package/dist/compositions.d.ts.map +1 -0
  18. package/dist/compositions.js +173 -0
  19. package/dist/compositions.js.map +1 -0
  20. package/dist/core/session/session-manager.d.ts +9 -2
  21. package/dist/core/session/session-manager.d.ts.map +1 -1
  22. package/dist/core/session/session-manager.js +28 -0
  23. package/dist/core/session/session-manager.js.map +1 -1
  24. package/dist/core/session/session-storage.d.ts.map +1 -1
  25. package/dist/core/session/session-storage.js +20 -3
  26. package/dist/core/session/session-storage.js.map +1 -1
  27. package/dist/core/session/types.d.ts +11 -2
  28. package/dist/core/session/types.d.ts.map +1 -1
  29. package/dist/core/session/types.js.map +1 -1
  30. package/dist/core/session-tools.d.ts +5 -0
  31. package/dist/core/session-tools.d.ts.map +1 -0
  32. package/dist/core/session-tools.js +17 -0
  33. package/dist/core/session-tools.js.map +1 -0
  34. package/dist/core/session.d.ts +19 -7
  35. package/dist/core/session.d.ts.map +1 -1
  36. package/dist/core/session.js +71 -39
  37. package/dist/core/session.js.map +1 -1
  38. package/dist/core/tools/bash.d.ts +1 -38
  39. package/dist/core/tools/bash.d.ts.map +1 -1
  40. package/dist/core/tools/bash.js +1 -207
  41. package/dist/core/tools/bash.js.map +1 -1
  42. package/dist/core/tools/edit-diff.d.ts +1 -24
  43. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  44. package/dist/core/tools/edit-diff.js +1 -135
  45. package/dist/core/tools/edit-diff.js.map +1 -1
  46. package/dist/core/tools/edit.d.ts +1 -26
  47. package/dist/core/tools/edit.d.ts.map +1 -1
  48. package/dist/core/tools/edit.js +1 -140
  49. package/dist/core/tools/edit.js.map +1 -1
  50. package/dist/core/tools/file-mutation-queue.d.ts +1 -1
  51. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  52. package/dist/core/tools/file-mutation-queue.js +1 -23
  53. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  54. package/dist/core/tools/file-search.d.ts +1 -13
  55. package/dist/core/tools/file-search.d.ts.map +1 -1
  56. package/dist/core/tools/file-search.js +1 -98
  57. package/dist/core/tools/file-search.js.map +1 -1
  58. package/dist/core/tools/glob.d.ts +1 -10
  59. package/dist/core/tools/glob.d.ts.map +1 -1
  60. package/dist/core/tools/glob.js +1 -43
  61. package/dist/core/tools/glob.js.map +1 -1
  62. package/dist/core/tools/grep.d.ts +1 -12
  63. package/dist/core/tools/grep.d.ts.map +1 -1
  64. package/dist/core/tools/grep.js +1 -66
  65. package/dist/core/tools/grep.js.map +1 -1
  66. package/dist/core/tools/load-skill.d.ts +1 -9
  67. package/dist/core/tools/load-skill.d.ts.map +1 -1
  68. package/dist/core/tools/load-skill.js +1 -21
  69. package/dist/core/tools/load-skill.js.map +1 -1
  70. package/dist/core/tools/path-boundary.d.ts +1 -4
  71. package/dist/core/tools/path-boundary.d.ts.map +1 -1
  72. package/dist/core/tools/path-boundary.js +1 -65
  73. package/dist/core/tools/path-boundary.js.map +1 -1
  74. package/dist/core/tools/read.d.ts +1 -16
  75. package/dist/core/tools/read.d.ts.map +1 -1
  76. package/dist/core/tools/read.js +1 -131
  77. package/dist/core/tools/read.js.map +1 -1
  78. package/dist/core/tools/write.d.ts +1 -16
  79. package/dist/core/tools/write.d.ts.map +1 -1
  80. package/dist/core/tools/write.js +1 -44
  81. package/dist/core/tools/write.js.map +1 -1
  82. package/dist/entry.js +30 -28
  83. package/dist/entry.js.map +1 -1
  84. package/dist/index.d.ts +6 -3
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +6 -3
  87. package/dist/index.js.map +1 -1
  88. package/dist/interactive/builtin.d.ts +17 -0
  89. package/dist/interactive/builtin.d.ts.map +1 -0
  90. package/dist/interactive/builtin.js +47 -0
  91. package/dist/interactive/builtin.js.map +1 -0
  92. package/dist/interactive/controller.d.ts +85 -0
  93. package/dist/interactive/controller.d.ts.map +1 -0
  94. package/dist/interactive/controller.js +211 -0
  95. package/dist/interactive/controller.js.map +1 -0
  96. package/dist/interactive/index.d.ts +3 -0
  97. package/dist/interactive/index.d.ts.map +1 -0
  98. package/dist/interactive/index.js +3 -0
  99. package/dist/interactive/index.js.map +1 -0
  100. package/dist/interactive-host-entry.d.ts +7 -0
  101. package/dist/interactive-host-entry.d.ts.map +1 -0
  102. package/dist/interactive-host-entry.js +123 -0
  103. package/dist/interactive-host-entry.js.map +1 -0
  104. package/dist/interactive-profile.d.ts +18 -0
  105. package/dist/interactive-profile.d.ts.map +1 -0
  106. package/dist/interactive-profile.js +290 -0
  107. package/dist/interactive-profile.js.map +1 -0
  108. package/dist/interactive-resources-entry.d.ts +7 -0
  109. package/dist/interactive-resources-entry.d.ts.map +1 -0
  110. package/dist/interactive-resources-entry.js +42 -0
  111. package/dist/interactive-resources-entry.js.map +1 -0
  112. package/dist/legacy-main.d.ts +39 -0
  113. package/dist/legacy-main.d.ts.map +1 -0
  114. package/dist/legacy-main.js +611 -0
  115. package/dist/legacy-main.js.map +1 -0
  116. package/dist/main.d.ts +15 -34
  117. package/dist/main.d.ts.map +1 -1
  118. package/dist/main.js +130 -518
  119. package/dist/main.js.map +1 -1
  120. package/dist/mcp/entries.d.ts +42 -0
  121. package/dist/mcp/entries.d.ts.map +1 -0
  122. package/dist/mcp/entries.js +98 -0
  123. package/dist/mcp/entries.js.map +1 -0
  124. package/dist/mcp-client-entry.d.ts +5 -0
  125. package/dist/mcp-client-entry.d.ts.map +1 -0
  126. package/dist/mcp-client-entry.js +6 -0
  127. package/dist/mcp-client-entry.js.map +1 -0
  128. package/dist/mcp-config-entry.d.ts +5 -0
  129. package/dist/mcp-config-entry.d.ts.map +1 -0
  130. package/dist/mcp-config-entry.js +6 -0
  131. package/dist/mcp-config-entry.js.map +1 -0
  132. package/dist/mcp-tools-entry.d.ts +5 -0
  133. package/dist/mcp-tools-entry.d.ts.map +1 -0
  134. package/dist/mcp-tools-entry.js +6 -0
  135. package/dist/mcp-tools-entry.js.map +1 -0
  136. package/dist/mcp-transport-entry.d.ts +5 -0
  137. package/dist/mcp-transport-entry.d.ts.map +1 -0
  138. package/dist/mcp-transport-entry.js +6 -0
  139. package/dist/mcp-transport-entry.js.map +1 -0
  140. package/dist/modes/interactive-entry.d.ts +18 -0
  141. package/dist/modes/interactive-entry.d.ts.map +1 -0
  142. package/dist/modes/interactive-entry.js +21 -0
  143. package/dist/modes/interactive-entry.js.map +1 -0
  144. package/dist/modes/interactive.d.ts +16 -6
  145. package/dist/modes/interactive.d.ts.map +1 -1
  146. package/dist/modes/interactive.js +81 -32
  147. package/dist/modes/interactive.js.map +1 -1
  148. package/dist/modes/json-entry.d.ts +13 -0
  149. package/dist/modes/json-entry.d.ts.map +1 -0
  150. package/dist/modes/json-entry.js +14 -0
  151. package/dist/modes/json-entry.js.map +1 -0
  152. package/dist/modes/json.d.ts +4 -1
  153. package/dist/modes/json.d.ts.map +1 -1
  154. package/dist/modes/json.js +3 -3
  155. package/dist/modes/json.js.map +1 -1
  156. package/dist/modes/print-entry.d.ts +10 -0
  157. package/dist/modes/print-entry.d.ts.map +1 -0
  158. package/dist/modes/print-entry.js +11 -0
  159. package/dist/modes/print-entry.js.map +1 -0
  160. package/dist/modes/tree-selector.d.ts.map +1 -1
  161. package/dist/modes/tree-selector.js +4 -0
  162. package/dist/modes/tree-selector.js.map +1 -1
  163. package/dist/plugin-dump-composition.d.ts +6 -0
  164. package/dist/plugin-dump-composition.d.ts.map +1 -0
  165. package/dist/plugin-dump-composition.js +7 -0
  166. package/dist/plugin-dump-composition.js.map +1 -0
  167. package/dist/plugin-inventory.d.ts +7 -0
  168. package/dist/plugin-inventory.d.ts.map +1 -0
  169. package/dist/plugin-inventory.js +8 -0
  170. package/dist/plugin-inventory.js.map +1 -0
  171. package/dist/plugin-manager.d.ts +6 -0
  172. package/dist/plugin-manager.d.ts.map +1 -0
  173. package/dist/plugin-manager.js +7 -0
  174. package/dist/plugin-manager.js.map +1 -0
  175. package/dist/plugin-trace.d.ts +6 -0
  176. package/dist/plugin-trace.d.ts.map +1 -0
  177. package/dist/plugin-trace.js +7 -0
  178. package/dist/plugin-trace.js.map +1 -0
  179. package/dist/plugins/dynamic-broker.d.ts +76 -0
  180. package/dist/plugins/dynamic-broker.d.ts.map +1 -0
  181. package/dist/plugins/dynamic-broker.js +852 -0
  182. package/dist/plugins/dynamic-broker.js.map +1 -0
  183. package/dist/plugins/profile.d.ts +15 -0
  184. package/dist/plugins/profile.d.ts.map +1 -0
  185. package/dist/plugins/profile.js +34 -0
  186. package/dist/plugins/profile.js.map +1 -0
  187. package/dist/plugins/runtime-host.d.ts +37 -0
  188. package/dist/plugins/runtime-host.d.ts.map +1 -0
  189. package/dist/plugins/runtime-host.js +95 -0
  190. package/dist/plugins/runtime-host.js.map +1 -0
  191. package/dist/plugins/trust.d.ts +8 -0
  192. package/dist/plugins/trust.d.ts.map +1 -0
  193. package/dist/plugins/trust.js +59 -0
  194. package/dist/plugins/trust.js.map +1 -0
  195. package/dist/project-trust-entry.d.ts +15 -0
  196. package/dist/project-trust-entry.d.ts.map +1 -0
  197. package/dist/project-trust-entry.js +15 -0
  198. package/dist/project-trust-entry.js.map +1 -0
  199. package/dist/rpc/client.d.ts +78 -6
  200. package/dist/rpc/client.d.ts.map +1 -1
  201. package/dist/rpc/client.js +172 -4
  202. package/dist/rpc/client.js.map +1 -1
  203. package/dist/rpc/dispatcher.d.ts +121 -0
  204. package/dist/rpc/dispatcher.d.ts.map +1 -0
  205. package/dist/rpc/dispatcher.js +994 -0
  206. package/dist/rpc/dispatcher.js.map +1 -0
  207. package/dist/rpc/index.d.ts +2 -0
  208. package/dist/rpc/index.d.ts.map +1 -1
  209. package/dist/rpc/index.js +2 -0
  210. package/dist/rpc/index.js.map +1 -1
  211. package/dist/rpc/lifecycle.d.ts +6 -0
  212. package/dist/rpc/lifecycle.d.ts.map +1 -0
  213. package/dist/rpc/lifecycle.js +24 -0
  214. package/dist/rpc/lifecycle.js.map +1 -0
  215. package/dist/rpc/protocol.d.ts +112 -31
  216. package/dist/rpc/protocol.d.ts.map +1 -1
  217. package/dist/rpc/protocol.js +569 -117
  218. package/dist/rpc/protocol.js.map +1 -1
  219. package/dist/rpc/server-service.d.ts +7 -0
  220. package/dist/rpc/server-service.d.ts.map +1 -0
  221. package/dist/rpc/server-service.js +3 -0
  222. package/dist/rpc/server-service.js.map +1 -0
  223. package/dist/rpc/server.d.ts +16 -19
  224. package/dist/rpc/server.d.ts.map +1 -1
  225. package/dist/rpc/server.js +59 -115
  226. package/dist/rpc/server.js.map +1 -1
  227. package/dist/rpc-client-sdk-entry.d.ts +12 -0
  228. package/dist/rpc-client-sdk-entry.d.ts.map +1 -0
  229. package/dist/rpc-client-sdk-entry.js +11 -0
  230. package/dist/rpc-client-sdk-entry.js.map +1 -0
  231. package/dist/rpc-entry.js +50 -18
  232. package/dist/rpc-entry.js.map +1 -1
  233. package/dist/rpc-server-entry.d.ts +8 -0
  234. package/dist/rpc-server-entry.d.ts.map +1 -0
  235. package/dist/rpc-server-entry.js +67 -0
  236. package/dist/rpc-server-entry.js.map +1 -0
  237. package/dist/runtime/attachment-store.d.ts +13 -0
  238. package/dist/runtime/attachment-store.d.ts.map +1 -0
  239. package/dist/runtime/attachment-store.js +93 -0
  240. package/dist/runtime/attachment-store.js.map +1 -0
  241. package/dist/runtime/bootstrap.d.ts +14 -0
  242. package/dist/runtime/bootstrap.d.ts.map +1 -0
  243. package/dist/runtime/bootstrap.js +29 -0
  244. package/dist/runtime/bootstrap.js.map +1 -0
  245. package/dist/runtime/interactive-host-service.d.ts +22 -0
  246. package/dist/runtime/interactive-host-service.d.ts.map +1 -0
  247. package/dist/runtime/interactive-host-service.js +2 -0
  248. package/dist/runtime/interactive-host-service.js.map +1 -0
  249. package/dist/runtime/interactive-resource-service.d.ts +11 -0
  250. package/dist/runtime/interactive-resource-service.d.ts.map +1 -0
  251. package/dist/runtime/interactive-resource-service.js +3 -0
  252. package/dist/runtime/interactive-resource-service.js.map +1 -0
  253. package/dist/runtime/plugin-inventory-entry.d.ts +5 -0
  254. package/dist/runtime/plugin-inventory-entry.d.ts.map +1 -0
  255. package/dist/runtime/plugin-inventory-entry.js +18 -0
  256. package/dist/runtime/plugin-inventory-entry.js.map +1 -0
  257. package/dist/runtime/plugin-inventory-service.d.ts +8 -0
  258. package/dist/runtime/plugin-inventory-service.d.ts.map +1 -0
  259. package/dist/runtime/plugin-inventory-service.js +3 -0
  260. package/dist/runtime/plugin-inventory-service.js.map +1 -0
  261. package/dist/runtime/plugin-manager-entry.d.ts +20 -0
  262. package/dist/runtime/plugin-manager-entry.d.ts.map +1 -0
  263. package/dist/runtime/plugin-manager-entry.js +96 -0
  264. package/dist/runtime/plugin-manager-entry.js.map +1 -0
  265. package/dist/runtime/plugin-observability-entry.d.ts +12 -0
  266. package/dist/runtime/plugin-observability-entry.d.ts.map +1 -0
  267. package/dist/runtime/plugin-observability-entry.js +73 -0
  268. package/dist/runtime/plugin-observability-entry.js.map +1 -0
  269. package/dist/runtime/product-host.d.ts +91 -0
  270. package/dist/runtime/product-host.d.ts.map +1 -0
  271. package/dist/runtime/product-host.js +529 -0
  272. package/dist/runtime/product-host.js.map +1 -0
  273. package/dist/runtime/session-factory.d.ts +17 -0
  274. package/dist/runtime/session-factory.d.ts.map +1 -0
  275. package/dist/runtime/session-factory.js +75 -0
  276. package/dist/runtime/session-factory.js.map +1 -0
  277. package/dist/runtime/session-host.d.ts +152 -0
  278. package/dist/runtime/session-host.d.ts.map +1 -0
  279. package/dist/runtime/session-host.js +895 -0
  280. package/dist/runtime/session-host.js.map +1 -0
  281. package/dist/runtime-core-entry.d.ts +11 -0
  282. package/dist/runtime-core-entry.d.ts.map +1 -0
  283. package/dist/runtime-core-entry.js +9 -0
  284. package/dist/runtime-core-entry.js.map +1 -0
  285. package/dist/session-factory-entry.d.ts +7 -0
  286. package/dist/session-factory-entry.d.ts.map +1 -0
  287. package/dist/session-factory-entry.js +9 -0
  288. package/dist/session-factory-entry.js.map +1 -0
  289. package/dist/session-store-jsonl-entry.d.ts +7 -0
  290. package/dist/session-store-jsonl-entry.d.ts.map +1 -0
  291. package/dist/session-store-jsonl-entry.js +33 -0
  292. package/dist/session-store-jsonl-entry.js.map +1 -0
  293. package/dist/startup.d.ts +12 -1
  294. package/dist/startup.d.ts.map +1 -1
  295. package/dist/startup.js +106 -15
  296. package/dist/startup.js.map +1 -1
  297. package/dist/subagents/index.d.ts +2 -0
  298. package/dist/subagents/index.d.ts.map +1 -0
  299. package/dist/subagents/index.js +2 -0
  300. package/dist/subagents/index.js.map +1 -0
  301. package/dist/subagents/service.d.ts +50 -0
  302. package/dist/subagents/service.d.ts.map +1 -0
  303. package/dist/subagents/service.js +521 -0
  304. package/dist/subagents/service.js.map +1 -0
  305. package/dist/ui-host.d.ts +125 -0
  306. package/dist/ui-host.d.ts.map +1 -0
  307. package/dist/ui-host.js +275 -0
  308. package/dist/ui-host.js.map +1 -0
  309. package/dist/web/assets/index-BwfuGMvv.css +1 -0
  310. package/dist/web/assets/index-DPUaJqFJ.js +41 -0
  311. package/dist/web/assets/index-DPUaJqFJ.js.map +1 -0
  312. package/dist/web/index.html +14 -0
  313. package/dist/web-command.d.ts +11 -0
  314. package/dist/web-command.d.ts.map +1 -0
  315. package/dist/web-command.js +114 -0
  316. package/dist/web-command.js.map +1 -0
  317. package/dist/web-frontend.d.ts +73 -0
  318. package/dist/web-frontend.d.ts.map +1 -0
  319. package/dist/web-frontend.js +249 -0
  320. package/dist/web-frontend.js.map +1 -0
  321. package/dist/web.d.ts +254 -0
  322. package/dist/web.d.ts.map +1 -0
  323. package/dist/web.js +740 -0
  324. package/dist/web.js.map +1 -0
  325. package/dist/webui-entry.d.ts +3 -0
  326. package/dist/webui-entry.d.ts.map +1 -0
  327. package/dist/webui-entry.js +73 -0
  328. package/dist/webui-entry.js.map +1 -0
  329. package/dist/webui.d.ts +91 -0
  330. package/dist/webui.d.ts.map +1 -0
  331. package/dist/webui.js +882 -0
  332. package/dist/webui.js.map +1 -0
  333. package/package.json +89 -8
package/dist/webui.js ADDED
@@ -0,0 +1,882 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
3
+ import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
4
+ import { createServer } from "node:http";
5
+ import { basename, extname, relative, resolve, sep } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { commandRegistryKey } from "@di-code/builtins";
8
+ import { createProjectTrustStore } from "./project-trust-entry.js";
9
+ import { RpcDispatcher } from "./rpc/dispatcher.js";
10
+ import { parseRpcRequest, RPC_PROTOCOL_VERSION } from "./rpc/protocol.js";
11
+ import { createManagedAttachmentStore } from "./runtime/attachment-store.js";
12
+ import { createProductHost } from "./runtime/product-host.js";
13
+ import { HostManager } from "./runtime/session-host.js";
14
+ import { loadStartupConfiguration, resolveStartupRuntime, } from "./startup.js";
15
+ function json(res, status, value) {
16
+ const body = JSON.stringify(value);
17
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
18
+ res.end(body);
19
+ }
20
+ function tokenFrom(req) {
21
+ const authorization = req.headers.authorization;
22
+ if (authorization?.startsWith("Bearer "))
23
+ return authorization.slice("Bearer ".length).trim();
24
+ const header = req.headers["x-di-code-token"];
25
+ return typeof header === "string" ? header : undefined;
26
+ }
27
+ function cookieToken(req) {
28
+ const cookies = req.headers.cookie;
29
+ if (!cookies)
30
+ return undefined;
31
+ for (const part of cookies.split(";")) {
32
+ const [name, value] = part.trim().split("=", 2);
33
+ if (name === "di_code_web")
34
+ return value;
35
+ }
36
+ return undefined;
37
+ }
38
+ const MIME_TYPES = {
39
+ ".css": "text/css; charset=utf-8",
40
+ ".html": "text/html; charset=utf-8",
41
+ ".ico": "image/x-icon",
42
+ ".js": "text/javascript; charset=utf-8",
43
+ ".json": "application/json; charset=utf-8",
44
+ ".map": "application/json; charset=utf-8",
45
+ ".svg": "image/svg+xml",
46
+ ".woff2": "font/woff2",
47
+ };
48
+ const execFileAsync = promisify(execFile);
49
+ async function body(req, maxBytes) {
50
+ const chunks = [];
51
+ let size = 0;
52
+ for await (const chunk of req) {
53
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
54
+ size += data.byteLength;
55
+ if (size > maxBytes)
56
+ throw new Error("Request body exceeds the permitted size.");
57
+ chunks.push(data);
58
+ }
59
+ return Buffer.concat(chunks).toString("utf8");
60
+ }
61
+ function requestFromValue(value) {
62
+ const line = JSON.stringify(value);
63
+ if (!line)
64
+ throw new Error("RPC body must be a JSON object.");
65
+ return parseRpcRequest(line);
66
+ }
67
+ /** Local-first HTTP/SSE adapter. It authenticates and queues transport messages; Agent work remains in RpcDispatcher. */
68
+ export class WebUiServer {
69
+ options;
70
+ hostManager;
71
+ clients = new Map();
72
+ clientsByResumeToken = new Map();
73
+ server;
74
+ addedWorkspaceRoots = new Set();
75
+ removedWorkspaceRoots = new Set();
76
+ workspaceRegistryLoaded = false;
77
+ workspaceNames = new Map();
78
+ tokenValue;
79
+ sseConnections = 0;
80
+ disposed = false;
81
+ constructor(options) {
82
+ if (options.token === undefined || options.token.length < 32)
83
+ throw new Error("WebUI requires a token of at least 32 characters.");
84
+ if (options.resumeTokenTtlMs !== undefined &&
85
+ (!Number.isSafeInteger(options.resumeTokenTtlMs) || options.resumeTokenTtlMs <= 0))
86
+ throw new Error("WebUI resume token TTL must be a positive integer.");
87
+ if ((options.maxSseConnectionsPerClient !== undefined &&
88
+ (!Number.isSafeInteger(options.maxSseConnectionsPerClient) || options.maxSseConnectionsPerClient <= 0)) ||
89
+ (options.maxSseConnections !== undefined &&
90
+ (!Number.isSafeInteger(options.maxSseConnections) || options.maxSseConnections <= 0)))
91
+ throw new Error("WebUI SSE connection limits must be positive integers.");
92
+ this.options = options;
93
+ this.tokenValue = options.token;
94
+ this.hostManager = new HostManager(options.context);
95
+ this.server = createServer((req, res) => void this.handle(req, res));
96
+ }
97
+ get token() {
98
+ return this.tokenValue;
99
+ }
100
+ rotateToken() {
101
+ this.tokenValue = randomBytes(32).toString("base64url");
102
+ for (const client of this.clients.values())
103
+ this.closeClient(client);
104
+ this.clients.clear();
105
+ this.clientsByResumeToken.clear();
106
+ return this.tokenValue;
107
+ }
108
+ revokeToken() {
109
+ this.tokenValue = randomBytes(32).toString("base64url");
110
+ for (const client of this.clients.values())
111
+ this.closeClient(client);
112
+ this.clients.clear();
113
+ this.clientsByResumeToken.clear();
114
+ }
115
+ listen() {
116
+ const host = this.options.host ?? "127.0.0.1";
117
+ if (host !== "127.0.0.1" && host !== "::1" && !this.options.allowRemote)
118
+ return Promise.reject(new Error("Remote WebUI binding requires explicit allowRemote."));
119
+ return new Promise((resolvePromise, reject) => {
120
+ this.server.once("error", reject);
121
+ this.server.listen(this.options.port ?? 0, host, () => {
122
+ const address = this.server.address();
123
+ if (!address || typeof address === "string")
124
+ return reject(new Error("WebUI did not expose a TCP address."));
125
+ resolvePromise({ host, port: address.port });
126
+ });
127
+ });
128
+ }
129
+ async close() {
130
+ if (this.disposed)
131
+ return;
132
+ this.disposed = true;
133
+ for (const client of this.clients.values())
134
+ this.closeClient(client);
135
+ await this.hostManager.dispose();
136
+ await new Promise((resolvePromise) => this.server.close(() => resolvePromise()));
137
+ }
138
+ async handle(req, res) {
139
+ try {
140
+ if (this.disposed)
141
+ return json(res, 503, { error: "WebUI is shutting down." });
142
+ if (!this.hostAllowed(req))
143
+ return json(res, 403, { error: "Host is not allowed." });
144
+ if (!this.originAllowed(req))
145
+ return json(res, 403, { error: "Origin is not allowed." });
146
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
147
+ if (req.method === "GET" && url.pathname === "/healthz")
148
+ return json(res, 200, { status: "ok" });
149
+ if (this.options.staticRoot && !isApiRoute(url.pathname)) {
150
+ if (await this.staticAsset(req, res, url))
151
+ return;
152
+ }
153
+ this.setCors(req, res);
154
+ if (req.method === "OPTIONS") {
155
+ res.writeHead(204).end();
156
+ return;
157
+ }
158
+ if (req.method === "GET" && url.pathname === "/api/session" && this.isDevelopmentSessionRequest(req)) {
159
+ this.setWebCookie(res);
160
+ res.writeHead(204).end();
161
+ return;
162
+ }
163
+ const client = this.authenticate(req, res, url.pathname.startsWith("/api/"));
164
+ if (!client)
165
+ return;
166
+ if (!this.rateAllowed(client))
167
+ return json(res, 429, { error: "Rate limit exceeded." });
168
+ if (req.method === "GET" && url.pathname === "/api/boot")
169
+ return await this.boot(res, client, url);
170
+ if (req.method === "POST" && url.pathname === "/api/workspaces")
171
+ return await this.addWorkspaceRoute(req, res);
172
+ if (req.method === "POST" && url.pathname === "/api/workspaces/rename")
173
+ return await this.renameWorkspaceRoute(req, res);
174
+ if (req.method === "POST" && url.pathname === "/api/workspaces/delete")
175
+ return await this.deleteWorkspaceRoute(req, res);
176
+ if (req.method === "POST" && url.pathname === "/api/workspaces/pick")
177
+ return await this.pickWorkspaceRoute(res);
178
+ if (req.method === "POST" && url.pathname === "/api/rpc")
179
+ return await this.rpc(req, res, client, url);
180
+ if (req.method === "GET" && url.pathname === "/api/events")
181
+ return await this.events(req, res, client, url);
182
+ if (req.method === "POST" && url.pathname === "/api/attachments")
183
+ return await this.attachments(req, res, client, url);
184
+ if (req.method === "POST" && url.pathname === "/rpc")
185
+ return await this.rpc(req, res, client, url);
186
+ if (req.method === "GET" && url.pathname === "/events")
187
+ return await this.events(req, res, client, url);
188
+ if (req.method === "POST" && url.pathname === "/attachments")
189
+ return await this.attachments(req, res, client, url);
190
+ json(res, 404, { error: "Not found." });
191
+ }
192
+ catch {
193
+ json(res, 400, { error: "Request rejected." });
194
+ }
195
+ }
196
+ authenticate(req, res, allowCookie) {
197
+ const token = tokenFrom(req) ?? (allowCookie ? cookieToken(req) : undefined);
198
+ if (!token || !constantEquals(token, this.tokenValue)) {
199
+ json(res, 401, { error: "Unauthorized." });
200
+ return undefined;
201
+ }
202
+ const requestedResumeToken = req.headers["x-di-code-resume-token"];
203
+ if (typeof requestedResumeToken === "string") {
204
+ const resumed = this.clientsByResumeToken.get(requestedResumeToken);
205
+ if (resumed && resumed.resumeTokenExpiresAt > Date.now()) {
206
+ this.rotateResumeToken(resumed);
207
+ res.setHeader("x-di-code-client-id", resumed.id);
208
+ return resumed;
209
+ }
210
+ if (resumed)
211
+ this.revokeResumeToken(resumed);
212
+ json(res, 401, { error: "Resume credential is invalid or expired." });
213
+ return undefined;
214
+ }
215
+ const requestedId = req.headers["x-di-code-client-id"];
216
+ const id = typeof requestedId === "string" && /^[A-Za-z0-9_-]{16,128}$/.test(requestedId)
217
+ ? requestedId
218
+ : randomBytes(18).toString("base64url");
219
+ res.setHeader("x-di-code-client-id", id);
220
+ let client = this.clients.get(id);
221
+ if (!client) {
222
+ client = {
223
+ id,
224
+ principal: `webui:${id}`,
225
+ resumeToken: randomBytes(24).toString("base64url"),
226
+ resumeTokenExpiresAt: Date.now() + (this.options.resumeTokenTtlMs ?? 10 * 60 * 1000),
227
+ dispatchers: new Map(),
228
+ productHosts: new Map(),
229
+ connections: new Set(),
230
+ requestTimes: [],
231
+ };
232
+ this.clients.set(id, client);
233
+ this.clientsByResumeToken.set(client.resumeToken, client);
234
+ }
235
+ return client;
236
+ }
237
+ rotateResumeToken(client) {
238
+ this.clientsByResumeToken.delete(client.resumeToken);
239
+ client.resumeToken = randomBytes(24).toString("base64url");
240
+ client.resumeTokenExpiresAt = Date.now() + (this.options.resumeTokenTtlMs ?? 10 * 60 * 1000);
241
+ this.clientsByResumeToken.set(client.resumeToken, client);
242
+ }
243
+ revokeResumeToken(client) {
244
+ this.clientsByResumeToken.delete(client.resumeToken);
245
+ client.resumeTokenExpiresAt = 0;
246
+ }
247
+ hostAllowed(req) {
248
+ const host = req.headers.host;
249
+ if (!host)
250
+ return false;
251
+ try {
252
+ const expected = this.options.host ?? "127.0.0.1";
253
+ return new URL(`http://${host}`).hostname === expected;
254
+ }
255
+ catch {
256
+ return false;
257
+ }
258
+ }
259
+ originAllowed(req) {
260
+ const origin = req.headers.origin;
261
+ if (origin === undefined)
262
+ return true;
263
+ const allowed = this.options.origins;
264
+ if (allowed)
265
+ return allowed.includes(origin) || origin === this.options.developmentOrigin;
266
+ if (origin === this.options.developmentOrigin)
267
+ return true;
268
+ if (origin === `http://${req.headers.host}`)
269
+ return true;
270
+ try {
271
+ const parsed = new URL(origin);
272
+ return parsed.protocol === "http:" && parsed.hostname === (this.options.host ?? "127.0.0.1");
273
+ }
274
+ catch {
275
+ return false;
276
+ }
277
+ }
278
+ setCors(req, res) {
279
+ const origin = req.headers.origin;
280
+ if (origin && this.originAllowed(req)) {
281
+ res.setHeader("access-control-allow-origin", origin);
282
+ res.setHeader("access-control-allow-headers", "authorization, content-type, x-di-code-token, x-di-code-client-id, x-di-code-resume-token, last-event-id");
283
+ res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
284
+ res.setHeader("vary", "Origin");
285
+ }
286
+ }
287
+ rateAllowed(client) {
288
+ const now = Date.now();
289
+ const config = this.options.rateLimit ?? { windowMs: 10_000, maxRequests: 60 };
290
+ while (client.requestTimes[0] !== undefined && client.requestTimes[0] < now - config.windowMs)
291
+ client.requestTimes.shift();
292
+ if (client.requestTimes.length >= config.maxRequests)
293
+ return false;
294
+ client.requestTimes.push(now);
295
+ return true;
296
+ }
297
+ async staticAsset(req, res, url) {
298
+ if (req.method !== "GET" && req.method !== "HEAD")
299
+ return false;
300
+ const configuredRoot = this.options.staticRoot;
301
+ if (!configuredRoot)
302
+ return false;
303
+ const staticRoot = await realpath(configuredRoot);
304
+ let pathname;
305
+ try {
306
+ pathname = decodeURIComponent(url.pathname);
307
+ }
308
+ catch {
309
+ json(res, 400, { error: "Invalid URL path." });
310
+ return true;
311
+ }
312
+ if (pathname.includes("\0")) {
313
+ json(res, 400, { error: "Invalid URL path." });
314
+ return true;
315
+ }
316
+ const requested = resolve(staticRoot, `.${pathname}`);
317
+ const pathIsInsideRoot = relative(staticRoot, requested);
318
+ if (pathIsInsideRoot.startsWith("..") || pathIsInsideRoot.startsWith(`..${sep}`)) {
319
+ json(res, 403, { error: "Static path is not allowed." });
320
+ return true;
321
+ }
322
+ const candidate = await this.fileOrFallback(requested === staticRoot ? resolve(staticRoot, "index.html") : requested, resolve(staticRoot, "index.html"));
323
+ if (!candidate)
324
+ return false;
325
+ const realCandidate = await realpath(candidate);
326
+ const candidatePath = relative(staticRoot, realCandidate);
327
+ if (candidatePath.startsWith("..") || candidatePath.startsWith(`..${sep}`)) {
328
+ json(res, 403, { error: "Static path is not allowed." });
329
+ return true;
330
+ }
331
+ this.setWebCookie(res);
332
+ const asset = await readFile(realCandidate);
333
+ const cacheControl = realCandidate.endsWith("index.html")
334
+ ? "no-cache"
335
+ : realCandidate.includes(`${sep}assets${sep}`)
336
+ ? "public, max-age=31536000, immutable"
337
+ : "public, max-age=3600";
338
+ res.writeHead(200, {
339
+ "content-type": MIME_TYPES[extname(realCandidate)] ?? "application/octet-stream",
340
+ "cache-control": cacheControl,
341
+ });
342
+ if (req.method === "GET")
343
+ res.end(asset);
344
+ else
345
+ res.end();
346
+ return true;
347
+ }
348
+ async fileOrFallback(path, fallback) {
349
+ try {
350
+ if ((await stat(path)).isFile())
351
+ return path;
352
+ }
353
+ catch { }
354
+ try {
355
+ if ((await stat(fallback)).isFile())
356
+ return fallback;
357
+ }
358
+ catch { }
359
+ return undefined;
360
+ }
361
+ setWebCookie(res) {
362
+ res.setHeader("set-cookie", `di_code_web=${this.tokenValue}; HttpOnly; SameSite=Strict; Path=/`);
363
+ }
364
+ isDevelopmentSessionRequest(req) {
365
+ return this.options.developmentOrigin !== undefined && req.headers.origin === this.options.developmentOrigin;
366
+ }
367
+ async boot(res, client, url) {
368
+ const { actor, dispatcher } = await this.actor(client, url);
369
+ const workspace = await this.resolveWorkspace(url);
370
+ const [capabilities, state, runtime] = await Promise.all([
371
+ dispatcher.dispatch({
372
+ version: RPC_PROTOCOL_VERSION,
373
+ kind: "request",
374
+ id: `boot-capabilities:${randomUUID()}`,
375
+ method: "get_capabilities",
376
+ params: { events: [] },
377
+ }),
378
+ dispatcher.dispatch({
379
+ version: RPC_PROTOCOL_VERSION,
380
+ kind: "request",
381
+ id: `boot-state:${randomUUID()}`,
382
+ method: "get_state",
383
+ params: {},
384
+ }),
385
+ dispatcher.dispatch({
386
+ version: RPC_PROTOCOL_VERSION,
387
+ kind: "request",
388
+ id: `boot-runtime:${randomUUID()}`,
389
+ method: "get_runtime",
390
+ params: {},
391
+ }),
392
+ ]);
393
+ json(res, 200, {
394
+ protocolVersion: RPC_PROTOCOL_VERSION,
395
+ capabilities: capabilities.ok ? capabilities.result : undefined,
396
+ state: state.ok ? state.result.state : actor.state(),
397
+ runtime: runtime.ok ? runtime.result : undefined,
398
+ workspaceId: workspace.id,
399
+ workspaces: (await this.authorizedWorkspaces()).map(({ id, name }) => ({ id, name })),
400
+ });
401
+ }
402
+ async actor(client, url) {
403
+ const allowed = (await this.resolveWorkspace(url)).root;
404
+ let dispatcher = client.dispatchers.get(allowed);
405
+ if (!dispatcher) {
406
+ const primary = await realpath(resolve(this.options.allowedRoot));
407
+ const projectTrusted = allowed.toLowerCase() === primary.toLowerCase()
408
+ ? (this.options.projectTrusted ?? false)
409
+ : (await createProjectTrustStore(resolve(this.options.agentDir, "trust.json")).get(allowed)) === true;
410
+ const startupConfiguration = await loadStartupConfiguration(allowed, process.env, this.options.agentDir);
411
+ const hostOptions = this.hostOptions(client.principal, allowed, this.resolveActorRuntime(startupConfiguration), projectTrusted);
412
+ let permissionMode = startupConfiguration.permissionMode ?? "ask";
413
+ let requestApproval;
414
+ const toolApproval = {
415
+ request: async (toolName, parameters, signal) => {
416
+ if (permissionMode === "allow")
417
+ return;
418
+ if (permissionMode === "deny")
419
+ throw new Error("Tool approval denied.");
420
+ if (!requestApproval)
421
+ throw new Error("Tool approval is unavailable.");
422
+ if (!(await requestApproval(toolName, parameters, signal)))
423
+ throw new Error("Tool approval denied.");
424
+ },
425
+ };
426
+ const actor = await this.hostManager.get({
427
+ ...hostOptions,
428
+ principal: client.principal,
429
+ cwd: allowed,
430
+ ...(toolApproval ? { toolApproval } : {}),
431
+ });
432
+ if (!actor.state().activeSession)
433
+ await actor.createSession();
434
+ const product = createProductHost({
435
+ context: this.options.context,
436
+ cwd: allowed,
437
+ agentDir: hostOptions.agentDir,
438
+ projectTrusted,
439
+ provider: hostOptions.provider,
440
+ model: hostOptions.model,
441
+ runtimeSnapshot: () => {
442
+ const ui = actor.ui();
443
+ return {
444
+ providerId: ui.providerId,
445
+ modelId: ui.modelId,
446
+ ...(ui.thinkingLevel ? { thinkingLevel: ui.thinkingLevel } : {}),
447
+ };
448
+ },
449
+ startupConfiguration,
450
+ reloadRuntime: async () => {
451
+ const refreshed = await loadStartupConfiguration(allowed, process.env, this.options.agentDir);
452
+ let runtime;
453
+ try {
454
+ runtime = resolveStartupRuntime(refreshed.environment, refreshed.providers, refreshed.defaults);
455
+ }
456
+ catch {
457
+ runtime = resolveStartupRuntime({ DI_CODE_PROVIDER: "faux" }, []);
458
+ }
459
+ actor.setRuntimeValue(runtime.provider, runtime.model);
460
+ return refreshed;
461
+ },
462
+ reloadConfiguration: () => loadStartupConfiguration(allowed, process.env, this.options.agentDir),
463
+ onPermissionModeChange: (mode) => {
464
+ permissionMode = mode;
465
+ },
466
+ refreshResources: (projectTrusted) => actor.refreshResources(projectTrusted),
467
+ });
468
+ const attachments = await createManagedAttachmentStore({
469
+ directory: resolve(this.clientTempRoot(client), "attachments"),
470
+ });
471
+ dispatcher = new RpcDispatcher({
472
+ session: actor,
473
+ commandRegistry: this.options.context.get(commandRegistryKey),
474
+ productState: { projectTrusted },
475
+ productHost: product,
476
+ attachmentStore: attachments,
477
+ });
478
+ const actorDispatcher = dispatcher;
479
+ requestApproval = (toolName, parameters, signal) => actorDispatcher.requestToolApproval(toolName, parameters, signal);
480
+ await actorDispatcher.dispatch({
481
+ version: RPC_PROTOCOL_VERSION,
482
+ kind: "request",
483
+ id: `webui-capabilities:${randomUUID()}`,
484
+ method: "get_capabilities",
485
+ params: {
486
+ events: [
487
+ "sequence",
488
+ "operation_update",
489
+ "snapshot_required",
490
+ "session_changed",
491
+ "tool_approval",
492
+ "product_audit",
493
+ ],
494
+ },
495
+ });
496
+ client.productHosts.set(allowed, product);
497
+ client.dispatchers.set(allowed, dispatcher);
498
+ }
499
+ const actor = await this.hostManager.get({
500
+ ...this.hostOptions(client.principal, allowed),
501
+ principal: client.principal,
502
+ cwd: allowed,
503
+ });
504
+ return { actor, dispatcher };
505
+ }
506
+ async authorizedWorkspaces() {
507
+ await this.loadWorkspaceRegistry();
508
+ const primary = await realpath(resolve(this.options.allowedRoot));
509
+ const candidates = [
510
+ this.options.allowedRoot,
511
+ ...(this.options.allowedWorkspaces ?? []),
512
+ ...this.addedWorkspaceRoots,
513
+ ];
514
+ const seen = new Set();
515
+ const result = [];
516
+ for (const candidate of candidates) {
517
+ let root;
518
+ try {
519
+ root = await realpath(resolve(candidate));
520
+ }
521
+ catch {
522
+ continue;
523
+ }
524
+ const key = root.toLowerCase();
525
+ if (this.removedWorkspaceRoots.has(key))
526
+ continue;
527
+ if (seen.has(key))
528
+ continue;
529
+ seen.add(key);
530
+ if (key !== primary.toLowerCase()) {
531
+ const trusted = await createProjectTrustStore(resolve(this.options.agentDir, "trust.json")).get(root);
532
+ if (!trusted)
533
+ continue;
534
+ }
535
+ result.push({
536
+ id: createHash("sha256").update(root).digest("hex").slice(0, 24),
537
+ name: this.workspaceNames.get(key) ?? (basename(root) || "Workspace"),
538
+ root,
539
+ });
540
+ }
541
+ return result;
542
+ }
543
+ async addWorkspaceRoute(req, res) {
544
+ const value = JSON.parse(await body(req, 64 * 1024));
545
+ if (typeof value !== "object" || value === null || typeof value.path !== "string")
546
+ throw new Error("Workspace path is required.");
547
+ const workspace = await this.addWorkspace(value.path);
548
+ json(res, 200, { workspace });
549
+ }
550
+ async renameWorkspaceRoute(req, res) {
551
+ const value = JSON.parse(await body(req, 64 * 1024));
552
+ if (typeof value !== "object" || value === null)
553
+ throw new Error("Workspace details are required.");
554
+ const input = value;
555
+ if (typeof input.workspaceId !== "string" || typeof input.name !== "string")
556
+ throw new Error("Workspace details are invalid.");
557
+ const workspace = await this.workspaceById(input.workspaceId);
558
+ const name = input.name.trim();
559
+ if (!name || name.length > 120)
560
+ throw new Error("Workspace name is invalid.");
561
+ this.workspaceNames.set(workspace.root.toLowerCase(), name);
562
+ await this.saveWorkspaceRegistry();
563
+ json(res, 200, { workspace: { id: workspace.id, name } });
564
+ }
565
+ async deleteWorkspaceRoute(req, res) {
566
+ const value = JSON.parse(await body(req, 64 * 1024));
567
+ if (typeof value !== "object" ||
568
+ value === null ||
569
+ typeof value.workspaceId !== "string")
570
+ throw new Error("Workspace ID is required.");
571
+ const workspace = await this.workspaceById(value.workspaceId);
572
+ const primary = await realpath(resolve(this.options.allowedRoot));
573
+ if (workspace.root.toLowerCase() === primary.toLowerCase())
574
+ throw new Error("The startup workspace cannot be deleted.");
575
+ this.addedWorkspaceRoots.delete(workspace.root);
576
+ this.removedWorkspaceRoots.add(workspace.root.toLowerCase());
577
+ this.workspaceNames.delete(workspace.root.toLowerCase());
578
+ await this.saveWorkspaceRegistry();
579
+ json(res, 200, { deleted: true, workspaceId: workspace.id });
580
+ }
581
+ async workspaceById(id) {
582
+ const workspace = (await this.authorizedWorkspaces()).find((item) => item.id === id);
583
+ if (!workspace)
584
+ throw new Error("Workspace is not authorized for this WebUI token.");
585
+ return workspace;
586
+ }
587
+ async pickWorkspaceRoute(res) {
588
+ const requestedPath = await this.pickWorkspacePath();
589
+ if (!requestedPath)
590
+ return json(res, 200, { cancelled: true });
591
+ const workspace = await this.addWorkspace(requestedPath);
592
+ json(res, 200, { cancelled: false, workspace });
593
+ }
594
+ async pickWorkspacePath() {
595
+ if (process.platform === "win32") {
596
+ const script = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; $dialog = New-Object System.Windows.Forms.FolderBrowserDialog; if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.Write($dialog.SelectedPath) }";
597
+ const result = await this.runWorkspacePicker("powershell.exe", ["-NoProfile", "-STA", "-Command", script], {
598
+ windowsHide: true,
599
+ });
600
+ return result.stdout.trim() || undefined;
601
+ }
602
+ if (process.platform === "darwin") {
603
+ const result = await this.runWorkspacePicker("osascript", [
604
+ "-e",
605
+ 'POSIX path of (choose folder with prompt "Choose a workspace")',
606
+ ]);
607
+ return result.stdout.trim() || undefined;
608
+ }
609
+ const result = await this.runWorkspacePicker("zenity", [
610
+ "--file-selection",
611
+ "--directory",
612
+ "--title=Choose a workspace",
613
+ ]);
614
+ return result.stdout.trim() || undefined;
615
+ }
616
+ async runWorkspacePicker(command, args, options = {}) {
617
+ try {
618
+ const result = await execFileAsync(command, [...args], {
619
+ ...options,
620
+ timeout: 5 * 60 * 1000,
621
+ maxBuffer: 64 * 1024,
622
+ });
623
+ return { stdout: result.stdout };
624
+ }
625
+ catch (cause) {
626
+ const error = cause;
627
+ // osascript and zenity use non-zero exit codes for an explicit cancel.
628
+ if (error.code === 1 || error.code === 255 || error.code === -128)
629
+ return { stdout: "" };
630
+ if (typeof error.code === "string" && error.code === "ENOENT")
631
+ throw new Error(`Unable to open the ${command} directory picker. Install ${command} or choose a workspace another way.`);
632
+ throw new Error(`Workspace directory picker failed.${error.stderr?.trim() ? ` ${error.stderr.trim()}` : ""}`);
633
+ }
634
+ }
635
+ /** Adds a validated local directory to this server's explicitly authorized workspace set. */
636
+ async addWorkspace(requestedPath) {
637
+ if (!requestedPath.trim() || requestedPath.includes("\0"))
638
+ throw new Error("Workspace path is invalid.");
639
+ const root = await realpath(resolve(requestedPath));
640
+ if (!(await stat(root)).isDirectory())
641
+ throw new Error("Workspace path is not a directory.");
642
+ const primary = await realpath(resolve(this.options.allowedRoot));
643
+ if (root.toLowerCase() !== primary.toLowerCase()) {
644
+ await createProjectTrustStore(resolve(this.options.agentDir, "trust.json")).set(root, true);
645
+ }
646
+ this.addedWorkspaceRoots.add(root);
647
+ this.removedWorkspaceRoots.delete(root.toLowerCase());
648
+ await this.saveWorkspaceRegistry();
649
+ const id = createHash("sha256").update(root).digest("hex").slice(0, 24);
650
+ return { id, name: this.workspaceNames.get(root.toLowerCase()) ?? (basename(root) || "Workspace") };
651
+ }
652
+ workspaceRegistryPath() {
653
+ return resolve(this.options.workspaceRegistryDir ?? this.options.agentDir, "webui", "workspaces.json");
654
+ }
655
+ async loadWorkspaceRegistry() {
656
+ if (this.workspaceRegistryLoaded)
657
+ return;
658
+ this.workspaceRegistryLoaded = true;
659
+ try {
660
+ const value = JSON.parse(await readFile(this.workspaceRegistryPath(), "utf8"));
661
+ if (Array.isArray(value.roots))
662
+ for (const root of value.roots)
663
+ if (typeof root === "string")
664
+ this.addedWorkspaceRoots.add(root);
665
+ if (Array.isArray(value.removed))
666
+ for (const root of value.removed)
667
+ if (typeof root === "string")
668
+ this.removedWorkspaceRoots.add(root);
669
+ if (value.names && typeof value.names === "object")
670
+ for (const [root, name] of Object.entries(value.names))
671
+ if (typeof name === "string")
672
+ this.workspaceNames.set(root, name);
673
+ }
674
+ catch { }
675
+ }
676
+ async saveWorkspaceRegistry() {
677
+ await mkdir(resolve(this.options.workspaceRegistryDir ?? this.options.agentDir, "webui"), {
678
+ recursive: true,
679
+ });
680
+ await writeFile(this.workspaceRegistryPath(), JSON.stringify({
681
+ roots: [...this.addedWorkspaceRoots],
682
+ removed: [...this.removedWorkspaceRoots],
683
+ names: Object.fromEntries(this.workspaceNames.entries()),
684
+ }), "utf8");
685
+ }
686
+ async resolveWorkspace(url) {
687
+ const workspaces = await this.authorizedWorkspaces();
688
+ const id = url.searchParams.get("workspaceId");
689
+ if (id) {
690
+ const selected = workspaces.find((workspace) => workspace.id === id);
691
+ if (selected)
692
+ return selected;
693
+ throw new Error("Workspace is not authorized for this WebUI token.");
694
+ }
695
+ // Preserve the embedding transport's existing path query while the SPA uses opaque workspace IDs.
696
+ const legacy = url.searchParams.get("workspace");
697
+ if (legacy) {
698
+ const requested = await realpath(resolve(legacy));
699
+ const selected = workspaces.find((workspace) => workspace.root.toLowerCase() === requested.toLowerCase());
700
+ if (selected)
701
+ return selected;
702
+ throw new Error("Workspace is not authorized for this WebUI token.");
703
+ }
704
+ const primary = workspaces[0];
705
+ if (!primary)
706
+ throw new Error("No trusted Workspace is available for this WebUI token.");
707
+ return primary;
708
+ }
709
+ resolveActorRuntime(configuration) {
710
+ const initialProvider = this.options.provider;
711
+ const initialModel = this.options.model;
712
+ const providerId = configuration.environment.DI_CODE_PROVIDER?.trim() || configuration.defaults?.providerId;
713
+ const modelId = configuration.environment.DI_CODE_MODEL?.trim() ||
714
+ (configuration.defaults?.providerId === providerId ? configuration.defaults?.modelId : undefined);
715
+ if (initialProvider && providerId === initialProvider.id && modelId) {
716
+ const model = initialProvider.models.find((candidate) => candidate.id === modelId);
717
+ if (model)
718
+ return { provider: initialProvider, model };
719
+ }
720
+ try {
721
+ return resolveStartupRuntime(configuration.environment, configuration.providers, configuration.defaults);
722
+ }
723
+ catch {
724
+ // Embedders may inject a runtime without a corresponding settings file.
725
+ if (initialProvider && initialModel)
726
+ return { provider: initialProvider, model: initialModel };
727
+ throw new Error("Provider is not configured.");
728
+ }
729
+ }
730
+ hostOptions(_principal, cwd, runtime, projectTrusted = this.options.projectTrusted) {
731
+ return {
732
+ cwd,
733
+ // SessionHost uses the same durable root as TUI. Browser identity only
734
+ // scopes transport state and must never become a settings root.
735
+ agentDir: resolve(this.options.agentDir),
736
+ projectTrusted,
737
+ noSkills: this.options.noSkills,
738
+ noContextFiles: this.options.noContextFiles,
739
+ skillPaths: this.options.skillPaths,
740
+ provider: runtime?.provider ?? this.options.provider,
741
+ model: runtime?.model ?? this.options.model,
742
+ signal: this.options.signal,
743
+ compaction: this.options.compaction,
744
+ };
745
+ }
746
+ clientTempRoot(client) {
747
+ const id = createHash("sha256").update(client.principal).digest("hex");
748
+ return resolve(this.options.agentDir, "webui", "actors", id);
749
+ }
750
+ async rpc(req, res, client, url) {
751
+ const request = requestFromValue(JSON.parse(await body(req, this.options.maxBodyBytes ?? 2 * 1024 * 1024)));
752
+ const { dispatcher } = await this.actor(client, url);
753
+ const response = await dispatcher.dispatch(request);
754
+ json(res, 200, response);
755
+ }
756
+ async attachments(req, res, client, url) {
757
+ // A 5 MiB binary attachment expands to nearly 7 MiB in base64 before JSON framing.
758
+ const value = JSON.parse(await body(req, this.options.maxBodyBytes ?? 10 * 1024 * 1024));
759
+ if (typeof value !== "object" || value === null)
760
+ throw new Error("Attachment body must be an object.");
761
+ const request = requestFromValue({
762
+ version: RPC_PROTOCOL_VERSION,
763
+ kind: "request",
764
+ id: randomUUID(),
765
+ method: "create_attachment",
766
+ params: value,
767
+ });
768
+ const { dispatcher } = await this.actor(client, url);
769
+ json(res, 200, await dispatcher.dispatch(request));
770
+ }
771
+ async events(req, res, client, url) {
772
+ if (typeof req.headers["x-di-code-resume-token"] === "string")
773
+ this.closeConnections(client);
774
+ const perClientLimit = this.options.maxSseConnectionsPerClient ?? 8;
775
+ const globalLimit = this.options.maxSseConnections ?? 64;
776
+ if (client.connections.size >= perClientLimit || this.sseConnections >= globalLimit) {
777
+ json(res, 429, { error: "SSE connection limit exceeded." });
778
+ return;
779
+ }
780
+ const { dispatcher } = await this.actor(client, url);
781
+ let released = false;
782
+ const connection = {
783
+ queue: [],
784
+ waiters: [],
785
+ maxQueue: 128,
786
+ closed: false,
787
+ closeResponse: () => res.end(),
788
+ release: () => {
789
+ if (released)
790
+ return;
791
+ released = true;
792
+ connection.closed = true;
793
+ connection.waiters.splice(0).forEach((wake) => void wake());
794
+ client.connections.delete(connection);
795
+ this.sseConnections = Math.max(0, this.sseConnections - 1);
796
+ },
797
+ };
798
+ client.connections.add(connection);
799
+ this.sseConnections += 1;
800
+ req.once("close", () => {
801
+ connection.release();
802
+ });
803
+ const unsubscribe = dispatcher.subscribe((message) => {
804
+ if (connection.closed)
805
+ return;
806
+ if (connection.queue.length >= connection.maxQueue) {
807
+ connection.queue.shift();
808
+ connection.queue.push({ version: 1, kind: "event", requestId: "webui", event: { type: "snapshot_required" } });
809
+ }
810
+ else
811
+ connection.queue.push(message);
812
+ connection.waiters.splice(0).forEach((wake) => void wake());
813
+ });
814
+ const keepalive = setInterval(() => {
815
+ if (connection.closed)
816
+ return;
817
+ if (connection.queue.length >= connection.maxQueue)
818
+ connection.queue.shift();
819
+ connection.queue.push({ keepalive: true });
820
+ connection.waiters.splice(0).forEach((wake) => void wake());
821
+ }, 15_000);
822
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
823
+ res.write(`event: ready\ndata: ${JSON.stringify({ resumeToken: client.resumeToken })}\n\n`);
824
+ const last = Number(req.headers["last-event-id"] ?? "0");
825
+ if (Number.isSafeInteger(last) && last >= 0)
826
+ await dispatcher.dispatch({
827
+ version: 1,
828
+ kind: "request",
829
+ id: `resume:${randomUUID()}`,
830
+ method: "resume_events",
831
+ params: { lastSequence: last },
832
+ });
833
+ try {
834
+ while (!connection.closed) {
835
+ const next = connection.queue.shift();
836
+ if (!next)
837
+ await new Promise((resolvePromise) => connection.waiters.push(resolvePromise));
838
+ else {
839
+ const payload = "keepalive" in next
840
+ ? ": keepalive\n\n"
841
+ : `${next.kind === "event" && next.sequence !== undefined ? `id: ${next.sequence}\n` : ""}data: ${JSON.stringify(next)}\n\n`;
842
+ if (!res.write(payload))
843
+ await new Promise((resolvePromise) => res.once("drain", resolvePromise));
844
+ }
845
+ }
846
+ }
847
+ finally {
848
+ clearInterval(keepalive);
849
+ unsubscribe();
850
+ connection.release();
851
+ }
852
+ }
853
+ closeConnections(client) {
854
+ for (const connection of client.connections) {
855
+ connection.release();
856
+ connection.closeResponse();
857
+ }
858
+ }
859
+ closeClient(client) {
860
+ this.closeConnections(client);
861
+ for (const dispatcher of client.dispatchers.values())
862
+ void dispatcher.dispose();
863
+ for (const product of client.productHosts.values())
864
+ void product.dispose();
865
+ client.dispatchers.clear();
866
+ client.productHosts.clear();
867
+ this.clientsByResumeToken.delete(client.resumeToken);
868
+ }
869
+ }
870
+ function constantEquals(left, right) {
871
+ const leftBytes = Buffer.from(left);
872
+ const rightBytes = Buffer.from(right);
873
+ return leftBytes.byteLength === rightBytes.byteLength && timingSafeEqual(leftBytes, rightBytes);
874
+ }
875
+ function isApiRoute(pathname) {
876
+ return (pathname === "/rpc" ||
877
+ pathname === "/events" ||
878
+ pathname === "/attachments" ||
879
+ pathname === "/healthz" ||
880
+ pathname.startsWith("/api/"));
881
+ }
882
+ //# sourceMappingURL=webui.js.map