@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,406 @@
1
+ /*
2
+ * SandboxManager:选择 backend、准备 workspace/scratch、执行 agent-code、收集 artifact。
3
+ */
4
+
5
+ import { createHash, randomUUID } from "node:crypto"
6
+ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
7
+ import { basename, dirname, join, relative, resolve, sep } from "node:path"
8
+
9
+ import type { SandboxAccess } from "../config/types.ts"
10
+
11
+ import { auditSandbox } from "./audit.ts"
12
+ import { BwrapBackend } from "./backends/bwrap.ts"
13
+ import { DirectBackend } from "./backends/direct.ts"
14
+ import { createSandboxMemoryLimit, type MemoryLimitState } from "./memory-limit.ts"
15
+ import { startSandboxProcess, type SandboxProcessHandle } from "./supervisor.ts"
16
+ import {
17
+ SandboxExecutionError,
18
+ SandboxUnavailableError,
19
+ type AgentCodeRequest,
20
+ type AgentCodeResult,
21
+ type SandboxArtifact,
22
+ type SandboxBackend,
23
+ type SandboxBackendStatus,
24
+ type AgentCodeTaskHandle,
25
+ type SandboxLimits,
26
+ type SandboxManagerOptions,
27
+ type StopMode,
28
+ } from "./types.ts"
29
+
30
+ const MAX_ARTIFACT_FILES = 32
31
+ const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024
32
+
33
+ interface ResolvedLimits extends SandboxLimits {
34
+ readonly timeoutMs: number
35
+ readonly memoryMB: number
36
+ readonly pids: number
37
+ readonly outputLimitBytes: number
38
+ }
39
+
40
+ function clampInteger(value: number, min: number, max: number): number {
41
+ return Math.min(max, Math.max(min, Math.trunc(value)))
42
+ }
43
+
44
+ function baseEnv(): Record<string, string> {
45
+ return {
46
+ PATH: "/usr/bin:/bin",
47
+ LANG: "C.UTF-8",
48
+ LC_ALL: "C.UTF-8",
49
+ TERM: "dumb",
50
+ }
51
+ }
52
+
53
+ async function ensureDirectory(path: string): Promise<void> {
54
+ await mkdir(path, { recursive: true })
55
+ }
56
+
57
+ function isInside(root: string, target: string): boolean {
58
+ return target === root || target.startsWith(`${root}${sep}`)
59
+ }
60
+
61
+ async function collectArtifacts(
62
+ scratchDir: string,
63
+ sandboxDir: string,
64
+ ): Promise<{ artifacts: SandboxArtifact[]; artifactDir: string | null }> {
65
+ const source = join(scratchDir, "artifacts")
66
+ const info = await stat(source).catch(() => null)
67
+ if (info === null || !info.isDirectory()) {
68
+ return { artifacts: [], artifactDir: null }
69
+ }
70
+
71
+ const destination = join(sandboxDir, "artifacts")
72
+ await ensureDirectory(destination)
73
+ const collected: SandboxArtifact[] = []
74
+ let total = 0
75
+
76
+ const entries = await readdir(source, { recursive: true, withFileTypes: true })
77
+ for (const entry of entries) {
78
+ if (!entry.isFile() || collected.length >= MAX_ARTIFACT_FILES) {
79
+ continue
80
+ }
81
+ const parent = entry.parentPath ?? source
82
+ const absolute = join(parent, entry.name)
83
+ const rel = relative(source, absolute)
84
+ if (rel.startsWith("..") || resolve(absolute) === resolve(source)) {
85
+ continue
86
+ }
87
+ const fileInfo = await stat(absolute)
88
+ if (fileInfo.size > MAX_ARTIFACT_BYTES || total + fileInfo.size > MAX_ARTIFACT_BYTES) {
89
+ continue
90
+ }
91
+ total += fileInfo.size
92
+ const data = await readFile(absolute)
93
+ const sha256 = createHash("sha256").update(data).digest("hex")
94
+ const target = join(destination, rel)
95
+ await ensureDirectory(dirname(target))
96
+ await writeFile(target, data)
97
+ collected.push({ path: rel.split(sep).join("/"), size: fileInfo.size, sha256 })
98
+ }
99
+
100
+ return { artifacts: collected, artifactDir: destination }
101
+ }
102
+
103
+ export class SandboxManager {
104
+ readonly #options: SandboxManagerOptions
105
+ #backend: SandboxBackend | null
106
+ #resolved: Promise<SandboxBackend> | null = null
107
+ #memoryLimitWarned = false
108
+ #active = 0
109
+ readonly #waiters: Array<() => void> = []
110
+
111
+ constructor(options: SandboxManagerOptions) {
112
+ this.#options = options
113
+ this.#backend = options.backend ?? null
114
+ }
115
+
116
+ async probe(): Promise<SandboxBackendStatus> {
117
+ if (this.#backend !== null) {
118
+ return await this.#backend.probe()
119
+ }
120
+ if (this.#options.settings.backend === "nsjail") {
121
+ return {
122
+ available: false,
123
+ backend: "nsjail",
124
+ reason: "nsjail 后端尚未实现",
125
+ version: null,
126
+ }
127
+ }
128
+ return await new BwrapBackend().probe()
129
+ }
130
+
131
+ async #resolveBackend(access: SandboxAccess): Promise<SandboxBackend> {
132
+ if (access === "danger-full-access") {
133
+ return new DirectBackend()
134
+ }
135
+ if (this.#backend !== null) {
136
+ return this.#backend
137
+ }
138
+ if (this.#resolved !== null) {
139
+ return await this.#resolved
140
+ }
141
+ this.#resolved = (async (): Promise<SandboxBackend> => {
142
+ const { settings, logger } = this.#options
143
+ if (settings.mode === "off") {
144
+ throw new SandboxUnavailableError("sandbox.mode = off,Agent 代码执行需要 sandbox")
145
+ }
146
+
147
+ const preferred = settings.backend
148
+ const candidates: SandboxBackend[] = []
149
+ if (preferred === "auto" || preferred === "bwrap") {
150
+ candidates.push(new BwrapBackend())
151
+ }
152
+ if (preferred === "nsjail") {
153
+ // nsjail 后端尚未实现,显式报告不可用
154
+ throw new SandboxUnavailableError("nsjail 后端尚未实现")
155
+ }
156
+
157
+ for (const candidate of candidates) {
158
+ const status = await candidate.probe()
159
+ if (status.available) {
160
+ return candidate
161
+ }
162
+ logger?.warn(
163
+ { backend: candidate.name, reason: status.reason },
164
+ "sandbox backend 不可用",
165
+ )
166
+ }
167
+
168
+ if (settings.mode === "best-effort") {
169
+ logger?.warn(
170
+ { backend: "direct" },
171
+ "sandbox 降级为 direct(无隔离),仅限开发/受信环境",
172
+ )
173
+ return new DirectBackend()
174
+ }
175
+ throw new SandboxUnavailableError("没有可用的 OS 级 sandbox 后端")
176
+ })()
177
+ return await this.#resolved
178
+ }
179
+
180
+ async #acquireSlot(): Promise<void> {
181
+ const max = this.#options.settings.maxConcurrent
182
+ if (this.#active < max) {
183
+ this.#active += 1
184
+ return
185
+ }
186
+ await new Promise<void>((resolve) => {
187
+ this.#waiters.push(resolve)
188
+ })
189
+ this.#active += 1
190
+ }
191
+
192
+ #releaseSlot(): void {
193
+ this.#active -= 1
194
+ const next = this.#waiters.shift()
195
+ next?.()
196
+ }
197
+
198
+ #limits(request: AgentCodeRequest): ResolvedLimits {
199
+ const settings = this.#options.settings
200
+ return {
201
+ timeoutMs: clampInteger(request.timeoutMs ?? settings.defaultTimeoutMs, 100, 600_000),
202
+ memoryMB: clampInteger(request.memoryMB ?? settings.defaultMemoryMB, 32, 8_192),
203
+ pids: clampInteger(64, 1, 512),
204
+ outputLimitBytes: clampInteger(
205
+ request.outputLimitBytes ?? settings.outputLimitBytes,
206
+ 1024,
207
+ 64 * 1024 * 1024,
208
+ ),
209
+ }
210
+ }
211
+
212
+ async startAgentCode(request: AgentCodeRequest): Promise<AgentCodeTaskHandle> {
213
+ if (typeof request.command !== "string" || request.command.trim() === "") {
214
+ throw new SandboxExecutionError("命令不能为空")
215
+ }
216
+ if (request.argv.some((item) => typeof item !== "string")) {
217
+ throw new SandboxExecutionError("argv 必须是字符串数组")
218
+ }
219
+
220
+ const access = request.access ?? this.#options.settings.access
221
+ if (
222
+ access !== "read-only" &&
223
+ access !== "workspace-write" &&
224
+ access !== "danger-full-access"
225
+ ) {
226
+ throw new SandboxExecutionError(`未知 sandbox 权限:${String(access)}`)
227
+ }
228
+ const workspaceDir = resolve(request.workspaceDir ?? process.cwd())
229
+ const cwd = resolve(request.cwd ?? workspaceDir)
230
+ if (access !== "danger-full-access" && !isInside(workspaceDir, cwd)) {
231
+ throw new SandboxExecutionError(`cwd 不在 workspace 内:${cwd}`)
232
+ }
233
+ const workspaceInfo = await stat(workspaceDir).catch(() => null)
234
+ if (workspaceInfo === null || !workspaceInfo.isDirectory()) {
235
+ throw new SandboxExecutionError(`workspace 不存在:${workspaceDir}`)
236
+ }
237
+ const cwdInfo = await stat(cwd).catch(() => null)
238
+ if (cwdInfo === null || !cwdInfo.isDirectory()) {
239
+ throw new SandboxExecutionError(`cwd 不存在:${cwd}`)
240
+ }
241
+
242
+ const backend = await this.#resolveBackend(access)
243
+ await this.#acquireSlot()
244
+ try {
245
+ return await this.#startAgentCodeLocked(request, backend, workspaceDir, cwd, access)
246
+ } catch (error) {
247
+ this.#releaseSlot()
248
+ throw error
249
+ }
250
+ }
251
+
252
+ async runAgentCode(request: AgentCodeRequest): Promise<AgentCodeResult> {
253
+ const task = await this.startAgentCode(request)
254
+ return await task.wait()
255
+ }
256
+
257
+ async #startAgentCodeLocked(
258
+ request: AgentCodeRequest,
259
+ backend: SandboxBackend,
260
+ workspaceDir: string,
261
+ cwd: string,
262
+ access: SandboxAccess,
263
+ ): Promise<AgentCodeTaskHandle> {
264
+ const limits = this.#limits(request)
265
+ const sandboxId = randomUUID()
266
+ const sandboxDir = join(this.#options.paths.dataDir, "sandbox", sandboxId)
267
+ const scratchDir = join(sandboxDir, "scratch")
268
+ await ensureDirectory(join(scratchDir, "tmp"))
269
+ await ensureDirectory(join(scratchDir, "artifacts"))
270
+
271
+ const env = baseEnv()
272
+ if (backend.name === "bwrap") {
273
+ env["HOME"] = "/scratch"
274
+ env["TMPDIR"] = "/scratch/tmp"
275
+ env["SCRATCH"] = "/scratch"
276
+ env["WORKSPACE"] = "/workspace"
277
+ } else {
278
+ env["HOME"] = scratchDir
279
+ env["TMPDIR"] = join(scratchDir, "tmp")
280
+ env["SCRATCH"] = scratchDir
281
+ env["WORKSPACE"] = workspaceDir
282
+ }
283
+
284
+ const memoryLimit: MemoryLimitState | null =
285
+ backend.name === "bwrap"
286
+ ? await createSandboxMemoryLimit({
287
+ bytes: limits.memoryMB * 1024 * 1024,
288
+ timeoutMs: limits.timeoutMs,
289
+ })
290
+ : null
291
+ if (memoryLimit !== null && !memoryLimit.active && !this.#memoryLimitWarned) {
292
+ this.#memoryLimitWarned = true
293
+ this.#options.logger?.warn(
294
+ { reason: memoryLimit.reason },
295
+ "沙箱物理内存限制不可用,仅保留 V8 堆上限与超时",
296
+ )
297
+ }
298
+
299
+ let processHandle: SandboxProcessHandle
300
+ try {
301
+ const baseSpec = backend.buildAgentLaunch({
302
+ workspaceDir,
303
+ scratchDir,
304
+ cwd,
305
+ access,
306
+ network: request.network,
307
+ command: request.command,
308
+ argv: request.argv,
309
+ env,
310
+ limits,
311
+ })
312
+ const spec =
313
+ memoryLimit !== null && memoryLimit.active ? memoryLimit.wrap(baseSpec) : baseSpec
314
+ processHandle = startSandboxProcess(spec, limits, {
315
+ stdin: request.stdin ?? "",
316
+ })
317
+ } catch (error) {
318
+ await memoryLimit?.finish()
319
+ this.#releaseSlot()
320
+ throw error
321
+ }
322
+
323
+ let finalize: Promise<AgentCodeResult> | null = null
324
+ const wait = (): Promise<AgentCodeResult> => {
325
+ finalize ??= (async (): Promise<AgentCodeResult> => {
326
+ try {
327
+ const processResult = await processHandle.done
328
+ const memory = await memoryLimit?.finish()
329
+ const oomKilled = memory?.oomKilled === true
330
+ const { artifacts, artifactDir } = await collectArtifacts(
331
+ scratchDir,
332
+ sandboxDir,
333
+ )
334
+ await rm(scratchDir, { recursive: true, force: true })
335
+ auditSandbox(this.#options.paths, {
336
+ type: "sandbox.agent-code",
337
+ sandboxId,
338
+ backend: backend.name,
339
+ access,
340
+ exitCode: processResult.exitCode,
341
+ timedOut: processResult.timedOut,
342
+ durationMs: processResult.durationMs,
343
+ artifacts: artifacts.length,
344
+ })
345
+ this.#options.logger?.info(
346
+ {
347
+ sandboxId,
348
+ backend: backend.name,
349
+ command: basename(request.command),
350
+ exitCode: processResult.exitCode,
351
+ timedOut: processResult.timedOut,
352
+ durationMs: processResult.durationMs,
353
+ },
354
+ "sandbox agent-code 执行完成",
355
+ )
356
+ return {
357
+ sandboxId,
358
+ backend: backend.name,
359
+ access,
360
+ exitCode: processResult.exitCode,
361
+ signal: processResult.signal,
362
+ stdout: processResult.stdout,
363
+ stderr: processResult.stderr,
364
+ truncated: processResult.truncated,
365
+ timedOut: processResult.timedOut,
366
+ memoryLimit: memoryLimit?.active === true ? "cgroup" : "none",
367
+ memoryLimitReason:
368
+ memoryLimit !== null && !memoryLimit.active ? memoryLimit.reason : null,
369
+ oomKilled,
370
+ durationMs: processResult.durationMs,
371
+ artifacts,
372
+ artifactDir:
373
+ artifactDir === null
374
+ ? null
375
+ : backend.name === "bwrap"
376
+ ? "/scratch/artifacts"
377
+ : artifactDir,
378
+ sandboxDir: backend.name === "bwrap" ? "/scratch" : sandboxDir,
379
+ hostArtifactDir: artifactDir,
380
+ hostSandboxDir: sandboxDir,
381
+ }
382
+ } finally {
383
+ this.#releaseSlot()
384
+ }
385
+ })()
386
+ void finalize.catch(() => undefined)
387
+ return finalize
388
+ }
389
+
390
+ return {
391
+ sandboxId,
392
+ backend: backend.name,
393
+ access,
394
+ get stdout() {
395
+ return processHandle.stdout
396
+ },
397
+ get stderr() {
398
+ return processHandle.stderr
399
+ },
400
+ stop(mode: StopMode) {
401
+ processHandle.stop(mode)
402
+ },
403
+ wait,
404
+ }
405
+ }
406
+ }
@@ -0,0 +1,245 @@
1
+ /*
2
+ * 沙箱物理内存限制。
3
+ *
4
+ * RLIMIT_AS 限制的是虚拟地址空间,会直接打死 V8 / JVM / Go 这类需要预留大块地址空间的
5
+ * 运行时,且并不能约束真实物理内存。这里改用 cgroup v2 的 memory.max:
6
+ *
7
+ * 1. 用 `systemd-run --user --scope -p MemoryMax=<bytes> -p MemorySwapMax=0` 起一个
8
+ * 临时 scope(用户 session 下无需 root);
9
+ * 2. 读 systemctl 的 ControlGroup 拿到 cgroup 路径;
10
+ * 3. 用一层 /bin/sh 包住真正的 sandbox 命令:先把 shell 自己写进 cgroup.procs,
11
+ * 再 exec bwrap,保证 bwrap 及其后代一出生就在受限 cgroup 里;
12
+ * 4. 结束后读 memory.events 判断是否发生过 oom_kill,再停掉 scope。
13
+ *
14
+ * 宿主没有 systemd user manager(例如容器、SSH 非登录环境)时返回 inactive,
15
+ * 调用方退回“只限制 V8 堆 + 超时”的方式,不阻塞命令执行。
16
+ */
17
+
18
+ import { spawn } from "node:child_process"
19
+ import { randomUUID } from "node:crypto"
20
+ import { accessSync, constants, readFileSync } from "node:fs"
21
+ import { join } from "node:path"
22
+
23
+ import type { LaunchSpec } from "./types.ts"
24
+
25
+ export const CGROUP_V2_ROOT = "/sys/fs/cgroup"
26
+
27
+ export interface MemoryLimitState {
28
+ /** 是否真的挂上了 cgroup 限制 */
29
+ readonly active: boolean
30
+ /** active=false 时的原因(给结果与日志用) */
31
+ readonly reason: string | null
32
+ /** 把启动命令包进受限 cgroup;inactive 时原样返回 */
33
+ wrap(spec: LaunchSpec): LaunchSpec
34
+ /** 读 oom 事件并清理 scope;可重复调用 */
35
+ finish(): Promise<{ oomKilled: boolean }>
36
+ }
37
+
38
+ export interface MemoryLimitCommandResult {
39
+ readonly code: number | null
40
+ readonly stdout: string
41
+ readonly stderr: string
42
+ }
43
+
44
+ /** 便于单测替换外部命令、文件与时间 */
45
+ export interface MemoryLimitDeps {
46
+ readonly platform: NodeJS.Platform
47
+ readonly env: NodeJS.ProcessEnv
48
+ readonly run: (command: string, args: readonly string[]) => Promise<MemoryLimitCommandResult>
49
+ readonly spawnHolder: (command: string, args: readonly string[]) => { readonly pid?: number }
50
+ readonly readText: (path: string) => string | null
51
+ readonly writable: (path: string) => boolean
52
+ readonly sleep: (ms: number) => Promise<void>
53
+ readonly randomSuffix: () => string
54
+ }
55
+
56
+ export interface MemoryLimitOptions {
57
+ /** 物理内存上限,字节 */
58
+ readonly bytes: number
59
+ /** 沙箱超时;用来给 scope 的 RuntimeMaxSec 留余量,避免泄漏 */
60
+ readonly timeoutMs: number
61
+ readonly deps?: Partial<MemoryLimitDeps>
62
+ }
63
+
64
+ const CGROUP_POLL_TIMES = 10
65
+ const CGROUP_POLL_INTERVAL_MS = 50
66
+ const SCOPE_RUNTIME_GRACE_MS = 120_000
67
+
68
+ function runCommand(command: string, args: readonly string[]): Promise<MemoryLimitCommandResult> {
69
+ return new Promise((resolveCommand) => {
70
+ const child = spawn(command, [...args], {
71
+ stdio: ["ignore", "pipe", "pipe"],
72
+ env: process.env,
73
+ })
74
+ const out: Buffer[] = []
75
+ const err: Buffer[] = []
76
+ child.stdout?.on("data", (chunk: Buffer) => out.push(chunk))
77
+ child.stderr?.on("data", (chunk: Buffer) => err.push(chunk))
78
+ child.once("error", (error) => {
79
+ resolveCommand({ code: null, stdout: "", stderr: error.message })
80
+ })
81
+ child.once("close", (code) => {
82
+ resolveCommand({
83
+ code,
84
+ stdout: Buffer.concat(out).toString("utf8"),
85
+ stderr: Buffer.concat(err).toString("utf8"),
86
+ })
87
+ })
88
+ })
89
+ }
90
+
91
+ const defaultDeps: MemoryLimitDeps = {
92
+ platform: process.platform,
93
+ env: process.env,
94
+ run: runCommand,
95
+ spawnHolder: (command, args) => {
96
+ const child = spawn(command, [...args], {
97
+ stdio: "ignore",
98
+ env: process.env,
99
+ detached: true,
100
+ })
101
+ child.unref()
102
+ return { pid: child.pid }
103
+ },
104
+ readText: (path) => {
105
+ try {
106
+ return readFileSync(path, "utf8")
107
+ } catch {
108
+ return null
109
+ }
110
+ },
111
+ writable: (path) => {
112
+ try {
113
+ accessSync(path, constants.W_OK)
114
+ return true
115
+ } catch {
116
+ return false
117
+ }
118
+ },
119
+ sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
120
+ randomSuffix: () => randomUUID().slice(0, 8),
121
+ }
122
+
123
+ function inactive(reason: string): MemoryLimitState {
124
+ return {
125
+ active: false,
126
+ reason,
127
+ wrap: (spec) => spec,
128
+ finish: async () => ({ oomKilled: false }),
129
+ }
130
+ }
131
+
132
+ /** 从 memory.events 里读 oom_kill 计数 */
133
+ export function parseOomKill(memoryEvents: string | null): boolean {
134
+ if (memoryEvents === null) {
135
+ return false
136
+ }
137
+ const match = /^oom_kill\s+(\d+)\s*$/m.exec(memoryEvents)
138
+ return match !== null && Number(match[1]) > 0
139
+ }
140
+
141
+ /** sandbox 命令外层包一层 sh:先把自己放进 cgroup,再 exec 原命令 */
142
+ export function wrapSpecInCgroup(spec: LaunchSpec, cgroupPath: string): LaunchSpec {
143
+ return {
144
+ ...spec,
145
+ command: "/bin/sh",
146
+ args: [
147
+ "-c",
148
+ 'echo $$ > "$1/cgroup.procs" || exit 126; shift; exec "$@"',
149
+ "sh",
150
+ cgroupPath,
151
+ spec.command,
152
+ ...spec.args,
153
+ ],
154
+ }
155
+ }
156
+
157
+ export async function createSandboxMemoryLimit(
158
+ options: MemoryLimitOptions,
159
+ ): Promise<MemoryLimitState> {
160
+ const deps: MemoryLimitDeps = { ...defaultDeps, ...options.deps }
161
+ const bytes = Math.trunc(options.bytes)
162
+ if (deps.platform !== "linux") {
163
+ return inactive("非 Linux 平台不使用 cgroup 内存限制")
164
+ }
165
+ if (!Number.isFinite(bytes) || bytes < 16 * 1024 * 1024) {
166
+ return inactive("内存上限过小,跳过 cgroup 限制")
167
+ }
168
+
169
+ const unit = `wellspring-sandbox-${process.pid}-${deps.randomSuffix()}`
170
+ const runtimeSec = Math.ceil((Math.max(1, options.timeoutMs) + SCOPE_RUNTIME_GRACE_MS) / 1000)
171
+ const holderArgs = [
172
+ "--user",
173
+ "--scope",
174
+ "--quiet",
175
+ "--collect",
176
+ "--unit",
177
+ unit,
178
+ "--property",
179
+ `MemoryMax=${bytes}`,
180
+ // 不把物理内存挤进 swap,否则 memory.max 会被换页绕过
181
+ "--property",
182
+ "MemorySwapMax=0",
183
+ "--property",
184
+ `RuntimeMaxSec=${runtimeSec}`,
185
+ "--",
186
+ "/bin/sleep",
187
+ "infinity",
188
+ ]
189
+ try {
190
+ deps.spawnHolder("systemd-run", holderArgs)
191
+ } catch (error) {
192
+ return inactive(
193
+ `systemd-run 不可用:${error instanceof Error ? error.message : String(error)}`,
194
+ )
195
+ }
196
+
197
+ const unitName = `${unit}.scope`
198
+ let cgroup: string | null = null
199
+ let lastError = ""
200
+ for (let attempt = 0; attempt < CGROUP_POLL_TIMES; attempt += 1) {
201
+ const shown = await deps.run("systemctl", [
202
+ "--user",
203
+ "show",
204
+ "-p",
205
+ "ControlGroup",
206
+ "--value",
207
+ unitName,
208
+ ])
209
+ const value = shown.stdout.trim()
210
+ if (value.startsWith("/")) {
211
+ cgroup = value
212
+ break
213
+ }
214
+ lastError = shown.stderr.trim()
215
+ await deps.sleep(CGROUP_POLL_INTERVAL_MS)
216
+ }
217
+ const stop = async (): Promise<void> => {
218
+ await deps.run("systemctl", ["--user", "stop", unitName])
219
+ }
220
+ if (cgroup === null) {
221
+ await stop()
222
+ return inactive(`systemd user scope 不可用${lastError === "" ? "" : `:${lastError}`}`)
223
+ }
224
+
225
+ const cgroupPath = join(CGROUP_V2_ROOT, cgroup)
226
+ if (!deps.writable(join(cgroupPath, "cgroup.procs"))) {
227
+ await stop()
228
+ return inactive(`无法写入 cgroup:${cgroupPath}`)
229
+ }
230
+
231
+ let finished: Promise<{ oomKilled: boolean }> | null = null
232
+ return {
233
+ active: true,
234
+ reason: null,
235
+ wrap: (spec) => wrapSpecInCgroup(spec, cgroupPath),
236
+ finish: () => {
237
+ finished ??= (async () => {
238
+ const oomKilled = parseOomKill(deps.readText(join(cgroupPath, "memory.events")))
239
+ await stop()
240
+ return { oomKilled }
241
+ })()
242
+ return finished
243
+ },
244
+ }
245
+ }