@xyit/wellspring 0.0.0-stage → 1.1.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 (464) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +853 -2
  3. package/dist/agent/pending-asks.d.ts +20 -0
  4. package/dist/agent/pending-asks.js +70 -0
  5. package/dist/agent/pending-asks.js.map +1 -0
  6. package/dist/agent/task-registry.d.ts +14 -0
  7. package/dist/agent/task-registry.js +80 -0
  8. package/dist/agent/task-registry.js.map +1 -0
  9. package/dist/agent/tool-context.d.ts +30 -0
  10. package/dist/agent/tool-context.js +10 -0
  11. package/dist/agent/tool-context.js.map +1 -0
  12. package/dist/agent/usage.d.ts +40 -0
  13. package/dist/agent/usage.js +122 -0
  14. package/dist/agent/usage.js.map +1 -0
  15. package/dist/api/host/agent.d.ts +165 -0
  16. package/dist/api/host/agent.js +5 -0
  17. package/dist/api/host/agent.js.map +1 -0
  18. package/dist/api/host/events.d.ts +2 -0
  19. package/dist/api/host/events.js +6 -0
  20. package/dist/api/host/events.js.map +1 -0
  21. package/dist/api/host/exec.d.ts +36 -0
  22. package/dist/api/host/exec.js +10 -0
  23. package/dist/api/host/exec.js.map +1 -0
  24. package/dist/api/host/fs.d.ts +24 -0
  25. package/dist/api/host/fs.js +7 -0
  26. package/dist/api/host/fs.js.map +1 -0
  27. package/dist/api/host/hooks.d.ts +210 -0
  28. package/dist/api/host/hooks.js +401 -0
  29. package/dist/api/host/hooks.js.map +1 -0
  30. package/dist/api/host/host.d.ts +36 -0
  31. package/dist/api/host/host.js +53 -0
  32. package/dist/api/host/host.js.map +1 -0
  33. package/dist/api/host/http.d.ts +58 -0
  34. package/dist/api/host/http.js +6 -0
  35. package/dist/api/host/http.js.map +1 -0
  36. package/dist/api/host/index.d.ts +15 -0
  37. package/dist/api/host/index.js +20 -0
  38. package/dist/api/host/index.js.map +1 -0
  39. package/dist/api/host/models.d.ts +65 -0
  40. package/dist/api/host/models.js +8 -0
  41. package/dist/api/host/models.js.map +1 -0
  42. package/dist/api/host/network.d.ts +22 -0
  43. package/dist/api/host/network.js +7 -0
  44. package/dist/api/host/network.js.map +1 -0
  45. package/dist/api/host/plugin.d.ts +138 -0
  46. package/dist/api/host/plugin.js +6 -0
  47. package/dist/api/host/plugin.js.map +1 -0
  48. package/dist/api/host/plugins.d.ts +49 -0
  49. package/dist/api/host/plugins.js +6 -0
  50. package/dist/api/host/plugins.js.map +1 -0
  51. package/dist/api/host/pty.d.ts +55 -0
  52. package/dist/api/host/pty.js +21 -0
  53. package/dist/api/host/pty.js.map +1 -0
  54. package/dist/api/host/session.d.ts +5 -0
  55. package/dist/api/host/session.js +52 -0
  56. package/dist/api/host/session.js.map +1 -0
  57. package/dist/api/host/version.d.ts +2 -0
  58. package/dist/api/host/version.js +3 -0
  59. package/dist/api/host/version.js.map +1 -0
  60. package/dist/api/host/workspace.d.ts +38 -0
  61. package/dist/api/host/workspace.js +9 -0
  62. package/dist/api/host/workspace.js.map +1 -0
  63. package/dist/api/plugin/http-client.d.ts +42 -0
  64. package/dist/api/plugin/http-client.js +364 -0
  65. package/dist/api/plugin/http-client.js.map +1 -0
  66. package/dist/api/plugin/http-protocol.d.ts +152 -0
  67. package/dist/api/plugin/http-protocol.js +49 -0
  68. package/dist/api/plugin/http-protocol.js.map +1 -0
  69. package/dist/api/result/result.d.ts +16 -0
  70. package/dist/api/result/result.js +43 -0
  71. package/dist/api/result/result.js.map +1 -0
  72. package/dist/commands/agent-turn.d.ts +63 -0
  73. package/dist/commands/agent-turn.js +687 -0
  74. package/dist/commands/agent-turn.js.map +1 -0
  75. package/dist/commands/ask-prompt.d.ts +6 -0
  76. package/dist/commands/ask-prompt.js +49 -0
  77. package/dist/commands/ask-prompt.js.map +1 -0
  78. package/dist/commands/config.d.ts +2 -0
  79. package/dist/commands/config.js +283 -0
  80. package/dist/commands/config.js.map +1 -0
  81. package/dist/commands/conversation.d.ts +51 -0
  82. package/dist/commands/conversation.js +630 -0
  83. package/dist/commands/conversation.js.map +1 -0
  84. package/dist/commands/doctor.d.ts +2 -0
  85. package/dist/commands/doctor.js +77 -0
  86. package/dist/commands/doctor.js.map +1 -0
  87. package/dist/commands/exec-approval.d.ts +3 -0
  88. package/dist/commands/exec-approval.js +80 -0
  89. package/dist/commands/exec-approval.js.map +1 -0
  90. package/dist/commands/help.d.ts +2 -0
  91. package/dist/commands/help.js +35 -0
  92. package/dist/commands/help.js.map +1 -0
  93. package/dist/commands/key.d.ts +2 -0
  94. package/dist/commands/key.js +226 -0
  95. package/dist/commands/key.js.map +1 -0
  96. package/dist/commands/plugin.d.ts +2 -0
  97. package/dist/commands/plugin.js +190 -0
  98. package/dist/commands/plugin.js.map +1 -0
  99. package/dist/commands/session.d.ts +2 -0
  100. package/dist/commands/session.js +167 -0
  101. package/dist/commands/session.js.map +1 -0
  102. package/dist/commands/shared.d.ts +35 -0
  103. package/dist/commands/shared.js +66 -0
  104. package/dist/commands/shared.js.map +1 -0
  105. package/dist/commands/start.d.ts +2 -0
  106. package/dist/commands/start.js +75 -0
  107. package/dist/commands/start.js.map +1 -0
  108. package/dist/commands/stop.d.ts +2 -0
  109. package/dist/commands/stop.js +24 -0
  110. package/dist/commands/stop.js.map +1 -0
  111. package/dist/commands/version.d.ts +2 -0
  112. package/dist/commands/version.js +33 -0
  113. package/dist/commands/version.js.map +1 -0
  114. package/dist/config/codes.d.ts +13 -0
  115. package/dist/config/codes.js +15 -0
  116. package/dist/config/codes.js.map +1 -0
  117. package/dist/config/credentials.d.ts +67 -0
  118. package/dist/config/credentials.js +214 -0
  119. package/dist/config/credentials.js.map +1 -0
  120. package/dist/config/defaults.d.ts +5 -0
  121. package/dist/config/defaults.js +31 -0
  122. package/dist/config/defaults.js.map +1 -0
  123. package/dist/config/index.d.ts +5 -0
  124. package/dist/config/index.js +10 -0
  125. package/dist/config/index.js.map +1 -0
  126. package/dist/config/loader.d.ts +10 -0
  127. package/dist/config/loader.js +68 -0
  128. package/dist/config/loader.js.map +1 -0
  129. package/dist/config/providers.d.ts +30 -0
  130. package/dist/config/providers.js +97 -0
  131. package/dist/config/providers.js.map +1 -0
  132. package/dist/config/types.d.ts +75 -0
  133. package/dist/config/types.js +4 -0
  134. package/dist/config/types.js.map +1 -0
  135. package/dist/config/validate.d.ts +4 -0
  136. package/dist/config/validate.js +391 -0
  137. package/dist/config/validate.js.map +1 -0
  138. package/dist/display/markdown.d.ts +7 -0
  139. package/dist/display/markdown.js +116 -0
  140. package/dist/display/markdown.js.map +1 -0
  141. package/dist/display/tool-label.d.ts +6 -0
  142. package/dist/display/tool-label.js +81 -0
  143. package/dist/display/tool-label.js.map +1 -0
  144. package/dist/event/event.d.ts +27 -0
  145. package/dist/event/event.js +152 -0
  146. package/dist/event/event.js.map +1 -0
  147. package/dist/index.d.ts +2 -0
  148. package/dist/index.js +308 -0
  149. package/dist/index.js.map +1 -0
  150. package/dist/input/codes.d.ts +15 -0
  151. package/dist/input/codes.js +20 -0
  152. package/dist/input/codes.js.map +1 -0
  153. package/dist/input/commands.d.ts +10 -0
  154. package/dist/input/commands.js +65 -0
  155. package/dist/input/commands.js.map +1 -0
  156. package/dist/input/help.d.ts +8 -0
  157. package/dist/input/help.js +51 -0
  158. package/dist/input/help.js.map +1 -0
  159. package/dist/input/index.d.ts +6 -0
  160. package/dist/input/index.js +10 -0
  161. package/dist/input/index.js.map +1 -0
  162. package/dist/input/parse-options.d.ts +11 -0
  163. package/dist/input/parse-options.js +51 -0
  164. package/dist/input/parse-options.js.map +1 -0
  165. package/dist/input/parse.d.ts +3 -0
  166. package/dist/input/parse.js +119 -0
  167. package/dist/input/parse.js.map +1 -0
  168. package/dist/input/types.d.ts +17 -0
  169. package/dist/input/types.js +2 -0
  170. package/dist/input/types.js.map +1 -0
  171. package/dist/logger/elog4js.d.ts +65 -0
  172. package/dist/logger/elog4js.js +305 -0
  173. package/dist/logger/elog4js.js.map +1 -0
  174. package/dist/logger/logger.d.ts +37 -0
  175. package/dist/logger/logger.js +196 -0
  176. package/dist/logger/logger.js.map +1 -0
  177. package/dist/network/index.d.ts +2 -0
  178. package/dist/network/index.js +6 -0
  179. package/dist/network/index.js.map +1 -0
  180. package/dist/network/openai.d.ts +117 -0
  181. package/dist/network/openai.js +443 -0
  182. package/dist/network/openai.js.map +1 -0
  183. package/dist/network/proxy.d.ts +9 -0
  184. package/dist/network/proxy.js +72 -0
  185. package/dist/network/proxy.js.map +1 -0
  186. package/dist/network/web.d.ts +34 -0
  187. package/dist/network/web.js +152 -0
  188. package/dist/network/web.js.map +1 -0
  189. package/dist/platform/clipboard.d.ts +15 -0
  190. package/dist/platform/clipboard.js +82 -0
  191. package/dist/platform/clipboard.js.map +1 -0
  192. package/dist/platform/exec.d.ts +28 -0
  193. package/dist/platform/exec.js +95 -0
  194. package/dist/platform/exec.js.map +1 -0
  195. package/dist/platform/file.d.ts +38 -0
  196. package/dist/platform/file.js +197 -0
  197. package/dist/platform/file.js.map +1 -0
  198. package/dist/platform/index.d.ts +7 -0
  199. package/dist/platform/index.js +12 -0
  200. package/dist/platform/index.js.map +1 -0
  201. package/dist/platform/notification.d.ts +14 -0
  202. package/dist/platform/notification.js +82 -0
  203. package/dist/platform/notification.js.map +1 -0
  204. package/dist/platform/open-external.d.ts +10 -0
  205. package/dist/platform/open-external.js +105 -0
  206. package/dist/platform/open-external.js.map +1 -0
  207. package/dist/platform/os.d.ts +20 -0
  208. package/dist/platform/os.js +86 -0
  209. package/dist/platform/os.js.map +1 -0
  210. package/dist/platform/path.d.ts +32 -0
  211. package/dist/platform/path.js +154 -0
  212. package/dist/platform/path.js.map +1 -0
  213. package/dist/plugin/access-guard.d.ts +1 -0
  214. package/dist/plugin/access-guard.js +40 -0
  215. package/dist/plugin/access-guard.js.map +1 -0
  216. package/dist/plugin/agent-service.d.ts +25 -0
  217. package/dist/plugin/agent-service.js +809 -0
  218. package/dist/plugin/agent-service.js.map +1 -0
  219. package/dist/plugin/agent-tools.d.ts +12 -0
  220. package/dist/plugin/agent-tools.js +31 -0
  221. package/dist/plugin/agent-tools.js.map +1 -0
  222. package/dist/plugin/capabilities.d.ts +26 -0
  223. package/dist/plugin/capabilities.js +192 -0
  224. package/dist/plugin/capabilities.js.map +1 -0
  225. package/dist/plugin/console-guard.d.ts +2 -0
  226. package/dist/plugin/console-guard.js +52 -0
  227. package/dist/plugin/console-guard.js.map +1 -0
  228. package/dist/plugin/data-access.d.ts +15 -0
  229. package/dist/plugin/data-access.js +57 -0
  230. package/dist/plugin/data-access.js.map +1 -0
  231. package/dist/plugin/fs.d.ts +3 -0
  232. package/dist/plugin/fs.js +93 -0
  233. package/dist/plugin/fs.js.map +1 -0
  234. package/dist/plugin/host-alias.d.ts +2 -0
  235. package/dist/plugin/host-alias.js +57 -0
  236. package/dist/plugin/host-alias.js.map +1 -0
  237. package/dist/plugin/index.d.ts +7 -0
  238. package/dist/plugin/index.js +11 -0
  239. package/dist/plugin/index.js.map +1 -0
  240. package/dist/plugin/install.d.ts +17 -0
  241. package/dist/plugin/install.js +163 -0
  242. package/dist/plugin/install.js.map +1 -0
  243. package/dist/plugin/loader.d.ts +45 -0
  244. package/dist/plugin/loader.js +739 -0
  245. package/dist/plugin/loader.js.map +1 -0
  246. package/dist/plugin/manage.d.ts +31 -0
  247. package/dist/plugin/manage.js +271 -0
  248. package/dist/plugin/manage.js.map +1 -0
  249. package/dist/plugin/models-service.d.ts +15 -0
  250. package/dist/plugin/models-service.js +317 -0
  251. package/dist/plugin/models-service.js.map +1 -0
  252. package/dist/plugin/network-service.d.ts +10 -0
  253. package/dist/plugin/network-service.js +87 -0
  254. package/dist/plugin/network-service.js.map +1 -0
  255. package/dist/plugin/package-manager.d.ts +46 -0
  256. package/dist/plugin/package-manager.js +148 -0
  257. package/dist/plugin/package-manager.js.map +1 -0
  258. package/dist/plugin/paths.d.ts +6 -0
  259. package/dist/plugin/paths.js +15 -0
  260. package/dist/plugin/paths.js.map +1 -0
  261. package/dist/plugin/registry.d.ts +17 -0
  262. package/dist/plugin/registry.js +82 -0
  263. package/dist/plugin/registry.js.map +1 -0
  264. package/dist/plugin/runtime.d.ts +9 -0
  265. package/dist/plugin/runtime.js +43 -0
  266. package/dist/plugin/runtime.js.map +1 -0
  267. package/dist/plugin/scaffold.d.ts +13 -0
  268. package/dist/plugin/scaffold.js +132 -0
  269. package/dist/plugin/scaffold.js.map +1 -0
  270. package/dist/plugin/state.d.ts +6 -0
  271. package/dist/plugin/state.js +38 -0
  272. package/dist/plugin/state.js.map +1 -0
  273. package/dist/plugin/zip.d.ts +7 -0
  274. package/dist/plugin/zip.js +138 -0
  275. package/dist/plugin/zip.js.map +1 -0
  276. package/dist/sandbox/audit.d.ts +12 -0
  277. package/dist/sandbox/audit.js +20 -0
  278. package/dist/sandbox/audit.js.map +1 -0
  279. package/dist/sandbox/backends/bwrap.d.ts +28 -0
  280. package/dist/sandbox/backends/bwrap.js +197 -0
  281. package/dist/sandbox/backends/bwrap.js.map +1 -0
  282. package/dist/sandbox/backends/direct.d.ts +7 -0
  283. package/dist/sandbox/backends/direct.js +32 -0
  284. package/dist/sandbox/backends/direct.js.map +1 -0
  285. package/dist/sandbox/bridge-state.d.ts +13 -0
  286. package/dist/sandbox/bridge-state.js +12 -0
  287. package/dist/sandbox/bridge-state.js.map +1 -0
  288. package/dist/sandbox/index.d.ts +5 -0
  289. package/dist/sandbox/index.js +6 -0
  290. package/dist/sandbox/index.js.map +1 -0
  291. package/dist/sandbox/manager.d.ts +8 -0
  292. package/dist/sandbox/manager.js +325 -0
  293. package/dist/sandbox/manager.js.map +1 -0
  294. package/dist/sandbox/memory-limit.d.ts +44 -0
  295. package/dist/sandbox/memory-limit.js +193 -0
  296. package/dist/sandbox/memory-limit.js.map +1 -0
  297. package/dist/sandbox/plugin-client.d.ts +80 -0
  298. package/dist/sandbox/plugin-client.js +1601 -0
  299. package/dist/sandbox/plugin-client.js.map +1 -0
  300. package/dist/sandbox/plugin-host.d.ts +1 -0
  301. package/dist/sandbox/plugin-host.js +1001 -0
  302. package/dist/sandbox/plugin-host.js.map +1 -0
  303. package/dist/sandbox/protocol.d.ts +43 -0
  304. package/dist/sandbox/protocol.js +96 -0
  305. package/dist/sandbox/protocol.js.map +1 -0
  306. package/dist/sandbox/rpc-timeouts.d.ts +28 -0
  307. package/dist/sandbox/rpc-timeouts.js +15 -0
  308. package/dist/sandbox/rpc-timeouts.js.map +1 -0
  309. package/dist/sandbox/supervisor.d.ts +31 -0
  310. package/dist/sandbox/supervisor.js +114 -0
  311. package/dist/sandbox/supervisor.js.map +1 -0
  312. package/dist/sandbox/types.d.ts +124 -0
  313. package/dist/sandbox/types.js +17 -0
  314. package/dist/sandbox/types.js.map +1 -0
  315. package/dist/service/control.d.ts +32 -0
  316. package/dist/service/control.js +117 -0
  317. package/dist/service/control.js.map +1 -0
  318. package/dist/session/compress.d.ts +3 -0
  319. package/dist/session/compress.js +42 -0
  320. package/dist/session/compress.js.map +1 -0
  321. package/dist/session/index.d.ts +4 -0
  322. package/dist/session/index.js +8 -0
  323. package/dist/session/index.js.map +1 -0
  324. package/dist/session/registry.d.ts +4 -0
  325. package/dist/session/registry.js +12 -0
  326. package/dist/session/registry.js.map +1 -0
  327. package/dist/session/replay.d.ts +10 -0
  328. package/dist/session/replay.js +109 -0
  329. package/dist/session/replay.js.map +1 -0
  330. package/dist/session/session.d.ts +40 -0
  331. package/dist/session/session.js +108 -0
  332. package/dist/session/session.js.map +1 -0
  333. package/dist/session/store.d.ts +58 -0
  334. package/dist/session/store.js +382 -0
  335. package/dist/session/store.js.map +1 -0
  336. package/dist/session/types.d.ts +56 -0
  337. package/dist/session/types.js +3 -0
  338. package/dist/session/types.js.map +1 -0
  339. package/dist/workspace/index.d.ts +7 -0
  340. package/dist/workspace/index.js +17 -0
  341. package/dist/workspace/index.js.map +1 -0
  342. package/dist/workspace/registry.d.ts +25 -0
  343. package/dist/workspace/registry.js +293 -0
  344. package/dist/workspace/registry.js.map +1 -0
  345. package/package.json +73 -5
  346. package/scripts/build-node-pty.mjs +55 -0
  347. package/scripts/rewrite-dts.mjs +36 -0
  348. package/scripts/run-tests.mjs +41 -0
  349. package/scripts/verify-sandbox.ts +98 -0
  350. package/src/agent/pending-asks.ts +88 -0
  351. package/src/agent/task-registry.ts +108 -0
  352. package/src/agent/tool-context.ts +47 -0
  353. package/src/agent/usage.ts +170 -0
  354. package/src/api/host/agent.ts +162 -0
  355. package/src/api/host/events.ts +7 -0
  356. package/src/api/host/exec.ts +47 -0
  357. package/src/api/host/fs.ts +33 -0
  358. package/src/api/host/hooks.ts +635 -0
  359. package/src/api/host/host.ts +99 -0
  360. package/src/api/host/http.ts +79 -0
  361. package/src/api/host/index.ts +20 -0
  362. package/src/api/host/models.ts +81 -0
  363. package/src/api/host/network.ts +32 -0
  364. package/src/api/host/plugin.ts +158 -0
  365. package/src/api/host/plugins.ts +59 -0
  366. package/src/api/host/pty.ts +73 -0
  367. package/src/api/host/session.ts +61 -0
  368. package/src/api/host/version.ts +2 -0
  369. package/src/api/host/workspace.ts +51 -0
  370. package/src/api/plugin/http-client.ts +492 -0
  371. package/src/api/plugin/http-protocol.ts +231 -0
  372. package/src/api/result/result.ts +52 -0
  373. package/src/commands/agent-turn.ts +817 -0
  374. package/src/commands/ask-prompt.ts +52 -0
  375. package/src/commands/config.ts +306 -0
  376. package/src/commands/conversation.ts +777 -0
  377. package/src/commands/doctor.ts +92 -0
  378. package/src/commands/exec-approval.ts +91 -0
  379. package/src/commands/help.ts +41 -0
  380. package/src/commands/key.ts +281 -0
  381. package/src/commands/plugin.ts +222 -0
  382. package/src/commands/session.ts +179 -0
  383. package/src/commands/shared.ts +103 -0
  384. package/src/commands/start.ts +88 -0
  385. package/src/commands/stop.ts +24 -0
  386. package/src/commands/version.ts +40 -0
  387. package/src/config/codes.ts +17 -0
  388. package/src/config/credentials.ts +302 -0
  389. package/src/config/defaults.ts +32 -0
  390. package/src/config/index.ts +10 -0
  391. package/src/config/loader.ts +80 -0
  392. package/src/config/providers.ts +133 -0
  393. package/src/config/types.ts +88 -0
  394. package/src/config/validate.ts +458 -0
  395. package/src/display/markdown.ts +137 -0
  396. package/src/display/tool-label.ts +87 -0
  397. package/src/event/event.ts +171 -0
  398. package/src/index.ts +344 -0
  399. package/src/input/codes.ts +21 -0
  400. package/src/input/commands.ts +76 -0
  401. package/src/input/help.ts +60 -0
  402. package/src/input/index.ts +10 -0
  403. package/src/input/parse-options.ts +66 -0
  404. package/src/input/parse.ts +138 -0
  405. package/src/input/types.ts +19 -0
  406. package/src/logger/elog4js.ts +366 -0
  407. package/src/logger/logger.ts +251 -0
  408. package/src/network/index.ts +6 -0
  409. package/src/network/openai.ts +571 -0
  410. package/src/network/proxy.ts +80 -0
  411. package/src/network/web.ts +206 -0
  412. package/src/platform/clipboard.ts +107 -0
  413. package/src/platform/exec.ts +128 -0
  414. package/src/platform/file.ts +235 -0
  415. package/src/platform/index.ts +12 -0
  416. package/src/platform/notification.ts +108 -0
  417. package/src/platform/open-external.ts +132 -0
  418. package/src/platform/os.ts +101 -0
  419. package/src/platform/path.ts +214 -0
  420. package/src/plugin/access-guard.ts +46 -0
  421. package/src/plugin/agent-service.ts +969 -0
  422. package/src/plugin/agent-tools.ts +43 -0
  423. package/src/plugin/capabilities.ts +240 -0
  424. package/src/plugin/console-guard.ts +66 -0
  425. package/src/plugin/data-access.ts +70 -0
  426. package/src/plugin/fs.ts +109 -0
  427. package/src/plugin/host-alias.ts +61 -0
  428. package/src/plugin/index.ts +11 -0
  429. package/src/plugin/install.ts +213 -0
  430. package/src/plugin/loader.ts +952 -0
  431. package/src/plugin/manage.ts +338 -0
  432. package/src/plugin/models-service.ts +383 -0
  433. package/src/plugin/network-service.ts +102 -0
  434. package/src/plugin/package-manager.ts +214 -0
  435. package/src/plugin/paths.ts +19 -0
  436. package/src/plugin/registry.ts +103 -0
  437. package/src/plugin/runtime.ts +57 -0
  438. package/src/plugin/scaffold.ts +158 -0
  439. package/src/plugin/state.ts +52 -0
  440. package/src/plugin/zip.ts +156 -0
  441. package/src/sandbox/audit.ts +38 -0
  442. package/src/sandbox/backends/bwrap.ts +251 -0
  443. package/src/sandbox/backends/direct.ts +43 -0
  444. package/src/sandbox/bridge-state.ts +29 -0
  445. package/src/sandbox/index.ts +5 -0
  446. package/src/sandbox/manager.ts +406 -0
  447. package/src/sandbox/memory-limit.ts +245 -0
  448. package/src/sandbox/plugin-client.ts +1881 -0
  449. package/src/sandbox/plugin-host.ts +1282 -0
  450. package/src/sandbox/protocol.ts +142 -0
  451. package/src/sandbox/pty-bridge.py +154 -0
  452. package/src/sandbox/rpc-timeouts.ts +43 -0
  453. package/src/sandbox/supervisor.ts +172 -0
  454. package/src/sandbox/types.ts +149 -0
  455. package/src/service/control.ts +155 -0
  456. package/src/session/compress.ts +47 -0
  457. package/src/session/index.ts +8 -0
  458. package/src/session/registry.ts +17 -0
  459. package/src/session/replay.ts +132 -0
  460. package/src/session/session.ts +134 -0
  461. package/src/session/store.ts +468 -0
  462. package/src/session/types.ts +61 -0
  463. package/src/workspace/index.ts +21 -0
  464. package/src/workspace/registry.ts +356 -0
@@ -0,0 +1,1881 @@
1
+ /*
2
+ * 宿主侧 sandbox 插件客户端:启动 child、握手、命令/钩子 RPC、host API 代理。
3
+ */
4
+
5
+ import { spawn, type ChildProcess } from "node:child_process"
6
+ import { createServer as createHttpServer, type Server as HttpServer } from "node:http"
7
+ import { createServer as createHttpsServer } from "node:https"
8
+ import { WebSocketServer, type WebSocket } from "ws"
9
+ import { randomUUID } from "node:crypto"
10
+ import { cp, mkdir, rm, stat } from "node:fs/promises"
11
+ import { basename, dirname, join, relative, resolve } from "node:path"
12
+
13
+ import type { Logger } from "pino"
14
+
15
+ import type { AppSetting } from "../config/types.ts"
16
+ import { eventBus, EventEntity } from "../event/event.ts"
17
+ import {
18
+ addWorkspace,
19
+ listWorkspaces,
20
+ removeWorkspace,
21
+ renameWorkspace,
22
+ touchWorkspace,
23
+ } from "../workspace/registry.ts"
24
+ import { getElog } from "../logger/elog4js.ts"
25
+ import type { ResolvedPaths } from "../platform/path.ts"
26
+ import { createPluginFileApi } from "../plugin/fs.ts"
27
+ import type { PluginDataService } from "../plugin/data-access.ts"
28
+ import type { HostCapabilities } from "../plugin/capabilities.ts"
29
+ import { getAgentToolContext, type AgentToolContext } from "../agent/tool-context.ts"
30
+ import { ensurePluginDataDir } from "../plugin/runtime.ts"
31
+ import { auditSandbox } from "./audit.ts"
32
+ import { RPC_TIMEOUT_MS } from "./rpc-timeouts.ts"
33
+ import { getHost } from "../api/host/host.ts"
34
+ import { getSession as findSession } from "../session/registry.ts"
35
+ import { registerExternalHook, shutdown as shutdownHost } from "../api/host/hooks.ts"
36
+ import type {
37
+ AgentCallToolResult,
38
+ AgentTool,
39
+ AgentToolIntroduction,
40
+ PluginCommand,
41
+ PluginConfig,
42
+ } from "../api/host/plugin.ts"
43
+ import type { HostFileApi, HostFileEntry, HostFileStat } from "../api/host/fs.ts"
44
+ import {
45
+ HOST_EXEC_DEFAULT_TIMEOUT_MS,
46
+ HOST_EXEC_MAX_OUTPUT_BYTES,
47
+ type HostExecChunk,
48
+ } from "../api/host/exec.ts"
49
+ import {
50
+ HOST_PTY_DEFAULT_COLS,
51
+ HOST_PTY_DEFAULT_ROWS,
52
+ HOST_PTY_MAX_COLS,
53
+ HOST_PTY_MAX_OUTPUT_CHARS,
54
+ HOST_PTY_MAX_POLL_CHARS,
55
+ HOST_PTY_MAX_ROWS,
56
+ HOST_PTY_MAX_SESSIONS,
57
+ HOST_PTY_MIN_COLS,
58
+ HOST_PTY_MIN_ROWS,
59
+ type HostPtyChunk,
60
+ type HostPtySession,
61
+ } from "../api/host/pty.ts"
62
+ import type { HttpRequest, HttpResponse } from "../api/host/http.ts"
63
+ import type { HostAgentChatInput } from "../api/host/agent.ts"
64
+ import type { HostModelsApi, HostModelProviderInput } from "../api/host/models.ts"
65
+ import type { HostNetworkApi, HostNetworkProxyInput } from "../api/host/network.ts"
66
+ import type { HostPluginInstallInput, HostPluginsApi } from "../api/host/plugins.ts"
67
+ import type { HostAgentService } from "../plugin/agent-service.ts"
68
+ import { HTTP_MAX_BODY_BYTES } from "../api/host/http.ts"
69
+ import {
70
+ FrameDecoder,
71
+ encodeMessage,
72
+ nextRequestId,
73
+ type RpcMessage,
74
+ type RpcRequest,
75
+ type RpcResponse,
76
+ } from "./protocol.ts"
77
+ import type { PluginLaunchInput, SandboxBackend, SandboxLimits } from "./types.ts"
78
+
79
+ const HOOK_KIND_BY_API: Readonly<Record<string, string>> = {
80
+ onMessage: "message",
81
+ onSessionStart: "sessionStart",
82
+ beforeTurn: "beforeTurn",
83
+ beforeStep: "beforeStep",
84
+ afterStep: "afterStep",
85
+ afterTurn: "afterTurn",
86
+ onAgentRunSync: "agentRunSync",
87
+ onAgentRunAsync: "agentRunAsync",
88
+ onAgentAsyncTaskComplete: "agentAsyncTaskComplete",
89
+ onAgentCallTool: "agentCallTool",
90
+ onAgentAsk: "agentAsk",
91
+ onAgentRead: "agentRead",
92
+ onAgentQueryTask: "agentQueryTask",
93
+ onAgentSubmit: "agentSubmit",
94
+ onAgentSetTasks: "agentSetTasks",
95
+ onAgentSetGoal: "agentSetGoal",
96
+ onAgentRequestPermission: "agentRequestPermission",
97
+ onUserAnswer: "userAnswer",
98
+ onInit: "init",
99
+ onShutdown: "shutdown",
100
+ }
101
+
102
+ const activeClients = new Set<PluginSandboxClient>()
103
+
104
+ export async function shutdownAllSandboxPlugins(): Promise<void> {
105
+ const clients = [...activeClients]
106
+ activeClients.clear()
107
+ await Promise.all(clients.map(async (client) => await client.close().catch(() => undefined)))
108
+ }
109
+
110
+ const PROJECT_ROOT = resolve(import.meta.dirname, "../..")
111
+
112
+ export interface SandboxPluginMetadata {
113
+ readonly id: string
114
+ readonly name: string
115
+ readonly version: string
116
+ readonly status: "ok" | "failed"
117
+ readonly deps: readonly string[]
118
+ }
119
+
120
+ export interface SandboxPluginCommandMeta {
121
+ readonly name: string
122
+ readonly summary: string
123
+ readonly usage?: string
124
+ readonly aliases?: readonly string[]
125
+ }
126
+
127
+ export interface SandboxAgentToolMeta {
128
+ readonly name: string
129
+ readonly introduction: AgentToolIntroduction
130
+ }
131
+
132
+ export interface SandboxPluginReady {
133
+ readonly plugin: SandboxPluginMetadata
134
+ readonly commands: readonly SandboxPluginCommandMeta[]
135
+ readonly agentTools: readonly SandboxAgentToolMeta[]
136
+ readonly hooks: readonly string[]
137
+ }
138
+
139
+ /** stat 但不存在时返回 null,用于 cwd 校验 */
140
+ async function statSafe(path: string) {
141
+ try {
142
+ return await stat(path)
143
+ } catch {
144
+ return null
145
+ }
146
+ }
147
+
148
+ /** 结束整棵进程树:detached 启动时先杀进程组,再兜底 kill */
149
+ function killSession(session: ExecSession): void {
150
+ const child = session.child
151
+ if (child.killed || child.exitCode !== null) return
152
+ if (process.platform !== "win32" && typeof child.pid === "number") {
153
+ try {
154
+ process.kill(-child.pid, "SIGKILL")
155
+ return
156
+ } catch {
157
+ /* 进程组可能已消失,退回单进程 kill */
158
+ }
159
+ }
160
+ try {
161
+ child.kill("SIGKILL")
162
+ } catch {
163
+ /* 已经退出 */
164
+ }
165
+ }
166
+
167
+ /** node-pty 进程的最小结构,避免把可选依赖变成静态类型依赖 */
168
+ interface NodePtyProcess {
169
+ readonly pid: number
170
+ onData(handler: (data: string) => void): void
171
+ onExit(handler: (event: { exitCode: number; signal?: number }) => void): void
172
+ write(data: string): void
173
+ resize(cols: number, rows: number): void
174
+ kill(signal?: string): void
175
+ }
176
+
177
+ interface NodePtyModule {
178
+ spawn(file: string, args: string[], options: Record<string, unknown>): NodePtyProcess
179
+ }
180
+
181
+ /** 后端无关的 PTY 句柄 */
182
+ interface PtyHandle {
183
+ readonly pid: number
184
+ write(data: string): void
185
+ resize(cols: number, rows: number): void
186
+ kill(): void
187
+ }
188
+
189
+ type PtyBackendName = "node-pty" | "python-pty"
190
+
191
+ /** PTY 会话的宿主侧状态 */
192
+ interface PtySession {
193
+ readonly handle: PtyHandle
194
+ readonly backend: PtyBackendName
195
+ readonly file: string
196
+ readonly cwd: string
197
+ cols: number
198
+ rows: number
199
+ output: string
200
+ delivered: number
201
+ done: boolean
202
+ exitCode: number | null
203
+ }
204
+
205
+ /** 无需原生模块的 PTY 后端:python3 + pty-bridge.py */
206
+ const PTY_BRIDGE_PATH = join(PROJECT_ROOT, "src", "sandbox", "pty-bridge.py")
207
+
208
+ let pythonPtyProbe: Promise<string | null> | null = null
209
+
210
+ /** 探测可用的 python 解释器;没有就返回 null */
211
+ async function probePython(): Promise<string | null> {
212
+ if (pythonPtyProbe === null) {
213
+ pythonPtyProbe = (async () => {
214
+ for (const candidate of ["python3", "python"]) {
215
+ const ok = await new Promise<boolean>((resolveOk) => {
216
+ try {
217
+ const probe = spawn(candidate, ["-c", "import pty, termios"], {
218
+ stdio: "ignore",
219
+ })
220
+ probe.on("error", () => resolveOk(false))
221
+ probe.on("close", (code) => resolveOk(code === 0))
222
+ } catch {
223
+ resolveOk(false)
224
+ }
225
+ })
226
+ if (ok) return candidate
227
+ }
228
+ return null
229
+ })()
230
+ }
231
+ return await pythonPtyProbe
232
+ }
233
+
234
+ /** PTY 尺寸夹在合理范围,避免异常值把终端撑爆;cols 与 rows 的最小值不同 */
235
+ function clampSize(value: unknown, fallback: number, min: number, max: number): number {
236
+ const size = typeof value === "number" ? Math.round(value) : Number.NaN
237
+ if (!Number.isFinite(size)) return fallback
238
+ return Math.min(max, Math.max(min, size))
239
+ }
240
+
241
+ function clampCols(value: unknown): number {
242
+ return clampSize(value, HOST_PTY_DEFAULT_COLS, HOST_PTY_MIN_COLS, HOST_PTY_MAX_COLS)
243
+ }
244
+
245
+ function clampRows(value: unknown): number {
246
+ return clampSize(value, HOST_PTY_DEFAULT_ROWS, HOST_PTY_MIN_ROWS, HOST_PTY_MAX_ROWS)
247
+ }
248
+
249
+ /** 默认登录 shell */
250
+ function defaultShell(): string {
251
+ if (process.platform === "win32") {
252
+ return process.env.ComSpec ?? "powershell.exe"
253
+ }
254
+ return process.env.SHELL ?? "/bin/bash"
255
+ }
256
+
257
+ /** node-pty 是可选依赖:只尝试加载一次,失败就记住不可用 */
258
+ let nodePtyPromise: Promise<NodePtyModule | null> | null = null
259
+ /** node-pty 加载失败的原因,便于排查是没装还是原生模块装坏了 */
260
+ let nodePtyError: string | null = null
261
+
262
+ async function loadNodePty(): Promise<NodePtyModule | null> {
263
+ if (nodePtyPromise === null) {
264
+ nodePtyPromise = (async () => {
265
+ try {
266
+ // 变量 specifier:node-pty 可能根本没装,不能做成静态 import
267
+ const specifier = "node-pty"
268
+ const module = (await import(specifier)) as unknown as {
269
+ spawn?: NodePtyModule["spawn"]
270
+ default?: { spawn?: NodePtyModule["spawn"] }
271
+ }
272
+ const spawnPty = module.spawn ?? module.default?.spawn
273
+ if (typeof spawnPty !== "function") {
274
+ nodePtyError = "node-pty 已加载但没有导出 spawn"
275
+ return null
276
+ }
277
+ nodePtyError = null
278
+ return module as unknown as NodePtyModule
279
+ } catch (error) {
280
+ // 未安装与原生模块加载失败是两类问题,原因要原样带出去
281
+ nodePtyError = error instanceof Error ? error.message : String(error)
282
+ return null
283
+ }
284
+ })()
285
+ }
286
+ return await nodePtyPromise
287
+ }
288
+
289
+ /** 终端会话的宿主侧状态 */
290
+ interface ExecSession {
291
+ readonly child: ChildProcess
292
+ readonly cwd: string
293
+ readonly startedAt: number
294
+ output: string
295
+ /** 已交付给插件 poll 的字符数 */
296
+ delivered: number
297
+ done: boolean
298
+ exitCode: number | null
299
+ timedOut: boolean
300
+ truncated: boolean
301
+ timer: NodeJS.Timeout | null
302
+ }
303
+
304
+ export interface PluginSandboxPermissions {
305
+ readonly hostApi: readonly string[]
306
+ readonly workspaceRead: boolean
307
+ readonly workspaceWrite: boolean
308
+ }
309
+
310
+ export interface PluginSandboxClientOptions {
311
+ readonly pluginId: string
312
+ readonly pluginName: string
313
+ readonly pluginVersion: string
314
+ readonly pluginDir: string
315
+ readonly pluginEntry: string
316
+ readonly paths: ResolvedPaths
317
+ readonly config: AppSetting
318
+ readonly pluginConfig: PluginConfig
319
+ readonly backend: SandboxBackend
320
+ readonly limits: SandboxLimits
321
+ readonly logger: Logger
322
+ readonly permissions: PluginSandboxPermissions
323
+ readonly dataAccess?: PluginDataService
324
+ readonly capabilities?: HostCapabilities
325
+ readonly agent?: HostAgentService
326
+ readonly models?: HostModelsApi
327
+ readonly network?: HostNetworkApi
328
+ readonly plugins?: HostPluginsApi
329
+ }
330
+
331
+ export class PluginSandboxError extends Error {
332
+ constructor(message: string) {
333
+ super(message)
334
+ this.name = "PluginSandboxError"
335
+ }
336
+ }
337
+
338
+ interface PendingRequest {
339
+ readonly resolve: (value: unknown) => void
340
+ readonly reject: (error: Error) => void
341
+ readonly timer: NodeJS.Timeout
342
+ }
343
+
344
+ function hostSnapshotData(): unknown {
345
+ const host = getHost()
346
+ return {
347
+ status: host.getStatus(),
348
+ os: host.getOS(),
349
+ paths: host.getPaths(),
350
+ plugins: host.getPlugins().map((plugin) => ({
351
+ id: plugin.id,
352
+ name: plugin.name,
353
+ version: plugin.version,
354
+ deps: plugin.deps,
355
+ commands: plugin.commands,
356
+ status: plugin.getStatus(),
357
+ })),
358
+ }
359
+ }
360
+
361
+ export class PluginSandboxClient {
362
+ readonly #options: PluginSandboxClientOptions
363
+ readonly #sandboxId = randomUUID()
364
+ readonly #pending = new Map<string, PendingRequest>()
365
+ readonly #decoder = new FrameDecoder()
366
+ readonly #eventUnsubscribes = new Map<string, () => void>()
367
+ readonly #hookUnsubscribes: Array<() => void> = []
368
+ #activeContext: AgentToolContext | null = null
369
+ readonly #httpServers = new Map<
370
+ string,
371
+ {
372
+ readonly server: HttpServer
373
+ readonly host: string
374
+ readonly port: number
375
+ readonly wsServer: WebSocketServer | null
376
+ }
377
+ >()
378
+ readonly #httpResponses = new Map<
379
+ string,
380
+ {
381
+ readonly response: import("node:http").ServerResponse
382
+ /** 响应头尚未应用时先缓存 chunk,避免 set headers 之后的写入报错 */
383
+ readonly chunks: string[]
384
+ ready: boolean
385
+ ended: boolean
386
+ }
387
+ >()
388
+ readonly #wsConnections = new Map<string, WebSocket>()
389
+ /** 终端执行会话:输出缓冲留在宿主侧,插件按需 poll */
390
+ readonly #execSessions = new Map<string, ExecSession>()
391
+ /** PTY 会话:真 tty,支持键盘输入与 ANSI 全屏程序 */
392
+ readonly #ptySessions = new Map<string, PtySession>()
393
+ #child: ChildProcess | null = null
394
+ #ready: SandboxPluginReady | null = null
395
+ #closed = false
396
+ #pluginDataDir: string
397
+ #sandboxDir: string
398
+ #hostSdkDir: string
399
+
400
+ constructor(options: PluginSandboxClientOptions) {
401
+ this.#options = options
402
+ this.#pluginDataDir = ""
403
+ this.#sandboxDir = join(options.paths.dataDir, "sandbox", this.#sandboxId)
404
+ this.#hostSdkDir = join(this.#sandboxDir, "host")
405
+ activeClients.add(this)
406
+ }
407
+
408
+ get sandboxId(): string {
409
+ return this.#sandboxId
410
+ }
411
+
412
+ get ready(): SandboxPluginReady | null {
413
+ return this.#ready
414
+ }
415
+
416
+ get pluginDataDir(): string {
417
+ return this.#pluginDataDir
418
+ }
419
+
420
+ /** sandbox 插件声明的 Agent 工具,call 走 RPC */
421
+ agentToolStubs(): readonly AgentTool[] {
422
+ const tools = this.#ready?.agentTools ?? []
423
+ return tools.map((meta) => ({
424
+ name: meta.name,
425
+ introduction: {
426
+ params: { ...meta.introduction.params },
427
+ content: meta.introduction.content,
428
+ },
429
+ call: async (...args: unknown[]): Promise<AgentCallToolResult> => {
430
+ const input = args.length === 1 ? args[0] : args
431
+ const previous = this.#activeContext
432
+ this.#activeContext = getAgentToolContext()
433
+ try {
434
+ return (await this.#request(
435
+ "plugin.agentTool",
436
+ { name: meta.name, args: input },
437
+ RPC_TIMEOUT_MS.agentTool,
438
+ )) as AgentCallToolResult
439
+ } finally {
440
+ this.#activeContext = previous
441
+ }
442
+ },
443
+ }))
444
+ }
445
+
446
+ async start(options: { readonly deferInit?: boolean } = {}): Promise<SandboxPluginReady> {
447
+ this.#pluginDataDir = await ensurePluginDataDir(this.#options.pluginId, this.#options.paths)
448
+ await mkdir(this.#sandboxDir, { recursive: true })
449
+
450
+ const isBwrap = this.#options.backend.name === "bwrap"
451
+ /*
452
+ * 总是把宿主 SDK 复制到 dataDir 再运行:
453
+ * - bwrap 后端把 dataDir 里的副本挂到 /host;
454
+ * - direct 后端从 node_modules 里直接跑 .ts 会触发
455
+ * ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,复制到 dataDir 后
456
+ * Node 才能按源码运行插件 host runner。
457
+ */
458
+ await this.#prepareHostSdk()
459
+
460
+ const pluginEntryRel = relative(this.#options.pluginDir, this.#options.pluginEntry)
461
+ const env: Record<string, string> = {
462
+ PATH: "/usr/bin:/bin",
463
+ LANG: "C.UTF-8",
464
+ LC_ALL: "C.UTF-8",
465
+ TERM: "dumb",
466
+ HOME: "/tmp",
467
+ TMPDIR: "/tmp",
468
+ }
469
+ const input: PluginLaunchInput = {
470
+ pluginDir: this.#options.pluginDir,
471
+ pluginEntryRel,
472
+ pluginDataDir: this.#pluginDataDir,
473
+ hostSdkDir: this.#hostSdkDir,
474
+ runnerPath: join(this.#hostSdkDir, "src", "sandbox", "plugin-host.ts"),
475
+ env,
476
+ limits: this.#options.limits,
477
+ }
478
+ const spec = this.#options.backend.buildPluginLaunch(input)
479
+ const child = spawn(spec.command, [...spec.args], {
480
+ cwd: spec.cwd,
481
+ env: { ...spec.env },
482
+ stdio: ["pipe", "pipe", "pipe"],
483
+ detached: true,
484
+ })
485
+ this.#child = child
486
+
487
+ let stderrBuffer = ""
488
+ child.stderr?.on("data", (chunk: Buffer) => {
489
+ stderrBuffer += chunk.toString("utf8")
490
+ if (stderrBuffer.length > 64 * 1024) {
491
+ stderrBuffer = stderrBuffer.slice(-64 * 1024)
492
+ }
493
+ })
494
+ child.stdout?.on("data", (chunk: Buffer) => {
495
+ let messages: RpcMessage[]
496
+ try {
497
+ messages = this.#decoder.push(chunk)
498
+ } catch (error) {
499
+ this.#failAll(error instanceof Error ? error : new Error(String(error)))
500
+ this.#kill()
501
+ return
502
+ }
503
+ for (const message of messages) {
504
+ void this.#handleMessage(message)
505
+ }
506
+ })
507
+ child.once("exit", (code, signal) => {
508
+ if (!this.#closed) {
509
+ this.#failAll(
510
+ new PluginSandboxError(
511
+ `插件 sandbox 退出(code=${String(code)}, signal=${String(signal)}):${stderrBuffer.trim()}`,
512
+ ),
513
+ )
514
+ }
515
+ this.#closed = true
516
+ })
517
+ child.once("error", (error) => {
518
+ this.#failAll(
519
+ new PluginSandboxError(
520
+ `插件 sandbox 启动失败:${error instanceof Error ? error.message : String(error)}`,
521
+ ),
522
+ )
523
+ this.#closed = true
524
+ })
525
+
526
+ const configureResult = (await this.#request("sandbox.configure", {
527
+ pluginId: this.#options.pluginId,
528
+ pluginName: this.#options.pluginName,
529
+ pluginVersion: this.#options.pluginVersion,
530
+ pluginEntry: isBwrap
531
+ ? `/plugin/${pluginEntryRel.split("\\").join("/")}`
532
+ : resolve(this.#options.pluginEntry),
533
+ pluginDataDir: isBwrap ? "/plugin-data" : this.#pluginDataDir,
534
+ pluginConfig: this.#options.pluginConfig,
535
+ paths: this.#options.paths,
536
+ config: this.#options.config,
537
+ host: this.#hasHostApi("getHost") ? hostSnapshotData() : null,
538
+ deferInit: options.deferInit === true,
539
+ })) as SandboxPluginReady
540
+
541
+ this.#ready = configureResult
542
+ if (options.deferInit !== true) {
543
+ this.#registerRemoteHooks(configureResult.hooks)
544
+ }
545
+ auditSandbox(this.#options.paths, {
546
+ type: "sandbox.plugin.start",
547
+ sandboxId: this.#sandboxId,
548
+ pluginId: this.#options.pluginId,
549
+ backend: this.#options.backend.name,
550
+ decision: "allowed",
551
+ permissions: this.#options.permissions.hostApi,
552
+ })
553
+ return configureResult
554
+ }
555
+
556
+ async initialize(): Promise<SandboxPluginReady> {
557
+ const result = (await this.#request("plugin.init", {})) as SandboxPluginReady
558
+ this.#ready = result
559
+ this.#registerRemoteHooks(result.hooks)
560
+ return result
561
+ }
562
+
563
+ async #request(method: string, params: unknown, timeoutMs = 30_000): Promise<unknown> {
564
+ if (this.#closed || this.#child === null) {
565
+ throw new PluginSandboxError("插件 sandbox 已关闭")
566
+ }
567
+ const id = nextRequestId("host")
568
+ return await new Promise<unknown>((resolveRequest, rejectRequest) => {
569
+ const timer = setTimeout(() => {
570
+ this.#pending.delete(id)
571
+ rejectRequest(new PluginSandboxError(`插件 RPC 超时:${method}`))
572
+ }, timeoutMs)
573
+ timer.unref()
574
+ this.#pending.set(id, {
575
+ resolve: (value) => {
576
+ clearTimeout(timer)
577
+ resolveRequest(value)
578
+ },
579
+ reject: (error) => {
580
+ clearTimeout(timer)
581
+ rejectRequest(error)
582
+ },
583
+ timer,
584
+ })
585
+ this.#send({ v: 1, type: "request", id, method, params, timeoutMs })
586
+ })
587
+ }
588
+
589
+ #notify(method: string, params: unknown): void {
590
+ if (this.#closed || this.#child === null) {
591
+ return
592
+ }
593
+ this.#send({ v: 1, type: "event", method, params })
594
+ }
595
+
596
+ #send(message: RpcMessage): void {
597
+ if (this.#child?.stdin === null || this.#child?.stdin === undefined) {
598
+ return
599
+ }
600
+ try {
601
+ this.#child.stdin.write(encodeMessage(message))
602
+ } catch {
603
+ // child 已退出;pending 会由 exit 处理
604
+ }
605
+ }
606
+
607
+ #respond(response: RpcResponse): void {
608
+ this.#send(response)
609
+ }
610
+
611
+ async #handleMessage(message: RpcMessage): Promise<void> {
612
+ if (message.type === "response") {
613
+ const pending = this.#pending.get(message.id)
614
+ if (pending === undefined) {
615
+ return
616
+ }
617
+ this.#pending.delete(message.id)
618
+ if (message.ok) {
619
+ pending.resolve(message.result)
620
+ } else {
621
+ pending.reject(new PluginSandboxError(message.error?.message ?? "插件 RPC 失败"))
622
+ }
623
+ return
624
+ }
625
+ if (message.type === "event") {
626
+ this.#handleEvent(message.method, message.params)
627
+ return
628
+ }
629
+ await this.#handleRequest(message)
630
+ }
631
+
632
+ #handleEvent(method: string, params: unknown): void {
633
+ const data = (params ?? {}) as Record<string, unknown>
634
+ if (method === "host.http.stream") {
635
+ const requestId = String(data["requestId"] ?? "")
636
+ const pending = this.#httpResponses.get(requestId)
637
+ if (pending === undefined) {
638
+ return
639
+ }
640
+ if (!pending.ready) {
641
+ if (data["end"] === true) {
642
+ pending.ended = true
643
+ } else {
644
+ pending.chunks.push(String(data["chunk"] ?? ""))
645
+ }
646
+ return
647
+ }
648
+ try {
649
+ if (data["end"] === true) {
650
+ this.#httpResponses.delete(requestId)
651
+ pending.response.end()
652
+ } else {
653
+ pending.response.write(String(data["chunk"] ?? ""))
654
+ }
655
+ } catch {
656
+ this.#httpResponses.delete(requestId)
657
+ }
658
+ return
659
+ }
660
+ const pluginLogger = this.#options.logger.child({ plugin: this.#options.pluginId })
661
+ if (method === "host.log") {
662
+ const level = String(data["level"] ?? "info") as "info" | "warn" | "error"
663
+ const fields = (data["fields"] as Record<string, unknown> | undefined) ?? {}
664
+ const message = String(data["message"] ?? "")
665
+ pluginLogger[level]?.(fields, message)
666
+ return
667
+ }
668
+ if (method === "host.console") {
669
+ const level = String(data["level"] ?? "info") as "info" | "warn" | "error"
670
+ const args = Array.isArray(data["args"]) ? data["args"].map(String) : []
671
+ pluginLogger.error(`插件 [${this.#options.pluginId}] 使用了不推荐的 console logger !`)
672
+ pluginLogger[level]?.(`${this.#options.pluginId} ${args.join(" ")}`)
673
+ return
674
+ }
675
+ if (method === "host.output") {
676
+ const kind = String(data["kind"] ?? "info") as "info" | "warn" | "error" | "print"
677
+ const text = String(data["text"] ?? "")
678
+ getElog().plugin(kind === "print" ? "info" : kind, this.#options.pluginId, text)
679
+ return
680
+ }
681
+ if (method === "host.event.publish") {
682
+ if (!this.#hasHostApi("event")) {
683
+ pluginLogger.warn("sandbox 插件没有 event 权限,已拒绝发布事件")
684
+ return
685
+ }
686
+ const name = String(data["name"] ?? "")
687
+ try {
688
+ eventBus.publish(name, EventEntity.create(data["data"]))
689
+ } catch (error) {
690
+ pluginLogger.warn(
691
+ { error: error instanceof Error ? error.message : String(error) },
692
+ "sandbox 插件发布事件失败",
693
+ )
694
+ }
695
+ return
696
+ }
697
+ if (method === "host.event.unsubscribe") {
698
+ const name = String(data["name"] ?? "")
699
+ const unsubscribe = this.#eventUnsubscribes.get(name)
700
+ unsubscribe?.()
701
+ this.#eventUnsubscribes.delete(name)
702
+ }
703
+ }
704
+
705
+ /** 模型供应商与切换:state / setActive / saveProvider / removeProvider / fetchModels / saveKey / clearKey */
706
+ async #handleModels(method: string, params: Record<string, unknown>): Promise<unknown> {
707
+ const service = this.#options.models
708
+ if (service === undefined) {
709
+ throw new PluginSandboxError("宿主没有模型配置服务")
710
+ }
711
+ if (method === "host.models.state") {
712
+ return await service.state()
713
+ }
714
+ if (method === "host.models.setActive") {
715
+ const model = params["model"]
716
+ const reasoning = params["reasoning"]
717
+ return await service.setActive(
718
+ String(params["providerId"] ?? ""),
719
+ typeof model === "string" && model.trim() !== "" ? model : null,
720
+ typeof reasoning === "string" && reasoning.trim() !== "" ? reasoning : null,
721
+ )
722
+ }
723
+ if (method === "host.models.saveProvider") {
724
+ return await service.saveProvider(params as unknown as HostModelProviderInput)
725
+ }
726
+ if (method === "host.models.removeProvider") {
727
+ return await service.removeProvider(String(params["id"] ?? ""))
728
+ }
729
+ if (method === "host.models.fetchModels") {
730
+ const candidate = params["apiKey"]
731
+ return await service.fetchModels(
732
+ String(params["id"] ?? ""),
733
+ typeof candidate === "string" ? candidate : undefined,
734
+ )
735
+ }
736
+ if (method === "host.models.saveKey") {
737
+ return await service.saveKey(String(params["id"] ?? ""), String(params["apiKey"] ?? ""))
738
+ }
739
+ if (method === "host.models.clearKey") {
740
+ return await service.clearKey(String(params["id"] ?? ""))
741
+ }
742
+ throw new PluginSandboxError(`不支持的 models 方法:${method}`)
743
+ }
744
+
745
+ async #handleNetwork(method: string, params: Record<string, unknown>): Promise<unknown> {
746
+ const service = this.#options.network
747
+ if (service === undefined) {
748
+ throw new PluginSandboxError("宿主没有网络配置服务")
749
+ }
750
+ if (method === "host.network.state") {
751
+ return await service.state()
752
+ }
753
+ if (method === "host.network.saveProxy") {
754
+ return await service.saveProxy(params as unknown as HostNetworkProxyInput)
755
+ }
756
+ throw new PluginSandboxError(`不支持的 network 方法:${method}`)
757
+ }
758
+
759
+ /** 插件管理:list / install / remove / enable / disable */
760
+ async #handlePlugins(method: string, params: Record<string, unknown>): Promise<unknown> {
761
+ const service = this.#options.plugins
762
+ if (service === undefined) {
763
+ throw new PluginSandboxError("宿主没有插件管理服务")
764
+ }
765
+ if (method === "host.plugins.list") {
766
+ return await service.list()
767
+ }
768
+ if (method === "host.plugins.install") {
769
+ return await service.install(params as unknown as HostPluginInstallInput)
770
+ }
771
+ if (method === "host.plugins.remove") {
772
+ return await service.remove(String(params["id"] ?? ""))
773
+ }
774
+ if (method === "host.plugins.enable") {
775
+ return await service.enable(String(params["id"] ?? ""))
776
+ }
777
+ if (method === "host.plugins.disable") {
778
+ return await service.disable(String(params["id"] ?? ""))
779
+ }
780
+ throw new PluginSandboxError(`不支持的 plugins 方法:${method}`)
781
+ }
782
+
783
+ /** 终端执行:run/poll/stop 三个动作,输出缓冲留在宿主侧 */
784
+ async #handleExec(method: string, params: Record<string, unknown>): Promise<unknown> {
785
+ if (method === "host.exec.run") {
786
+ return await this.#execRun(params)
787
+ }
788
+ if (method === "host.exec.poll") {
789
+ return this.#execPoll(String(params["sessionId"] ?? ""))
790
+ }
791
+ if (method === "host.exec.stop") {
792
+ this.#execStop(String(params["sessionId"] ?? ""))
793
+ return {}
794
+ }
795
+ throw new PluginSandboxError(`不支持的 exec 方法:${method}`)
796
+ }
797
+
798
+ async #execRun(params: Record<string, unknown>): Promise<{
799
+ sessionId: string
800
+ pid: number | null
801
+ cwd: string
802
+ }> {
803
+ const command = String(params["command"] ?? "").trim()
804
+ if (command === "") {
805
+ throw new PluginSandboxError("命令不能为空")
806
+ }
807
+ if (command.length > 8000) {
808
+ throw new PluginSandboxError("命令过长")
809
+ }
810
+ const rawCwd = String(params["cwd"] ?? "")
811
+ const cwd = rawCwd === "" ? process.cwd() : resolve(rawCwd)
812
+ const stat = await statSafe(cwd)
813
+ if (!stat?.isDirectory()) {
814
+ throw new PluginSandboxError(`工作目录不存在或不是目录:${cwd}`)
815
+ }
816
+ const extraEnv: Record<string, string> = {}
817
+ const rawEnv = params["env"]
818
+ if (rawEnv !== null && typeof rawEnv === "object" && !Array.isArray(rawEnv)) {
819
+ for (const [key, value] of Object.entries(rawEnv as Record<string, unknown>)) {
820
+ if (typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
821
+ extraEnv[key] = value
822
+ }
823
+ }
824
+ }
825
+ const rawTimeout = params["timeoutMs"]
826
+ const timeoutMs =
827
+ typeof rawTimeout === "number" && Number.isFinite(rawTimeout) && rawTimeout > 0
828
+ ? Math.min(rawTimeout, HOST_EXEC_DEFAULT_TIMEOUT_MS)
829
+ : HOST_EXEC_DEFAULT_TIMEOUT_MS
830
+
831
+ const sessionId = randomUUID()
832
+ const child = spawn(command, {
833
+ cwd,
834
+ shell: true,
835
+ env: { ...process.env, ...extraEnv, TERM: "dumb", NO_COLOR: "1" },
836
+ stdio: ["ignore", "pipe", "pipe"],
837
+ // 独立进程组,stop 时能连带子进程一起结束
838
+ detached: process.platform !== "win32",
839
+ })
840
+ const session: ExecSession = {
841
+ child,
842
+ cwd,
843
+ startedAt: Date.now(),
844
+ output: "",
845
+ delivered: 0,
846
+ done: false,
847
+ exitCode: null,
848
+ timedOut: false,
849
+ truncated: false,
850
+ timer: null,
851
+ }
852
+
853
+ const append = (chunk: Buffer | string): void => {
854
+ if (session.truncated) return
855
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8")
856
+ if (session.output.length + text.length > HOST_EXEC_MAX_OUTPUT_BYTES) {
857
+ session.output += text.slice(0, HOST_EXEC_MAX_OUTPUT_BYTES - session.output.length)
858
+ session.truncated = true
859
+ killSession(session)
860
+ return
861
+ }
862
+ session.output += text
863
+ }
864
+
865
+ child.stdout?.on("data", append)
866
+ child.stderr?.on("data", append)
867
+ child.on("error", (error) => {
868
+ append(`
869
+ [exec] ${error.message}
870
+ `)
871
+ finish(null)
872
+ })
873
+
874
+ const timer = setTimeout(() => {
875
+ session.timedOut = true
876
+ killSession(session)
877
+ }, timeoutMs)
878
+ Object.assign(session, { timer })
879
+
880
+ let finished = false
881
+ function finish(code: number | null): void {
882
+ if (finished) return
883
+ finished = true
884
+ session.done = true
885
+ session.exitCode = code
886
+ if (session.timer !== null) clearTimeout(session.timer)
887
+ }
888
+ child.on("close", (code) => finish(code))
889
+
890
+ this.#execSessions.set(sessionId, session)
891
+ // 只保留最近 32 个会话,避免长时间使用后内存堆积
892
+ if (this.#execSessions.size > 32) {
893
+ for (const [id, entry] of this.#execSessions) {
894
+ if (this.#execSessions.size <= 32) break
895
+ if (entry.done) this.#execSessions.delete(id)
896
+ }
897
+ }
898
+ return { sessionId, pid: child.pid ?? null, cwd }
899
+ }
900
+
901
+ #execPoll(sessionId: string): HostExecChunk {
902
+ const session = this.#execSessions.get(sessionId)
903
+ if (session === undefined) {
904
+ throw new PluginSandboxError("执行会话不存在或已过期")
905
+ }
906
+ const chunk = session.output.slice(session.delivered)
907
+ session.delivered = session.output.length
908
+ return {
909
+ chunk,
910
+ done: session.done,
911
+ exitCode: session.exitCode,
912
+ timedOut: session.timedOut,
913
+ truncated: session.truncated,
914
+ }
915
+ }
916
+
917
+ #execStop(sessionId: string): void {
918
+ const session = this.#execSessions.get(sessionId)
919
+ if (session === undefined || session.done) return
920
+ killSession(session)
921
+ }
922
+
923
+ /** PTY:available/spawn/poll/write/resize/kill */
924
+ async #handlePty(method: string, params: Record<string, unknown>): Promise<unknown> {
925
+ if (method === "host.pty.available") {
926
+ const module = await loadNodePty()
927
+ if (module !== null) {
928
+ return { available: true, backend: "node-pty" }
929
+ }
930
+ const python = await probePython()
931
+ if (python !== null) {
932
+ // node-pty 缺失时用 python 自建 pty,仍是真 tty
933
+ return { available: true, backend: "python-pty", reason: nodePtyError ?? null }
934
+ }
935
+ return {
936
+ available: false,
937
+ reason: `node-pty 与 python3 均不可用:${nodePtyError ?? "未安装"}`,
938
+ }
939
+ }
940
+ if (method === "host.pty.spawn") {
941
+ return await this.#ptySpawn(params)
942
+ }
943
+ if (method === "host.pty.poll") {
944
+ return this.#ptyPoll(String(params["sessionId"] ?? ""))
945
+ }
946
+ if (method === "host.pty.write") {
947
+ const session = this.#ptySession(String(params["sessionId"] ?? ""))
948
+ const data = String(params["data"] ?? "")
949
+ if (data !== "" && !session.done) session.handle.write(data)
950
+ return {}
951
+ }
952
+ if (method === "host.pty.resize") {
953
+ const session = this.#ptySession(String(params["sessionId"] ?? ""))
954
+ const cols = clampCols(params["cols"])
955
+ const rows = clampRows(params["rows"])
956
+ if (!session.done && (cols !== session.cols || rows !== session.rows)) {
957
+ session.cols = cols
958
+ session.rows = rows
959
+ try {
960
+ session.handle.resize(cols, rows)
961
+ } catch {
962
+ /* 进程可能已退出 */
963
+ }
964
+ }
965
+ return {}
966
+ }
967
+ if (method === "host.pty.kill") {
968
+ const session = this.#ptySession(String(params["sessionId"] ?? ""))
969
+ if (!session.done) {
970
+ try {
971
+ session.handle.kill()
972
+ } catch {
973
+ /* 已经退出 */
974
+ }
975
+ }
976
+ return {}
977
+ }
978
+ throw new PluginSandboxError(`不支持的 pty 方法:${method}`)
979
+ }
980
+
981
+ #ptySession(sessionId: string): PtySession {
982
+ const session = this.#ptySessions.get(sessionId)
983
+ if (session === undefined) {
984
+ throw new PluginSandboxError("PTY 会话不存在或已过期")
985
+ }
986
+ return session
987
+ }
988
+
989
+ async #ptySpawn(params: Record<string, unknown>): Promise<HostPtySession> {
990
+ const module = await loadNodePty()
991
+ const python = module === null ? await probePython() : null
992
+ if (module === null && python === null) {
993
+ throw new PluginSandboxError(
994
+ `没有可用的 PTY 后端:node-pty 与 python3 均不可用(${nodePtyError ?? "未安装"})`,
995
+ )
996
+ }
997
+ const rawCwd = String(params["cwd"] ?? "")
998
+ const cwd = rawCwd === "" ? process.cwd() : resolve(rawCwd)
999
+ const stat = await statSafe(cwd)
1000
+ if (!stat?.isDirectory()) {
1001
+ throw new PluginSandboxError(`工作目录不存在或不是目录:${cwd}`)
1002
+ }
1003
+ const file = String(params["file"] ?? "").trim() || defaultShell()
1004
+ const rawArgs = params["args"]
1005
+ const args = Array.isArray(rawArgs)
1006
+ ? rawArgs.filter((item): item is string => typeof item === "string")
1007
+ : []
1008
+ const extraEnv: Record<string, string> = {}
1009
+ const rawEnv = params["env"]
1010
+ if (rawEnv !== null && typeof rawEnv === "object" && !Array.isArray(rawEnv)) {
1011
+ for (const [key, value] of Object.entries(rawEnv as Record<string, unknown>)) {
1012
+ if (typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1013
+ extraEnv[key] = value
1014
+ }
1015
+ }
1016
+ }
1017
+ const cols = clampCols(params["cols"])
1018
+ const rows = clampRows(params["rows"])
1019
+
1020
+ const env = {
1021
+ ...process.env,
1022
+ ...extraEnv,
1023
+ TERM: "xterm-256color",
1024
+ COLORTERM: "truecolor",
1025
+ }
1026
+ const onData: Array<(data: string) => void> = []
1027
+ const onExit: Array<(code: number | null) => void> = []
1028
+ let handle: PtyHandle
1029
+ let backend: PtyBackendName
1030
+
1031
+ if (module !== null) {
1032
+ // 首选:node-pty 原生模块
1033
+ const pty = module.spawn(file, args, { name: "xterm-256color", cols, rows, cwd, env })
1034
+ pty.onData((data) => {
1035
+ for (const handler of onData) handler(data)
1036
+ })
1037
+ pty.onExit((event) => {
1038
+ const code = typeof event.exitCode === "number" ? event.exitCode : null
1039
+ for (const handler of onExit) handler(code)
1040
+ })
1041
+ handle = {
1042
+ pid: pty.pid,
1043
+ write: (data) => pty.write(data),
1044
+ resize: (nextCols, nextRows) => pty.resize(nextCols, nextRows),
1045
+ kill: () => pty.kill(),
1046
+ }
1047
+ backend = "node-pty"
1048
+ } else {
1049
+ // 回退:python3 自建 pty,控制命令走 stdin 的 JSON 行
1050
+ const config = JSON.stringify({ shell: file, args, cwd, cols, rows, env: extraEnv })
1051
+ const child = spawn(python as string, [PTY_BRIDGE_PATH, config], {
1052
+ stdio: ["pipe", "pipe", "pipe"],
1053
+ cwd,
1054
+ env,
1055
+ detached: process.platform !== "win32",
1056
+ })
1057
+ const send = (payload: Record<string, unknown>): void => {
1058
+ if (child.stdin === null || child.stdin.destroyed) return
1059
+ try {
1060
+ child.stdin.write(`${JSON.stringify(payload)}
1061
+ `)
1062
+ } catch {
1063
+ /* 桥接已退出 */
1064
+ }
1065
+ }
1066
+ child.stdout?.on("data", (chunk: Buffer) => {
1067
+ const text = chunk.toString("utf8")
1068
+ for (const handler of onData) handler(text)
1069
+ })
1070
+ child.stderr?.on("data", (chunk: Buffer) => {
1071
+ // 桥接的报错也当终端输出,便于在控制台里看到
1072
+ const text = chunk.toString("utf8")
1073
+ for (const handler of onData) handler(text)
1074
+ })
1075
+ child.on("close", (code) => {
1076
+ for (const handler of onExit) handler(typeof code === "number" ? code : null)
1077
+ })
1078
+ handle = {
1079
+ pid: child.pid ?? 0,
1080
+ write: (data) => send({ type: "input", data }),
1081
+ resize: (nextCols, nextRows) =>
1082
+ send({ type: "resize", cols: nextCols, rows: nextRows }),
1083
+ kill: () => {
1084
+ send({ type: "kill" })
1085
+ if (process.platform !== "win32" && typeof child.pid === "number") {
1086
+ try {
1087
+ process.kill(-child.pid, "SIGKILL")
1088
+ return
1089
+ } catch {
1090
+ /* 进程组已消失 */
1091
+ }
1092
+ }
1093
+ try {
1094
+ child.kill("SIGKILL")
1095
+ } catch {
1096
+ /* 已退出 */
1097
+ }
1098
+ },
1099
+ }
1100
+ backend = "python-pty"
1101
+ }
1102
+
1103
+ const session: PtySession = {
1104
+ handle,
1105
+ backend,
1106
+ file,
1107
+ cwd,
1108
+ cols,
1109
+ rows,
1110
+ output: "",
1111
+ delivered: 0,
1112
+ done: false,
1113
+ exitCode: null,
1114
+ }
1115
+ onData.push((data) => {
1116
+ session.output += data
1117
+ // 超上限时丢最旧的一半,同步回退已读游标
1118
+ if (session.output.length > HOST_PTY_MAX_OUTPUT_CHARS) {
1119
+ const drop = session.output.length - Math.floor(HOST_PTY_MAX_OUTPUT_CHARS / 2)
1120
+ session.output = session.output.slice(drop)
1121
+ session.delivered = Math.max(0, session.delivered - drop)
1122
+ }
1123
+ })
1124
+ onExit.push((code) => {
1125
+ session.done = true
1126
+ session.exitCode = code
1127
+ })
1128
+
1129
+ const sessionId = randomUUID()
1130
+ this.#ptySessions.set(sessionId, session)
1131
+ // 只保留最近的会话,避免长期使用后堆积
1132
+ if (this.#ptySessions.size > HOST_PTY_MAX_SESSIONS) {
1133
+ for (const [id, entry] of this.#ptySessions) {
1134
+ if (this.#ptySessions.size <= HOST_PTY_MAX_SESSIONS) break
1135
+ if (entry.done) this.#ptySessions.delete(id)
1136
+ }
1137
+ }
1138
+ return { sessionId, pid: handle.pid, file, cwd, cols, rows }
1139
+ }
1140
+
1141
+ #ptyPoll(sessionId: string): HostPtyChunk {
1142
+ const session = this.#ptySession(sessionId)
1143
+ const pending = session.output.slice(
1144
+ session.delivered,
1145
+ session.delivered + HOST_PTY_MAX_POLL_CHARS,
1146
+ )
1147
+ session.delivered += pending.length
1148
+ return { chunk: pending, done: session.done, exitCode: session.exitCode }
1149
+ }
1150
+
1151
+ #hasHostApi(name: string): boolean {
1152
+ return this.#options.permissions.hostApi.includes(name)
1153
+ }
1154
+
1155
+ #requireHostApi(name: string): void {
1156
+ if (!this.#hasHostApi(name)) {
1157
+ throw new PluginSandboxError(`插件没有 hostApi.${name} 权限`)
1158
+ }
1159
+ }
1160
+
1161
+ async #handleRequest(request: RpcRequest): Promise<void> {
1162
+ try {
1163
+ const params = (request.params ?? {}) as Record<string, unknown>
1164
+ let result: unknown
1165
+ if (request.method === "host.agent.chat") {
1166
+ this.#requireHostApi("agent")
1167
+ if (this.#options.agent === undefined) {
1168
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1169
+ }
1170
+ result = await this.#options.agent.chat(params as unknown as HostAgentChatInput)
1171
+ } else if (request.method === "host.agent.stream") {
1172
+ this.#requireHostApi("agent")
1173
+ if (this.#options.agent === undefined) {
1174
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1175
+ }
1176
+ const streamId = String(params["streamId"] ?? "")
1177
+ const input = params["input"] as unknown as HostAgentChatInput
1178
+ result = await this.#options.agent.stream(input, (event) => {
1179
+ this.#notify("sandbox.agent.stream", { streamId, event })
1180
+ })
1181
+ } else if (request.method === "host.agent.validateWorkspace") {
1182
+ this.#requireHostApi("agent")
1183
+ if (this.#options.agent === undefined) {
1184
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1185
+ }
1186
+ result = await this.#options.agent.validateWorkspace(String(params["path"] ?? ""))
1187
+ } else if (request.method === "host.agent.pickDirectory") {
1188
+ this.#requireHostApi("agent")
1189
+ if (this.#options.agent === undefined) {
1190
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1191
+ }
1192
+ result = await this.#options.agent.pickDirectory()
1193
+ } else if (request.method === "host.agent.searchWorkspaceFiles") {
1194
+ this.#requireHostApi("agent")
1195
+ if (this.#options.agent === undefined) {
1196
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1197
+ }
1198
+ result = await this.#options.agent.searchWorkspaceFiles(
1199
+ String(params["root"] ?? ""),
1200
+ String(params["query"] ?? ""),
1201
+ )
1202
+ } else if (request.method === "host.agent.interject") {
1203
+ this.#requireHostApi("agent")
1204
+ if (this.#options.agent === undefined) {
1205
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1206
+ }
1207
+ result = await this.#options.agent.interject(
1208
+ String(params["sessionId"] ?? ""),
1209
+ String(params["message"] ?? ""),
1210
+ )
1211
+ } else if (request.method === "host.agent.stop") {
1212
+ this.#requireHostApi("agent")
1213
+ if (this.#options.agent === undefined) {
1214
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1215
+ }
1216
+ result = await this.#options.agent.stop(String(params["sessionId"] ?? ""))
1217
+ } else if (request.method === "host.agent.respondApproval") {
1218
+ this.#requireHostApi("agent")
1219
+ if (this.#options.agent === undefined) {
1220
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1221
+ }
1222
+ result = await this.#options.agent.respondApproval(
1223
+ String(params["approvalId"] ?? ""),
1224
+ params["approved"] === true,
1225
+ )
1226
+ } else if (request.method === "host.agent.respondAsk") {
1227
+ this.#requireHostApi("agent")
1228
+ if (this.#options.agent === undefined) {
1229
+ throw new PluginSandboxError("宿主没有 Agent 服务")
1230
+ }
1231
+ result = await this.#options.agent.respondAsk(
1232
+ String(params["askId"] ?? ""),
1233
+ String(params["answer"] ?? ""),
1234
+ )
1235
+ } else if (request.method.startsWith("host.models.")) {
1236
+ this.#requireHostApi("models")
1237
+ if (this.#options.models === undefined) {
1238
+ throw new PluginSandboxError("宿主没有模型配置服务")
1239
+ }
1240
+ result = await this.#handleModels(request.method, params)
1241
+ } else if (request.method.startsWith("host.network.")) {
1242
+ this.#requireHostApi("network")
1243
+ if (this.#options.network === undefined) {
1244
+ throw new PluginSandboxError("宿主没有网络配置服务")
1245
+ }
1246
+ result = await this.#handleNetwork(request.method, params)
1247
+ } else if (request.method.startsWith("host.workspaces.")) {
1248
+ this.#requireHostApi("workspaces")
1249
+ if (request.method === "host.workspaces.list") {
1250
+ result = await listWorkspaces(this.#options.paths)
1251
+ } else if (request.method === "host.workspaces.add") {
1252
+ result = await addWorkspace(
1253
+ {
1254
+ root: String(params["root"] ?? ""),
1255
+ ...(typeof params["name"] === "string" ? { name: params["name"] } : {}),
1256
+ },
1257
+ this.#options.paths,
1258
+ )
1259
+ } else if (request.method === "host.workspaces.remove") {
1260
+ result = await removeWorkspace(String(params["id"] ?? ""), this.#options.paths)
1261
+ } else if (request.method === "host.workspaces.rename") {
1262
+ result = await renameWorkspace(
1263
+ String(params["id"] ?? ""),
1264
+ String(params["name"] ?? ""),
1265
+ this.#options.paths,
1266
+ )
1267
+ } else if (request.method === "host.workspaces.touch") {
1268
+ result = await touchWorkspace(String(params["id"] ?? ""), this.#options.paths)
1269
+ } else {
1270
+ throw new Error(`未知 host 方法:${request.method}`)
1271
+ }
1272
+ } else if (request.method.startsWith("host.plugins.")) {
1273
+ this.#requireHostApi("plugins")
1274
+ if (this.#options.plugins === undefined) {
1275
+ throw new PluginSandboxError("宿主没有插件管理服务")
1276
+ }
1277
+ result = await this.#handlePlugins(request.method, params)
1278
+ } else if (request.method === "host.http.createServer") {
1279
+ this.#requireHostApi("http")
1280
+ const serverId = String(params["serverId"] ?? "")
1281
+ const host = String(params["host"] ?? "127.0.0.1")
1282
+ const port = typeof params["port"] === "number" ? params["port"] : 0
1283
+ if (host.trim() === "" || /\s/.test(host)) {
1284
+ throw new PluginSandboxError(`非法监听地址:${host}`)
1285
+ }
1286
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) {
1287
+ throw new PluginSandboxError(`非法端口:${String(port)}`)
1288
+ }
1289
+ const tls = params["tls"] as
1290
+ | { cert?: unknown; key?: unknown; ca?: unknown }
1291
+ | undefined
1292
+ if (
1293
+ tls !== undefined &&
1294
+ (typeof tls.cert !== "string" ||
1295
+ typeof tls.key !== "string" ||
1296
+ tls.cert.trim() === "" ||
1297
+ tls.key.trim() === "")
1298
+ ) {
1299
+ throw new PluginSandboxError("TLS 需要 cert 与 key PEM")
1300
+ }
1301
+ const requestListener = async (
1302
+ req: import("node:http").IncomingMessage,
1303
+ res: import("node:http").ServerResponse,
1304
+ ): Promise<void> => {
1305
+ try {
1306
+ const chunks: Buffer[] = []
1307
+ let size = 0
1308
+ for await (const chunk of req) {
1309
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
1310
+ size += buffer.length
1311
+ if (size > HTTP_MAX_BODY_BYTES) {
1312
+ throw new Error("HTTP 请求体超过上限")
1313
+ }
1314
+ chunks.push(buffer)
1315
+ }
1316
+ const httpRequest: HttpRequest = {
1317
+ method: req.method ?? "GET",
1318
+ url: req.url ?? "/",
1319
+ headers: Object.fromEntries(
1320
+ Object.entries(req.headers).map(([key, value]) => [
1321
+ key,
1322
+ Array.isArray(value) ? value.join(", ") : (value ?? ""),
1323
+ ]),
1324
+ ),
1325
+ body: Buffer.concat(chunks).toString("base64"),
1326
+ remoteAddress: req.socket.remoteAddress ?? null,
1327
+ }
1328
+ const requestId = nextRequestId("http-req")
1329
+ this.#httpResponses.set(requestId, {
1330
+ response: res,
1331
+ chunks: [],
1332
+ ready: false,
1333
+ ended: false,
1334
+ })
1335
+ res.on("close", () => this.#httpResponses.delete(requestId))
1336
+ const response = (await this.#request(
1337
+ "plugin.http",
1338
+ { serverId, requestId, request: httpRequest },
1339
+ 180_000,
1340
+ )) as HttpResponse
1341
+ res.statusCode = response.status ?? 200
1342
+ for (const [key, value] of Object.entries(response.headers ?? {})) {
1343
+ res.setHeader(key, value)
1344
+ }
1345
+ if (response.stream === true) {
1346
+ const pending = this.#httpResponses.get(requestId)
1347
+ if (pending !== undefined) {
1348
+ pending.ready = true
1349
+ for (const chunk of pending.chunks) {
1350
+ try {
1351
+ pending.response.write(chunk)
1352
+ } catch {
1353
+ this.#httpResponses.delete(requestId)
1354
+ return
1355
+ }
1356
+ }
1357
+ pending.chunks.length = 0
1358
+ if (pending.ended) {
1359
+ this.#httpResponses.delete(requestId)
1360
+ pending.response.end()
1361
+ }
1362
+ }
1363
+ return
1364
+ }
1365
+ this.#httpResponses.delete(requestId)
1366
+ const body =
1367
+ response.body === undefined
1368
+ ? Buffer.alloc(0)
1369
+ : Buffer.from(
1370
+ response.body,
1371
+ response.bodyEncoding === "base64" ? "base64" : "utf8",
1372
+ )
1373
+ res.end(body)
1374
+ } catch (error) {
1375
+ res.statusCode = 500
1376
+ res.end(error instanceof Error ? error.message : String(error))
1377
+ }
1378
+ }
1379
+ const server =
1380
+ tls === undefined
1381
+ ? createHttpServer(requestListener)
1382
+ : createHttpsServer(
1383
+ {
1384
+ cert: String(tls.cert ?? ""),
1385
+ key: String(tls.key ?? ""),
1386
+ ...(typeof tls.ca === "string" ? { ca: tls.ca } : {}),
1387
+ },
1388
+ requestListener,
1389
+ )
1390
+ let wsServer: WebSocketServer | null = null
1391
+ if (params["websocket"] === true) {
1392
+ wsServer = new WebSocketServer({ noServer: true })
1393
+ server.on("upgrade", (req, socket, head) => {
1394
+ wsServer?.handleUpgrade(req, socket, head, (ws) => {
1395
+ const connectionId = nextRequestId("ws")
1396
+ this.#wsConnections.set(connectionId, ws)
1397
+ const headers = Object.fromEntries(
1398
+ Object.entries(req.headers).map(([key, value]) => [
1399
+ key,
1400
+ Array.isArray(value) ? value.join(", ") : (value ?? ""),
1401
+ ]),
1402
+ )
1403
+ const httpRequest: HttpRequest = {
1404
+ method: req.method ?? "GET",
1405
+ url: req.url ?? "/",
1406
+ headers,
1407
+ body: "",
1408
+ remoteAddress: req.socket.remoteAddress ?? null,
1409
+ }
1410
+ void this.#request("plugin.websocket.connect", {
1411
+ serverId,
1412
+ connectionId,
1413
+ request: httpRequest,
1414
+ }).catch(() => ws.close())
1415
+ ws.on("message", (data, isBinary) => {
1416
+ void this.#request("plugin.websocket.message", {
1417
+ connectionId,
1418
+ data: Buffer.from(data as Buffer).toString("base64"),
1419
+ binary: isBinary,
1420
+ }).catch(() => undefined)
1421
+ })
1422
+ ws.on("close", (code, reason) => {
1423
+ this.#wsConnections.delete(connectionId)
1424
+ void this.#request("plugin.websocket.close", {
1425
+ connectionId,
1426
+ code,
1427
+ reason: reason.toString(),
1428
+ }).catch(() => undefined)
1429
+ })
1430
+ })
1431
+ })
1432
+ }
1433
+ await new Promise<void>((resolveListen, rejectListen) => {
1434
+ server.once("error", rejectListen)
1435
+ server.listen(port, host, () => {
1436
+ server.off("error", rejectListen)
1437
+ resolveListen()
1438
+ })
1439
+ })
1440
+ const address = server.address()
1441
+ const actualPort =
1442
+ typeof address === "object" && address !== null ? address.port : port
1443
+ this.#httpServers.set(serverId, { server, host, port: actualPort, wsServer })
1444
+ result = { serverId, host, port: actualPort }
1445
+ } else if (request.method === "host.http.closeServer") {
1446
+ this.#requireHostApi("http")
1447
+ const serverId = String(params["serverId"] ?? "")
1448
+ const record = this.#httpServers.get(serverId)
1449
+ if (record !== undefined) {
1450
+ this.#httpServers.delete(serverId)
1451
+ for (const ws of this.#wsConnections.values()) {
1452
+ ws.close()
1453
+ }
1454
+ record.wsServer?.close()
1455
+ // SSE / 长轮询会保持连接,server.close() 要等它们全部断开;
1456
+ // 关闭时给一点收尾时间,然后强制断开剩余连接,避免 Ctrl+C 卡住。
1457
+ const server = record.server
1458
+ await new Promise<void>((resolveClose) => {
1459
+ let settled = false
1460
+ const finish = (): void => {
1461
+ if (settled) {
1462
+ return
1463
+ }
1464
+ settled = true
1465
+ clearTimeout(forceTimer)
1466
+ resolveClose()
1467
+ }
1468
+ server.close(() => finish())
1469
+ server.closeIdleConnections()
1470
+ const forceTimer = setTimeout(() => {
1471
+ server.closeAllConnections()
1472
+ finish()
1473
+ }, 800)
1474
+ forceTimer.unref()
1475
+ })
1476
+ }
1477
+ result = {}
1478
+ } else if (request.method === "host.websocket.send") {
1479
+ this.#requireHostApi("http")
1480
+ const connectionId = String(params["connectionId"] ?? "")
1481
+ const ws = this.#wsConnections.get(connectionId)
1482
+ if (ws === undefined) {
1483
+ throw new PluginSandboxError(`WebSocket 连接不存在:${connectionId}`)
1484
+ }
1485
+ ws.send(String(params["data"] ?? ""))
1486
+ result = {}
1487
+ } else if (request.method === "host.websocket.close") {
1488
+ this.#requireHostApi("http")
1489
+ const connectionId = String(params["connectionId"] ?? "")
1490
+ const ws = this.#wsConnections.get(connectionId)
1491
+ if (ws !== undefined) {
1492
+ this.#wsConnections.delete(connectionId)
1493
+ ws.close(
1494
+ typeof params["code"] === "number" ? params["code"] : 1000,
1495
+ String(params["reason"] ?? ""),
1496
+ )
1497
+ }
1498
+ result = {}
1499
+ } else if (request.method.startsWith("host.pty.")) {
1500
+ this.#requireHostApi("pty")
1501
+ result = await this.#handlePty(request.method, params)
1502
+ } else if (request.method.startsWith("host.exec.")) {
1503
+ this.#requireHostApi("exec")
1504
+ result = await this.#handleExec(request.method, params)
1505
+ } else if (request.method === "host.pluginData") {
1506
+ this.#requireHostApi("fs")
1507
+ const targetId = String(params["targetId"] ?? "")
1508
+ const method = String(params["method"] ?? "")
1509
+ const args = Array.isArray(params["args"]) ? params["args"] : []
1510
+ if (this.#options.dataAccess === undefined) {
1511
+ throw new PluginSandboxError("宿主没有 plugin data 服务")
1512
+ }
1513
+ const api = await this.#options.dataAccess.open(this.#options.pluginId, targetId)
1514
+ if (api === null) {
1515
+ throw new PluginSandboxError(`没有插件 data 访问权限:${targetId}`)
1516
+ }
1517
+ const target = (api as unknown as Record<string, unknown>)[method]
1518
+ if (typeof target !== "function") {
1519
+ throw new PluginSandboxError(`不支持的 data 方法:${method}`)
1520
+ }
1521
+ try {
1522
+ const value = await (
1523
+ target as (...values: unknown[]) => Promise<unknown>
1524
+ ).apply(api, args)
1525
+ result = value instanceof Uint8Array ? [...value] : value
1526
+ } catch (error) {
1527
+ throw new PluginSandboxError(
1528
+ error instanceof Error ? error.message : String(error),
1529
+ )
1530
+ }
1531
+ } else if (request.method.startsWith("host.fs.")) {
1532
+ this.#requireHostApi("fs")
1533
+ result = await this.#handleFs(request.method, params, false)
1534
+ } else if (request.method.startsWith("host.workspaceFs.")) {
1535
+ this.#requireHostApi("fs")
1536
+ if (!this.#options.permissions.workspaceRead) {
1537
+ throw new PluginSandboxError("插件没有 workspace 读权限")
1538
+ }
1539
+ result = await this.#handleFs(request.method, params, true)
1540
+ } else if (request.method === "host.event.subscribe") {
1541
+ this.#requireHostApi("event")
1542
+ const name = String(params["name"] ?? "")
1543
+ if (!this.#eventUnsubscribes.has(name)) {
1544
+ const unsubscribe = eventBus.subscribe(name, (event) => {
1545
+ this.#notify("sandbox.event", { name, data: event.getData() })
1546
+ })
1547
+ this.#eventUnsubscribes.set(name, unsubscribe)
1548
+ }
1549
+ result = {}
1550
+ } else if (request.method === "host.shutdown") {
1551
+ this.#requireHostApi("shutdown")
1552
+ shutdownHost()
1553
+ result = {}
1554
+ } else if (request.method.startsWith("host.capabilities.")) {
1555
+ this.#requireHostApi("capabilities")
1556
+ if (this.#options.capabilities === undefined) {
1557
+ throw new PluginSandboxError("宿主没有 capabilities 服务")
1558
+ }
1559
+ const name = request.method.slice("host.capabilities.".length)
1560
+ if (name === "executeCode") {
1561
+ result = await this.#options.capabilities.executeCode(
1562
+ params as never,
1563
+ this.#activeContext ?? undefined,
1564
+ )
1565
+ } else if (name === "startBackgroundCode") {
1566
+ result = await this.#options.capabilities.startBackgroundCode(
1567
+ params as never,
1568
+ this.#activeContext ?? undefined,
1569
+ )
1570
+ } else if (name === "queryTask") {
1571
+ result = this.#options.capabilities.queryTask(
1572
+ typeof params["taskId"] === "string" ? params["taskId"] : undefined,
1573
+ )
1574
+ } else if (name === "stopTask") {
1575
+ result = this.#options.capabilities.stopTask(
1576
+ typeof params["taskId"] === "string" ? params["taskId"] : undefined,
1577
+ params["mode"] === "ctrl_c" ? "ctrl_c" : "kill",
1578
+ )
1579
+ } else if (name === "webSearch") {
1580
+ result = await this.#options.capabilities.webSearch(
1581
+ String(params["query"] ?? ""),
1582
+ typeof params["count"] === "number" ? params["count"] : undefined,
1583
+ )
1584
+ } else if (name === "webFetch") {
1585
+ result = await this.#options.capabilities.webFetch(String(params["url"] ?? ""))
1586
+ } else {
1587
+ throw new PluginSandboxError(`未知 capability:${name}`)
1588
+ }
1589
+ } else if (request.method === "host.getHost") {
1590
+ this.#requireHostApi("getHost")
1591
+ result = hostSnapshotData()
1592
+ } else if (request.method === "host.session.exists") {
1593
+ result = findSession(String(params["id"] ?? "")) !== null
1594
+ } else if (request.method === "host.session.message") {
1595
+ const target = findSession(String(params["id"] ?? ""))
1596
+ if (target === null) {
1597
+ throw new PluginSandboxError("会话不存在")
1598
+ }
1599
+ const content = Array.isArray(params["content"])
1600
+ ? params["content"].map(String)
1601
+ : [String(params["content"] ?? "")]
1602
+ await target.message(content, String(params["type"] ?? "message"))
1603
+ result = {}
1604
+ } else {
1605
+ throw new Error(`未知 host 方法:${request.method}`)
1606
+ }
1607
+ this.#respond({ v: 1, type: "response", id: request.id, ok: true, result })
1608
+ } catch (error) {
1609
+ const message = error instanceof Error ? error.message : String(error)
1610
+ if (message.includes("权限")) {
1611
+ auditSandbox(this.#options.paths, {
1612
+ type: "sandbox.permission",
1613
+ sandboxId: this.#sandboxId,
1614
+ pluginId: this.#options.pluginId,
1615
+ backend: this.#options.backend.name,
1616
+ decision: "denied",
1617
+ method: request.method,
1618
+ reason: message,
1619
+ })
1620
+ }
1621
+ this.#respond({
1622
+ v: 1,
1623
+ type: "response",
1624
+ id: request.id,
1625
+ ok: false,
1626
+ error: {
1627
+ code: "host.rpc-failed",
1628
+ message,
1629
+ },
1630
+ })
1631
+ }
1632
+ }
1633
+
1634
+ async #handleFs(
1635
+ method: string,
1636
+ params: Record<string, unknown>,
1637
+ workspace: boolean,
1638
+ ): Promise<unknown> {
1639
+ const api: HostFileApi = workspace
1640
+ ? createPluginFileApi(process.cwd())
1641
+ : createPluginFileApi(this.#pluginDataDir)
1642
+ const path = String(params["path"] ?? "")
1643
+ const action = method.replace(/^host\.(fs|workspaceFs)\./, "")
1644
+ if (
1645
+ workspace &&
1646
+ ["writeText", "writeBytes", "writeJson", "mkdir", "remove"].includes(action)
1647
+ ) {
1648
+ if (!this.#options.permissions.workspaceWrite) {
1649
+ throw new PluginSandboxError("插件没有 workspace 写权限")
1650
+ }
1651
+ const context = this.#activeContext
1652
+ if (context !== null && context.permission === "read-only") {
1653
+ throw new PluginSandboxError("当前 Agent 权限是 read-only,不能修改工作区文件")
1654
+ }
1655
+ }
1656
+ switch (action) {
1657
+ case "readText":
1658
+ return { text: await api.readText(path) }
1659
+ case "readBytes": {
1660
+ const data = await api.readBytes(path)
1661
+ return { data: data === null ? null : Buffer.from(data).toString("base64") }
1662
+ }
1663
+ case "readJson":
1664
+ return { value: await api.readJson(path) }
1665
+ case "writeText":
1666
+ await api.writeText(path, String(params["text"] ?? ""))
1667
+ return {}
1668
+ case "writeBytes":
1669
+ await api.writeBytes(
1670
+ path,
1671
+ new Uint8Array(Buffer.from(String(params["data"] ?? ""), "base64")),
1672
+ )
1673
+ return {}
1674
+ case "writeJson":
1675
+ await api.writeJson(path, params["value"])
1676
+ return {}
1677
+ case "list":
1678
+ return { entries: await api.list(path) }
1679
+ case "stat":
1680
+ return { stat: await api.stat(path) }
1681
+ case "mkdir":
1682
+ await api.mkdir(path)
1683
+ return {}
1684
+ case "remove":
1685
+ await api.remove(path)
1686
+ return {}
1687
+ case "exists":
1688
+ return { exists: await api.exists(path) }
1689
+ default:
1690
+ throw new Error(`未知 fs 方法:${method}`)
1691
+ }
1692
+ }
1693
+
1694
+ #registerRemoteHooks(hooks: readonly string[]): void {
1695
+ for (const hook of hooks) {
1696
+ const kind = HOOK_KIND_BY_API[hook]
1697
+ if (kind === undefined) {
1698
+ continue
1699
+ }
1700
+ const unregister = registerExternalHook(
1701
+ kind as never,
1702
+ this.#options.pluginId,
1703
+ async (payload) => {
1704
+ const result = (await this.runHook(
1705
+ hook,
1706
+ payload as Record<string, unknown>,
1707
+ )) as {
1708
+ prompt?: string[]
1709
+ }
1710
+ if (hook === "onMessage" || hook === "onSessionStart") {
1711
+ const target = payload as { prompt?: string[] }
1712
+ if (Array.isArray(result.prompt) && Array.isArray(target.prompt)) {
1713
+ target.prompt.splice(0, target.prompt.length, ...result.prompt)
1714
+ }
1715
+ }
1716
+ },
1717
+ )
1718
+ this.#hookUnsubscribes.push(unregister)
1719
+ }
1720
+ }
1721
+
1722
+ async runCommand(
1723
+ name: string,
1724
+ argv: readonly string[],
1725
+ cwd: string,
1726
+ json: boolean,
1727
+ ): Promise<number> {
1728
+ const result = (await this.#request("plugin.command", {
1729
+ name,
1730
+ argv: [...argv],
1731
+ cwd,
1732
+ json,
1733
+ })) as { exitCode?: number }
1734
+ return result.exitCode ?? 0
1735
+ }
1736
+
1737
+ async runHook(hook: string, payload: Record<string, unknown>): Promise<unknown> {
1738
+ return await this.#request("plugin.hook", { hook, payload }, 30_000)
1739
+ }
1740
+
1741
+ commandStubs(): PluginCommand[] {
1742
+ if (this.#ready === null) {
1743
+ return []
1744
+ }
1745
+ return this.#ready.commands.map((command) => ({
1746
+ name: command.name,
1747
+ summary: command.summary,
1748
+ usage: command.usage,
1749
+ aliases: command.aliases,
1750
+ run: async (ctx) => this.runCommand(command.name, ctx.argv, ctx.cwd, ctx.json),
1751
+ }))
1752
+ }
1753
+
1754
+ async close(): Promise<void> {
1755
+ activeClients.delete(this)
1756
+ for (const [serverId, record] of this.#httpServers) {
1757
+ this.#httpServers.delete(serverId)
1758
+ await new Promise<void>((resolveClose) => record.server.close(() => resolveClose()))
1759
+ }
1760
+ if (this.#child === null) {
1761
+ return
1762
+ }
1763
+ auditSandbox(this.#options.paths, {
1764
+ type: "sandbox.plugin.stop",
1765
+ sandboxId: this.#sandboxId,
1766
+ pluginId: this.#options.pluginId,
1767
+ backend: this.#options.backend.name,
1768
+ })
1769
+ try {
1770
+ await this.#request("sandbox.shutdown", {}, 5_000)
1771
+ } catch {
1772
+ // 超时或已退出
1773
+ }
1774
+ this.#closed = true
1775
+ this.#kill()
1776
+ // 先收掉仍在跑的进程,再清监听,避免成了孤儿
1777
+ for (const session of this.#execSessions.values()) {
1778
+ if (!session.done) killSession(session)
1779
+ }
1780
+ this.#execSessions.clear()
1781
+ for (const session of this.#ptySessions.values()) {
1782
+ if (session.done) continue
1783
+ try {
1784
+ session.handle.kill()
1785
+ } catch {
1786
+ /* 已经退出 */
1787
+ }
1788
+ }
1789
+ this.#ptySessions.clear()
1790
+ for (const unsubscribe of this.#eventUnsubscribes.values()) {
1791
+ unsubscribe()
1792
+ }
1793
+ for (const unsubscribe of this.#hookUnsubscribes) {
1794
+ unsubscribe()
1795
+ }
1796
+ await rm(this.#sandboxDir, { recursive: true, force: true }).catch(() => undefined)
1797
+ }
1798
+
1799
+ async #prepareHostSdk(): Promise<void> {
1800
+ const copies: Array<[string, string]> = [
1801
+ [
1802
+ join(PROJECT_ROOT, "src", "api", "host"),
1803
+ join(this.#hostSdkDir, "src", "api", "host"),
1804
+ ],
1805
+ [
1806
+ join(PROJECT_ROOT, "src", "api", "plugin"),
1807
+ join(this.#hostSdkDir, "src", "api", "plugin"),
1808
+ ],
1809
+ [
1810
+ join(PROJECT_ROOT, "src", "event", "event.ts"),
1811
+ join(this.#hostSdkDir, "src", "event", "event.ts"),
1812
+ ],
1813
+ [
1814
+ join(PROJECT_ROOT, "src", "session", "session.ts"),
1815
+ join(this.#hostSdkDir, "src", "session", "session.ts"),
1816
+ ],
1817
+ [
1818
+ join(PROJECT_ROOT, "src", "session", "registry.ts"),
1819
+ join(this.#hostSdkDir, "src", "session", "registry.ts"),
1820
+ ],
1821
+ [
1822
+ join(PROJECT_ROOT, "src", "session", "replay.ts"),
1823
+ join(this.#hostSdkDir, "src", "session", "replay.ts"),
1824
+ ],
1825
+ [
1826
+ join(PROJECT_ROOT, "src", "session", "types.ts"),
1827
+ join(this.#hostSdkDir, "src", "session", "types.ts"),
1828
+ ],
1829
+ [
1830
+ join(PROJECT_ROOT, "src", "workspace", "index.ts"),
1831
+ join(this.#hostSdkDir, "src", "workspace", "index.ts"),
1832
+ ],
1833
+ [
1834
+ join(PROJECT_ROOT, "src", "sandbox", "bridge-state.ts"),
1835
+ join(this.#hostSdkDir, "src", "sandbox", "bridge-state.ts"),
1836
+ ],
1837
+ [
1838
+ join(PROJECT_ROOT, "src", "sandbox", "protocol.ts"),
1839
+ join(this.#hostSdkDir, "src", "sandbox", "protocol.ts"),
1840
+ ],
1841
+ [
1842
+ join(PROJECT_ROOT, "src", "sandbox", "rpc-timeouts.ts"),
1843
+ join(this.#hostSdkDir, "src", "sandbox", "rpc-timeouts.ts"),
1844
+ ],
1845
+ [
1846
+ join(PROJECT_ROOT, "src", "sandbox", "plugin-host.ts"),
1847
+ join(this.#hostSdkDir, "src", "sandbox", "plugin-host.ts"),
1848
+ ],
1849
+ [
1850
+ join(PROJECT_ROOT, "src", "plugin", "host-alias.ts"),
1851
+ join(this.#hostSdkDir, "src", "plugin", "host-alias.ts"),
1852
+ ],
1853
+ [
1854
+ join(PROJECT_ROOT, "src", "plugin", "access-guard.ts"),
1855
+ join(this.#hostSdkDir, "src", "plugin", "access-guard.ts"),
1856
+ ],
1857
+ ]
1858
+ for (const [source, target] of copies) {
1859
+ await mkdir(dirname(target), { recursive: true })
1860
+ await cp(source, target, { recursive: true })
1861
+ }
1862
+ }
1863
+
1864
+ #failAll(error: Error): void {
1865
+ for (const pending of this.#pending.values()) {
1866
+ pending.reject(error)
1867
+ }
1868
+ this.#pending.clear()
1869
+ }
1870
+
1871
+ #kill(): void {
1872
+ if (this.#child?.pid !== undefined) {
1873
+ try {
1874
+ process.kill(-this.#child.pid, "SIGKILL")
1875
+ } catch {
1876
+ this.#child.kill("SIGKILL")
1877
+ }
1878
+ }
1879
+ this.#closed = true
1880
+ }
1881
+ }